A leitura do loop de mensagem async for msg in ws:
levanta concurrent.futures._base.CancelledError
baixo nível quando a conexão é fechada inesperadamente.
Esperado para obter a mensagem com o tipo aiohtto.http_websocket.WSMsgType.ERROR
, ou interromper o loop silenciosamente, ou pelo menos aiohtto.http_websocket.WebSocketError
.
Execute os dois scripts a seguir server.py
e client.py
, então pare client.py
por Ctrl+C
.
import logging
from aiohttp import web
logger = logging.getLogger(__name__)
async def index(request):
ws = web.WebSocketResponse()
request.app['websockets'].add(ws)
try:
await ws.prepare(request)
logger.debug('Connected')
async for msg in ws:
logger.info('Received: %r', msg.data)
except Exception:
logger.exception('Error')
logger.debug('Disconnected')
request.app['websockets'].discard(ws)
return ws
async def on_shutdown(app):
for ws in app['websockets']:
await ws.close()
app['websockets'].clear()
def main():
logging.basicConfig(level=logging.DEBUG)
app = web.Application()
app['websockets'] = set()
app.router.add_get('/', index)
app.on_shutdown.append(on_shutdown)
web.run_app(app, host='127.0.0.1', port=9000)
if __name__ == '__main__':
main()
import asyncio
import aiohttp
async def communicate(loop):
async with aiohttp.ClientSession(loop=loop) as session:
async with session.ws_connect('http://127.0.0.1:9000') as ws:
while True:
await ws.send_str('Hello')
await asyncio.sleep(1, loop=loop)
def main():
loop = asyncio.get_event_loop()
loop.run_until_complete(communicate(loop))
if __name__ == '__main__':
main()
server.py
$ python server.py
DEBUG:asyncio:Using selector: EpollSelector
======== Running on http://127.0.0.1:9000 ========
(Press CTRL+C to quit)
DEBUG:__main__:Connected
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
INFO:__main__:Received: 'Hello'
ERROR:__main__:Error
Traceback (most recent call last):
File "server.py", line 16, in index
async for msg in ws:
File "/home/vagrant/project/workspace/pyenv_dev/lib64/python3.5/site-packages/aiohttp/web_ws.py", line 343, in __anext__
msg = yield from self.receive()
File "/home/vagrant/project/workspace/pyenv_dev/lib64/python3.5/site-packages/aiohttp/web_ws.py", line 273, in receive
msg = yield from self._reader.read()
File "/home/vagrant/project/workspace/pyenv_dev/lib64/python3.5/site-packages/aiohttp/streams.py", line 627, in read
return (yield from super().read())
File "/home/vagrant/project/workspace/pyenv_dev/lib64/python3.5/site-packages/aiohttp/streams.py", line 509, in read
yield from self._waiter
File "/usr/lib64/python3.5/asyncio/futures.py", line 380, in __iter__
yield self # This tells Task to wait for completion.
File "/usr/lib64/python3.5/asyncio/tasks.py", line 304, in _wakeup
future.result()
File "/usr/lib64/python3.5/asyncio/futures.py", line 285, in result
raise CancelledError
concurrent.futures._base.CancelledError
DEBUG:__main__:Disconnected
INFO:aiohttp.access:- - - [06/Jul/2017:11:41:25 +0000] "GET / HTTP/1.1" 101 0 "-" "Python/3.5 aiohttp/2.2.3"
SO: CentOS Linux 7
Kernel do Linux: 3.10.0-514.16.1.el7.x86_64
Python: 3.5.3
aiohttp: 2.2.3
Tecnicamente, aiohttp cria uma tarefa por solicitação do cliente.
Na desconexão do cliente, o sistema interrompe a tarefa o mais rápido possível.
A única maneira de fazer isso é cancelando a tarefa (vamos supor que o manipulador da web esteja aguardando a resposta do banco de dados ou outro serviço, queremos cancelá-lo também sem esperar por uma operação explícita na conexão com o cliente do websocket).
Task.cancel()
é feito enviando asyncio.CancelledError
exception, a classe de exceção é derivada do padrão Exception
. Este é um comportamento assíncio, nada específico para o próprio aiohttp.
A única coisa que eu poderia sugerir é capturar CancelledError
em seu manipulador explicitamente:
try:
...
except asyncio.CancelledError:
pass
except Exception as exc:
log(exc)
Ou você pode simplesmente não pegar um tipo tão amplo como Exception
.
Vejo duas opções:
CancelledError
é normal no mundo assíncrono.CancelledError
e retornar closed
mensagem. Eu acho que essa é a melhor solução para o manipulador de webocket.Talvez seja melhor introduzir uma exceção ConnectionClosed separada, da mesma forma que foi feito na biblioteca de websockets ?
Tecnicamente, aiohttp cria uma tarefa por solicitação do cliente.
Na desconexão do cliente, o sistema interrompe a tarefa o mais rápido possível.
A única maneira de fazer isso é cancelando a tarefa (vamos supor que o manipulador da web esteja aguardando a resposta do banco de dados ou outro serviço, queremos cancelá-lo também sem esperar por uma operação explícita na conexão com o cliente do websocket).
Acho que acabei de ser mordido por esse comportamento. Eu tinha um código como este:
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
Depois de colocá-lo em produção, descobri que a parte final de acquire_resource_3()
seria ignorada silenciosamente. Mais registros revelaram que CancelledError
estava sendo gerado dentro de acquire_resource_3
. Aqui está o que acho que aconteceu:
async for msg in ws
loop sai, o AsyncExitStack
começa a se desenrolar, a parte de saída de acquire_resource_3
começa a executar, atinge um await
aiohttp
cancela a tarefa do manipuladorCancelledError
é gerado dentro de handler
no atual await
, que está dentro de acquire_resource_3
, portanto, a parte restante de acquire_resource_3
é ignoradaacquire_resource_2
e acquire_resource_1
ainda é executada normalmente, pois da perspectiva deles eles estão simplesmente saindo de um contexto assíncrono em uma exceçãoEste é um problema realmente estranho, principalmente porque quebra a expectativa de que a parte existente de um gerenciador de contexto sempre será executada. Tive que basicamente proteger todos os contextos assíncronos do cancelamento, como este:
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()
Existe uma maneira melhor de fazer isso?
Comentários muito úteis
Talvez seja melhor introduzir uma exceção ConnectionClosed separada, da mesma forma que foi feito na biblioteca de websockets ?