2

Question first, context below. How can I perform some server-side action (eg, cleanup) based on a cancellation of an RPC from the client with an async gRPC python server?

In my microservice, I have an asyncio gRPC server whose main RPCs are bidirectional streams.

On the client side (which is also using asyncio), when I cancel something, it raises an asyncio.CancelledError which is caught and not reraised by the grpc core:

https://github.com/grpc/grpc/blob/master/src/python/grpcio/grpc/_cython/_cygrpc/aio/server.pyx.pxi#L679

    except asyncio.CancelledError:
        _LOGGER.debug('RPC cancelled for servicer method [%s]', _decode(rpc_state.method()))

So I cannot rely on catching the asyncio.CancelledError in my own code, because it's caught beforehand and not reraised.

The shared context is supposed to contain information as to whether the RPC was canceled on the client side, by calling .cancel() from the RPC call and being able to see if it was canceled by calling .cancelled():

https://grpc.github.io/grpc/python/grpc_asyncio.html#shared-context

abstract cancel()

Cancels the RPC.

Idempotent and has no effect if the RPC has already terminated.

Returns

    A bool indicates if the cancellation is performed or not.
Return type

    bool

abstract cancelled()

Return True if the RPC is cancelled.

The RPC is cancelled when the cancellation was requested with cancel().

Returns

    A bool indicates whether the RPC is cancelled or not.
Return type

    bool

However, this shared context is not attached to the context variable given to the RPC on the server side by the gRPC generated code. (I cannot run context.cancelled() or context.add_done_callback; they're not present)

So, again, the question: How can I perform some server-side action (eg, cleanup) based on a cancellation of an RPC from the client with an async gRPC python server?

brunston
  • 1,244
  • 1
  • 10
  • 18

1 Answers1

4

thanks for the post. We are aware of this issue, and adding support for those two methods is on our roadmap.

For a short term solution, you can use try-catch and decorators. The client-side-cancellation is observed as an asyncio.CancelledError in method handler. Here is a modified helloworld example:

Server code:

class Greeter(helloworld_pb2_grpc.GreeterServicer):

    async def SayHello(
            self, request: helloworld_pb2.HelloRequest,
            context: grpc.aio.ServicerContext) -> helloworld_pb2.HelloReply:
        try:
            await asyncio.sleep(4)
        except asyncio.CancelledError:
            print('RPC cancelled')
            raise
        return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)

Client code:

async def run() -> None:
    async with grpc.aio.insecure_channel('localhost:50051') as channel:
        stub = helloworld_pb2_grpc.GreeterStub(channel)
        call = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
        await asyncio.sleep(2)
        call.cancel()
        print(await call.code())

Lidi Zheng
  • 1,801
  • 8
  • 13