Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions cassandra/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions cassandra/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
36 changes: 34 additions & 2 deletions cassandra/protocol_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,46 @@ 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):
rate_limit_error = ProtocolFeatures.maybe_parse_rate_limit_error(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):
Expand Down Expand Up @@ -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)

Expand Down
92 changes: 92 additions & 0 deletions tests/unit/test_protocol_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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