Skip to content

Exclude zero-token nodes from query routing#252

Open
sylwiaszunejko wants to merge 3 commits into
scylladb:masterfrom
sylwiaszunejko:zero-token-nodes
Open

Exclude zero-token nodes from query routing#252
sylwiaszunejko wants to merge 3 commits into
scylladb:masterfrom
sylwiaszunejko:zero-token-nodes

Conversation

@sylwiaszunejko

Copy link
Copy Markdown

Zero-token coordinator-only nodes can appear in topology metadata with an explicit empty token list. Treat those hosts as non-routing nodes while preserving hosts whose token metadata is absent or unknown.

Update RoundRobinPolicy and DCAwareRoundRobinPolicy to mark zero-token hosts as Ignored and omit them from generated query plans, including cached DC-aware host lists.

Add unit coverage for both built-in round-robin policies to verify zero-token hosts are skipped and token-owning hosts remain routable.

Fixes: https://scylladb.atlassian.net/browse/DRIVER-191

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change tracks whether host token metadata is present and marks hosts with explicitly empty token sets as ignored. Round-robin policies exclude these hosts from query plans and restore routing when tokens are later assigned. Tests cover host distance transitions, policy routing, and connection-pool creation after token acquisition. Build and test targets now use serialized MSBuild execution settings.

Sequence Diagram(s)

sequenceDiagram
  participant HostMetadata
  participant Host
  participant LoadBalancingPolicy
  participant PrepareHandler

  HostMetadata->>Host: SetInfo(tokens)
  Host->>Host: update distance and UP/DOWN state
  LoadBalancingPolicy->>Host: evaluate IsZeroTokenNode
  Host-->>LoadBalancingPolicy: Ignored or routable
  PrepareHandler->>LoadBalancingPolicy: request query plan
  LoadBalancingPolicy-->>PrepareHandler: eligible hosts
  PrepareHandler->>PrepareHandler: create connection pool for routable host
Loading

Suggested reviewers: dkropachev

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: zero-token nodes are excluded from query routing.
Description check ✅ Passed The description matches the PR contents and clearly describes the routing and test updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dkropachev dkropachev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check all the places where Host.SetInfo is called and make sure that node pools are updated.
At minimum we should have a test when node goes from 0 token to owning some tokens, because it is expected scenario.
And we need to make sure that Session is going to create node pool for that node in such case.

@sylwiaszunejko
sylwiaszunejko force-pushed the zero-token-nodes branch 2 times, most recently from cc0139f to e31691d Compare July 13, 2026 10:27
@sylwiaszunejko

Copy link
Copy Markdown
Author

Please check all the places where Host.SetInfo is called and make sure that node pools are updated. At minimum we should have a test when node goes from 0 token to owning some tokens, because it is expected scenario. And we need to make sure that Session is going to create node pool for that node in such case.

When a host gains tokens, IsZeroTokenNode becomes false, but the _distance field is left as-is (still Ignored from before). So no DistanceChanged event fires here. The correction happens lazily, the next time any code path calls RetrieveAndSetDistance (on the next query or prepare against that host).
Because RoundRobinPolicy.Distance / DCAwareRoundRobinPolicy.Distance now return Local/Remote once IsZeroTokenNode is false, this call flips the host from Ignored back to Local and SetDistance raises DistanceChanged(Ignored → Local) and this triggers the node pool update.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
Makefile (2)

78-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate LoadBalancingPolicyTests in Scylla filter.

FullyQualifiedName!~LoadBalancingPolicyTests appears twice in the TEST_INTEGRATION_SCYLLA_FILTER exclusion list. The second occurrence is redundant and can be removed to avoid confusion during future maintenance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` at line 78, Remove the second duplicate
FullyQualifiedName!~LoadBalancingPolicyTests entry from
TEST_INTEGRATION_SCYLLA_FILTER, preserving the first occurrence and all other
exclusions unchanged.

78-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider breaking the long filter expression across lines.

The TEST_INTEGRATION_SCYLLA_FILTER is a single ~1.2 KB line, making it difficult to scan for specific exclusions or detect duplicates. Using Make's line-continuation backslash would improve readability without changing behavior.

♻️ Suggested formatting
TEST_INTEGRATION_SCYLLA_FILTER ?= \
  (FullyQualifiedName!~ClientWarningsTests & \
   FullyQualifiedName!~CustomPayloadTests & \
   FullyQualifiedName!~Connect_With_Ssl_Test & \
   ... \
   FullyQualifiedName!~RetryPolicyTests)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` at line 78, Reformat the TEST_INTEGRATION_SCYLLA_FILTER assignment
