From 48c7f404646c4a807b3e2287a48bf736349faa70 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Fri, 26 Sep 2025 06:46:37 -0400 Subject: [PATCH 1/3] cluster: default metadata_request_timeout to control_connection_timeout metadata_request_timeout and control_connection_timeout are going hand by hand, control_connection_timeout is client side timeout, while metadata_request_timeout is server side. In general case they always have to be eual. --- cassandra/cluster.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 4661e629eb..bc039ed0b5 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1083,10 +1083,19 @@ def default_retry_policy(self, policy): used for columns in this cluster. """ - metadata_request_timeout = datetime.timedelta(seconds=2) + metadata_request_timeout: Optional[float] = None """ - Timeout for all queries used by driver it self. - Supported only by Scylla clusters. + Specifies a server-side timeout (in seconds) for all internal driver queries, + such as schema metadata lookups and cluster topology requests. + + The timeout is enforced by appending `USING TIMEOUT ` to queries + executed by the driver. + + - A value of `0` disables explicit timeout enforcement. In this case, + the driver does not add `USING TIMEOUT`, and the timeout is determined + by the server's defaults. + - Only supported when connected to Scylla clusters. + - If not explicitly set, defaults to the value of `control_connection_timeout`. """ @property @@ -1303,8 +1312,6 @@ def __init__(self, self.no_compact = no_compact self.auth_provider = auth_provider - if metadata_request_timeout is not None: - self.metadata_request_timeout = metadata_request_timeout if load_balancing_policy is not None: if isinstance(load_balancing_policy, type): @@ -1415,6 +1422,7 @@ def __init__(self, self.cql_version = cql_version self.max_schema_agreement_wait = max_schema_agreement_wait self.control_connection_timeout = control_connection_timeout + self.metadata_request_timeout = self.control_connection_timeout if metadata_request_timeout is None else metadata_request_timeout self.idle_heartbeat_interval = idle_heartbeat_interval self.idle_heartbeat_timeout = idle_heartbeat_timeout self.schema_event_refresh_window = schema_event_refresh_window From 24de1d5ee8837303c465b7b152961f8b22e1e2a4 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Fri, 26 Sep 2025 06:50:45 -0400 Subject: [PATCH 2/3] control_connection: read actual metadata_request_timeout from Cluster.metadata_request_timeout PR #361 that have introduced `metadata_request_timeout` intentionally pulled actual `metadata_request_timeout` from `control_connection_timeout`, because both `metadata_request_timeout` and `control_connection_timeout` has to be inline. Since this PR brings `Cluster.metadata_request_timeout` back, we need to pull it from `Cluster.metadata_request_timeout` --- cassandra/cluster.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cassandra/cluster.py b/cassandra/cluster.py index bc039ed0b5..14c5cb4bd9 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -1214,7 +1214,7 @@ def __init__(self, cloud=None, scylla_cloud=None, shard_aware_options=None, - metadata_request_timeout=None, + metadata_request_timeout: Optional[float] = None, column_encryption_policy=None, application_info:Optional[ApplicationInfoBase]=None ): @@ -3622,9 +3622,11 @@ def _try_connect(self, host): if connection.features.sharding_info is not None: self._uses_peers_v2 = False - # Cassandra does not support "USING TIMEOUT" - self._metadata_request_timeout = None if connection.features.sharding_info is None \ - else datetime.timedelta(seconds=self._cluster.control_connection_timeout) + # Only ScyllaDB supports "USING TIMEOUT" + # Sharding information signals it is ScyllaDB + self._metadata_request_timeout = None if connection.features.sharding_info is None or not self._cluster.metadata_request_timeout \ + else datetime.timedelta(seconds=self._cluster.metadata_request_timeout) + self._tablets_routing_v1 = connection.features.tablets_routing_v1 # use weak references in both directions From c6e127aa60d45ab3fcd513909a3cb435193537b8 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Fri, 26 Sep 2025 12:17:20 -0400 Subject: [PATCH 3/3] tests: add more integration tests for metadata_request_timeout New behavior needs to be tested: 1. metadata_request_timeout can be borrowed from control_connection_timeout 2. metadata_request_timeout can be set to 0, to disable it. 3. metadata_request_timeout can be set to some value to override default. 4. When both metadata_request_timeout and control_connection_timeou it should be disabled --- tests/integration/standard/test_metadata.py | 40 ++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/tests/integration/standard/test_metadata.py b/tests/integration/standard/test_metadata.py index 7b675fdf7b..d3ccafd76d 100644 --- a/tests/integration/standard/test_metadata.py +++ b/tests/integration/standard/test_metadata.py @@ -20,6 +20,8 @@ import sys import time import os +from typing import Optional + from packaging.version import Version from unittest.mock import Mock, patch import pytest @@ -1323,20 +1325,39 @@ def test_token(self): cluster.shutdown() -class MetadataTimeoutTest(unittest.TestCase): +class TestMetadataTimeout: """ Test of TokenMap creation and other behavior. """ - def test_timeout(self): - cluster = TestCluster() - cluster.metadata_request_timeout = None + @pytest.mark.parametrize( + "opts, expected_query_chunk", + [ + ( + {"metadata_request_timeout": None}, + # Should be borrowed from control_connection_timeout + "USING TIMEOUT 2000ms" + ), + ( + {"metadata_request_timeout": 0.0}, + False + ), + ( + {"metadata_request_timeout": 4.0}, + "USING TIMEOUT 4000ms" + ), + ( + {"metadata_request_timeout": None, "control_connection_timeout": None}, + False, + ) + ], + ids=["default", "zero", "4s", "both none"] + ) + def test_timeout(self, opts, expected_query_chunk): + cluster = TestCluster(**opts) stmts = [] class ConnectionWrapper(cluster.connection_class): - def __init__(self, *args, **kwargs): - super(ConnectionWrapper, self).__init__(*args, **kwargs) - def send_msg(self, msg, request_id, cb, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=None): if isinstance(msg, QueryMessage): @@ -1351,7 +1372,10 @@ def send_msg(self, msg, request_id, cb, encoder=ProtocolHandler.encode_message, for stmt in stmts: if "SELECT now() FROM system.local WHERE key='local'" in stmt: continue - assert "USING TIMEOUT 2000ms" in stmt, f"query `{stmt}` does not contain `USING TIMEOUT 2000ms`" + if expected_query_chunk: + assert expected_query_chunk in stmt, f"query `{stmt}` does not contain `{expected_query_chunk}`" + else: + assert 'USING TIMEOUT' not in stmt, f"query `{stmt}` should not contain `USING TIMEOUT`" class KeyspaceAlterMetadata(unittest.TestCase):