chore: handle unary gRPC call ordering in KeyAwareChannel#4336
chore: handle unary gRPC call ordering in KeyAwareChannel#4336
Conversation
Summary of ChangesHello @rahul2393, 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 resolves a critical issue in the Highlights
Changelog
Activity
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. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request effectively addresses a crash in KeyAwareChannel caused by out-of-order gRPC calls for unary operations. The approach of buffering requests, half-close, and message compression until the delegate is initialized is sound and well-implemented. The changes are clear and align with the problem description. I have one minor suggestion to refactor a small piece of duplicated code to improve maintainability. Overall, this is a solid fix.
google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyAwareChannel.java
Outdated
Show resolved
Hide resolved
olavloite
left a comment
There was a problem hiding this comment.
I think that this class could use some more tests, especially for the non-happy path. Are we sure that everything works as we expect in case of errors? And cancelled calls? And timeouts? etc.
| boolean enableLocationApi = | ||
| Boolean.parseBoolean(System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR)); | ||
| options.isEnableLocationApi() | ||
| || Boolean.parseBoolean(System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR)); |
There was a problem hiding this comment.
The || Boolean.parseBoolean(System.getenv(EXPERIMENTAL_LOCATION_API_ENV_VAR) should not be necessary (and is also slightly counter-productive). The default for options.isEnableLocationApi() is to read the value from the environment variable. Only if a test has specified that SpannerOptions should use a fake environment, will it not read the environment variable. And in a case like that, the fake environment should also be able to override any environment variable that has been set, in order to get deterministic behavior (that is: The fake environment should be able to set the value to false, and it should not be overridden by what might have been set in a system environment variable).
| private boolean cancelled; | ||
| @Nullable private String cancelMessage; | ||
| @Nullable private Throwable cancelCause; |
There was a problem hiding this comment.
This can be simplified into one field: cancelledStatus. The if (cancelled) checks in the other places in this file can then be replaced with if (this.cancelledStatus != null)
| cancelled = true; | ||
| cancelMessage = message; | ||
| cancelCause = cause; | ||
| if (responseListener != null) { | ||
| responseListener.onClose( | ||
| io.grpc.Status.CANCELLED.withDescription(message).withCause(cause), new Metadata()); | ||
| } |
There was a problem hiding this comment.
I think that we should try to preserve any trailers that might be present in the cause. And just create the Cancelled status once here, and re-use everywhere it is needed.
| cancelled = true; | |
| cancelMessage = message; | |
| cancelCause = cause; | |
| if (responseListener != null) { | |
| responseListener.onClose( | |
| io.grpc.Status.CANCELLED.withDescription(message).withCause(cause), new Metadata()); | |
| } | |
| this.cancelledStatus = CANCELLED.withDescription(message).withCause(cause); | |
| Metadata trailers = cause == null ? new Metadata() : Status.trailersFromThrowable(cause); | |
| this.cancelledTrailers = trailers == null ? new Metadata() : trailers; | |
| if (responseListener != null) { | |
| responseListener.onClose(this.cancelledStatus, this.cancelledTrailers); | |
| } |
| if (pendingMessageCompression != null) { | ||
| delegate.setMessageCompression(pendingMessageCompression); | ||
| } |
There was a problem hiding this comment.
this.pendingMessageCompression is never set back to null, meaning that once it has been set, this if statement will be true for every invocation.
| if (databaseId != null && reqBuilder.hasMutationKey()) { | ||
| finder = parentChannel.getOrCreateChannelFinder(databaseId); | ||
| ChannelEndpoint routed = finder.findServer(reqBuilder); | ||
| if (endpoint == null) { |
There was a problem hiding this comment.
(Not changed in this pull request, but still relevant): This if statement is redundant. endpoint is always null at this point, so the entire if statement can be removed. And if that is not intentional, then that might be an indication of a bug.
Motivation
"Delegate call not initialized before use. sendMessage was likely not called."
What changed
Impact