Skip to content

Commit 3309e8f

Browse files
committed
session: refresh stale pool creation by endpoint
1 parent dd48a4e commit 3309e8f

3 files changed

Lines changed: 167 additions & 51 deletions

File tree

cassandra/cluster.py

Lines changed: 60 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4073,46 +4073,46 @@ def callback(pool, errors):
40734073
else:
40744074
with host.lock:
40754075
endpoint_changed = not self._endpoints_match(host.endpoint, creation_endpoint)
4076-
if endpoint_changed:
4077-
log.debug(
4078-
"Discarding stale connection pool for host %s; endpoint changed from %s",
4079-
host, creation_endpoint)
4080-
self._invalidate_pool_creation(host, expected_endpoint=creation_endpoint)
4081-
discard_pool = True
4082-
else:
4083-
# Rebuild by identity so endpoint hash changes do not
4084-
# leave stale pool entries behind.
4085-
retained_pools = {}
4086-
for pool_host, host_pool in self._pools.items():
4087-
if pool_host is host:
4088-
previous_pools.append(host_pool)
4089-
else:
4090-
retained_pools[pool_host] = host_pool
4091-
4092-
# Keep the current metadata host keyed by identity.
4093-
metadata_host = host
4094-
if isinstance(self.cluster.metadata, Metadata):
4095-
metadata_host = self.cluster.metadata.get_host_by_host_id(host.host_id)
4096-
4097-
target_host = metadata_host if metadata_host is not None else host
4098-
target_host_matches = False
4099-
for pool_host in tuple(retained_pools):
4100-
if pool_host is target_host:
4101-
target_host_matches = True
4102-
elif pool_host == target_host:
4103-
previous_pools.append(retained_pools.pop(pool_host))
4104-
4105-
if target_host_matches:
4106-
reuse_existing_pool = True
4076+
if endpoint_changed:
4077+
log.debug(
4078+
"Discarding stale connection pool for host %s; endpoint changed from %s",
4079+
host, creation_endpoint)
4080+
self._invalidate_pool_creation(host, expected_endpoint=creation_endpoint)
4081+
discard_pool = True
41074082
else:
4108-
source_host = new_pool.host
4109-
if (source_host is not target_host and
4110-
target_host.sharding_info is None):
4111-
target_host.sharding_info = source_host.sharding_info
4112-
new_pool.host = target_host
4113-
retained_pools[target_host] = new_pool
4114-
self._pools = retained_pools
4115-
self._clear_pool_creation(host, creation_epoch)
4083+
# Rebuild by identity so endpoint hash changes do not
4084+
# leave stale pool entries behind.
4085+
retained_pools = {}
4086+
for pool_host, host_pool in self._pools.items():
4087+
if pool_host is host:
4088+
previous_pools.append(host_pool)
4089+
else:
4090+
retained_pools[pool_host] = host_pool
4091+
4092+
# Keep the current metadata host keyed by identity.
4093+
metadata_host = host
4094+
if isinstance(self.cluster.metadata, Metadata):
4095+
metadata_host = self.cluster.metadata.get_host_by_host_id(host.host_id)
4096+
4097+
target_host = metadata_host if metadata_host is not None else host
4098+
target_host_matches = False
4099+
for pool_host in tuple(retained_pools):
4100+
if pool_host is target_host:
4101+
target_host_matches = True
4102+
elif pool_host == target_host:
4103+
previous_pools.append(retained_pools.pop(pool_host))
4104+
4105+
if target_host_matches:
4106+
reuse_existing_pool = True
4107+
else:
4108+
source_host = new_pool.host
4109+
if (source_host is not target_host and
4110+
target_host.sharding_info is None):
4111+
target_host.sharding_info = source_host.sharding_info
4112+
new_pool.host = target_host
4113+
retained_pools[target_host] = new_pool
4114+
self._pools = retained_pools
4115+
self._clear_pool_creation(host, creation_epoch)
41164116

41174117
if reuse_existing_pool:
41184118
log.debug("Reusing existing connection pool for host %s", host)
@@ -4141,7 +4141,13 @@ def callback(pool, errors):
41414141
with self._lock:
41424142
state = self._get_pool_creation_state(host)
41434143
if state.creation_epoch is not None:
4144-
return state.future
4144+
with host.lock:
4145+
endpoint_changed = not self._endpoints_match(
4146+
host.endpoint, state.endpoint)
4147+
if not endpoint_changed:
4148+
return state.future
4149+
self._invalidate_pool_creation(
4150+
host, expected_endpoint=state.endpoint)
41454151

41464152
creation_epoch = state.advance()
41474153
state.creation_epoch = creation_epoch
@@ -4156,7 +4162,8 @@ def callback(pool, errors):
41564162
state.future = future
41574163
return future
41584164

