Beim Lesen der Nachrichtenschleife async for msg in ws:
wird concurrent.futures._base.CancelledError
niedriger Ebene
Es wird erwartet, dass eine Nachricht vom Typ aiohtto.http_websocket.WSMsgType.ERROR
abgerufen oder die Schleife stillschweigend gestoppt wird, oder mindestens aiohtto.http_websocket.WebSocketError
.
Führen Sie die folgenden zwei Skripte server.py
und client.py
und stoppen Sie dann client.py
durch 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"
Betriebssystem: CentOS Linux 7
Linux-Kernel: 3.10.0-514.16.1.el7.x86_64
Python: 3.5.3
aiohttp: 2.2.3
Technisch erstellt aiohttp eine Aufgabe pro Client-Anfrage.
Beim Trennen des Clients stoppt das System die Task so schnell wie möglich.
Die einzige Möglichkeit, dies zu tun, ist das Abbrechen der Aufgabe (angenommen, der Web-Handler wartet auf eine Antwort von der DB oder einem anderen Dienst, wir möchten ihn auch abbrechen, ohne auf eine explizite Operation über die Verbindung zum Websocket-Client zu warten).
Task.cancel()
wird durch das Senden von asyncio.CancelledError
Ausnahme ausgeführt, die Ausnahmeklasse wird vom Standard Exception
. Dies ist ein asyncio-Verhalten, nichts Spezifisches für aiohttp selbst.
Das einzige, was ich vorschlagen könnte, ist, CancelledError
explizit in Ihrem Handler zu fangen:
try:
...
except asyncio.CancelledError:
pass
except Exception as exc:
log(exc)
Oder Sie könnten einfach nicht so breite Texte wie Exception
.
Ich sehe zwei Möglichkeiten:
CancelledError
ist in der asynchronen Welt normal.CancelledError
abfangen und closed
Nachricht zurückgeben. Ich denke, dies ist die bessere Lösung für Webocket-Handler.Vielleicht ist es besser, eine separate ConnectionClosed-Ausnahme einzuführen, genau wie in der Websockets- Bibliothek?
Technisch erstellt aiohttp eine Aufgabe pro Client-Anfrage.
Beim Trennen des Clients stoppt das System die Task so schnell wie möglich.
Die einzige Möglichkeit, dies zu tun, ist das Abbrechen der Aufgabe (angenommen, der Web-Handler wartet auf eine Antwort von der DB oder einem anderen Dienst, wir möchten ihn auch abbrechen, ohne auf eine explizite Operation über die Verbindung zum Websocket-Client zu warten).
Ich glaube, ich wurde gerade von diesem Verhalten gebissen. Ich hatte einen Code wie diesen:
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
Nachdem ich es in Produktion genommen hatte, stellte ich fest, dass der aufregende Teil von acquire_resource_3()
stillschweigend übersprungen würde. Weitere Protokollierung ergab, dass ein CancelledError
innerhalb von acquire_resource_3
. Folgendes ist meiner Meinung nach passiert:
async for msg in ws
Schleife wird beendet, AsyncExitStack
beginnt sich abzuwickeln, der Ausgangsteil von acquire_resource_3
beginnt mit der Ausführung, trifft auf await
aiohttp
bricht die Handler-Aufgabe abCancelledError
wird innerhalb von handler
zum aktuellen await
erhöht, der innerhalb von acquire_resource_3
, daher wird der verbleibende Teil von acquire_resource_3
übersprungenacquire_resource_2
und acquire_resource_1
immer noch normal ausgeführt, da sie aus ihrer Sicht einfach einen asynchronen Kontext bei einer Ausnahme verlassenDies ist ein wirklich seltsames Problem, insbesondere weil es die Erwartung bricht, dass der Exit-Teil eines Kontextmanagers immer ausgeführt wird. Ich musste im Grunde alle asynchronen Kontexte wie folgt vor dem Abbruch schützen:
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()
Gibt es einen besseren Weg, dies zu tun?
Hilfreichster Kommentar
Vielleicht ist es besser, eine separate ConnectionClosed-Ausnahme einzuführen, genau wie in der Websockets- Bibliothek?