grpc / grpc/grpc

[aio] Not awaiting an asynchronous call silently ignores all errors

Open
#29,679 3 comments 0 reactions 1 assignee Claimed by @gnossen View on GitHub
kind/bug lang/Python platform/Windows priority/P2
Dominant language
C++
Stars
45.3k
Forks
11.4k
Avg merge
2d 12h
Merged PRs (30d)
47

Description

### What version of gRPC and what language are you using?
Python grpc 1.46.0

### What operating system (Linux, Windows,...) and version?
Windows 10 build 19044.1645 (21H2)

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

### What did you do?
I am writing integration test cases for my gRPC C++ server project in Python using pytest and asyncio. I am using a grpc.aio channel to connect to the gRPC service. This means that I have to use `await` on every gRPC call in Python. So I can make a basic test like this (suppose `proto_grpc` is the module containing the generated gRPC Python code and a service called `Service`):

~~~python
async def test_ping() -> None:
async with grpc.aio.insecure_channel("localhost:50051") as channel:
service = proto_grpc.Service(channel)
await service.Ping(Empty())
~~~

This is fine. If the Ping request fails, the gRPC call will throw an exception and the test will fail. Suppose that I forget the `await` keyword in the test, which is quite an easy miss to make (which also no linter will be able to detect, since there are no type hints in the generated gRPC service class that would indicate that the function must be awaited):

~~~python
async def test_ping() -> None:
async with grpc.aio.insecure_channel("localhost:50051") as channel:
service = proto_grpc.Service(channel)
service.Ping(Empty())
~~~

The test will succeed. There will not be any warning or error. However, Ping() will not be called on the server (since the call is silently cancelled during channel teardown - https://github.com/grpc/grpc/blob/03e9ac6f1f3132b81a2133ed63864fc72cd93e57/src/python/grpcio/grpc/aio/_channel.py#L364-L365 ), so you might still notice something is odd.

You can go one step further, though:

~~~python
async def test_ping() -> None:
async with grpc.aio.insecure_channel("localhost:50051") as channel:
service = proto_grpc.Service(channel)
service.StartPing(Empty())
[... some other test code ...]
await service.StopPing()
~~~

For all intents and purposes, this will *seem* to work completely fine. As before, the test will succeed without any warning or error. But now, even StartPing and StopPing will both be called on the server (because awaiting StopPing runs the event loop on which the StartPing() request task is already scheduled).

**However:** If the StartPing request *fails* at the server, the test will still *succeed*. This is because as described in https://github.com/grpc/grpc/blob/03e9ac6f1f3132b81a2133ed63864fc72cd93e57/src/python/grpcio/grpc/aio/_call.py#L281-L285 the exception handling is designed to ignore this situation. I think that what is coined "spam" in that comment, is a very valuable warning in some situations.

### What did you expect to see?

A warning similar to when you forget to await a normal coroutine:
~~~python
asyncio.sleep(1)
# -> RuntimeWarning: coroutine 'sleep' was never awaited
~~~

### What did you see instead?

No warning or error.

### Anything else we should know about your project / environment?

I know this issue might be specific to writing Python test cases for gRPC services and that in other circumstances, it might be totally valid to just ignore all of the exceptions etc. Maybe you could at least consider making this an opt-out feature?

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.