Skip to content

Commit c9cd560

Browse files
committed
response-future: return timeouts to original pool
1 parent ce324f0 commit c9cd560

2 files changed

Lines changed: 57 additions & 30 deletions

File tree

cassandra/cluster.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5482,6 +5482,7 @@ class ResponseFuture(object):
54825482
_errbacks = None
54835483
_current_host = None
54845484
_connection = None
5485+
_connection_pool = None
54855486
_query_retries = 0
54865487
_start_time = None
54875488
_metrics = None
@@ -5578,7 +5579,7 @@ def _on_timeout(self, _attempts=0):
55785579
# Capture connection stats before pool.return_connection() can alter state
55795580
conn_in_flight = self._connection.in_flight
55805581

5581-
pool = self.session._get_pool_by_host_identity(self._current_host)
5582+
pool = self._connection_pool
55825583
if pool and not pool.is_shutdown:
55835584
# Do not return the stream ID to the pool yet. We cannot reuse it
55845585
# because the node might still be processing the query and will
@@ -5679,6 +5680,7 @@ def _query(self, host, message=None, cb=None):
56795680
else:
56805681
connection, request_id = pool.borrow_connection(timeout=2.0)
56815682
self._connection = connection
5683+
self._connection_pool = pool
56825684
result_meta = self.prepared_statement.result_metadata if self.prepared_statement else []
56835685

56845686
if cb is None:

tests/unit/test_response_future.py

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,24 @@
1313
# limitations under the License.
1414

1515
import unittest
16+
import uuid
1617

1718
from collections import deque
1819
from threading import RLock
1920
from unittest.mock import Mock, MagicMock, ANY
2021

2122
from cassandra import ConsistencyLevel, Unavailable, SchemaTargetType, SchemaChangeType, OperationTimedOut
2223
from cassandra.cluster import Session, ResponseFuture, NoHostAvailable, ProtocolVersion
23-
from cassandra.connection import Connection, ConnectionException
24+
from cassandra.connection import Connection, ConnectionException, DefaultEndPoint
2425
from cassandra.protocol import (ReadTimeoutErrorMessage, WriteTimeoutErrorMessage,
2526
UnavailableErrorMessage, ResultMessage, QueryMessage,
2627
OverloadedErrorMessage, IsBootstrappingErrorMessage,
2728
PreparedQueryNotFound, PrepareMessage, ServerError,
2829
RESULT_KIND_ROWS, RESULT_KIND_SET_KEYSPACE,
2930
RESULT_KIND_SCHEMA_CHANGE, RESULT_KIND_PREPARED,
3031
ProtocolHandler)
31-
from cassandra.policies import RetryPolicy, ExponentialBackoffRetryPolicy
32-
from cassandra.pool import NoConnectionsAvailable
32+
from cassandra.policies import RetryPolicy, ExponentialBackoffRetryPolicy, SimpleConvictionPolicy
33+
from cassandra.pool import Host, NoConnectionsAvailable
3334
from cassandra.query import SimpleStatement
3435
from tests.util import assertEqual, assertIsInstance
3536
import pytest
@@ -52,7 +53,7 @@ def make_pool(self):
5253
def make_session(self):
5354
session = self.make_basic_session()
5455
session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1', 'ip2']
55-
session._pools.get.return_value = self.make_pool()
56+
session._get_pool_by_host_identity.return_value = self.make_pool()
5657
return session
5758

5859
def make_response_future(self, session):
@@ -66,7 +67,7 @@ def make_mock_response(self, col_names, rows):
6667
def test_result_message(self):
6768
session = self.make_basic_session()
6869
session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1', 'ip2']
69-
pool = session._pools.get.return_value
70+
pool = session._get_pool_by_host_identity.return_value
7071
pool.is_shutdown = False
7172

7273
connection = Mock(spec=Connection)
@@ -75,7 +76,7 @@ def test_result_message(self):
7576
rf = self.make_response_future(session)
7677
rf.send_request()
7778

78-
rf.session._pools.get.assert_called_once_with('ip1')
79+
rf.session._get_pool_by_host_identity.assert_called_once_with('ip1')
7980
pool.borrow_connection.assert_called_once_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY)
8081

8182
connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[])
@@ -87,7 +88,7 @@ def test_result_message(self):
8788

8889
def test_unknown_result_class(self):
8990
session = self.make_session()
90-
pool = session._pools.get.return_value
91+
pool = session._get_pool_by_host_identity.return_value
9192
connection = Mock(spec=Connection)
9293
pool.borrow_connection.return_value = (connection, 1)
9394

@@ -151,7 +152,7 @@ def test_heartbeat_defunct_deadlock(self):
151152

152153
session = self.make_basic_session()
153154
session.cluster._default_load_balancing_policy.make_query_plan.return_value = [Mock(), Mock()]
154-
session._pools.get.return_value = pool
155+
session._get_pool_by_host_identity.return_value = pool
155156

156157
query = SimpleStatement("SELECT * FROM foo")
157158
message = QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE)
@@ -252,7 +253,7 @@ def test_retry_policy_says_ignore(self):
252253

253254
def test_retry_policy_says_retry(self):
254255
session = self.make_session()
255-
pool = session._pools.get.return_value
256+
pool = session._get_pool_by_host_identity.return_value
256257

257258
query = SimpleStatement("INSERT INFO foo (a, b) VALUES (1, 2)")
258259
message = QueryMessage(query=query, consistency_level=ConsistencyLevel.QUORUM)
@@ -266,7 +267,7 @@ def test_retry_policy_says_retry(self):
266267
rf = ResponseFuture(session, message, query, 1, retry_policy=retry_policy)
267268
rf.send_request()
268269

269-
rf.session._pools.get.assert_called_once_with('ip1')
270+
rf.session._get_pool_by_host_identity.assert_called_once_with('ip1')
270271
pool.borrow_connection.assert_called_once_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY)
271272
connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[])
272273

@@ -285,13 +286,13 @@ def test_retry_policy_says_retry(self):
285286

286287
# it should try again with the same host since this was
287288
# an UnavailableException
288-
rf.session._pools.get.assert_called_with(host)
289+
rf.session._get_pool_by_host_identity.assert_called_with(host)
289290
pool.borrow_connection.assert_called_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY)
290291
connection.send_msg.assert_called_with(rf.message, 2, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[])
291292

292293
def test_retry_with_different_host(self):
293294
session = self.make_session()
294-
pool = session._pools.get.return_value
295+
pool = session._get_pool_by_host_identity.return_value
295296

296297
connection = Mock(spec=Connection)
297298
pool.borrow_connection.return_value = (connection, 1)
@@ -300,7 +301,7 @@ def test_retry_with_different_host(self):
300301
rf.message.consistency_level = ConsistencyLevel.QUORUM
301302
rf.send_request()
302303

303-
rf.session._pools.get.assert_called_once_with('ip1')
304+
rf.session._get_pool_by_host_identity.assert_called_once_with('ip1')
304305
pool.borrow_connection.assert_called_once_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY)
305306
connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[])
306307
assert ConsistencyLevel.QUORUM == rf.message.consistency_level
@@ -319,7 +320,7 @@ def test_retry_with_different_host(self):
319320
rf._retry_task(False, host)
320321

321322
# it should try with a different host
322-
rf.session._pools.get.assert_called_with('ip2')
323+
rf.session._get_pool_by_host_identity.assert_called_with('ip2')
323324
pool.borrow_connection.assert_called_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY)
324325
connection.send_msg.assert_called_with(rf.message, 2, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[])
325326

@@ -328,13 +329,13 @@ def test_retry_with_different_host(self):
328329

329330
def test_all_retries_fail(self):
330331
session = self.make_session()
331-
pool = session._pools.get.return_value
332+
pool = session._get_pool_by_host_identity.return_value
332333
connection = Mock(spec=Connection)
333334
pool.borrow_connection.return_value = (connection, 1)
334335

