La lectura del bucle de mensajes async for msg in ws:
aumenta el nivel bajo concurrent.futures._base.CancelledError
cuando la conexión se cierra inesperadamente.
Se esperaba recibir un mensaje con el tipo aiohtto.http_websocket.WSMsgType.ERROR
, o detener silenciosamente el bucle, o al menos aiohtto.http_websocket.WebSocketError
.
Ejecute los siguientes dos scripts server.py
y client.py
, luego detenga 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 de Linux: 3.10.0-514.16.1.el7.x86_64
Python: 3.5.3
aiohttp: 2.2.3
Técnicamente, aiohttp crea una tarea por solicitud del cliente.
Al desconectarse el cliente, el sistema detiene la tarea lo antes posible.
La única forma de hacerlo es cancelando la tarea (supongamos que el controlador web está esperando la respuesta de la base de datos u otro servicio, también queremos cancelarlo sin esperar una operación explícita sobre la conexión al cliente websocket).
Task.cancel()
se realiza enviando asyncio.CancelledError
excepción, la clase de excepción se deriva del estándar Exception
. Este es un comportamiento de asyncio, nada específico de aiohttp en sí.
Lo único que podría sugerir es capturar CancelledError
en su controlador explícitamente:
try:
...
except asyncio.CancelledError:
pass
except Exception as exc:
log(exc)
O simplemente no podría captar un tipo tan amplio como Exception
.
Veo dos opciones:
CancelledError
es normal en el mundo asincrónico.CancelledError
y devuelve closed
mensaje. Creo que esta es una mejor solución para el controlador webocket.¿Quizás es mejor introducir una excepción ConnectionClosed separada, de la misma manera que se hizo en la biblioteca websockets ?
Técnicamente, aiohttp crea una tarea por solicitud del cliente.
Al desconectarse el cliente, el sistema detiene la tarea lo antes posible.
La única forma de hacerlo es cancelando la tarea (supongamos que el controlador web está esperando la respuesta de la base de datos u otro servicio, también queremos cancelarlo sin esperar una operación explícita sobre la conexión al cliente websocket).
Creo que este comportamiento me acaba de morder. Tenía un 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
Después de ponerlo en producción, descubrí que la parte de salida de acquire_resource_3()
se omitiría silenciosamente. Más registros revelaron que se estaba recaudando CancelledError
dentro de acquire_resource_3
. Esto es lo que creo que sucedió:
async for msg in ws
bucle AsyncExitStack
comienza a desenrollarse, la parte de salida de acquire_resource_3
comienza a ejecutarse, golpea un await
aiohttp
cancela la tarea del controladorCancelledError
se recauda dentro de handler
en el actual await
, que está dentro de acquire_resource_3
, por lo tanto, se omite la parte restante de acquire_resource_3
acquire_resource_2
y acquire_resource_1
todavía se ejecuta normalmente, ya que desde su perspectiva simplemente están saliendo de un contexto asíncrono en una excepciónEste es un problema realmente extraño, particularmente porque rompe la expectativa de que la parte saliente de un administrador de contexto siempre se ejecutará. Básicamente, tuve que proteger todos los contextos asíncronos de la cancelación, así:
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()
¿Hay una mejor manera de hacer esto?
Comentario más útil
¿Quizás es mejor introducir una excepción ConnectionClosed separada, de la misma manera que se hizo en la biblioteca websockets ?