Skip to content

Commit bcc2d3d

Browse files
committed
pool: honor explicit SSL config for shard-aware ports
Problem: shard-aware endpoint selection treated legacy ssl_options as SSL-enabled only when the dict was truthy. An explicit empty ssl_options={} was therefore handled like plaintext and could select the non-SSL shard-aware port, diverging from the cluster-level SSL-enabled check. Fix: treat SSL as enabled when ssl_context is set or ssl_options is not None. SSL-enabled configurations now use the SSL shard-aware port when advertised and otherwise fall back to regular non-shard-aware connections instead of the plaintext shard-aware port. Unit coverage now includes ssl_context, non-empty ssl_options, and empty ssl_options.
1 parent e605de2 commit bcc2d3d

2 files changed

Lines changed: 62 additions & 14 deletions

File tree

cassandra/pool.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -675,15 +675,26 @@ def disable_advanced_shard_aware(self, secs):
675675
self.advanced_shardaware_block_until = max(time.time() + secs, self.advanced_shardaware_block_until)
676676

677677
def _get_shard_aware_endpoint(self):
678+
"""
679+
Return an endpoint for the advertised shard-aware port, if usable.
680+
681+
Plaintext clusters use shard_aware_port. SSL-enabled clusters use only
682+
shard_aware_port_ssl; if it is absent, return None so the pool opens a
683+
regular SSL connection instead of falling back to the plaintext port.
684+
Explicit ssl_options={}, like ssl_context, marks the cluster SSL-enabled.
685+
"""
678686
if (self.advanced_shardaware_block_until and self.advanced_shardaware_block_until > time.time()) or \
679687
self._session.cluster.shard_aware_options.disable_shardaware_port:
680688
return None
681689

690+
cluster = self._session.cluster
691+
ssl_enabled = cluster.ssl_context is not None or cluster.ssl_options is not None
692+
682693
endpoint = None
683-
if self._session.cluster.ssl_options and self.host.sharding_info.shard_aware_port_ssl:
694+
if ssl_enabled and self.host.sharding_info.shard_aware_port_ssl:
684695
endpoint = copy.copy(self.host.endpoint)
685696
endpoint._port = self.host.sharding_info.shard_aware_port_ssl
686-
elif self.host.sharding_info.shard_aware_port:
697+
elif not ssl_enabled and self.host.sharding_info.shard_aware_port:
687698
endpoint = copy.copy(self.host.endpoint)
688699
endpoint._port = self.host.sharding_info.shard_aware_port
689700

@@ -918,5 +929,3 @@ def open_count(self):
918929
@property
919930
def _excess_connection_limit(self):
920931
return self.host.sharding_info.shards_count * self.max_excess_connections_per_shard_multiplier
921-
922-

tests/unit/test_shard_aware.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,19 @@ class MockSession(MagicMock):
3232
is_shutdown = False
3333
keyspace = "ks1"
3434

35-
def __init__(self, is_ssl=False, *args, **kwargs):
35+
def __init__(self, ssl_options=None, ssl_context=None, sharding_info=None,
36+
*args, **kwargs):
3637
super(MockSession, self).__init__(*args, **kwargs)
3738
self.cluster = MagicMock()
38-
if is_ssl:
39-
self.cluster.ssl_options = {'some_ssl_options': True}
40-
else:
41-
self.cluster.ssl_options = None
39+
self.cluster.ssl_options = ssl_options
40+
self.cluster.ssl_context = ssl_context
4241
self.cluster.shard_aware_options = ShardAwareOptions()
4342
self.cluster.executor = ThreadPoolExecutor(max_workers=2)
4443
self.cluster.signal_connection_failure = lambda *args, **kwargs: False
4544
self.cluster.connection_factory = self.mock_connection_factory
4645
self.connection_counter = 0
4746
self.futures = []
47+
self.sharding_info = sharding_info
4848

