Skip to content

Commit bea45ac

Browse files
committed
fix: detect ScyllaDB via SUPPORTED protocol extensions, not shard count
The driver previously identified ScyllaDB by the presence of shard-related fields (SCYLLA_NR_SHARDS, SCYLLA_SHARD, etc.) in the SUPPORTED response. When shard-awareness is disabled on the server side (allow_shard_aware_drivers: false) those fields are absent, causing the driver to misidentify a ScyllaDB cluster as Cassandra. The immediate consequence is that the driver enables peers_v2 (which ScyllaDB does not support) and fails to connect -- the same regression described in scylladb/gocql#902. Fix (mirrors scylladb/gocql#903): * Add ProtocolFeatures.is_scylla: set to True whenever ANY known Scylla-specific extension key (SCYLLA_LWT_ADD_METADATA_MARK, SCYLLA_RATE_LIMIT_ERROR, TABLETS_ROUTING_V1) is present in the SUPPORTED response, or whenever sharding_info is populated. This flag stays True even when sharding is disabled. * ProtocolFeatures.sharding_info remains None when sharding is disabled, so shard-aware connection pooling (pool.py) is correctly left inactive. * cluster.py: use is_scylla (not sharding_info is not None) to gate the peers_v2 disable and the USING TIMEOUT metadata request timeout. * metadata.py _is_not_scylla(): use is_scylla instead of the previous check (shard_id is None), which was always False after the OPTIONS exchange and therefore silently broke Cassandra trigger-metadata queries. * protocol_features.py parse_sharding_info(): fix a latent crash -- if SCYLLA_PARTITIONER or SCYLLA_SHARDING_ALGORITHM were present without SCYLLA_SHARD, int(None) would raise TypeError. Default shard_id to 0. Fixes: scylladb/python-driver#<TBD> See also: scylladb/gocql#902, scylladb/gocql#903 Fixes: #909
1 parent 763af09 commit bea45ac

6 files changed

Lines changed: 131 additions & 16 deletions

File tree

cassandra/c_shard_info.pyx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ cdef class ShardingInfo():
2929

3030
def __init__(self, shard_id, shards_count, partitioner, sharding_algorithm, sharding_ignore_msb, shard_aware_port,
3131
shard_aware_port_ssl):
32-
self.shards_count = int(shards_count)
32+
self.shards_count = int(shards_count) if shards_count else 0
3333
self.partitioner = partitioner
3434
self.sharding_algorithm = sharding_algorithm
35-
self.sharding_ignore_msb = int(sharding_ignore_msb)
35+
self.sharding_ignore_msb = int(sharding_ignore_msb) if sharding_ignore_msb else 0
3636
self.shard_aware_port = int(shard_aware_port) if shard_aware_port else 0
3737
self.shard_aware_port_ssl = int(shard_aware_port_ssl) if shard_aware_port_ssl else 0
3838

cassandra/cluster.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3888,14 +3888,13 @@ def _try_connect(self, endpoint):
38883888
"registering watchers and refreshing schema and topology",
38893889
connection)
38903890

3891-
# Indirect way to determine if conencted to a ScyllaDB cluster, which does not support peers_v2
3892-
# If sharding information is available, it's a ScyllaDB cluster, so do not use peers_v2 table.
3893-
if connection.features.sharding_info is not None:
3891+
# ScyllaDB does not support peers_v2. Use is_scylla (not sharding_info)
3892+
# so that clusters with shard-awareness disabled are still detected correctly.
3893+
if connection.features.is_scylla:
38943894
self._uses_peers_v2 = False
38953895

3896-
# Only ScyllaDB supports "USING TIMEOUT"
3897-
# Sharding information signals it is ScyllaDB
3898-
self._metadata_request_timeout = None if connection.features.sharding_info is None or not self._cluster.metadata_request_timeout \
3896+
# Only ScyllaDB supports "USING TIMEOUT". Use is_scylla for the same reason.
3897+
self._metadata_request_timeout = None if not connection.features.is_scylla or not self._cluster.metadata_request_timeout \
38993898
else datetime.timedelta(seconds=self._cluster.metadata_request_timeout)
39003899

39013900
self._tablets_routing_v1 = connection.features.tablets_routing_v1

cassandra/metadata.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,8 +2578,14 @@ class SchemaParserV3(SchemaParserV22):
25782578
_SELECT_VIEWS = "SELECT * FROM system_schema.views"
25792579

25802580
def _is_not_scylla(self):
2581-
"""Check if NOT connected to ScyllaDB by checking for shard awareness."""
2582-
return getattr(getattr(self.connection, 'features', None), 'shard_id', None) is None
2581+
"""Check if NOT connected to ScyllaDB.
2582+
2583+
Uses the is_scylla flag from ProtocolFeatures, which is set from the
2584+
presence of Scylla-specific extension keys in the SUPPORTED response
2585+
(e.g. SCYLLA_LWT_ADD_METADATA_MARK, SCYLLA_RATE_LIMIT_ERROR) and
2586+
therefore remains True even when shard-awareness is disabled.
2587+
"""
2588+
return not getattr(getattr(self.connection, 'features', None), 'is_scylla', False)
25832589