4159-
def remove_pool(self, host, expected_host=None, expected_endpoint=None):
4165+
def remove_pool(self, host, expected_host=None, expected_endpoint=None,
4166+
expected_pool=None):
41604167
removed_pools = []
41614168
cleanup_context = _POOL_CLEANUP_EPOCH.get()
41624169
with self._lock:
@@ -4187,11 +4194,12 @@ def remove_pool(self, host, expected_host=None, expected_endpoint=None):
41874194
if expected_endpoint is not None:
41884195
remove_all = self._endpoints_match(host.endpoint, expected_endpoint)
41894196

4190-
if remove_all:
4191-
self._invalidate_pool_creation(host)
4192-
else:
4193-
self._invalidate_pool_creation(
4194-
host, expected_endpoint=expected_endpoint)
4197+
if expected_pool is None:
4198+
if remove_all:
4199+
self._invalidate_pool_creation(host)
4200+
else:
4201+
self._invalidate_pool_creation(
4202+
host, expected_endpoint=expected_endpoint)
41954203

41964204
retained_pools = {}
41974205
for pool_host, host_pool in self._pools.items():
@@ -4201,7 +4209,8 @@ def remove_pool(self, host, expected_host=None, expected_endpoint=None):
42014209

42024210
matches = self._pool_matches_expected(
42034211
host_pool, expected_host=expected_host,
4204-
expected_endpoint=None if remove_all else expected_endpoint)
4212+
expected_endpoint=None if remove_all else expected_endpoint,
4213+
expected_pool=expected_pool)
42054214
if matches:
42064215
removed_pools.append(host_pool)
42074216
else:
@@ -4230,7 +4239,10 @@ def _shutdown_removed_pools(pools):
42304239
for pool in pools:
42314240
pool.shutdown()
42324241

4233-
def _pool_matches_expected(self, pool, expected_host=None, expected_endpoint=None):
4242+
def _pool_matches_expected(self, pool, expected_host=None,
4243+
expected_endpoint=None, expected_pool=None):
4244+
if expected_pool is not None and pool is not expected_pool:
4245+
return False
42344246
if expected_host is not None and pool.host is not expected_host:
42354247
return False
42364248
if expected_endpoint is not None:

cassandra/pool.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,8 @@ def return_connection(self, connection, stream_was_orphaned=False):
597597
# Drop only this stale pool; endpoint reuse may already belong to
598598
# a replacement host instance.
599599
future = self._session.remove_pool(
600-
self.host, expected_host=self.host, expected_endpoint=self.endpoint)
600+
self.host, expected_host=self.host,
601+
expected_endpoint=self.endpoint, expected_pool=self)
601602
if future:
602603
future.add_done_callback(lambda f: self._session.update_created_pools())
603604
with self._lock:
@@ -645,7 +646,8 @@ def on_orphaned_stream_released(self):
645646

646647
def _remove_stale_pool(self, expected_endpoint):
647648
future = self._session.remove_pool(
648-
self.host, expected_host=self.host, expected_endpoint=expected_endpoint)
649+
self.host, expected_host=self.host,
650+
expected_endpoint=expected_endpoint, expected_pool=self)
649651
if future:
650652
future.add_done_callback(lambda f: self._session.update_created_pools())
651653

tests/unit/test_cluster.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from cassandra.cluster import _Scheduler, Session, Cluster, default_lbp_factory, \
2727
ExecutionProfile, _ConfigMode, EXEC_PROFILE_DEFAULT
2828
from cassandra.connection import ClientRoutesEndPoint, ConnectionException, DefaultEndPoint, SniEndPoint
29-
from cassandra.pool import Host, _HostReconnectionHandler
29+
from cassandra.pool import Host, HostConnection, _HostReconnectionHandler
3030
from cassandra.policies import HostDistance, RetryPolicy, RoundRobinPolicy, DowngradingConsistencyRetryPolicy, SimpleConvictionPolicy
3131
from cassandra.query import SimpleStatement, named_tuple_factory, tuple_factory
3232
from tests.unit.utils import mock_session_pools
@@ -323,6 +323,28 @@ def test_schedule_unique_keeps_client_route_events_for_distinct_ports(self, *_):
323323

324324
class SessionPoolRaceTest(unittest.TestCase):
325325

326+
class _EndpointSwapIfPoolUnpublishedOnFirstExitLock(object):
327+
328+
def __init__(self, host, new_endpoint, pool_is_published):
329+
self._lock = RLock()
330+
self._host = host
331+
self._new_endpoint = new_endpoint
332+
self._pool_is_published = pool_is_published
333+
self._exits = 0
334+
self.pool_was_published_on_first_exit = None
335+
336+
def __enter__(self):
337+
self._lock.acquire()
338+
return self
339+
340+
def __exit__(self, exc_type, exc_value, traceback):
341+
self._lock.release()
342+
self._exits += 1
343+
if self._exits == 1:
344+
self.pool_was_published_on_first_exit = self._pool_is_published()
345+
if not self.pool_was_published_on_first_exit:
346+
self._host.endpoint = self._new_endpoint
347+
326348
class _DuplicatePoolEntries(object):
327349

