Skip to content

Add table-aware GetReplicas API and deprecate legacy overloads#207

Open
sylwiaszunejko wants to merge 1 commit into
scylladb:masterfrom
sylwiaszunejko:get-replicas-api
Open

Add table-aware GetReplicas API and deprecate legacy overloads#207
sylwiaszunejko wants to merge 1 commit into
scylladb:masterfrom
sylwiaszunejko:get-replicas-api

Conversation

@sylwiaszunejko

@sylwiaszunejko sylwiaszunejko commented Mar 18, 2026

Copy link
Copy Markdown

The current replica resolution APIs exposed by Metadata:

public ICollection<HostShard> GetReplicas(string keyspaceName, byte[] partitionKey)
public ICollection<HostShard> GetReplicas(byte[] partitionKey)

Because the current C# APIs do not accept a table name, the tablet-based path cannot be used. This effectively forces replica resolution to assume vnode-based replication, which is incorrect for tablet-enabled clusters.

This PR introduces new API:

public ICollection<HostShard> GetReplicas(string keyspaceName, string tableName, byte[] partitionKey)

Fixes: #197

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

This PR centralizes “tablet-aware” replica resolution behind a new GetReplicas(keyspace, table, partitionKey) API, and updates token-aware query planning and tests to use it.

Changes:

  • Added Metadata.GetReplicas(string keyspaceName, string tableName, byte[] partitionKey) with tablet-map-first resolution and token-map fallback; marked older overloads as [Obsolete].
  • Extended ICluster/Cluster with a new GetReplicas(string keyspace, string table, byte[] partitionKey) overload and deprecated older overloads.
  • Updated TokenAwarePolicy and related unit/integration tests to call the new overload.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Cassandra/Policies/TokenAwarePolicy.cs Uses the new cluster replica API (keyspace + table) during query planning.
src/Cassandra/Metadata.cs Implements tablet-aware replica resolution and deprecates older overloads.
src/Cassandra/ICluster.cs Adds new replica-resolution overload and marks older methods obsolete.
src/Cassandra/Cluster.cs Implements new interface overload and routes obsolete overloads through the new method.
src/Cassandra.Tests/StatementTests.cs Updates mock setup/verification for new overload.
src/Cassandra.Tests/PoliciesUnitTests.cs Updates mocks for new overload in token-aware policy tests.
src/Cassandra.IntegrationTests/MetadataTests/TokenMapTopologyChangeTests.cs Updates calls to pass table name (null) to new overload.
src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeTests.cs Same as above.
src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeMetadataSyncTests.cs Same as above.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/Cassandra/Metadata.cs
Comment thread src/Cassandra/ICluster.cs Outdated
@sylwiaszunejko
sylwiaszunejko force-pushed the get-replicas-api branch 4 times, most recently from b17fdd2 to 6918688 Compare March 20, 2026 09:24

@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.

Let's do the following with API:

  1. Have API on Metadata per sharding type: vnode, tablets, (maybe later we will add StrongConsistency)
  2. Each of these APIs won't assert on table/ks configuration or anything like that and if information is missing it will not fallback
  3. Have a single generic API (on Cluster or Session) that allows you to get replicas for all the cases that will pull table/ks config and use proper API, so that user do not need to look or know sharding configuration.

@wprzytula

wprzytula commented Mar 23, 2026

Copy link
Copy Markdown

Let's do the following with API:

  1. Have API on Metadata per sharding type: vnode, tablets, (maybe later we will add StrongConsistency)

  2. Each of these APIs won't assert on table/ks configuration or anything like that and if information is missing it will not fallback

  3. Have a single generic API (on Cluster or Session) that allows you to get replicas for all the cases that will pull table/ks config and use proper API, so that user do not need to look or know sharding configuration.

Why? Why not make Metadata have public APIs that are robust and can handle all cases?

wprzytula
wprzytula previously approved these changes Mar 23, 2026
@sylwiaszunejko

Copy link
Copy Markdown
Author

Let's do the following with API:

1. Have API on Metadata per sharding type: vnode, tablets, (maybe later we will add StrongConsistency)

2. Each of these APIs won't assert on table/ks configuration or anything like that and if information is missing it will not fallback

3. Have a single generic API (on Cluster or Session) that allows you to get replicas for all the cases that will pull table/ks config and use proper API, so that user do not need to look or know sharding configuration.

@dkropachev @wprzytula @Lorak-mmk as for now I don't think any driver have think kind of API, shouldn't this be discussed on cross driver lvl and for now we should just have Metadata lvl API that handles all cases? WDYT?

@Lorak-mmk

Copy link
Copy Markdown

I agree. I also don't see much point in those granular APIs, its just more opportunity for user to shoot themselves in the foot.

@dkropachev

