Skip to content

fix(grpc): propagate invocation cancellation to the transport - #8078

Merged
yuluo-yx merged 1 commit into
apache:masterfrom
yangjj-iso:fix/8020-grpc-call-cancellation
Oct 24, 2026
Merged

yuluo-yx merged 1 commit into
apache:masterfrom
yangjj-iso:fix/8020-grpc-call-cancellation

Conversation

@yangjj-iso

Copy link
Copy Markdown
Contributor

Fixes #8020.

Cancelling the gateway result currently cancels only the independent invocation future. Bind that future's cancellation to the actual ClientCall, and share an idempotent transport cancellation wrapper with aggregate-overflow cleanup. Normal completion and upstream failure do not introduce an extra cancellation.

Validation on OpenJDK 17.0.20:

  • All 90 gRPC module tests pass, with zero failures/errors/skips.
  • Ten new recording-transport cases exercise the real call/invoke path without mocking invoke(): all four RPC modes, cancellation before a response and during streaming, Reactor cancellation, normal/error completion, 50 completion/cancellation races, and overflow followed by downstream cancellation.
  • Removing the production fix reproduces six failures in the new suite; restoring it passes.
  • Affected-reactor compilation, Checkstyle, root Apache RAT and git diff --check pass.

These tests use a recording ClientCall, not a deployed gRPC server. Full-repository and Docker E2E suites were not run. Local Mockito tests use the existing Byte Buddy agent because self-attachment is unavailable. The separate outbound-readiness work in PR #8035 may need integration when either change lands.

  • Submit regression tests.
  • Full ./mvnw clean install -Dmaven.javadoc.skip=true (targeted validation is recorded above).

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review (Aias00)

Verdict: APPROVE. I reproduced both directions: the new suite is green on the head and fails 6/10 when the production hunk is reverted, so the tests actually pin the behavior. The fix also stays inside the lifecycle it touches rather than restructuring the client.

What I verified

  • mvn -pl shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-rpc/shenyu-plugin-grpc -am test: 90 tests, 0 failures, 0 errors, 0 skipped, including ShenyuGrpcCancellationTest 10/10.
  • Reverted only the AtomicBoolean + ForwardingClientCall + listener hunk (leaving the tests untouched) and re-ran: Tests run: 10, Failures: 6 — cancellationBeforeResponseReachesTransport (all four MethodTypes), cancellationDuringStreamingReachesTransport, reactorCancellationReachesTransport. That matches the PR description exactly, and the six are the right six: they are the ones that assert transport cancellation rather than the future's own flag.
  • The pre-existing ShenyuGrpcClientTest still passes 4/4, so the mock-based test was not deleted to make room for the new one — it is still there, now backed by a real-transport suite next to it.

The mechanism is right, and narrow

  • invoke() returns doneObserver.getCompletionFuture(), and call() cancels that future via result.whenComplete. Registering a directExecutor listener on that same future is the minimal way to bridge "future cancelled" to ClientCall.cancel, and it lands on the existing object rather than introducing a second lifecycle.
  • completion.isCancelled() is the correct guard: normal completion and upstream failure both complete the future, so neither fires an extra cancel. normalCompletionDoesNotCancelTransport and upstreamFailureDoesNotCancelTransportAgain assert exactly that (0 cancellations), which is the regression I would have expected this change to introduce.
  • Sharing the AtomicBoolean-guarded wrapper with MessageWriter.bindCall(call) is the right call: aggregate overflow and downstream cancellation now converge on one cleanup, and overflowAndDownstreamCancellationShareTransportCleanup proves it by asserting the count stays at exactly 1 across both triggers. Before this, overflow held the raw call while the invocation future held nothing.
  • ForwardingClientCall.SimpleForwardingClientCall keeps start/sendMessage/halfClose untouched, so no behavior changes for the four RPC modes beyond cancellation.

Nits (non-blocking)

  1. completionRacingCancellationCleansTransportAtMostOnce asserts call.cancellations.get() <= 1, which is satisfied by 0 as well — the assertion cannot fail on its own. assertTrue(result.isDone()) is doing all the work in that loop. If the intent is "never double-clean", pair it with an invariant that actually holds under both outcomes, e.g. assert that cancellations == 1 exactly when the cancel branch won, or record which future completed first and assert the count matches that branch.
  2. The wrapper drops a second cancel call outright, including its message/cause. For overflow-then-downstream-cancel that is fine, but if the first cancellation carries a generic cause and a later one is more specific, the transport only ever sees the first. Worth a one-line comment stating that the first cancellation wins by design, so the next reader does not "fix" it into a message overwrite.
  3. invoke() still returns null for an unknown MethodType while having already registered the listener on completion. Pre-existing, not introduced here, but the new code makes the null-return path slightly more visible; a throw would be friendlier than a NPE three frames later. Separate change, not this PR.

Merge-order note

PR #8035 (fix(grpc): drain client and bidi streams according to outbound readiness) touches the same client-streaming/bidi paths. Both can land, but whichever merges second will need to re-run the other's suite: this change adds a cancel wrapper around the call that #8035's drain logic will also hold. Worth coordinating so the second one is not merged on a stale green.

CI

Green on 3fd18968: ci / build / build (shenyu-integrated-test-grpc) / build (shenyu-integrated-test-k8s-ingress-grpc) / build (shenyu-integrated-test-k8s-gateway-api-http) / e2e-case (e2e-grpc-sync-compose ...) / CodeQL Analyze (java) / pr_build / rat_check / check-license-header / registry_dependencies / build-docker-images all pass.

@yuluo-yx
yuluo-yx merged commit d021746 into apache:master Oct 24, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Cancelling a gateway gRPC result does not cancel the underlying ClientCall

3 participants