4949
def submit(self, fn, *args, **kwargs):
5050
logging.info("Scheduling %s with args: %s, kwargs: %s", fn, args, kwargs)
@@ -60,8 +60,13 @@ def mock_connection_factory(self, *args, **kwargs):
6060
connection.is_closed = False
6161
connection.orphaned_threshold_reached = False
6262
connection.endpoint = args[0]
63-
sharding_info = ShardingInfo(shard_id=1, shards_count=4, partitioner="", sharding_algorithm="", sharding_ignore_msb=0, shard_aware_port=19042, shard_aware_port_ssl=19045)
64-
connection.features = ProtocolFeatures(shard_id=kwargs.get('shard_id', self.connection_counter), sharding_info=sharding_info)
63+
sharding_info = self.sharding_info or ShardingInfo(
64+
shard_id=1, shards_count=4, partitioner="",
65+
sharding_algorithm="", sharding_ignore_msb=0,
66+
shard_aware_port=19042, shard_aware_port_ssl=19045)
67+
connection.features = ProtocolFeatures(
68+
shard_id=kwargs.get('shard_id', self.connection_counter),
69+
sharding_info=sharding_info)
6570
self.connection_counter += 1
6671

6772
return connection
@@ -98,8 +103,12 @@ def test_advanced_shard_aware_port(self):
98103
host = MagicMock()
99104
host.endpoint = DefaultEndPoint("1.2.3.4")
100105

101-
for port, is_ssl in [(19042, False), (19045, True)]:
102-
session = MockSession(is_ssl=is_ssl)
106+
for port, ssl_options, ssl_context in [
107+
(19042, None, None),
108+
(19045, {'some_ssl_options': True}, None),
109+
(19045, {}, None),
110+
(19045, None, object())]:
111+
session = MockSession(ssl_options=ssl_options, ssl_context=ssl_context)
103112
pool = HostConnection(host=host, host_distance=HostDistance.REMOTE, session=session)
104113
try:
105114
for f in session.futures:
@@ -114,6 +123,36 @@ def test_advanced_shard_aware_port(self):
114123
finally:
115124
session.cluster.executor.shutdown(wait=True)
116125

126+
def test_ssl_advanced_shard_aware_port_requires_ssl_port(self):
127+
"""
128+
Test that SSL connections do not fall back to the plaintext
129+
shard-aware port when the SSL shard-aware port is unavailable.
130+
"""
131+
host = MagicMock()
132+
host.endpoint = DefaultEndPoint("1.2.3.4")
133+
sharding_info = ShardingInfo(
134+
shard_id=1, shards_count=4, partitioner="", sharding_algorithm="",
135+
sharding_ignore_msb=0, shard_aware_port=19042,
136+
shard_aware_port_ssl=None)
137+
for label, ssl_options, ssl_context in [
138+
('ssl_options', {'some_ssl_options': True}, None),
139+
('empty_ssl_options', {}, None),
140+
('ssl_context', None, object())]:
141+
with self.subTest(label=label):
142+
session = MockSession(
143+
ssl_options=ssl_options,
144+
ssl_context=ssl_context,
145+
sharding_info=sharding_info)
146+
pool = HostConnection(host=host, host_distance=HostDistance.REMOTE, session=session)
147+
148+
try:
149+
for f in session.futures:
150+
f.result()
151+
152+
assert pool._get_shard_aware_endpoint() is None
153+
finally:
154+
session.cluster.executor.shutdown(wait=True)
155+
117156
def test_advanced_shard_aware_cooldown(self):
118157
"""
119158
`disable_advanced_shard_aware` must suppress the shard-aware endpoint for
@@ -123,7 +162,7 @@ def test_advanced_shard_aware_cooldown(self):
123162
"""
124163
host = MagicMock()
125164
host.endpoint = DefaultEndPoint("1.2.3.4")
126-
session = MockSession(is_ssl=False)
165+
session = MockSession()
127166

128167
pool = HostConnection(host=host, host_distance=HostDistance.REMOTE, session=session)
129168
for f in session.futures:

0 commit comments

Comments
 (0)