Open kr41 opened 7 years ago
Technically aiohttp creates a task per client request. On client disconnection the system stops the task ASAP. The only way to do it is task cancelling (let's assume web handler is waiting response from DB or other service, we want to cancel it too without waiting for explicit operation over connection to websocket client).
Task.cancel()
is done by sending asyncio.CancelledError
exception, the exception class is derived from standard Exception
. This is asyncio behavior, nothing specific to aiohttp itself.
The only thing I could suggest is catching CancelledError
in your handler explicitly:
try:
...
except asyncio.CancelledError:
pass
except Exception as exc:
log(exc)
Or you could just don't catch so broad type like Exception
.
I see two options:
CancelledError
is normal in async world.CancelledError
and return closed
message. I think, this is better solution for webocket handler.Maybe it's better to introduce a separate ConnectionClosed exception, in the same way as was done in websockets library?
Technically aiohttp creates a task per client request. On client disconnection the system stops the task ASAP. The only way to do it is task cancelling (let's assume web handler is waiting response from DB or other service, we want to cancel it too without waiting for explicit operation over connection to websocket client).
I think I have just been bitten by this behavior. I had some code like this:
async def handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async with contextlib.AsyncExitStack() as stack:
# acquire_resource_X are async context managers
await stack.enter_async_context(acquire_resource_1())
await stack.enter_async_context(acquire_resource_2())
await stack.enter_async_context(acquire_resource_3())
async for msg in ws:
# do stuff
await ws.close()
return ws
After putting it in production I found that the exiting part of acquire_resource_3()
would be silently skipped. More logging revealed that a CancelledError
was being raised inside acquire_resource_3
. Here's what I think happened:
async for msg in ws
loop exits, the AsyncExitStack
starts to unwind, the exiting part of acquire_resource_3
starts to execute, hits an await
aiohttp
cancels the handler taskCancelledError
is raised inside handler
at the current await
, which is inside acquire_resource_3
, therefore the remaining part of acquire_resource_3
is skippedacquire_resource_2
and acquire_resource_1
still executes normally, since from their perspective they are simply exiting an async context on an exceptionThis is a really weird problem, particularly because how it breaks the expectation that the exiting part of a context manager will always run. I had to basically shield all the async contexts from cancellation, like this:
async def handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
await asyncio.shield(asyncio.ensure_future(actually_do_stuff(ws)))
return ws
async def actually_do_stuff(ws):
async with contextlib.AsyncExitStack() as stack:
# acquire_resource_X are async context managers
await stack.enter_async_context(acquire_resource_1())
await stack.enter_async_context(acquire_resource_2())
await stack.enter_async_context(acquire_resource_3())
async for msg in ws:
# do stuff
await ws.close()
Is there a better way to do this?
Actual behaviour
Reading message loop
async for msg in ws:
raises low-levelconcurrent.futures._base.CancelledError
when connection is closed unexpectedly.Expected behaviour
Expected to get message with type
aiohtto.http_websocket.WSMsgType.ERROR
, or silently stop the loop, or at leastaiohtto.http_websocket.WebSocketError
.Steps to reproduce
Run the following two scripts
server.py
andclient.py
, then stopclient.py
byCtrl+C
.server.py
client.py
Log output of
server.py
Your environment
OS: CentOS Linux 7 Linux kernel: 3.10.0-514.16.1.el7.x86_64 Python: 3.5.3 aiohttp: 2.2.3