chore: add routing hints and cache updates for begin/commit#4392
chore: add routing hints and cache updates for begin/commit#4392
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the Spanner client library's key-aware routing capabilities by integrating routing hints for mutation-based Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request enhances the Spanner client library by introducing routing hints for mutation-based BeginTransaction and Commit requests. The ChannelFinder now includes logic to apply routing hints to CommitRequests, and KeyAwareChannel has been updated to process CacheUpdates from Transaction and CommitResponse messages, enabling dynamic routing. A new computeKeys method was added to KeyRecipeCache to handle mutations. Corresponding tests have been added to validate these new routing behaviors. A suggestion was made to improve readability in KeyAwareChannel.java by removing a redundant null check and combining if statements.
| if (databaseId != null) { | ||
| finder = parentChannel.getOrCreateChannelFinder(databaseId); | ||
| } | ||
| if (finder != null) { | ||
| finder.fillRoutingHint(reqBuilder); | ||
| } |
There was a problem hiding this comment.
| reqBuilder.getRoutingHintBuilder()); | ||
| } | ||
|
|
||
| private ChannelEndpoint fillRoutingHint( |
There was a problem hiding this comment.
Maybe not directly related to this change, but I noticed that this method is unused. Can we remove it?
| if (finder != null) { | ||
| finder.fillRoutingHint(reqBuilder); | ||
| } | ||
| if (!reqBuilder.getTransactionId().isEmpty()) { |
There was a problem hiding this comment.
TransactionId is empty if the CommitRequest has a single-use read/write transaction. Meaning that we skip this for 'write-at-least-once' transactions. Is that intentional? Normally, such a CommitRequest would contain mutations, meaning that we would be able to route in using the same logic as a BeginTransactionRequest that contains a mutation key, right?
| public ChannelEndpoint findServer(BeginTransactionRequest.Builder reqBuilder) { | ||
| if (!reqBuilder.hasMutationKey()) { | ||
| if (!reqBuilder.hasMutationKey() | ||
| || !recipeCache.computeKeys( |
There was a problem hiding this comment.
I find some of this code very hard to read. E.g. the fact that computeKeys(..) returns a boolean and has the side-effect that it modifies the RoutingHintBuilder that it gets (but only if it returns true, but also sometimes when it returns false, see also below) makes this very hard to understand. I think it would be good if we would take a 'readability sweep' of both this method, but also the code in general.
|
|
||
| boolean computeKeys(Mutation mutation, RoutingHint.Builder hintBuilder) { | ||
| if (!schemaGeneration.isEmpty()) { | ||
| hintBuilder.setSchemaGeneration(schemaGeneration); |
There was a problem hiding this comment.
This updates the hint builder, but the method might still return false if the TargetRange below is null. That seems weird. I would expect this method only to modify the hint builder if it also returns true. Can we otherwise at least add some documentation to the method that explains when it returns true/false, and when it modifies the hint builder?
| if (!request.getTransactionId().isEmpty()) { | ||
| endpoint = parentChannel.affinityEndpoint(request.getTransactionId()); | ||
| transactionIdToClear = request.getTransactionId(); | ||
| CommitRequest.Builder reqBuilder = ((CommitRequest) message).toBuilder(); |
There was a problem hiding this comment.
This could potentially be a very heavy operation, as a CommitRequest could contain a very large number of mutations (e.g. during a bulk load). Converting it back to a builder means copying all of those mutations. I think that we should only call toBuilder() if we really know that we need the builder (i.e. if we know that we will be setting a routing hint).
This is also true for the other types of requests here, but those are less likely to be as heavy as a CommitRequest.
| } | ||
|
|
||
| @Test | ||
| public void beginTransactionWithMutationKeyAddsRoutingHint() throws Exception { |
There was a problem hiding this comment.
Can we add a test for CommitRequest with a single-use read/write transaction?
| harness.defaultManagedChannel.latestCall(); | ||
|
|
||
| assertThat(beginDelegate.lastMessage).isNotNull(); | ||
| assertThat(beginDelegate.lastMessage.getRoutingHint().getDatabaseId()).isEqualTo(7L); |
There was a problem hiding this comment.
nit: use assertEquals(..) (and in general try to stick to JUnit assertions)
| } | ||
| } | ||
|
|
||
| boolean computeKeys(Mutation mutation, RoutingHint.Builder hintBuilder) { |
There was a problem hiding this comment.
This method in general is quite hard to read, as it both returns a value that indicates whether the method did something or not, and has a potential side-effect of (maybe) populating the hintBuilder. Is there a better way to do this?
There was a problem hiding this comment.
removed that helper from the mutation routing flow.
| request = reqBuilder.build(); | ||
| } | ||
| if (!request.getTransactionId().isEmpty()) { | ||
| endpoint = parentChannel.affinityEndpoint(request.getTransactionId()); |
There was a problem hiding this comment.
This overrides the endpoint that was filled above, even in the case if parentChannel.affinityEndpoint(..) returns null, which means that it will fall back to the default endpoint. Is that intentional? (I'm not sure if it can happen in real life, though....)
There was a problem hiding this comment.
In case when transactionID is present but no affinity, we should just forward it to the default channel, example
For ExecuteSql / Read, if the first statement falls back to the default endpoint, we do not record default-host affinity because allowDefaultAffinity is false on that path. That means a later CommitRequest can carry a valid transactionId while affinityEndpoint still returns null, in this case we should just forward to default host.
… use for beginTransaction RPC
Summary
This change extends key-aware routing to include mutation-based
BeginTransactionandCommitrequest payloads, and teaches the response path to consumeCacheUpdatefromTransactionandCommitResponse.What changed
RoutingHintonBeginTransactionwhen amutationKeyis presentRoutingHintonCommitfrom the first mutation in the requestCommit/RollbackCacheUpdatefrom:TransactionCommitResponseKeyAwareChanneltests for the new request/response behavior