Skip to content

Cancel DelayedClientCall when application listener throws#12761

Open
jnowjack-lucidchart wants to merge 1 commit intogrpc:masterfrom
jnowjack-lucidchart:jnowjack-exception-handling-in-delayed-client-call
Open

Cancel DelayedClientCall when application listener throws#12761
jnowjack-lucidchart wants to merge 1 commit intogrpc:masterfrom
jnowjack-lucidchart:jnowjack-exception-handling-in-delayed-client-call

Conversation

@jnowjack-lucidchart
Copy link
Copy Markdown

Align DelayedClientCall.DelayedListener with ClientCallImpl's existing behavior for listener exceptions. When the application listener throws from onHeaders/onMessage/onReady, catch the Throwable, cancel the call with CANCELLED (cause = the throwable), and swallow subsequent callbacks. When onClose throws, log and continue, matching ClientCallImpl.closeObserver. If onClose arrives from the transport after a prior callback threw, override its status/trailers with the captured CANCELLED so a server-supplied OK can't mask the local failure.

Previously, a throw from the application listener escaped to the callExecutor's uncaught-exception handler. The real call was not cancelled and the transport kept delivering callbacks to an already broken listener, different from how the same bug behaves on a normal ClientCallImpl, and a timing-dependent inconsistency depending on whether callbacks arrived before or after setCall + drain completed.

Trade-off: listener-callback throws are no longer visible to the executor's UncaughtExceptionHandler (they're attached as Status.cause instead). This matches ClientCallImpl and is the intended behavior.

Exception handling for the outer drainPendingCalls loop (realCall.sendMessage/request/halfClose/cancel) remains unaddressed; that TODO is preserved.

Note:
This change only handles exceptions thrown by the application listener. I don't try and solve the problems that #12737 is attempting to fix. My motivation is to fix the root cause behind bazelbuild/bazel#29316

Align DelayedClientCall.DelayedListener with ClientCallImpl's existing
behavior for listener exceptions. When the application listener throws
from onHeaders/onMessage/onReady, catch the Throwable, cancel the call
with CANCELLED (cause = the throwable), and swallow subsequent
callbacks. When onClose throws, log and continue, matching
ClientCallImpl.closeObserver. If onClose arrives from the transport
after a prior callback threw, override its status/trailers with the
captured CANCELLED so a server-supplied OK can't mask the local
failure.

Previously, a throw from the application listener escaped to the
callExecutor's uncaught-exception handler. The real call was not
cancelled and the transport kept delivering callbacks to an already
broken listener, different from how the same bug behaves on a normal
ClientCallImpl, and a timing-dependent inconsistency depending on
whether callbacks arrived before or after setCall + drain completed.

Trade-off: listener-callback throws are no longer visible to the
executor's UncaughtExceptionHandler (they're attached as Status.cause
instead). This matches ClientCallImpl and is the intended behavior.

Exception handling for the outer drainPendingCalls loop
(realCall.sendMessage/request/halfClose/cancel) remains unaddressed;
that TODO is preserved.
@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla bot commented Apr 16, 2026

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: jnowjack-lucidchart / name: Jacob Nowjack (8e59f3e)

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.

1 participant