diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 12ade2018f..83fd55ef07 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -3892,14 +3892,13 @@ def _try_connect(self, endpoint): "registering watchers and refreshing schema and topology", connection) - # Indirect way to determine if conencted to a ScyllaDB cluster, which does not support peers_v2 - # If sharding information is available, it's a ScyllaDB cluster, so do not use peers_v2 table. - if connection.features.sharding_info is not None: + # ScyllaDB does not support peers_v2. Use is_scylla (not sharding_info) + # so that clusters with shard-awareness disabled are still detected correctly. + if connection.features.is_scylla: self._uses_peers_v2 = False - # 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 \ + # Only ScyllaDB supports "USING TIMEOUT". Use is_scylla for the same reason. + self._metadata_request_timeout = None if not connection.features.is_scylla 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 diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 43399b7152..72bb5ff4da 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -2578,8 +2578,14 @@ class SchemaParserV3(SchemaParserV22): _SELECT_VIEWS = "SELECT * FROM system_schema.views" def _is_not_scylla(self): - """Check if NOT connected to ScyllaDB by checking for shard awareness.""" - return getattr(getattr(self.connection, 'features', None), 'shard_id', None) is None + """Check if NOT connected to ScyllaDB. + + Uses ProtocolFeatures.is_scylla, which is set from Scylla-specific + extension keys in the SUPPORTED response and therefore stays True + even when shard-awareness is disabled server-side. + """ + features = getattr(self.connection, 'features', None) + return not (features is not None and features.is_scylla) _table_name_col = 'table_name' diff --git a/cassandra/protocol_features.py b/cassandra/protocol_features.py index 1bad379208..c410e39d46 100644 --- a/cassandra/protocol_features.py +++ b/cassandra/protocol_features.py @@ -17,15 +17,17 @@ class ProtocolFeatures(object): sharding_info = None tablets_routing_v1 = False lwt_info = None + is_scylla = False # Keyword-only so that independently developed protocol extensions can add # new fields without conflicting over positional-argument order. - def __init__(self, *, rate_limit_error=None, shard_id=0, sharding_info=None, tablets_routing_v1=False, lwt_info=None): + def __init__(self, *, rate_limit_error=None, shard_id=0, sharding_info=None, tablets_routing_v1=False, lwt_info=None, is_scylla=False): self.rate_limit_error = rate_limit_error self.shard_id = shard_id self.sharding_info = sharding_info self.tablets_routing_v1 = tablets_routing_v1 self.lwt_info = lwt_info + self.is_scylla = is_scylla @staticmethod def parse_from_supported(supported): @@ -33,8 +35,28 @@ def parse_from_supported(supported): shard_id, sharding_info = ProtocolFeatures.parse_sharding_info(supported) tablets_routing_v1 = ProtocolFeatures.parse_tablets_info(supported) lwt_info = ProtocolFeatures.parse_lwt_info(supported) + is_scylla = ProtocolFeatures.detect_scylla(supported, sharding_info) return ProtocolFeatures(rate_limit_error=rate_limit_error, shard_id=shard_id, sharding_info=sharding_info, - tablets_routing_v1=tablets_routing_v1, lwt_info=lwt_info) + tablets_routing_v1=tablets_routing_v1, lwt_info=lwt_info, is_scylla=is_scylla) + + @staticmethod + def detect_scylla(supported, sharding_info): + """Detect ScyllaDB from SUPPORTED extensions, independent of shard awareness. + + ScyllaDB is identified by the presence of any known Scylla-specific + extension key in the SUPPORTED response. Checking only shard-related + fields (SCYLLA_NR_SHARDS, etc.) is insufficient because those are + absent when shard-awareness is disabled on the server side + (allow_shard_aware_drivers: false), which would cause the driver to + misidentify a ScyllaDB cluster as Cassandra and, for example, try + to query the peers_v2 table that ScyllaDB does not support. + """ + return ( + LWT_ADD_METADATA_MARK in supported + or RATE_LIMIT_ERROR_EXTENSION in supported + or TABLETS_ROUTING_V1 in supported + or sharding_info is not None + ) @staticmethod def maybe_parse_rate_limit_error(supported): @@ -76,6 +98,16 @@ def parse_sharding_info(options): sharding_algorithm == "biased-token-round-robin" or sharding_ignore_msb): return 0, None + if shard_id is None or shards_count is None: + # ScyllaDB gates all sharding fields together behind a single + # allow_shard_aware_drivers server-side config, so this should + # not happen in practice. If it does, don't fabricate a partial + # ShardingInfo (that would break the invariant, relied on + # elsewhere, that a non-None sharding_info implies a real, + # positive shard count) -- just disable shard-aware routing. + log.warning("Incomplete Scylla sharding info in SUPPORTED (options=%s); disabling shard-aware routing", options) + return 0, None + return int(shard_id), _ShardingInfo(shard_id, shards_count, partitioner, sharding_algorithm, sharding_ignore_msb, shard_aware_port, shard_aware_port_ssl) diff --git a/tests/unit/test_protocol_features.py b/tests/unit/test_protocol_features.py index 895c384f7e..3ab5aee820 100644 --- a/tests/unit/test_protocol_features.py +++ b/tests/unit/test_protocol_features.py @@ -22,3 +22,95 @@ class OptionsHolder(object): assert protocol_features.rate_limit_error == 123 assert protocol_features.shard_id == 0 assert protocol_features.sharding_info is None + + # ----------------------------------------------------------------- + # Tests for is_scylla detection (independent of shard awareness) + # Regression for: ScyllaDB misidentified as Cassandra when sharding + # is disabled (allow_shard_aware_drivers: false). + # ----------------------------------------------------------------- + + def test_is_scylla_detected_via_lwt(self): + """ScyllaDB is recognised from SCYLLA_LWT_ADD_METADATA_MARK alone.""" + pf = ProtocolFeatures.parse_from_supported({ + 'SCYLLA_LWT_ADD_METADATA_MARK': ['LWT_OPTIMIZATION_META_BIT_MASK=8'], + }) + assert pf.is_scylla is True + assert pf.shard_id == 0 + assert pf.sharding_info is None # no shard-aware connections expected + + def test_is_scylla_detected_via_rate_limit(self): + """ScyllaDB is recognised from SCYLLA_RATE_LIMIT_ERROR alone.""" + pf = ProtocolFeatures.parse_from_supported({ + 'SCYLLA_RATE_LIMIT_ERROR': ['ERROR_CODE=42'], + }) + assert pf.is_scylla is True + assert pf.shard_id == 0 + assert pf.sharding_info is None + + def test_is_scylla_detected_via_tablets(self): + """ScyllaDB is recognised from TABLETS_ROUTING_V1 alone.""" + pf = ProtocolFeatures.parse_from_supported({ + 'TABLETS_ROUTING_V1': [''], + }) + assert pf.is_scylla is True + assert pf.shard_id == 0 + assert pf.sharding_info is None + + def test_is_scylla_detected_via_sharding(self): + """ScyllaDB with full sharding is recognised and sharding_info is populated. + + Deliberately omits SCYLLA_LWT_ADD_METADATA_MARK so this test isolates + is_scylla detection via sharding_info, not the LWT extension key. + """ + pf = ProtocolFeatures.parse_from_supported({ + 'SCYLLA_SHARD': ['3'], + 'SCYLLA_NR_SHARDS': ['12'], + 'SCYLLA_PARTITIONER': ['org.apache.cassandra.dht.Murmur3Partitioner'], + 'SCYLLA_SHARDING_ALGORITHM': ['biased-token-round-robin'], + 'SCYLLA_SHARDING_IGNORE_MSB': ['12'], + }) + assert pf.is_scylla is True + assert pf.shard_id == 3 + assert pf.sharding_info is not None + assert pf.sharding_info.shards_count == 12 + + def test_cassandra_is_not_scylla(self): + """Pure Cassandra SUPPORTED response must not set is_scylla.""" + pf = ProtocolFeatures.parse_from_supported({ + 'CQL_VERSION': ['3.0.0'], + 'COMPRESSION': ['lz4', 'snappy'], + }) + assert pf.is_scylla is False + assert pf.sharding_info is None + + def test_scylla_without_sharding_no_crash(self): + """ + Regression test for F1: SCYLLA_PARTITIONER present but SCYLLA_SHARD / + SCYLLA_NR_SHARDS absent must not raise TypeError. ScyllaDB gates all + sharding fields together server-side, so this is not expected in + practice, but if it happens the driver must not fabricate a partial + ShardingInfo -- it should just disable shard-aware routing while + still recognising the server as ScyllaDB via the LWT extension key. + """ + pf = ProtocolFeatures.parse_from_supported({ + 'SCYLLA_PARTITIONER': ['org.apache.cassandra.dht.Murmur3Partitioner'], + 'SCYLLA_LWT_ADD_METADATA_MARK': ['LWT_OPTIMIZATION_META_BIT_MASK=8'], + }) + assert pf.is_scylla is True + # Incomplete sharding fields -- no ShardingInfo is fabricated. + assert pf.shard_id == 0 + assert pf.sharding_info is None + + def test_scylla_sharding_algorithm_only_no_crash(self): + """ + Regression: SCYLLA_SHARDING_ALGORITHM present without SCYLLA_SHARD / + SCYLLA_NR_SHARDS must not raise TypeError, and must not fabricate a + partial ShardingInfo. + """ + pf = ProtocolFeatures.parse_from_supported({ + 'SCYLLA_SHARDING_ALGORITHM': ['biased-token-round-robin'], + 'SCYLLA_RATE_LIMIT_ERROR': ['ERROR_CODE=42'], + }) + assert pf.is_scylla is True + assert pf.shard_id == 0 + assert pf.sharding_info is None