Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async API ServicerContext cancelled() returning False during callback #36193

Open
purujitgoyal opened this issue Mar 28, 2024 · 3 comments
Open

Comments

@purujitgoyal
Copy link

purujitgoyal commented Mar 28, 2024

What version of gRPC and what language are you using?

grpcio: 1.62.1
grpcio-tools: 1.62.1

What operating system (Linux, Windows,...) and version?

Linux

What runtime / compiler are you using (e.g. python version or version of gcc)

Python 3.11

What did you do?

I am working with the async api and having problems with handling client cancellations on the server side. Here's a small prototype built using this example describing the problem:

async_server.py

def callback(context: grpc.aio.ServicerContext):
    logging.info("Inside callback")
    logging.info(context.cancelled())


class Greeter(MultiGreeterServicer):
    async def sayHello(
        self, request: HelloRequest, context: grpc.aio.ServicerContext
    ) -> HelloReply:
        logging.info("Serving sayHello request %s", request)
        context.add_done_callback(callback)
        try:
            for i in range(10):
                await asyncio.sleep(1)
                yield HelloReply(message=f"Hello number {i}, {request.name}!")
        except asyncio.CancelledError:
            logging.info("RPC cancelled")
        finally:
            await asyncio.sleep(2)
            logging.info("Inside finally")

async_client.py

async def run() -> None:
    async with grpc.aio.insecure_channel("localhost:50051") as channel:
        stub = hellostreamingworld_pb2_grpc.MultiGreeterStub(channel)
        hello_stream = stub.sayHello(hellostreamingworld_pb2.HelloRequest(name="you"))
        try:
            while True:
                response = await hello_stream.read()
                if response == grpc.aio.EOF:
                    break
                logging.info("Greeter client received from direct read: " + response.message)
                await asyncio.sleep(2)
                hello_stream.cancel()
        except asyncio.CancelledError:
            pass


if __name__ == "__main__":
    logging.basicConfig()
    asyncio.run(run())

What did you expect to see?

context.cancelled() should return True inside the callback function

What did you see instead?

context.cancelled() returns False inside the callback function

Anything else we should know about your project / environment?

Separate question, why does the await statement inside the finally block not throw the CancelledError like the one in the try block? What is the preferred way to handle cancellations from client side, catching the Cancelled Error and doing cleanup in finally or through callback function?

@XuanWang-Amos
Copy link
Contributor

Hi, looks like it's the same issue mentioned here: https://stackoverflow.com/questions/68491834/handle-client-side-cancellation-in-grpc-python-asyncio

In short, the client-side-cancellation is observed as an asyncio.CancelledError in method handler. Currently we recommend use except asyncio.CancelledError to caught and handle client cancel on server side.

@purujitgoyal
Copy link
Author

Yes, it's the same issue and I used the approach mentioned in the SO answer, but that answer was before the cancelled and done methods were introduced in #27767.

Anyway, what would you recommend for the cleanup in this case? The cleanup is an async function as well.

  1. await it inside the finally block
  2. Use asyncio.create_task in add_done_callback

@XuanWang-Amos
Copy link
Contributor

Both looks fine to me so I guess the answer is depends on your user case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants