Aiohttp: WebSocketの読み取りメッセージループは、接続が予期せず閉じられたときに低レベルのCanceledErrorを発生させます

作成日 2017年07月06日  ·  4コメント  ·  ソース: aio-libs/aiohttp

実際の動作

メッセージループasync for msg in ws:を読み取ると、接続が予期せず閉じられたときに低レベルのconcurrent.futures._base.CancelledError発生します。

期待される動作

タイプaiohtto.http_websocket.WSMsgType.ERRORメッセージを受け取るか、ループをサイレントに停止するか、少なくともaiohtto.http_websocket.WebSocketErrorです。

再現する手順

次の2つのスクリプトserver.pyclient.pyから、 client.pyCtrl+C停止します。

server.py

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()

client.py

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"

あなたの環境

OS:CentOS Linux 7
Linuxカーネル:3.10.0-514.16.1.el7.x86_64
Python:3.5.3
aiohttp:2.2.3

最も参考になるコメント

WebSocketライブラリで行われたのと同じ方法で、別のConnectionClosed例外を導入する方が良いのではないでしょうか。

全てのコメント4件

技術的には、aiohttpはクライアント要求ごとにタスクを作成します。
クライアントが切断されると、システムはタスクをできるだけ早く停止します。
これを行う唯一の方法はタスクのキャンセルです(WebハンドラーがDBまたは他のサービスからの応答を待機していると仮定し、WebSocketクライアントへの接続を介した明示的な操作を待たずにキャンセルしたいとします)。

Task.cancel()は、 asyncio.CancelledError例外を送信することによって実行されます。例外クラスは、標準のExceptionから派生します。 これは非同期動作であり、aiohttp自体に固有のものではありません。

私が提案できる唯一のことは、ハンドラーでCancelledErrorを明示的にキャッチすることです。

try:
    ...
except asyncio.CancelledError:
    pass
except Exception as exc:
    log(exc)

または、 Exceptionような幅広いタイプをキャッチできない可能性があります。

2つのオプションがあります。

  • 何も変更しないでください。非同期の世界ではCancelledErrorは正常です。
  • CancelledErrorをキャッチし、 closedメッセージを返します。 これはwebocketハンドラーにとってより良い解決策だと思います。

WebSocketライブラリで行われたのと同じ方法で、別のConnectionClosed例外を導入する方が良いのではないでしょうか。

技術的には、aiohttpはクライアント要求ごとにタスクを作成します。
クライアントが切断されると、システムはタスクをできるだけ早く停止します。
これを行う唯一の方法はタスクのキャンセルです(WebハンドラーがDBまたは他のサービスからの応答を待機していると仮定し、WebSocketクライアントへの接続を介した明示的な操作を待たずにキャンセルしたいとします)。

この行動に噛まれたばかり

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

それを本番環境に置いた後、 acquire_resource_3()の既存の部分が黙ってスキップされることに気づきました。 より多くのロギングにより、 CancelledErroracquire_resource_3内で発生していることが明らかになりました。 これが私が起こったと思うことです:

  • クライアントがWebSocketを閉じます
  • async for msg in wsループが終了し、 AsyncExitStackが巻き戻され始め、 acquire_resource_3の終了部分が実行を開始し、 awaitにヒットします。
  • aiohttpはハンドラータスクをキャンセルします
  • CancelledErrorは、現在のawait内のhandler内で発生します。これは、 acquire_resource_3にあるため、 acquire_resource_3の残りの部分はスキップされます。
  • acquire_resource_2acquire_resource_1の終了部分は、それらの観点からは例外で非同期コンテキストを終了しているだけなので、引き続き正常に実行されます。

これは本当に奇妙な問題です。特に、コンテキストマネージャーの既存の部分が常に実行されるという期待をどのように破るかが原因です。 基本的に、次のように、すべての非同期コンテキストをキャンセルから保護する必要がありました。

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()

これを行うためのより良い方法はありますか?

このページは役に立ちましたか?
0 / 5 - 0 評価