Skip to content

core: exclude client and hedging cancellations from callcounters for outlier-detection - #12923

Open
AgraVator wants to merge 15 commits into
grpc:masterfrom
AgraVator:outlier-detection-hedging-cancellations
Open

core: exclude client and hedging cancellations from callcounters for outlier-detection#12923
AgraVator wants to merge 15 commits into
grpc:masterfrom
AgraVator:outlier-detection-hedging-cancellations

Conversation

@AgraVator

Copy link
Copy Markdown
Contributor

fixes #12834

@AgraVator AgraVator changed the title outlier-detection: exclude client and hedging cancellations from callcounters core: exclude client and hedging cancellations from callcounters for outlier-detection Jul 24, 2026
@AgraVator
AgraVator marked this pull request as ready for review July 27, 2026 08:58
@AgraVator
AgraVator requested a review from kannanjgithub July 27, 2026 08:58
Status status, RpcProgress rpcProgress, Metadata trailers) {
if (!listenerClosed) {
listenerClosed = true;
if (status.getCode() == Status.Code.CANCELLED) {

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.

This method is for transport callback for stream closure and it is too late at this point to make a distinction whether the cancellation is app/retriable stream initiated or from the remote endpoint. This should instead be done in method cancel that sets this.cancelled = true; and that call only happens for app/retriable stream initiated cancellation, and not for remote server initiated cancellations, and happens before the transport stream is closed.

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.

In addition, we would need the guard in AbstractClientStream.cancel against late cancellations:

     @Override
     public final void cancel(Status reason) {
       Preconditions.checkArgument(!reason.isOk(), "Should not cancel with OK status");
       
       if (cancelled || transportState().listenerClosed) {   //  <----- GUARD 
         return;
       }
       
      cancelled = true;
      statsTraceCtx.clientCancelled(reason);
      abstractClientStreamSink().cancel(reason);
    }

@AgraVator AgraVator Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Placing statsTraceCtx.clientCancelled(reason) inside cancel() on the calling thread (app thread or deadline timer thread) could result in a race condition. Had it like that in the initial commits but then eric suggested to use transportReadyStatus() instead.

@AgraVator
AgraVator force-pushed the outlier-detection-hedging-cancellations branch from a573991 to bf90eb5 Compare August 3, 2026 12:20
@AgraVator
AgraVator requested a review from kannanjgithub August 3, 2026 13:13

@GuardedBy("this")
final void closeOnCancel(Status status) {
if (!isClosed() && statsTraceContext != null) {

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.

We need to add unit tests for these changes in BinderClientTransportTest.java. testCancelStream_notifiesTracer in it already tests client initiated cancellation, we can enhance the same.

Status status, RpcProgress rpcProgress, Metadata trailers, boolean stopDelivery) {
if (!listenerClosed) {
listenerClosed = true;
if (stopDelivery) {

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.

Using stopDelivery == true as the only indicator of a client-initiated cancellation is flawed because its meaning is context-dependent:

  1. Client Cancels (App / Hedging): Passes stopDelivery = true.
  2. Client Timeouts (DEADLINE_EXCEEDED): Passes stopDelivery = true.
  3. Transport Forceful Shutdown (UNAVAILABLE): Passes stopDelivery = true.
  4. Abrupt Network Drop (UNAVAILABLE): Passes stopDelivery = false.
  5. Server Sends Reset (CANCEL or INTERNAL): Passes stopDelivery = false.

(3) is a problem. In Netty transport for example, NettyClientHandler.forcefulClose explicitly sets stopDelivery = true (passing Status.UNAVAILABLE or similar).

Change this to:

         // We must ensure the status code actually reflects a client-initiated action!
         if (stopDelivery && (status.getCode() == Status.Code.CANCELLED || 
                             status.getCode() == Status.Code.DEADLINE_EXCEEDED)) {
          statsTraceCtx.clientCancelled(status);
          }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, we definitely shouldn't be doing any logic here based on the status code.

What is wrong with "Transport Forceful Shutdown'? That is defined as cancelling all streams, so it should behave the same as call.cancel().

I can believe we may need to change some cases to stop using stopDelivery=true; I bet there are/were some cases where it previously didn't matter what the value was. I think we can rename stopDelivery to cancelled, as that's the only time we should be discarding data that we've received.

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.

I wanted to count transport forceful shutdown in response to network drop as failure because it is not client app initiated although it might be client network stack initiated. Envoy does count connection drops as failures (LocalOriginConnectFailed) without any HTTP status code received from the remote peer.

@Override
public final void cancel(Status reason) {
Preconditions.checkArgument(!reason.isOk(), "Should not cancel with OK status");
if (cancelled || transportState().isListenerClosed()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why are we doing this? I'm worried this is misleading, as this doesn't prevent cancel() from happening twice because it is racy. And do we actually need to prevent this from happening twice, since it is within the transport thread that we'll check listenerClosed.

Also, this is not thread-safe, as listenerClosed is being accessed from multiple threads concurrently.

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.

This was added with my previous suggestion where we were calling statsTraceCtx.clientCancelled(status); from this method, and there was a need to check if the transport had already closed. With the new approach of stopDelivery, this check is no longer required and we don't need the method TransportState.isListenerClosed.
Yes, the thread-unsafe read was overlooked.

Status status, RpcProgress rpcProgress, Metadata trailers, boolean stopDelivery) {
if (!listenerClosed) {
listenerClosed = true;
if (stopDelivery) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, we definitely shouldn't be doing any logic here based on the status code.

What is wrong with "Transport Forceful Shutdown'? That is defined as cancelling all streams, so it should behave the same as call.cancel().

I can believe we may need to change some cases to stop using stopDelivery=true; I bet there are/were some cases where it previously didn't matter what the value was. I think we can rename stopDelivery to cancelled, as that's the only time we should be discarding data that we've received.

*/
private void closeListener(
Status status, RpcProgress rpcProgress, Metadata trailers) {
Status status, RpcProgress rpcProgress, Metadata trailers, boolean stopDelivery) {

@ejona86 ejona86 Aug 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we could call statsTraceCtx.clientCancelled() from transportReportStatus and just ignore whether listenerClosed and have the tracer figure out whether the cancellation actually mattered. Or we could check listenerClosed within transportReportStatus(). Calling clientCancelled within transportReportStatus will deliver the cancellation notification sooner, which seems beneficial. Close() has to be delayed until all resources are cleaned up because after close we can't use them any more, but cancellation notification wouldn't need to be. But overall, I think some variations here wouldn't matter much for years (that is to say, the current code is fair).

}
}

protected final boolean isListenerClosed() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The reason we access variables in the transport state from methods is for the method to provide synchronization guarantees within the scope of this class (where it is easy to check). This is obviously not providing any synchronization and thus is broken.

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.

Outlier detection behavior for client cancellations and hedged streams

3 participants