暂停Python asyncio协同程序

时间:2018-01-02 03:05:52

标签: python python-3.x asynchronous concurrency python-asyncio

由于我的项目很大程度上依赖于异步网络I / O,我总是不得不期待一些奇怪的网络错误:它是我连接到API中断的服务,还是我自己的服务器有网络问题, 或者是其他东西。这样的问题出现了,而且没有真正的解决方法。因此,我最终试图找到一种方法,在发生此类网络问题时,从外部有效地“暂停”协程的执行,直到重新建立连接。我的方法是编写一个装饰器pausable,它接受​​一个参数pause这是一个协程函数,它将yield编辑from / await,如下所示:< / p>

def pausable(pause, resume_check=None, delay_start=None):
    if not asyncio.iscoroutinefunction(pause):
        raise TypeError("pause must be a coroutine function")
    if not (delay_start is None or asyncio.iscoroutinefunction(delay_start)):
        raise TypeError("delay_start must be a coroutine function")

    def wrapper(coro):
        @asyncio.coroutine
        def wrapped(*args, **kwargs):
            if delay_start is not None:
                yield from delay_start()
            for x in coro(*args, **kwargs):
                try:
                    yield from pause()
                    yield x
                # catch exceptions the regular discord.py user might not catch
                except (asyncio.CancelledError,
                        aiohttp.ClientError,
                        websockets.WebSocketProtocolError,
                        ConnectionClosed,
                        # bunch of other network errors
                        ) as ex:
                    if any((resume_check() if resume_check is not None else False and
                            isinstance(ex, asyncio.CancelledError),
                            # clean disconnect
                            isinstance(ex, ConnectionClosed) and ex.code == 1000,
                            # connection issue
                            not isinstance(ex, ConnectionClosed))):
                        yield from pause()
                        yield x
                    else:
                        raise

        return wrapped
    return wrapper

特别注意这一点:

for x in coro(*args, **kwargs):
    yield from pause()
    yield x

使用示例(readyasyncio.Event):

@pausable(ready.wait, resume_check=restarting_enabled, delay_start=ready.wait)
@asyncio.coroutine
def send_test_every_minute():
    while True:
        yield from client.send("Test")
        yield from asyncio.sleep(60)

但是,这似乎不起作用,对我来说它似乎不是一个优雅的解决方案。是否有与Python 3.5.3及更高版本兼容的工作解决方案?与Python 3.4.4及更高版本的兼容性是可取的。

附录

仅仅try / {{}}在协同程序中提出的需要暂停的异常对我来说既不可能也不可行,因为它严重违反了核心代码设计原则(DRY)我想遵守;换句话说,除了这么多协程功能中的这么多例外情况会使我的代码变得混乱。

1 个答案:

答案 0 :(得分:0)

关于当前解决方案的几句话。

for x in coro(*args, **kwargs):
    try:
        yield from pause()
        yield x
    except
        ...

您无法通过这种方式捕获异常:

  • 异常在for-loop之外引发
  • 生成器在第一次异常后仍然耗尽(不可用)

@asyncio.coroutine
def test():
    yield from asyncio.sleep(1)
    raise RuntimeError()
    yield from asyncio.sleep(1)
    print('ok')



@asyncio.coroutine
def main():
    coro = test()
    try:
        for x in coro:
            try:
                yield x
            except Exception:
                print('Exception is NOT here.')
    except Exception:
        print('Exception is here.')

        try:
            next(coro)
        except StopIteration:
            print('And after first exception generator is exhausted.')


if __name__ ==  '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.close()

输出:

Exception is here.
And after first exception generator is exhausted.

即使可以恢复,也要考虑如果协同程序因异常而已经进行了一些清理操作会发生什么。

鉴于上述所有情况,如果某个协程只引发了异常选项,那么您可以禁止此异常(如果需要)并重新运行此协同程序。如果您愿意,可以在某些活动后重新运行。像这样:

def restart(ready_to_restart):
    def wrapper(func):
        @asyncio.coroutine
        def wrapped(*args, **kwargs):
            while True:
                try:
                    return (yield from func(*args, **kwargs))
                except (ConnectionClosed,
                        aiohttp.ClientError,
                        websockets.WebSocketProtocolError,
                        ConnectionClosed,
                        # bunch of other network errors
                        ) as ex:
                    yield from ready_to_restart.wait()


ready_to_restart = asyncio.Event()  # set it when you sure network is fine 
                                    # and you're ready to restart

<强> UPD

  

但是,如何让协程继续保持原样   现在打断了?

只是为了说清楚:

@asyncio.coroutine
def test():
    with aiohttp.ClientSession() as client:
        yield from client.request_1()
        # STEP 1:
        # Let's say line above raises error

        # STEP 2:
        # Imagine you you somehow maged to return to this place
        # after exception above to resume execution.
        # But what is state of 'client' now?
        # It's was freed by context manager when we left coroutine.
        yield from client.request_2()

在异常从外部传播之后,函数或协同程序也不会被设计为恢复执行。

只有想到的是将复杂的操作分解为可重新启动的小操作,而整个复杂的操作可以存储它的状态:

@asyncio.coroutine
def complex_operation():
    with aiohttp.ClientSession() as client:
        res = yield from step_1(client)

        # res/client - is a state of complex_operation.
        # It can be used by re-startable steps.

        res = yield from step_2(client, res)


@restart(ready_to_restart)
@asyncio.coroutine
def step_1():
    # ...


@restart(ready_to_restart)
@asyncio.coroutine
def step_2():
    # ...