Copy link
Copy Markdown
Collaborator

Let's do the following with API:

1. Have API on Metadata per sharding type: vnode, tablets, (maybe later we will add StrongConsistency)

2. Each of these APIs won't assert on table/ks configuration or anything like that and if information is missing it will not fallback

3. Have a single generic API (on Cluster or Session) that allows you to get replicas for all the cases that will pull table/ks config and use proper API, so that user do not need to look or know sharding configuration.

@dkropachev @wprzytula @Lorak-mmk as for now I don't think any driver have think kind of API, shouldn't this be discussed on cross driver lvl and for now we should just have Metadata lvl API that handles all cases? WDYT?

We will defenitely need this API when we will start implementing batching logic, to split queries into a batches, but we can defere having this PR for now.

Comment thread src/Cassandra/Metadata.cs
Comment on lines +295 to +296
if (!string.IsNullOrEmpty(keyspaceName) && !string.IsNullOrEmpty(tableName))
{

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.

Suggested change
if (!string.IsNullOrEmpty(keyspaceName) && !string.IsNullOrEmpty(tableName))
{
if (string.IsNullOrEmpty(keyspaceName) || string.IsNullOrEmpty(tableName))
{
return _tokenMap.GetReplicas(keyspaceName, token);
}

Comment thread src/Cassandra/Metadata.cs
var token = _tokenMap.Factory.Hash(partitionKey);
if (!string.IsNullOrEmpty(keyspaceName) && !string.IsNullOrEmpty(tableName))
{
var tabletReplicas = TabletMap.GetReplicas(keyspaceName, tableName, token);

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.

The problem here is that it will fallback to tokenmap when tablets are empty, which is acceptable, but then we need to create an issue to address it later.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What else should we do? I believe Rust Driver assumes token ring routing if there's no tablet information for given token.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correction: it uses token ring routing if there is no tablet info for given table. If there are some tablets for given table, but there is no tablet for given token, it will use non-token-aware routing.

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.

  1. What does it mean non-token-aware routing ?
  2. AFAIR a year ago we have discussed exactly this issue and came to conclusion not to reply with token ring replicas in such case, but return empty replicas instead

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What does it mean non-token-aware routing ?

It means routing that we would do for non-token-aware query. I'm not sure what is not clear about that tbh.
In Rust Driver it is the same as having empty replica list, and in fact it is how we do things underneath:

        if let Some(tablets) = self.tablets.tablets_for_table(table_spec) {
            let replicas: Option<&[(Arc<Node>, Shard)]> = if let Some(datacenter) = datacenter {
                tablets.dc_replicas_for_token(token, datacenter)
            } else {
                tablets.replicas_for_token(token)
            };
            ReplicaSet {
                inner: ReplicaSetInner::PlainSharded(replicas.unwrap_or(
                    // The table is a tablet table, but we don't have information for given token.
                    // Let's just return empty set in this case.
                    &[],
                )),
                token,
            }

@dkropachev dkropachev May 20, 2026

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.

What does it mean non-token-aware routing ?

It means routing that we would do for non-token-aware query. I'm not sure what is not clear about that tbh. In Rust Driver it is the same as having empty replica list, and in fact it is how we do things underneath:

        if let Some(tablets) = self.tablets.tablets_for_table(table_spec) {
            let replicas: Option<&[(Arc<Node>, Shard)]> = if let Some(datacenter) = datacenter {
                tablets.dc_replicas_for_token(token, datacenter)
            } else {
                tablets.replicas_for_token(token)
            };
            ReplicaSet {
                inner: ReplicaSetInner::PlainSharded(replicas.unwrap_or(
                    // The table is a tablet table, but we don't have information for given token.
                    // Let's just return empty set in this case.
                    &[],
                )),
                token,
            }

What happens if a client calls get_token_endpoints on a table that has never been queried?

I thought that when we discussed [that issue](scylladb/scylla-rust-driver#1129), we agreed on exactly this wording - that the driver would check the table metadata to determine whether the table uses tablets, rather than relying solely on the presence of tablets.

I clearly remember you pointing out that if we base it only on tablet presence, the user would not be able to tell whether the table is vnode-based with no tablets yet, or it is vnode-based.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I remember the issue you linked, but I don't remember the discussion.
Quoting the linked issue:

If target table is not tablet-based it should return old vnode-based result
If target table is tablet-based it should return known tablet replicas for given token
if there is no tablet record for given token, it should return empty list

We do that in Rust Driver. Where we are misunderstanding each other is precise definition of " If target table is not tablet-based".
I understand it as " If target table is not tablet-based according to current driver knowledge". The thing is that driver knowledge is based solely on tablet hints currently.

Is that a big problem? I don't think so. The replica list is best-effort anyway (we might not have tablets for given token). Vnode-based list doesn't seem like a much bigger problem than an empty list to me. When you start using those replicas for routing and issuing queries, you'll get some hints soon anyway, and start routing properly (or getting empty lists). So there will be at most a few wrong queries.

We do have an issue open to fetch info about keyspace being tablet based or not: scylladb/scylla-rust-driver#1534
It is primarly to expose this info to the user, but we would also use it for routing, solving the issue above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@dkropachev is this explanation enough? WDYT?

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.

Is that a big problem? I don't think so. The replica list is best-effort anyway (we might not have tablets for given token). Vnode-based list doesn't seem like a much bigger problem than an empty list to me. When you start using those replicas for routing and issuing queries, you'll get some hints soon anyway, and start routing properly (or getting empty lists). So there will be at most a few wrong queries.

It is important, it will work fine only if there is a flow of prepared queries towards target table, but it might be not the case, you never know how users will use it.
That is why it is imporant to make API as user-friendly as possible and as clear as possible.

Imagine yourself as a user, you have an API that returns proper info if it has enough information, but if it does not have it, it returns random BS that you can't even destinct from proper reply.
And on other hand API that returns nothing when it does not have information, now at least you can make decision what to do, error out, guess, skip or try to fill it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I agree that detecting tablet-based tables from schema makes sense, which is why we have an issue to address this.
Does it impact this PR in any way?

@wprzytula

Copy link
Copy Markdown

@dkropachev @wprzytula @Lorak-mmk as for now I don't think any driver have think kind of API, shouldn't this be discussed on cross driver lvl and for now we should just have Metadata lvl API that handles all cases? WDYT?

We will defenitely need this API when we will start implementing batching logic, to split queries into a batches

What batching logic do you mean? Is it the same as in scylladb/scylla-rust-driver#468?

@sylwiaszunejko

Copy link
Copy Markdown
Author

@dkropachev can you look at it again? I believe behavior I added is in line with Rust driver, and we should discuss granular APIs on cross driver level separately, do you agree?

@sylwiaszunejko
sylwiaszunejko requested a review from dkropachev May 20, 2026 07:12
@dkropachev

Copy link
Copy Markdown
Collaborator

@dkropachev @wprzytula @Lorak-mmk as for now I don't think any driver have think kind of API, shouldn't this be discussed on cross driver lvl and for now we should just have Metadata lvl API that handles all cases? WDYT?

We will defenitely need this API when we will start implementing batching logic, to split queries into a batches

What batching logic do you mean? Is it the same as in scylladb/scylla-rust-driver#468?

Yes exactly that.

@dkropachev

Copy link
Copy Markdown
Collaborator

@dkropachev can you look at it again? I believe behavior I added is in line with Rust driver, and we should discuss granular APIs on cross driver level separately, do you agree?

There is one thing left to clear out.

@sylwiaszunejko

Copy link
Copy Markdown
Author

@dkropachev can you look at it again? I believe behavior I added is in line with Rust driver, and we should discuss granular APIs on cross driver level separately, do you agree?

There is one thing left to clear out.

@dkropachev what thing? In terms of batching logic, cannot we add additional API when implementing it?

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sylwiaszunejko, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 591b092e-bf66-471e-91e3-3092f22cb8b7

📥 Commits

Reviewing files that changed from the base of the PR and between 74ef1bc and 96b0c7f.

📒 Files selected for processing (12)
  • src/Cassandra.IntegrationTests/Core/MetadataTests.cs
  • src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeMetadataSyncTests.cs
  • src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeTests.cs
  • src/Cassandra.IntegrationTests/MetadataTests/TokenMapTopologyChangeTests.cs
  • src/Cassandra.IntegrationTests/Policies/Tests/LoadBalancingPolicyShortTests.cs
  • src/Cassandra.IntegrationTests/ScyllaLwtTests.cs
  • src/Cassandra.Tests/PoliciesUnitTests.cs
  • src/Cassandra.Tests/StatementTests.cs
  • src/Cassandra/Cluster.cs
  • src/Cassandra/ICluster.cs
  • src/Cassandra/Metadata.cs
  • src/Cassandra/Policies/TokenAwarePolicy.cs
📝 Walkthrough

Walkthrough

This change adds a tablet-aware GetReplicas(keyspace, table, partitionKey) API to Metadata, Cluster, and ICluster, marking the existing GetReplicas(partitionKey) and GetReplicas(keyspace, partitionKey) overloads [Obsolete] and delegating them to the new overload with a null table. Metadata.GetReplicas consults TabletMap when keyspace/table are provided, falling back to token-map resolution otherwise. TokenAwarePolicy was updated to call the new overload directly, removing prior manual tablet-hash fallback logic. Unit and integration tests were updated to match the new call signature.

Sequence Diagram(s)

sequenceDiagram
    participant TokenAwarePolicy
    participant Cluster
    participant Metadata
    participant TabletMap
    participant TokenMap

    TokenAwarePolicy->>Cluster: GetReplicas(keyspace, table, rawRoutingKey)
    Cluster->>Metadata: GetReplicas(keyspace, table, partitionKey)
    Metadata->>Metadata: compute token from partitionKey
    alt keyspace and table provided
        Metadata->>TabletMap: GetReplicas(keyspace, table, token)
        TabletMap-->>Metadata: tablet replicas (or empty)
    end
    alt tablet replicas found
        Metadata-->>Cluster: tablet-based replicas
    else fallback
        Metadata->>TokenMap: GetReplicas(keyspace, token)
        TokenMap-->>Metadata: token-map replicas
        Metadata-->>Cluster: token-map replicas
    end
    Cluster-->>TokenAwarePolicy: replica set
Loading

Possibly related PRs

  • scylladb/csharp-driver#227: Further modifies TokenAwarePolicy.NewQueryPlan to use cluster.GetReplicas(keyspace, table, partitionKey), in the same codepath modified here.

Suggested labels: area/Driver_-_csharp-driver

Suggested reviewers: dkropachev, wprzytula

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% 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 Title matches the main change: adding a table-aware GetReplicas API and deprecating older overloads.
Description check ✅ Passed Description describes the same table-aware GetReplicas API and tablet-related motivation as the code changes.
Linked Issues check ✅ Passed The PR implements issue #197 by adding the table-aware overload, deprecating legacy overloads, and preserving fallback replica resolution.
Out of Scope Changes check ✅ Passed The code changes and test updates stay focused on the GetReplicas API and tablet-aware replica resolution.

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.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeMetadataSyncTests.cs (1)

189-228: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wrong keyspace variable in assertion.

Line 225 checks GetReplicas(keyspaceName, ...), but the preceding lookup on line 223 uses keyspaceName1. This assertion should verify keyspaceName1's replicas to actually test that all keyspaces (not just keyspaceName) were refreshed.

🐛 Proposed fix
                 replicas = newCluster.Metadata.TokenToReplicasMap.GetByKeyspace(keyspaceName1);
                 Assert.AreEqual(newCluster.Metadata.Hosts.Sum(h => h.Tokens.Count()), replicas.Count);
-                Assert.AreEqual(3, newCluster.Metadata.GetReplicas(keyspaceName, null, Encoding.UTF8.GetBytes("123")).Count);
+                Assert.AreEqual(3, newCluster.Metadata.GetReplicas(keyspaceName1, null, Encoding.UTF8.GetBytes("123")).Count);
🤖 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
`@src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeMetadataSyncTests.cs`
around lines 189 - 228, The assertion after the TokenToReplicasMap lookup is
using the wrong keyspace variable. In
TokenMap_Should_RefreshTokenMapForAllKeyspaces_When_RefreshSchemaWithoutKeyspaceIsCalled,
update the GetReplicas check that follows the keyspaceName1 lookup so it
verifies keyspaceName1 instead of keyspaceName, ensuring the test באמת confirms
all keyspaces were refreshed.
🤖 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.

Outside diff comments:
In
`@src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeMetadataSyncTests.cs`:
- Around line 189-228: The assertion after the TokenToReplicasMap lookup is
using the wrong keyspace variable. In
TokenMap_Should_RefreshTokenMapForAllKeyspaces_When_RefreshSchemaWithoutKeyspaceIsCalled,
update the GetReplicas check that follows the keyspaceName1 lookup so it
verifies keyspaceName1 instead of keyspaceName, ensuring the test באמת confirms
all keyspaces were refreshed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 82bcc3cf-a385-480c-a01a-15c68f192f85

📥 Commits

Reviewing files that changed from the base of the PR and between 9eef16d and 74ef1bc.

📒 Files selected for processing (11)
  • src/Cassandra.IntegrationTests/Core/MetadataTests.cs
  • src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeMetadataSyncTests.cs
  • src/Cassandra.IntegrationTests/MetadataTests/TokenMapSchemaChangeTests.cs
  • src/Cassandra.IntegrationTests/MetadataTests/TokenMapTopologyChangeTests.cs
  • src/Cassandra.IntegrationTests/Policies/Tests/LoadBalancingPolicyShortTests.cs
  • src/Cassandra.Tests/PoliciesUnitTests.cs
  • src/Cassandra.Tests/StatementTests.cs
  • src/Cassandra/Cluster.cs
  • src/Cassandra/ICluster.cs
  • src/Cassandra/Metadata.cs
  • src/Cassandra/Policies/TokenAwarePolicy.cs

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.

Add Metadata.GetReplicas APIs With Table Name (Tablet-Compatible)

5 participants