Skip to content

Commit 83e458c

Browse files
committed
Add paging for ControlConnection topology queries
The ControlConnection now fetches all pages for system.peers and system.local queries using a new _fetch_all_pages helper. This ensures large clusters with many nodes complete topology discovery rather than only reading the first page. This replaces the old pattern of making a single query via wait_for_responses with sequential _fetch_all_pages calls via wait_for_response. Fixes #620
1 parent c1bfd54 commit 83e458c

2 files changed

Lines changed: 157 additions & 17 deletions

File tree

cassandra/cluster.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3724,6 +3724,45 @@ def _clear_watcher(conn, expiring_weakref):
37243724
pass
37253725

37263726

3727+
def _fetch_all_pages(connection, query_msg, timeout, fail_on_error=True):
3728+
response = connection.wait_for_response(query_msg, timeout=timeout, fail_on_error=fail_on_error)
3729+
3730+
if not fail_on_error:
3731+
success, result = response
3732+
if not success:
3733+
return response
3734+
else:
3735+
result = response
3736+
3737+
if not result or not result.paging_state:
3738+
return response if not fail_on_error else result
3739+
3740+
all_rows = list(result.parsed_rows) if result.parsed_rows else []
3741+
original_paging_state = query_msg.paging_state
3742+
3743+
try:
3744+
while result and result.paging_state:
3745+
query_msg.paging_state = result.paging_state
3746+
page_response = connection.wait_for_response(query_msg, timeout=timeout, fail_on_error=fail_on_error)
3747+
3748+
if not fail_on_error:
3749+
page_success, page_result = page_response
3750+
if not page_success:
3751+
return page_response
3752+
result = page_result
3753+
else:
3754+
result = page_response
3755+
3756+
if result and result.parsed_rows:
3757+
all_rows.extend(result.parsed_rows)
3758+
finally:
3759+
query_msg.paging_state = original_paging_state
3760+
3761+
result.parsed_rows = all_rows
3762+
3763+
return (True, result) if not fail_on_error else result
3764+
3765+
37273766
class ControlConnection(object):
37283767
"""
37293768
Internal
@@ -3925,23 +3964,27 @@ def _try_connect(self, endpoint):
39253964
sel_peers = self._get_peers_query(self.PeersQueryType.PEERS, connection)
39263965
sel_local = self._SELECT_LOCAL if self._token_meta_enabled else self._SELECT_LOCAL_NO_TOKENS
39273966
peers_query = QueryMessage(query=maybe_add_timeout_to_query(sel_peers, self._metadata_request_timeout),
3928-
consistency_level=ConsistencyLevel.ONE)
3967+
consistency_level=ConsistencyLevel.ONE,
3968+
fetch_size=self._schema_meta_page_size)
39293969
local_query = QueryMessage(query=maybe_add_timeout_to_query(sel_local, self._metadata_request_timeout),
3930-
consistency_level=ConsistencyLevel.ONE)
3931-
(peers_success, peers_result), (local_success, local_result) = connection.wait_for_responses(
3932-
peers_query, local_query, timeout=self._timeout, fail_on_error=False)
3970+
consistency_level=ConsistencyLevel.ONE,
3971+
fetch_size=self._schema_meta_page_size)
3972+
3973+
local_success, local_result = _fetch_all_pages(connection, local_query, self._timeout, fail_on_error=False)
39333974

39343975
if not local_success:
39353976
raise local_result
39363977

3978+
peers_success, peers_result = _fetch_all_pages(connection, peers_query, self._timeout, fail_on_error=False)
3979+
39373980
if not peers_success:
39383981
# error with the peers v2 query, fallback to peers v1
39393982
self._uses_peers_v2 = False
39403983
sel_peers = self._get_peers_query(self.PeersQueryType.PEERS, connection)
39413984
peers_query = QueryMessage(query=maybe_add_timeout_to_query(sel_peers, self._metadata_request_timeout),
3942-
consistency_level=ConsistencyLevel.ONE)
3943-
peers_result = connection.wait_for_response(
3944-
peers_query, timeout=self._timeout)
3985+
consistency_level=ConsistencyLevel.ONE,
3986+
fetch_size=self._schema_meta_page_size)
3987+
peers_result = _fetch_all_pages(connection, peers_query, self._timeout)
39453988

39463989
shared_results = (peers_result, local_result)
39473990
self._refresh_node_list_and_token_map(connection, preloaded_results=shared_results)
@@ -4084,11 +4127,14 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None,
40844127
log.debug("[control connection] Refreshing node list and token map")
40854128
sel_local = self._SELECT_LOCAL
40864129
peers_query = QueryMessage(query=maybe_add_timeout_to_query(sel_peers, self._metadata_request_timeout),
4087-
consistency_level=cl)
4130+
consistency_level=cl,
4131+
fetch_size=self._schema_meta_page_size)
40884132
local_query = QueryMessage(query=maybe_add_timeout_to_query(sel_local, self._metadata_request_timeout),
4089-
consistency_level=cl)
4090-
peers_result, local_result = connection.wait_for_responses(
4091-
peers_query, local_query, timeout=self._timeout)
4133+
consistency_level=cl,
4134+
fetch_size=self._schema_meta_page_size)
4135+
4136+
peers_result = _fetch_all_pages(connection, peers_query, self._timeout)
4137+
local_result = _fetch_all_pages(connection, local_query, self._timeout)
40924138

40934139
peers_result = dict_factory(peers_result.column_names, peers_result.parsed_rows)
40944140

tests/unit/test_control_connection.py

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
import unittest
1616

1717
from concurrent.futures import ThreadPoolExecutor
18-
from unittest.mock import Mock, ANY, call, patch
18+
from unittest.mock import Mock, ANY, call, patch, MagicMock
1919

20-
from cassandra import OperationTimedOut, SchemaTargetType, SchemaChangeType
21-
from cassandra.protocol import ResultMessage, RESULT_KIND_ROWS
20+
from cassandra import OperationTimedOut, SchemaTargetType, SchemaChangeType, ConsistencyLevel
21+
from cassandra.protocol import ResultMessage, RESULT_KIND_ROWS, QueryMessage
2222
from cassandra.cluster import ControlConnection, _Scheduler, ProfileManager, EXEC_PROFILE_DEFAULT, ExecutionProfile
2323
from cassandra.pool import Host
2424
from cassandra.connection import EndPoint, DefaultEndPoint, DefaultEndPointFactory
@@ -168,6 +168,18 @@ def __init__(self):
168168
]
169169
self.wait_for_responses = Mock(return_value=_node_meta_results(self.local_results, self.peer_results))
170170

171+
def wait_for_response_side_effect(query_msg, timeout=None, fail_on_error=True):
172+
result = ResultMessage(kind=RESULT_KIND_ROWS)
173+
if "peers" in query_msg.query.lower():
174+
result.column_names = self.peer_results[0]
175+
result.parsed_rows = self.peer_results[1]
176+
else:
177+
result.column_names = self.local_results[0]
178+
result.parsed_rows = self.local_results[1]
179+
result.paging_state = None
180+
return result
181+
self.wait_for_response = Mock(side_effect=wait_for_response_side_effect)
182+
171183

172184
class FakeTime(object):
173185

@@ -312,6 +324,88 @@ def test_wait_for_schema_agreement_none_timeout(self):
312324
cc._time = self.time
313325
assert cc._wait_for_schema_agreement()
314326

327+
def test_topology_queries_use_paging(self):
328+
self.control_connection.refresh_node_list_and_token_map()
329+
assert self.connection.wait_for_response.called
330+
calls = self.connection.wait_for_response.call_args_list
331+
for call in calls:
332+
query_msg = call[0][0]
333+
assert isinstance(query_msg, QueryMessage)
334+
assert query_msg.fetch_size == self.control_connection._schema_meta_page_size
335+
336+
def test_topology_queries_fetch_all_pages(self):
337+
from cassandra.cluster import _fetch_all_pages
338+
mock_connection = MagicMock()
339+
mock_connection.endpoint = DefaultEndPoint("192.168.1.0")
340+
mock_connection.original_endpoint = mock_connection.endpoint
341+
first_page = ResultMessage(kind=RESULT_KIND_ROWS)
342+
first_page.column_names = ["rpc_address", "peer", "schema_version", "data_center", "rack", "tokens", "host_id"]
343+
first_page.parsed_rows = [["192.168.1.1", "10.0.0.1", "a", "dc1", "rack1", ["1", "101", "201"], "uuid2"]]
344+
first_page.paging_state = b"has_more_pages"
345+
second_page = ResultMessage(kind=RESULT_KIND_ROWS)
346+
second_page.column_names = ["rpc_address", "peer", "schema_version", "data_center", "rack", "tokens", "host_id"]
347+
second_page.parsed_rows = [["192.168.1.2", "10.0.0.2", "a", "dc1", "rack1", ["2", "102", "202"], "uuid3"]]
348+
second_page.paging_state = None
349+
mock_connection.wait_for_response.side_effect = [first_page, second_page]
350+
self.control_connection._connection = mock_connection
351+
query_msg = QueryMessage(query="SELECT * FROM system.peers",
352+
consistency_level=ConsistencyLevel.ONE,
353+
fetch_size=self.control_connection._schema_meta_page_size)
354+
result = _fetch_all_pages(mock_connection, query_msg, timeout=5)
355+
assert len(result.parsed_rows) == 2
356+
assert result.parsed_rows[0][0] == "192.168.1.1"
357+
assert result.parsed_rows[1][0] == "192.168.1.2"
358+
assert result.paging_state is None
359+
assert mock_connection.wait_for_response.call_count == 2
360+
361+
def test_topology_queries_fetch_all_pages_fail_on_error_false(self):
362+
from cassandra.cluster import _fetch_all_pages
363+
mock_connection = MagicMock()
364+
mock_connection.endpoint = DefaultEndPoint("192.168.1.0")
365+
mock_connection.original_endpoint = mock_connection.endpoint
366+
first_page = ResultMessage(kind=RESULT_KIND_ROWS)
367+
first_page.column_names = ["rpc_address", "peer", "schema_version", "data_center", "rack", "tokens", "host_id"]
368+
first_page.parsed_rows = [["192.168.1.1", "10.0.0.1", "a", "dc1", "rack1", ["1", "101", "201"], "uuid2"]]
369+
first_page.paging_state = b"has_more_pages"
370+
second_page = ResultMessage(kind=RESULT_KIND_ROWS)
371+
second_page.column_names = ["rpc_address", "peer", "schema_version", "data_center", "rack", "tokens", "host_id"]
372+
second_page.parsed_rows = [["192.168.1.2", "10.0.0.2", "a", "dc1", "rack1", ["2", "102", "202"], "uuid3"]]
373+
second_page.paging_state = None
374+
mock_connection.wait_for_response.side_effect = [
375+
(True, first_page),
376+
(True, second_page),
377+
]
378+
query_msg = QueryMessage(query="SELECT * FROM system.peers",
379+
consistency_level=ConsistencyLevel.ONE,
380+
fetch_size=self.control_connection._schema_meta_page_size)
381+
success, result = _fetch_all_pages(mock_connection, query_msg, timeout=5, fail_on_error=False)
382+
assert success
383+
assert len(result.parsed_rows) == 2
384+
assert result.parsed_rows[0][0] == "192.168.1.1"
385+
assert result.parsed_rows[1][0] == "192.168.1.2"
386+
assert mock_connection.wait_for_response.call_count == 2
387+
388+
def test_topology_queries_fetch_all_pages_page_failure(self):
389+
from cassandra.cluster import _fetch_all_pages
390+
mock_connection = MagicMock()
391+
mock_connection.endpoint = DefaultEndPoint("192.168.1.0")
392+
mock_connection.original_endpoint = mock_connection.endpoint
393+
first_page = ResultMessage(kind=RESULT_KIND_ROWS)
394+
first_page.column_names = ["rpc_address", "peer", "schema_version", "data_center", "rack", "tokens", "host_id"]
395+
first_page.parsed_rows = [["192.168.1.1", "10.0.0.1", "a", "dc1", "rack1", ["1", "101", "201"], "uuid2"]]
396+
first_page.paging_state = b"has_more_pages"
397+
mock_connection.wait_for_response.side_effect = [
398+
(True, first_page),
399+
(False, OperationTimedOut()),
400+
]
401+
query_msg = QueryMessage(query="SELECT * FROM system.peers",
402+
consistency_level=ConsistencyLevel.ONE,
403+
fetch_size=self.control_connection._schema_meta_page_size)
404+
success, error = _fetch_all_pages(mock_connection, query_msg, timeout=5, fail_on_error=False)
405+
assert not success
406+
assert isinstance(error, OperationTimedOut)
407+
assert mock_connection.wait_for_response.call_count == 2
408+
315409
def test_refresh_nodes_and_tokens(self):
316410
self.control_connection.refresh_node_list_and_token_map()
317411
meta = self.cluster.metadata
@@ -328,7 +422,7 @@ def test_refresh_nodes_and_tokens(self):
328422
assert host.datacenter == "dc1"
329423
assert host.rack == "rack1"
330424

331-
assert self.connection.wait_for_responses.call_count == 1
425+
assert self.connection.wait_for_response.call_count == 2
332426

333427
def test_refresh_nodes_and_tokens_with_invalid_peers(self):
334428
def refresh_and_validate_added_hosts():
@@ -444,11 +538,11 @@ def test_refresh_nodes_and_tokens_remove_host(self):
444538

445539
def test_refresh_nodes_and_tokens_timeout(self):
446540

447-
def bad_wait_for_responses(*args, **kwargs):
541+
def bad_wait_for_response(*args, **kwargs):
448542
assert kwargs['timeout'] == self.control_connection._timeout
449543
raise OperationTimedOut()
450544

451-
self.connection.wait_for_responses = bad_wait_for_responses
545+
self.connection.wait_for_response = Mock(side_effect=bad_wait_for_response)
452546
self.control_connection.refresh_node_list_and_token_map()
453547
self.cluster.executor.submit.assert_called_with(self.control_connection._reconnect)
454548

0 commit comments

Comments
 (0)