335336
rf = self.make_response_future(session)
336337
rf.send_request()
337-
rf.session._pools.get.assert_called_once_with('ip1')
338+
rf.session._get_pool_by_host_identity.assert_called_once_with('ip1')
338339

339340
result = Mock(spec=IsBootstrappingErrorMessage, info={})
340341
host = Mock()
@@ -346,7 +347,7 @@ def test_all_retries_fail(self):
346347
rf._retry_task(False, host)
347348

348349
# it should try with a different host
349-
rf.session._pools.get.assert_called_with('ip2')
350+
rf.session._get_pool_by_host_identity.assert_called_with('ip2')
350351

351352
result = Mock(spec=IsBootstrappingErrorMessage, info={})
352353
rf._set_result(host, None, None, result)
@@ -360,15 +361,15 @@ def test_all_retries_fail(self):
360361

361362
def test_exponential_retry_policy_fail(self):
362363
session = self.make_session()
363-
pool = session._pools.get.return_value
364+
pool = session._get_pool_by_host_identity.return_value
364365
connection = Mock(spec=Connection)
365366
pool.borrow_connection.return_value = (connection, 1)
366367

367368
query = SimpleStatement("SELECT * FROM foo")
368369
message = QueryMessage(query=query, consistency_level=ConsistencyLevel.ONE)
369370
rf = ResponseFuture(session, message, query, 1, retry_policy=ExponentialBackoffRetryPolicy(2))
370371
rf.send_request()
371-
rf.session._pools.get.assert_called_once_with('ip1')
372+
rf.session._get_pool_by_host_identity.assert_called_once_with('ip1')
372373

373374
result = Mock(spec=IsBootstrappingErrorMessage, info={})
374375
host = Mock()
@@ -384,7 +385,7 @@ def test_exponential_retry_policy_fail(self):
384385
def test_all_pools_shutdown(self):
385386
session = self.make_basic_session()
386387
session.cluster._default_load_balancing_policy.make_query_plan.return_value = ['ip1', 'ip2']
387-
session._pools.get.return_value.is_shutdown = True
388+
session._get_pool_by_host_identity.return_value.is_shutdown = True
388389

389390
rf = ResponseFuture(session, Mock(), Mock(), 1)
390391
rf.send_request()
@@ -399,7 +400,7 @@ def test_first_pool_shutdown(self):
399400
pool_shutdown.is_shutdown = True
400401
pool_ok = self.make_pool()
401402
pool_ok.is_shutdown = True
402-
session._pools.get.side_effect = [pool_shutdown, pool_ok]
403+
session._get_pool_by_host_identity.side_effect = [pool_shutdown, pool_ok]
403404

404405
rf = self.make_response_future(session)
405406
rf.send_request()
@@ -424,7 +425,7 @@ def test_timeout_getting_connection_from_pool(self):
424425
connection = Mock(spec=Connection)
425426
second_pool.borrow_connection.return_value = (connection, 1)
426427

427-
session._pools.get.side_effect = [first_pool, second_pool]
428+
session._get_pool_by_host_identity.side_effect = [first_pool, second_pool]
428429

429430
rf = self.make_response_future(session)
430431
rf.send_request()
@@ -459,7 +460,7 @@ def test_callback(self):
459460

460461
def test_errback(self):
461462
session = self.make_session()
462-
pool = session._pools.get.return_value
463+
pool = session._get_pool_by_host_identity.return_value
463464
connection = Mock(spec=Connection)
464465
pool.borrow_connection.return_value = (connection, 1)
465466

@@ -508,7 +509,7 @@ def test_multiple_callbacks(self):
508509

509510
def test_multiple_errbacks(self):
510511
session = self.make_session()
511-
pool = session._pools.get.return_value
512+
pool = session._get_pool_by_host_identity.return_value
512513
connection = Mock(spec=Connection)
513514
pool.borrow_connection.return_value = (connection, 1)
514515

@@ -581,7 +582,7 @@ def test_add_callbacks(self):
581582