328350
def __init__(self, entries):
@@ -522,6 +544,64 @@ def make_pool(host, distance, pool_session, endpoint=None):
522544
assert session._pools == {}
523545
created_pools[0].shutdown.assert_called_once_with()
524546

547+
def test_add_or_renew_pool_invalidates_creation_after_endpoint_change(self):
548+
host = self._make_host("127.0.0.1")
549+
old_endpoint = host.endpoint
550+
new_endpoint = DefaultEndPoint("127.0.0.2")
551+
_, session, executor = self._make_cluster_and_session([host])
552+
created_pools = []
553+
554+
def make_pool(host, distance, pool_session, endpoint=None):
555+
pool = self._make_pool(host, distance, pool_session, endpoint)
556+
created_pools.append(pool)
557+
return pool
558+
559+
with patch("cassandra.cluster.HostConnection", side_effect=make_pool):
560+
old_future = session.add_or_renew_pool(
561+
host, is_host_addition=False)
562+
host.endpoint = new_endpoint
563+
564+
new_future = session.add_or_renew_pool(
565+
host, is_host_addition=False)
566+
567+
assert new_future is not old_future
568+
assert len(executor.submissions) == 2
569+
570+
executor.run_next()
571+
executor.run_next()
572+
573+
assert old_future.result() is False
574+
assert new_future.result() is True
575+
assert created_pools[0].endpoint == old_endpoint
576+
assert created_pools[1].endpoint == new_endpoint
577+
created_pools[0].shutdown.assert_called_once_with()
578+
created_pools[1].shutdown.assert_not_called()
579+
assert session._pools[host] is created_pools[1]
580+
581+
def test_pool_creation_publishes_before_endpoint_lock_is_released(self):
582+
host = self._make_host("127.0.0.1")
583+
new_endpoint = DefaultEndPoint("127.0.0.2")
584+
cluster, session, executor = self._make_cluster_and_session([host])
585+
created_pools = []
586+
587+
def make_pool(host, distance, pool_session, endpoint=None):
588+
pool = self._make_pool(host, distance, pool_session, endpoint)
589+
created_pools.append(pool)
590+
return pool
591+
592+
with patch("cassandra.cluster.HostConnection", side_effect=make_pool):
593+
future = session.add_or_renew_pool(
594+
host, is_host_addition=False)
595+
host.lock = self._EndpointSwapIfPoolUnpublishedOnFirstExitLock(
596+
host, new_endpoint, lambda: bool(session._pools))
597+
598+
executor.run_next()
599+
600+
assert host.lock.pool_was_published_on_first_exit is True
601+
assert future.result() is True
602+
assert session._pools[host] is created_pools[0]
603+
created_pools[0].shutdown.assert_not_called()
604+
525605
def test_update_created_pools_removes_stale_pool_for_down_host_after_endpoint_change(self):
526606
host = self._make_host("127.0.0.1")
527607
host.set_down()
@@ -701,6 +781,28 @@ def make_pool(host, distance, pool_session, endpoint=None):
701781
assert created_pools[0].endpoint == host.endpoint
702782
created_pools[0].shutdown.assert_not_called()
703783

784+
def test_stale_host_connection_cleanup_after_endpoint_flip_back_preserves_current_pool(self):
785+
host = self._make_host("127.0.0.1")
786+
old_endpoint = host.endpoint
787+
replacement_endpoint = DefaultEndPoint("127.0.0.2")
788+
cluster, session, executor = self._make_cluster_and_session([host])
789+
790+
host.endpoint = replacement_endpoint
791+
host.endpoint = old_endpoint
792+
current_pool = self._make_pool(
793+
host, HostDistance.LOCAL, session, endpoint=old_endpoint)
794+
session._pools[host] = current_pool
795+
796+
stale_pool = HostConnection.__new__(HostConnection)
797+
stale_pool.host = host
798+
stale_pool.endpoint = old_endpoint
799+
stale_pool._session = session
800+
801+
stale_pool._remove_stale_pool(old_endpoint)
802+
803+
assert session._get_pool_by_host_identity(host) is current_pool
804+
current_pool.shutdown.assert_not_called()
805+
704806
def test_add_or_renew_pool_tags_pool_with_creation_endpoint(self):
705807
host = self._make_host("127.0.0.1")
706808
old_endpoint = host.endpoint

0 commit comments

Comments
 (0)