25842590
_table_name_col = 'table_name'
25852591

cassandra/protocol_features.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,43 @@ class ProtocolFeatures(object):
1717
sharding_info = None
1818
tablets_routing_v1 = False
1919
lwt_info = None
20+
is_scylla = False
2021

21-
def __init__(self, rate_limit_error=None, shard_id=0, sharding_info=None, tablets_routing_v1=False, lwt_info=None):
22+
def __init__(self, rate_limit_error=None, shard_id=0, sharding_info=None, tablets_routing_v1=False, lwt_info=None, is_scylla=False):
2223
self.rate_limit_error = rate_limit_error
2324
self.shard_id = shard_id
2425
self.sharding_info = sharding_info
2526
self.tablets_routing_v1 = tablets_routing_v1
2627
self.lwt_info = lwt_info
28+
self.is_scylla = is_scylla
2729

2830
@staticmethod
2931
def parse_from_supported(supported):
3032
rate_limit_error = ProtocolFeatures.maybe_parse_rate_limit_error(supported)
3133
shard_id, sharding_info = ProtocolFeatures.parse_sharding_info(supported)
3234
tablets_routing_v1 = ProtocolFeatures.parse_tablets_info(supported)
3335
lwt_info = ProtocolFeatures.parse_lwt_info(supported)
34-
return ProtocolFeatures(rate_limit_error, shard_id, sharding_info, tablets_routing_v1, lwt_info)
36+
is_scylla = ProtocolFeatures.detect_scylla(supported, sharding_info)
37+
return ProtocolFeatures(rate_limit_error, shard_id, sharding_info, tablets_routing_v1, lwt_info, is_scylla)
38+
39+
@staticmethod
40+
def detect_scylla(supported, sharding_info):
41+
"""Detect ScyllaDB from SUPPORTED extensions, independent of shard awareness.
42+
43+
ScyllaDB is identified by the presence of any known Scylla-specific
44+
extension key in the SUPPORTED response. Checking only shard-related
45+
fields (SCYLLA_NR_SHARDS, etc.) is insufficient because those are
46+
absent when shard-awareness is disabled on the server side
47+
(allow_shard_aware_drivers: false), which would cause the driver to
48+
misidentify a ScyllaDB cluster as Cassandra and, for example, try
49+
to query the peers_v2 table that ScyllaDB does not support.
50+
"""
51+
return (
52+
LWT_ADD_METADATA_MARK in supported
53+
or RATE_LIMIT_ERROR_EXTENSION in supported
54+
or TABLETS_ROUTING_V1 in supported
55+
or sharding_info is not None
56+
)
3557

3658
@staticmethod
3759
def maybe_parse_rate_limit_error(supported):
@@ -73,8 +95,12 @@ def parse_sharding_info(options):
7395
sharding_algorithm == "biased-token-round-robin" or sharding_ignore_msb):
7496
return 0, None
7597

76-
return int(shard_id), _ShardingInfo(shard_id, shards_count, partitioner, sharding_algorithm, sharding_ignore_msb,
77-
shard_aware_port, shard_aware_port_ssl)
98+
# SCYLLA_SHARD may be absent even when other shard fields are present
99+
# (e.g. the connection landed on shard 0 and the server omits the field).
100+
# Default to 0 to avoid int(None) crash.
101+
resolved_shard_id = int(shard_id) if shard_id is not None else 0
102+
return resolved_shard_id, _ShardingInfo(shard_id, shards_count, partitioner, sharding_algorithm, sharding_ignore_msb,
103+
shard_aware_port, shard_aware_port_ssl)
78104

79105

80106
@staticmethod

cassandra/shard_info.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
class _ShardingInfo(object):
2222

2323
def __init__(self, shard_id, shards_count, partitioner, sharding_algorithm, sharding_ignore_msb, shard_aware_port, shard_aware_port_ssl):
24-
self.shards_count = int(shards_count)
24+
self.shards_count = int(shards_count) if shards_count else 0
2525
self.partitioner = partitioner
2626
self.sharding_algorithm = sharding_algorithm
27-
self.sharding_ignore_msb = int(sharding_ignore_msb)
27+
self.sharding_ignore_msb = int(sharding_ignore_msb) if sharding_ignore_msb else 0
2828
self.shard_aware_port = int(shard_aware_port) if shard_aware_port else None
2929
self.shard_aware_port_ssl = int(shard_aware_port_ssl) if shard_aware_port_ssl else None
3030

tests/unit/test_protocol_features.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,87 @@ class OptionsHolder(object):
2222
assert protocol_features.rate_limit_error == 123
2323
assert protocol_features.shard_id == 0
2424
assert protocol_features.sharding_info is None
25+
26+
# -----------------------------------------------------------------
27+
# Tests for is_scylla detection (independent of shard awareness)
28+
# Regression for: ScyllaDB misidentified as Cassandra when sharding
29+
# is disabled (allow_shard_aware_drivers: false).
30+
# -----------------------------------------------------------------
31+
32+
def test_is_scylla_detected_via_lwt(self):
33+
"""ScyllaDB is recognised from SCYLLA_LWT_ADD_METADATA_MARK alone."""
34+
pf = ProtocolFeatures.parse_from_supported({
35+
'SCYLLA_LWT_ADD_METADATA_MARK': ['LWT_OPTIMIZATION_META_BIT_MASK=8'],
36+
})
37+
assert pf.is_scylla is True
38+
assert pf.sharding_info is None # no shard-aware connections expected
39+
40+
def test_is_scylla_detected_via_rate_limit(self):
41+
"""ScyllaDB is recognised from SCYLLA_RATE_LIMIT_ERROR alone."""
42+
pf = ProtocolFeatures.parse_from_supported({
43+
'SCYLLA_RATE_LIMIT_ERROR': ['ERROR_CODE=42'],
44+
})
45+
assert pf.is_scylla is True
46+
assert pf.sharding_info is None
47+
48+
def test_is_scylla_detected_via_tablets(self):
49+
"""ScyllaDB is recognised from TABLETS_ROUTING_V1 alone."""
50+
pf = ProtocolFeatures.parse_from_supported({
51+
'TABLETS_ROUTING_V1': [''],
52+
})
53+
assert pf.is_scylla is True
54+
assert pf.sharding_info is None
55+
56+
def test_is_scylla_detected_via_sharding(self):
57+
"""ScyllaDB with full sharding is recognised and sharding_info is populated."""
58+
pf = ProtocolFeatures.parse_from_supported({
59+
'SCYLLA_SHARD': ['3'],
60+
'SCYLLA_NR_SHARDS': ['12'],
61+
'SCYLLA_PARTITIONER': ['org.apache.cassandra.dht.Murmur3Partitioner'],
62+
'SCYLLA_SHARDING_ALGORITHM': ['biased-token-round-robin'],
63+
'SCYLLA_SHARDING_IGNORE_MSB': ['12'],
64+
'SCYLLA_LWT_ADD_METADATA_MARK': ['LWT_OPTIMIZATION_META_BIT_MASK=8'],
65+
})
66+
assert pf.is_scylla is True
67+
assert pf.sharding_info is not None
68+
assert pf.sharding_info.shards_count == 12
69+
70+
def test_cassandra_is_not_scylla(self):
71+
"""Pure Cassandra SUPPORTED response must not set is_scylla."""
72+
pf = ProtocolFeatures.parse_from_supported({
73+
'CQL_VERSION': ['3.0.0'],
74+
'COMPRESSION': ['lz4', 'snappy'],
75+
})
76+
assert pf.is_scylla is False
77+
assert pf.sharding_info is None
78+
79+
def test_scylla_without_sharding_no_crash(self):
80+
"""
81+
Regression test for F1: SCYLLA_PARTITIONER present but SCYLLA_NR_SHARDS
82+
and SCYLLA_SHARDING_IGNORE_MSB absent must not raise TypeError.
83+
Mirrors the scenario where only some shard fields are advertised.
84+
"""
85+
# Should not raise even though shards_count / sharding_ignore_msb are None.
86+
pf = ProtocolFeatures.parse_from_supported({
87+
'SCYLLA_PARTITIONER': ['org.apache.cassandra.dht.Murmur3Partitioner'],
88+
'SCYLLA_LWT_ADD_METADATA_MARK': ['LWT_OPTIMIZATION_META_BIT_MASK=8'],
89+
})
90+
assert pf.is_scylla is True
91+
# SCYLLA_PARTITIONER passes the sharding guard, so sharding_info is populated
92+
# with zero defaults rather than crashing.
93+
assert pf.sharding_info is not None
94+
assert pf.sharding_info.shards_count == 0
95+
assert pf.sharding_info.sharding_ignore_msb == 0
96+
97+
def test_scylla_sharding_algorithm_only_no_crash(self):
98+
"""
99+
Regression: SCYLLA_SHARDING_ALGORITHM present without SCYLLA_NR_SHARDS
100+
must not raise TypeError.
101+
"""
102+
pf = ProtocolFeatures.parse_from_supported({
103+
'SCYLLA_SHARDING_ALGORITHM': ['biased-token-round-robin'],
104+
'SCYLLA_RATE_LIMIT_ERROR': ['ERROR_CODE=42'],
105+
})
106+
assert pf.is_scylla is True
107+
assert pf.sharding_info is not None
108+
assert pf.sharding_info.shards_count == 0

0 commit comments

Comments
 (0)