Skip to content

Commit 32572ec

Browse files
committed
schema-agreement: isolate session queries from shared executor
Session.wait_for_schema_agreement() originally dispatched its per-host probes through Session.submit(), which routes onto the cluster-wide executor shared with pool creation and metadata work. That executor is small by default, so unrelated queued tasks can delay schema-agreement probes long enough to produce false timeouts or make the wait look hung before the queries even start. Create a short-lived executor sized to the schema-agreement parallelism, build it through Cluster._create_thread_pool_executor() so the existing reactor-specific executor selection still applies, and shut it down after the check completes. Update the unit tests to inject the dedicated executor and add coverage proving schema agreement still succeeds when the shared executor is blocked.
1 parent 469b73e commit 32572ec

3 files changed

Lines changed: 87 additions & 57 deletions

File tree

cassandra/cluster.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3441,51 +3441,56 @@ def _get_schema_mismatches_for_scope(self, deadline, scope: Literal['rack', 'dc'
34413441
consistency_level=cl)
34423442

34433443
parallelism = max(1, min(parallelism, len(hosts)))
3444-
for offset in range(0, len(hosts), parallelism):
3445-
remaining = deadline - time.time()
3446-
if remaining <= 0:
3447-
for host in hosts[offset:]:
3448-
errors[host.endpoint] = "Timed out before querying host"
3449-
break
3450-
3451-
futures = {}
3452-
for host in hosts[offset:offset + parallelism]:
3453-
future = self.submit(self._query_local_schema_version, host, query, deadline)
3454-
if future is None:
3455-
errors[host.endpoint] = "Schema agreement executor unavailable"
3444+
# Use a dedicated executor so schema agreement does not get blocked by
3445+
# unrelated work already queued on the shared session executor.
3446+
executor = self.cluster._create_thread_pool_executor(max_workers=parallelism)
3447+
try:
3448+
for offset in range(0, len(hosts), parallelism):
3449+
remaining = deadline - time.time()
3450+
if remaining <= 0:
3451+
for host in hosts[offset:]:
3452+
errors[host.endpoint] = "Timed out before querying host"
3453+
break
3454+
3455+
futures = {}
3456+
for host in hosts[offset:offset + parallelism]:
3457+
futures[executor.submit(self._query_local_schema_version, host, query, deadline)] = host
3458+
3459+
if not futures:
34563460
continue
3457-
futures[future] = host
34583461

3459-
if not futures:
3460-
continue
3461-
3462-
remaining = deadline - time.time()
3463-
if remaining <= 0:
3464-
for future, host in futures.items():
3462+
remaining = deadline - time.time()
3463+
if remaining <= 0:
3464+
for future, host in futures.items():
3465+
future.cancel()
3466+
errors[host.endpoint] = "Timed out before querying host"
3467+
for host in hosts[offset + parallelism:]:
3468+
errors[host.endpoint] = "Timed out before querying host"
3469+
break
3470+
3471+
done, not_done = wait_futures(tuple(futures), timeout=remaining)
3472+
for future in not_done:
34653473
future.cancel()
3474+
host = futures[future]
34663475
errors[host.endpoint] = "Timed out before querying host"
3467-
for host in hosts[offset + parallelism:]:
3468-
errors[host.endpoint] = "Timed out before querying host"
3469-
break
3470-
3471-
done, not_done = wait_futures(tuple(futures), timeout=remaining)
3472-
for future in not_done:
3473-
future.cancel()
3474-
host = futures[future]
3475-
errors[host.endpoint] = "Timed out before querying host"
3476-
3477-
for future in done:
3478-
host = futures[future]
3479-
schema_version, error = future.result()
3480-
if error is not None:
3481-
errors[host.endpoint] = error
3482-
continue
3483-
versions[schema_version].add(host.endpoint)
34843476

3485-
if not_done:
3486-
for host in hosts[offset + parallelism:]:
3487-
errors[host.endpoint] = "Timed out before querying host"
3488-
break
3477+
for future in done:
3478+
host = futures[future]
3479+
schema_version, error = future.result()
3480+
if error is not None:
3481+
errors[host.endpoint] = error
3482+
continue
3483+
versions[schema_version].add(host.endpoint)
3484+
3485+
if not_done:
3486+
for host in hosts[offset + parallelism:]:
3487+
errors[host.endpoint] = "Timed out before querying host"
3488+
break
3489+
finally:
3490+
try:
3491+
executor.shutdown(wait=False, cancel_futures=True)
3492+
except TypeError:
3493+
executor.shutdown(wait=False)
34893494

34903495
if len(versions) == 1 and None not in versions and not errors:
34913496
log.debug("[session] Local schemas match")

tests/unit/test_cluster.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -450,16 +450,20 @@ def test_wait_for_schema_agreement_limits_parallel_queries_to_default(self, mock
450450
session, _ = self._new_schema_agreement_session(["a"] * 11)
451451
batch_sizes = []
452452

453-
def submit(fn, host, query, deadline):
454-
future = Future()
455-
future.set_result(fn(host, query, deadline))
456-
return future
457-
458453
def fake_wait(futures, timeout=None, return_when=None):
459454
batch_sizes.append(len(futures))
460455
return set(futures), set()
461456

462-
session.submit = Mock(side_effect=submit)
457+
class ImmediateExecutor(object):
458+
def submit(self, fn, *args, **kwargs):
459+
future = Future()
460+
future.set_result(fn(*args, **kwargs))
461+
return future
462+
463+
def shutdown(self, wait=False, cancel_futures=False):
464+
return None
465+
466+
session.cluster._create_thread_pool_executor = Mock(return_value=ImmediateExecutor())
463467
mocked_wait_futures.side_effect = fake_wait
464468

465469
assert session.wait_for_schema_agreement(wait_time=1)

tests/unit/test_session_schema_agreement.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from concurrent.futures import Future
1+
from concurrent.futures import Future, ThreadPoolExecutor
22
from datetime import timedelta
33
from types import SimpleNamespace
44
from unittest.mock import Mock
55
import uuid
6+
from threading import Event
67

78
import pytest
89

@@ -33,6 +34,19 @@ def _get_connection_for_routing_key(self):
3334
return self.connection
3435

3536

37+
class ImmediateExecutor:
38+
def submit(self, fn, *args, **kwargs):
39+
future = Future()
40+
try:
41+
future.set_result(fn(*args, **kwargs))
42+
except Exception as exc:
43+
future.set_exception(exc)
44+
return future
45+
46+
def shutdown(self, wait=False, cancel_futures=False):
47+
return None
48+
49+
3650
def _mock_schema_response(schema_version):
3751
return SimpleNamespace(column_names=["schema_version"], parsed_rows=[[schema_version]])
3852

@@ -53,28 +67,20 @@ def _new_session(schema_versions, distances=None, metadata_request_timeout=timed
5367

5468
cluster = SimpleNamespace(
5569
max_schema_agreement_wait=10,
70+
executor=ImmediateExecutor(),
5671
control_connection=SimpleNamespace(
5772
_timeout=timeout,
5873
_metadata_request_timeout=metadata_request_timeout,
5974
),
6075
)
76+
cluster._create_thread_pool_executor = Mock(return_value=ImmediateExecutor())
6177

6278
session = Session.__new__(Session)
6379
session.cluster = cluster
6480
session._profile_manager = SimpleNamespace(distance=lambda host: distance_map.get(host, HostDistance.LOCAL))
6581
session._pools = {}
6682
session.is_shutdown = False
6783

68-
def submit(fn, host, query, deadline):
69-
future = Future()
70-
try:
71-
future.set_result(fn(host, query, deadline))
72-
except Exception as exc:
73-
future.set_exception(exc)
74-
return future
75-
76-
session.submit = submit
77-
7884
for host, schema_version in zip(hosts, schema_versions):
7985
connection = Mock(endpoint=host.endpoint)
8086
connection.wait_for_response.return_value = _mock_schema_response(schema_version)
@@ -115,3 +121,18 @@ def test_wait_for_schema_agreement_bypasses_non_positive_wait_time(wait_time, cl
115121

116122
for connection in connections.values():
117123
assert connection.wait_for_response.call_count == 0
124+
125+
126+
def test_wait_for_schema_agreement_is_not_blocked_by_shared_executor():
127+
session, _, _ = _new_session(["a", "a"])
128+
busy_executor = ThreadPoolExecutor(max_workers=1)
129+
block = Event()
130+
131+
session.cluster.executor = busy_executor
132+
busy_executor.submit(block.wait)
133+
134+
try:
135+
assert session.wait_for_schema_agreement(wait_time=0.1)
136+
finally:
137+
block.set()
138+
busy_executor.shutdown(wait=True)

0 commit comments

Comments
 (0)