582583
def test_prepared_query_not_found(self):
583584
session = self.make_session()
584-
pool = session._pools.get.return_value
585+
pool = session._get_pool_by_host_identity.return_value
585586
connection = Mock(spec=Connection)
586587
pool.borrow_connection.return_value = (connection, 1)
587588

@@ -606,7 +607,7 @@ def test_prepared_query_not_found(self):
606607

607608
def test_prepared_query_not_found_bad_keyspace(self):
608609
session = self.make_session()
609-
pool = session._pools.get.return_value
610+
pool = session._get_pool_by_host_identity.return_value
610611
connection = Mock(spec=Connection)
611612
pool.borrow_connection.return_value = (connection, 1)
612613

@@ -655,7 +656,7 @@ def test_timeout_does_not_release_stream_id(self):
655656
session = self.make_basic_session()
656657
session.cluster._default_load_balancing_policy.make_query_plan.return_value = [Mock(endpoint='ip1'), Mock(endpoint='ip2')]
657658
pool = self.make_pool()
658-
session._pools.get.return_value = pool
659+
session._get_pool_by_host_identity.return_value = pool
659660
connection = Mock(spec=Connection, lock=RLock(), _requests={}, request_ids=deque(),
660661
orphaned_request_ids=set(), orphaned_threshold=256, in_flight=3)
661662
pool.borrow_connection.return_value = (connection, 1)
@@ -675,6 +676,30 @@ def test_timeout_does_not_release_stream_id(self):
675676
assert len(connection.request_ids) == 0, \
676677
"Request IDs should be empty but it's not: {}".format(connection.request_ids)
677678

679+
def test_timeout_returns_orphan_to_original_pool_after_endpoint_swap(self):
680+
session = self.make_basic_session()
681+
host = Host(DefaultEndPoint('127.0.0.1'), SimpleConvictionPolicy,
682+
host_id=uuid.uuid4())
683+
session.cluster._default_load_balancing_policy.make_query_plan.return_value = [host]
684+
old_pool = self.make_pool()
685+
replacement_pool = self.make_pool()
686+
connection = Mock(spec=Connection, lock=RLock(), _requests={}, request_ids=deque(),
687+
orphaned_request_ids=set(), orphaned_threshold=256, in_flight=1)
688+
old_pool.borrow_connection.return_value = (connection, 1)
689+
session._get_pool_by_host_identity.side_effect = [old_pool, replacement_pool]
690+
691+
rf = self.make_response_future(session)
692+
rf.send_request()
693+
connection._requests[1] = (connection._handle_options_response,
694+
ProtocolHandler.decode_message, [])
695+
host.endpoint = DefaultEndPoint('127.0.0.2')
696+
697+
rf._on_timeout()
698+
699+
replacement_pool.return_connection.assert_not_called()
700+
old_pool.return_connection.assert_called_once_with(
701+
connection, stream_was_orphaned=True)
702+
678703
def test_single_host_query_plan_exhausted_after_one_retry(self):
679704
"""
680705
Test that when a specific host is provided, the query plan is properly
@@ -686,7 +711,7 @@ def test_single_host_query_plan_exhausted_after_one_retry(self):
686711
"""
687712
session = self.make_basic_session()
688713
pool = self.make_pool()
689-
session._pools.get.return_value = pool
714+
session._get_pool_by_host_identity.return_value = pool
690715

691716
# Create a specific host
692717
specific_host = Mock()
@@ -702,7 +727,7 @@ def test_single_host_query_plan_exhausted_after_one_retry(self):
702727
rf.send_request()
703728

704729
# Verify initial request was sent
705-
rf.session._pools.get.assert_called_once_with(specific_host)
730+
rf.session._get_pool_by_host_identity.assert_called_once_with(specific_host)
706731
pool.borrow_connection.assert_called_once_with(timeout=ANY, routing_key=ANY, keyspace=ANY, table=ANY)
707732
connection.send_msg.assert_called_once_with(rf.message, 1, cb=ANY, encoder=ProtocolHandler.encode_message, decoder=ProtocolHandler.decode_message, result_metadata=[])
708733

0 commit comments

Comments
 (0)