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

重现步骤

运行以下两个脚本server.pyclient.py ,然后通过Ctrl+C停止client.py Ctrl+C

服务器.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()

客户端.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"

您的环境

操作系统:CentOS Linux 7
Linux 内核:3.10.0-514.16.1.el7.x86_64
蟒蛇:3.5.3
aiohttp: 2.2.3

最有用的评论

也许最好引入一个单独的 ConnectionClosed 异常,就像在websockets库中所做的那样?

所有4条评论

从技术上讲,aiohttp 根据客户端请求创建一个任务。
在客户端断开连接时,系统会尽快停止任务。
唯一的方法是取消任务(假设 web 处理程序正在等待来自 DB 或其他服务的响应,我们也想取消它而不等待通过连接到 websocket 客户端的显式操作)。

Task.cancel()是通过发送asyncio.CancelledError异常来完成的,异常类派生自标准Exception 。 这是 asyncio 行为,没有特定于 aiohttp 本身。

我唯一能建议的是在您的处理程序中明确捕获CancelledError

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

或者你不能像Exception这样广泛的类型。

我看到两个选项:

  • 不要改变任何东西, CancelledError在异步世界中是正常的。
  • 捕获CancelledError并返回closed消息。 我认为,这对于 webocket 处理程序来说是更好的解决方案。

也许最好引入一个单独的 ConnectionClosed 异常,就像在websockets库中所做的那样?

从技术上讲,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是在handler内在当前await内引发的,它在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 等级

相关问题

jonringer picture jonringer  ·  4评论

ZeusFSX picture ZeusFSX  ·  5评论

ahuigo picture ahuigo  ·  5评论

rckclmbr picture rckclmbr  ·  5评论

Codeberg-AsGithubAlternative-buhtz picture Codeberg-AsGithubAlternative-buhtz  ·  3评论