line / armeria

Your go-to microservice framework for any situation, from the creator of Netty et al. You can build any type of microservice leveraging your favorite technologies, including gRPC, Thrift, Kotlin, Retrofit, Reactive Streams, Spring Boot and Dropwizard.
https://armeria.dev
Apache License 2.0
4.73k stars 899 forks source link

Support gRPC cancellation #5570

Open ikhoon opened 3 months ago

ikhoon commented 3 months ago

Currently, Armeria gRPC implementation does not support cancellation at the gRPC level. https://github.com/grpc/grpc-java/blob/1d6f1f1b4251191bddb9d6605fc8f8152275b6b7/examples/src/main/java/io/grpc/examples/cancellation/CancellationClient.java#L57

When a call is canceled, the upstream sends RST_FRAME. We can make the same effect by calling RequestContext.cancel().

It sounds easy but the implementation on the client side could be tricky. On the call path, ClientInterceptor is called first, and then ArmeriaChannel is called. As ClientRequestContext is created in ArmeriaChannel, ClientRequestContext is inaccessible in ClientInterceptor. So our aim to call 'RequestContext.cancel()' is not feasible.

The idea I came up with is to add a ClientInterceptor in the GrpcClientBuilder that gets executed first. The interceptor registers a CancellationListener to CancellationContext. The reference of the registered CancellationListener should be also added to CallOptions so that we can update ClientRequestContext to the reference in ArmeriaChannel.newCall().

static CallOptions.Key<CancellationHandler> KEY = CallOptions.Key.create(CancellationHandler.class.getName());

class CancellableClientInterceptor {
    ...
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
           MethodDescriptor<ReqT, RespT> method, CallOptions callOptions,
           Channel next) {
       // CancellationHandler will defer the cancellation until `ClientRequestContext` to this.
       final CancellationHandler handler = new CancellationHandler();
       // KEY will be used in ArmeriaChannel.newCall() to get `handler`
       callOptions = callOptions.withOption(KEY, handler);
       final CancellableContext context = Context.current().withCancellation();
       context.addListener(handler, MoreExecutors.directExecutor());
       Context previous = context.attach();
       try {
           return next.newCall(method, callOptions);
       } finally {
           context.detach(previous);
       }
   }
}
my4-dev commented 2 months ago

Hi, @ikhoon ~

I understand that this issue requirement is To call RequestContext.cancel() when gRPC level cancellation occurs.

Thank you for sharing your idea and implementation outline. I'm not completely sure the following implementation and how this handler can access to ClientRequestContext.

       // CancellationHandler will defer the cancellation until `ClientRequestContext` to this.
       final CancellationHandler handler = new CancellationHandler();

Do you have your idea about this already? If so, it would be helpful if you could share.

ikhoon commented 2 months ago

We can set ClientRequestContext to CancellationHandler in the following method. https://github.com/line/armeria/blob/cc0509adaad6bbc37df2b3d749ddf0cb36a8ee2a/grpc/src/main/java/com/linecorp/armeria/internal/client/grpc/ArmeriaChannel.java#L140

CancellationHandler invokes ClientRequestContext.cancel() when it is set if CancellationListener.cancelled() was called before.

By the way, this issue is labeled with sprint so a sprint member is scheduled to work on this. 😅 How about looking for another issue?

my4-dev commented 2 months ago

Oh! Got it. I'm sorry for misunderstanding of this labeling system. Thank you for sharing your idea.