Files
Alexey MilovidovandClaude Opus 4.8 21ebcdf51e Do not retry the XDBC bridge metadata request, fixing a hung check
The `odbc` and `jdbc` table functions infer the remote table structure
from the XDBC bridge during query analysis, in
`ITableFunctionXDBC::getActualTableStructure`. This request used the
default `ReadSettings`, so `http_max_tries` was `10`: when the bridge (or
the driver behind it) stopped responding mid-request, it retried up to
ten times, each attempt blocking for up to `http_receive_timeout`.

Because this runs during analysis, before any pipeline executor exists,
it cannot observe query cancellation (`KILL QUERY`, `max_execution_time`),
so a cancelled query stayed in the processlist for minutes and tripped
the stress-test hung check ("possible deadlock found").

`startBridgeIfNot` has already verified the bridge responds, so the
metadata request talks to a known-alive local subprocess. Make a single
attempt (`http_max_tries` = 1) and let the error propagate instead of
retrying.

Added an integration test that points the JDBC bridge at a mock which
answers the ping handshake but never responds to `columns_info`, and
asserts the query fails promptly instead of retrying for minutes.

CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=107461&sha=4a93325d05f03a26c28369c7fe7de652a5f2f155&name_0=PR&name_1=Stress%20test%20%28amd_tsan%29
Related: https://github.com/ClickHouse/ClickHouse/pull/107461

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-18 00:53:38 +00:00

63 lines
2.2 KiB
Python

import os
import time
import pytest
from helpers.cluster import ClickHouseCluster
from helpers.mock_servers import start_mock_servers
cluster = ClickHouseCluster(__file__)
# Must match <jdbc_bridge><port> in configs/jdbc_bridge.xml.
MOCK_BRIDGE_PORT = 9020
# http_receive_timeout used for the test query. The metadata request blocks for one such
# timeout per attempt. With the fix it makes a single attempt (~RECEIVE_TIMEOUT seconds);
# before the fix it retried http_max_tries (= 10) times, taking well over ten times longer.
RECEIVE_TIMEOUT = 3
node = cluster.add_instance(
"node",
main_configs=["configs/jdbc_bridge.xml"],
)
def start_unresponsive_bridge():
script_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mocks")
# Run the mock inside the server's own container, reachable at 127.0.0.1:MOCK_BRIDGE_PORT.
start_mock_servers(
cluster,
script_dir,
[("unresponsive_bridge.py", "node", MOCK_BRIDGE_PORT)],
)
@pytest.fixture(scope="module")
def started_cluster():
try:
cluster.start()
start_unresponsive_bridge()
yield cluster
finally:
cluster.shutdown()
def test_jdbc_table_function_does_not_hang_on_unresponsive_bridge(started_cluster):
# The mock bridge answers the ping handshake but never responds to /columns_info, so
# structure inference for the `jdbc` table function cannot complete. The query must fail
# promptly (after a single receive timeout) instead of retrying the metadata request and
# keeping the query alive for minutes — which is what tripped the stress-test hung check.
start = time.time()
error = node.query_and_get_error(
"SELECT * FROM jdbc('dummy_datasource', 'some_table')",
settings={"http_receive_timeout": RECEIVE_TIMEOUT},
)
elapsed = time.time() - start
# The unresponsive bridge must make the query fail, not return data.
assert error != ""
# One attempt is ~RECEIVE_TIMEOUT seconds; ten retries would be well over 30 seconds.
assert (
elapsed < 15
), f"jdbc structure inference took {elapsed:.1f}s; the bridge metadata request is being retried instead of failing fast"