Skip to content

Commit dd48a4e

Browse files
committed
session: avoid stale endpoint pool reuse
1 parent c9cd560 commit dd48a4e

3 files changed

Lines changed: 69 additions & 5 deletions

File tree

cassandra/cluster.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4303,6 +4303,7 @@ def update_created_pools(self):
43034303
with self._lock:
43044304
with host.lock:
43054305
host_endpoint = host.endpoint
4306+
host_is_up = host.is_up
43064307
pool = self._get_pool_by_host_identity(
43074308
host, expected_endpoint=host_endpoint)
43084309
any_pool = pool or self._get_pool_by_host_identity(host)
@@ -4320,8 +4321,10 @@ def update_created_pools(self):
43204321
# on_up() keeps host.is_up False until this future succeeds.
43214322
future = self._reuse_or_invalidate_pool_creation(
43224323
host, pool_creation_future)
4323-
elif host.is_up in (True, None):
4324+
elif host_is_up in (True, None):
43244325
future = self.add_or_renew_pool(host, False)
4326+
elif any_pool is not None:
4327+
future = self.remove_pool(host)
43254328
elif pool_creation_future is not None:
43264329
future = self._reuse_or_invalidate_pool_creation(
43274330
host, pool_creation_future)
@@ -5662,7 +5665,13 @@ def _query(self, host, message=None, cb=None):
56625665
if message is None:
56635666
message = self.message
56645667

5665-
pool = self.session._get_pool_by_host_identity(host)
5668+
if isinstance(host, Host):
5669+
with host.lock:
5670+
expected_endpoint = host.endpoint
5671+
pool = self.session._get_pool_by_host_identity(
5672+
host, expected_endpoint=expected_endpoint)
5673+
else:
5674+
pool = self.session._get_pool_by_host_identity(host)
56665675
if not pool:
56675676
self._errors[host] = ConnectionException("Host has been marked down or removed")
56685677
return None

tests/unit/test_cluster.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,17 @@ def test_schedule_unique_keeps_client_route_events_for_distinct_ports(self, *_):
323323

324324
class SessionPoolRaceTest(unittest.TestCase):
325325

326+
class _DuplicatePoolEntries(object):
327+
328+
def __init__(self, entries):
329+
self._entries = entries
330+
331+
def __len__(self):
332+
return len(self._entries)
333+
334+
def items(self):
335+
return list(self._entries)
336+
326337
@staticmethod
327338
def _make_host(address):
328339
return Host(address, SimpleConvictionPolicy, host_id=uuid.uuid4())
@@ -511,6 +522,22 @@ def make_pool(host, distance, pool_session, endpoint=None):
511522
assert session._pools == {}
512523
created_pools[0].shutdown.assert_called_once_with()
513524

525+
def test_update_created_pools_removes_stale_pool_for_down_host_after_endpoint_change(self):
526+
host = self._make_host("127.0.0.1")
527+
host.set_down()
528+
cluster, session, executor = self._make_cluster_and_session([host])
529+
stale_pool = self._make_pool(host, HostDistance.LOCAL, session)
530+
session._pools[host] = stale_pool
531+
532+
host.endpoint = DefaultEndPoint("127.0.0.2")
533+
534+
futures = session.update_created_pools()
535+
536+
assert len(futures) == 1
537+
executor.run_next()
538+
assert session._pools == {}
539+
stale_pool.shutdown.assert_called_once_with()
540+
514541
def test_update_created_pools_replaces_pool_after_endpoint_change(self):
515542
host = self._make_host("127.0.0.1")
516543
old_endpoint = host.endpoint
@@ -628,11 +655,13 @@ def test_remove_pool_expected_endpoint_preserves_replacement_pool(self):
628655
old_endpoint = host.endpoint
629656
cluster, session, executor = self._make_cluster_and_session([host])
630657
stale_pool = self._make_pool(host, HostDistance.LOCAL, session)
631-
session._pools[host] = stale_pool
632658

633659
host.endpoint = DefaultEndPoint("127.0.0.2")
634660
replacement_pool = self._make_pool(host, HostDistance.LOCAL, session)
635-
session._pools[host] = replacement_pool
661+
session._pools = self._DuplicatePoolEntries([
662+
(host, stale_pool),
663+
(host, replacement_pool),
664+
])
636665

637666
assert len(session._pools) == 2
638667

tests/unit/test_response_future.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from unittest.mock import Mock, MagicMock, ANY
2121

2222
from cassandra import ConsistencyLevel, Unavailable, SchemaTargetType, SchemaChangeType, OperationTimedOut
23-
from cassandra.cluster import Session, ResponseFuture, NoHostAvailable, ProtocolVersion
23+
from cassandra.cluster import Cluster, Session, ResponseFuture, NoHostAvailable, ProtocolVersion
2424
from cassandra.connection import Connection, ConnectionException, DefaultEndPoint
2525
from cassandra.protocol import (ReadTimeoutErrorMessage, WriteTimeoutErrorMessage,
2626
UnavailableErrorMessage, ResultMessage, QueryMessage,
@@ -700,6 +700,32 @@ def test_timeout_returns_orphan_to_original_pool_after_endpoint_swap(self):
700700
old_pool.return_connection.assert_called_once_with(
701701
connection, stream_was_orphaned=True)
702702

703+
def test_query_does_not_borrow_stale_pool_after_endpoint_swap(self):
704+
session = self.make_basic_session()
705+
host = Host(DefaultEndPoint('127.0.0.1'), SimpleConvictionPolicy,
706+
host_id=uuid.uuid4())
707+
stale_pool = self.make_pool()
708+
stale_pool.host = host
709+
stale_pool.endpoint = host.endpoint
710+
stale_pool.is_shutdown = False
711+
connection = Mock(spec=Connection)
712+
stale_pool.borrow_connection.return_value = (connection, 1)
713+
714+
session._lock = RLock()
715+
session._pools = {host: stale_pool}
716+
session._endpoints_match = Session._endpoints_match.__get__(session, Session)
717+
session._pool_matches_expected = Session._pool_matches_expected.__get__(session, Session)
718+
session._get_pool_by_host_identity = Session._get_pool_by_host_identity.__get__(session, Session)
719+
session.cluster._endpoints_match.side_effect = Cluster._endpoints_match
720+
session.cluster._default_load_balancing_policy.make_query_plan.return_value = [host]
721+
host.endpoint = DefaultEndPoint('127.0.0.2')
722+
723+
rf = self.make_response_future(session)
724+
725+
assert not rf.send_request()
726+
stale_pool.borrow_connection.assert_not_called()
727+
assert isinstance(rf._errors[host], ConnectionException)
728+
703729
def test_single_host_query_plan_exhausted_after_one_retry(self):
704730
"""
705731
Test that when a specific host is provided, the query plan is properly

0 commit comments

Comments
 (0)