across multiple Makefile lines using backslash continuations, placing each
exclusion on its own line while preserving the existing expression, exclusions,
ordering, and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Makefile`:
- Line 78: Remove the second duplicate
FullyQualifiedName!~LoadBalancingPolicyTests entry from
TEST_INTEGRATION_SCYLLA_FILTER, preserving the first occurrence and all other
exclusions unchanged.
- Line 78: Reformat the TEST_INTEGRATION_SCYLLA_FILTER assignment across
multiple Makefile lines using backslash continuations, placing each exclusion on
its own line while preserving the existing expression, exclusions, ordering, and
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04f72e0e-e765-4434-b475-5a244461e534

📥 Commits

Reviewing files that changed from the base of the PR and between 107482f and 1bf577c.

📒 Files selected for processing (7)
  • Makefile
  • src/Cassandra.Tests/HostTests.cs
  • src/Cassandra.Tests/PoliciesUnitTests.cs
  • src/Cassandra.Tests/Requests/PrepareHandlerTests.cs
  • src/Cassandra/Host.cs
  • src/Cassandra/Policies/DCAwareRoundRobinPolicy.cs
  • src/Cassandra/Policies/RoundRobinPolicy.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Cassandra/Policies/RoundRobinPolicy.cs
  • src/Cassandra/Host.cs

Comment thread src/Cassandra/Host.cs
Comment thread src/Cassandra/Policies/DCAwareRoundRobinPolicy.cs Outdated
Comment thread src/Cassandra/Host.cs Outdated
Comment thread src/Cassandra/Host.cs
Comment thread src/Cassandra/Connections/HostConnectionPool.cs Outdated
Comment thread src/Cassandra/Connections/HostConnectionPool.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Excludes explicit zero-token nodes from built-in query routing while allowing hosts to become routable after gaining tokens.

Changes:

  • Marks zero-token hosts ignored and filters query plans.
  • Tracks token-state transitions and coordinates connection pools.
  • Adds routing, host-transition, and pool-concurrency tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
RoundRobinPolicy.cs Filters zero-token hosts.
DCAwareRoundRobinPolicy.cs Filters local and remote plans.
Host.cs Tracks zero-token state.
HostConnectionPool.cs Handles empty-pool draining.
TestHelper.cs Models absent versus empty tokens.
PrepareHandlerTests.cs Tests routing restoration.
PoliciesUnitTests.cs Tests policy filtering and balancing.
HostTests.cs Tests token-state transitions.
HostConnectionPoolTests.cs Tests pool transition races.
Makefile Serializes test builds.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Cassandra/Policies/DCAwareRoundRobinPolicy.cs
Comment thread src/Cassandra/Connections/HostConnectionPool.cs Outdated
Comment thread src/Cassandra/Policies/RoundRobinPolicy.cs Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/Cassandra/Connections/HostConnectionPool.cs:571

  • When retries expire, this branch resets the pool from Closing even though _connectionOpenTcs still represents an active open. If the host remains Ignored, that open can later pass the IsClosing checks in CreateOpenConnection and add a connection to the ignored host—the race this method is intended to prevent. Keep the pool closing until the open is cancelled/completes, or also reject the completed connection based on the current distance.
            if (Volatile.Read(ref _connectionOpenTcs) == null || retriesLeft <= 0)
            {
                if (retriesLeft <= 0)
                {
                    HostConnectionPool.Logger.Warning(

Comment thread Makefile Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 18:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/Cassandra/Connections/HostConnectionPool.cs:570

  • When the retry budget expires, _connectionOpenTcs is explicitly still non-null, yet this branch resets the pool from Closing to Init. The delayed open then passes both IsClosing checks in CreateOpenConnection and can add a connection that is never drained from an Ignored host. Keep the pool closing until that open settles, or add cancellation/generation tracking so a timed-out open can never publish its connection before resetting the state.
            if (Volatile.Read(ref _connectionOpenTcs) == null || retriesLeft <= 0)
            {
                if (retriesLeft <= 0)
                {

Comment thread src/Cassandra/Policies/RoundRobinPolicy.cs Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 18:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/Cassandra/Connections/HostConnectionPool.cs:569

  • When the retry count reaches zero while _connectionOpenTcs is still non-null, this branch invokes the ignored-host drain handler and changes the pool from Closing back to Init. A connection open taking longer than 30 seconds can then pass the IsClosing checks when it eventually completes and be added to the ignored host's pool, defeating the protection this method adds. Drain completion must wait for or cancel/observe that open rather than resetting the state while it remains in flight.
            if (Volatile.Read(ref _connectionOpenTcs) == null || retriesLeft <= 0)
            {
                if (retriesLeft <= 0)

Comment thread src/Cassandra/Host.cs Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 19:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/Cassandra/Connections/HostConnectionPool.cs:569

  • After 30 retries this resets the pool to Init even though _connectionOpenTcs still represents an in-flight open. If the host is still Ignored and that open later completes, both IsClosing checks in CreateOpenConnection are now false, so the connection is added to the ignored host—the exact race this method is meant to prevent. Keep a generation/ignored-state guard through completion (or otherwise ensure a late connection is discarded) before forcing the state reset.
            if (Volatile.Read(ref _connectionOpenTcs) == null || retriesLeft <= 0)
            {
                if (retriesLeft <= 0)

Comment thread src/Cassandra/Policies/DCAwareRoundRobinPolicy.cs Outdated
Copilot AI review requested due to automatic review settings July 21, 2026 19:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/Cassandra/Connections/HostConnectionPool.cs:571

  • When the retry budget expires, _connectionOpenTcs can still represent an active socket open, yet this branch resets the pool to Init. When that open eventually returns, CreateOpenConnection sees IsClosing == false and adds the connection to the zero-token/ignored host. A slow or stalled open lasting over 30 seconds therefore defeats the drain's safety guarantee. Keep the pool closing until the in-flight task actually clears (or cancel/version that attempt) rather than forcing completion.
            if (Volatile.Read(ref _connectionOpenTcs) == null || retriesLeft <= 0)
            {
                if (retriesLeft <= 0)
                {
                    HostConnectionPool.Logger.Warning(

Copilot AI review requested due to automatic review settings July 22, 2026 07:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/Cassandra/Connections/HostConnectionPool.cs:574

  • The 30-second fallback reopens the race this method is meant to prevent. If a connection attempt remains in DoCreateAndOpen past the retry limit, this resets the pool to Init; when that attempt later completes, its IsClosing checks pass and it can add a connection to an ignored host. Do not complete the drain while _connectionOpenTcs is still non-null (or await that task directly) rather than forcing completion on elapsed time.
            if (Volatile.Read(ref _connectionOpenTcs) == null || retriesLeft <= 0)
            {
                if (retriesLeft <= 0)
                {
                    HostConnectionPool.Logger.Warning(
                        "Pool #{0} to {1}: in-flight connection open did not complete within {2} s; " +
                        "forcing drain completion.",
                        GetHashCode(), _host.Address, HostConnectionPool.EmptyDrainMaxRetries);

Introduce the notion of a zero-token, coordinator-only node on Host.
Such nodes appear in topology metadata with an empty token set, while
hosts whose token metadata is absent or unknown must keep their previous
behaviour.

Expose Host.IsZeroTokenNode, tracked via a dedicated flag that is set
whenever a row carries the tokens column. Because Scylla returns the
empty non-frozen tokens set as NULL in system.local/system.peers, a
present-but-NULL tokens column is treated as an empty token set (a real
zero-token node); only an absent column leaves the token state unknown,
so a host is not misclassified in either direction.

When fresh metadata marks an existing host as zero-token, force its
distance to Ignored so session pools receive DistanceChanged and drain
instead of keeping query connections open. When a host transitions out
of the zero-token state (it gains tokens after previously having none),
do not call BringUpIfDown(): a token update does not prove liveness, as
system.peers can still contain a down peer. The connection pool's
successful open will call BringUpIfDown() once a real connection
confirms the node is reachable.

Add HostTests covering the distance change on becoming zero-token, the
no-BringUpIfDown-on-token-gain behaviour, and the NULL/absent tokens
column cases.
Update RoundRobinPolicy and DCAwareRoundRobinPolicy to report zero-token
hosts as Ignored and omit them from generated query plans, while keeping
the cached host lists able to observe a zero-token host that later gains
tokens.

Enforce the exclusion centrally in Cluster.RetrieveAndSetDistance: it
now returns Ignored for a zero-token host before consulting the load
balancing policy. This is the single choke point every routing path
funnels through (request handler, prepare handler, control connection,
session warmup), so the guarantee holds even for custom load balancing
policies that do not check Host.IsZeroTokenNode, and keeps the distance
consistent with the Ignored value Host.SetInfo forces when the node is
first observed as zero-token. The built-in policies still check
IsZeroTokenNode in NewQueryPlan because that also drives query-plan
filtering, not just the reported distance.

For DCAwareRoundRobinPolicy, filter the local hosts down to the routable
ones before applying the round-robin modulo. Rotating over the full list
and skipping zero-token hosts afterwards biased the first attempt towards
the host following a zero-token node; filtering first preserves a fair
round-robin across the routable hosts.

Add unit coverage for both built-in policies: zero-token exclusion,
zero-token-to-token routing, balanced first-host distribution, and the
prepare-handler flow that creates a connection pool once a zero-token
host gains tokens.
When a host transitions to Ignored (e.g. a zero-token node) and its pool
is empty, FinishEmptyDrain polls until any in-flight connection open
completes before resetting the pool state to Init, preventing a race
where the completing open would add a connection to a now-ignored host.

Introduce EmptyDrainMaxRetries (30 attempts, 1 s apart) so the polling
loop cannot spin forever. If the open does not complete in time, log a
warning and force the drain to completion anyway.

After a drain the pool might have missed a reconnection trigger fired
while it was Closing (e.g. an Ignored->Local distance change): the
OnDistanceChanged ScheduleReconnection call returns early because
IsClosing is still true. A shared CompleteDrain helper, run from both
the empty (FinishEmptyDrain) and non-empty (DrainConnectionsTimer) drain
paths, checks _distance after resetting state and calls
ScheduleReconnection when the host is now routable. It also resets
_canCreateForeground first, mirroring OnDistanceChanged and OnHostUp, so
EnsureCreate calls do not fail in the window before the first connection
is established.

Add HostConnectionPoolTests covering the three new behaviours:
- Empty pool drain does not block a later Ignored->Local reconnection.
- In-flight open is discarded when the pool becomes Ignored.
- Distance changed to Local during FinishEmptyDrain polling triggers
  reconnection after the drain completes.
Copilot AI review requested due to automatic review settings July 22, 2026 08:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/Cassandra/Connections/HostConnectionPool.cs:571

  • A fixed 30-second cutoff reopens the pool while the connection open is still in flight. ConnectTimeoutMillis is configurable above 30 seconds (and opening can include multiple timed phases), so the open can later observe Init and add a connection to a host whose distance is still Ignored; that connection is then never drained because there is no new distance transition. Keep the pool closing until the open completes, or make the connection-add path reject connections whenever _distance == HostDistance.Ignored instead of forcing completion here.
            if (Volatile.Read(ref _connectionOpenTcs) == null || retriesLeft <= 0)
            {
                if (retriesLeft <= 0)
                {
                    HostConnectionPool.Logger.Warning(

@wprzytula wprzytula left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basic question to the very approach taken by this PR. Cover letter says:

Zero-token coordinator-only nodes can appear in topology metadata with an explicit empty token list. Treat those hosts as non-routing nodes while preserving hosts whose token metadata is absent or unknown.

The bolded parts seem contradictory. If those nodes are intended to be coordinator-only (so their purpose is to serve as request coordinators when drivers contact them to execute statements), then why exclude those nodes from routing? This defeats their purpose in the cluster IMO.

@@ -53,15 +53,114 @@ public void Should_UseHostIdEmpty_When_HostIdIsNull()
Assert.AreEqual(Guid.Empty, host.HostId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expose Host.IsZeroTokenNode, tracked via a dedicated flag that is set whenever a row carries the tokens column.

🤔 I don't understand. Why would the zero-token flag be set always if the tokens column is set, even if nonempty?

@@ -53,15 +53,114 @@ public void Should_UseHostIdEmpty_When_HostIdIsNull()
Assert.AreEqual(Guid.Empty, host.HostId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only an absent column leaves the token state unknown, so a host is not misclassified in either direction.

❓ Do you know if and when such situation (lack of tokens column) may happen?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants