在单个任务中捕获异常并重新启动它们

时间:2019-03-15 13:56:45

标签: python python-3.x python-3.6 python-asyncio

如果我在一个顶级类中创建一系列asyncio任务,那么所有这些任务基本上应该永远运行,就像这样:

self.event_loop.create_task(...)
self.event_loop.create_task(...)
self.event_loop.create_task(...)
...

self.event_loop.run_forever()

# Once we fall out of the event loop, collect all remaining tasks,
# cancel them, and terminate the asyncio event loop
tasks = asyncio.Task.all_tasks()
group = asyncio.gather(*tasks, return_exceptions=True)
group.cancel()
self.event_loop.run_until_complete(group)
self.event_loop.close()

上面的代码无法处理以下情况,我发现自己越来越需要这种情况,并且在Googling或asyncio文档中也没有看到示例:

如果其中一个任务因异常而失败,则该异常不会得到处理-其他所有任务都将继续执行,但是一个任务只是默默地暂停(除了异常输出)。

所以,我该怎么办

  • 设置要处理的异常,以使失败不再消失
  • 最重要的是,重新启动失败的任务,仅针对该任务重新有效地运行self.event_loop.create_task(...)吗?这似乎需要找到在事件循环中接收到异常的任务,将其删除,然后添加一个新任务-如何做到这一点对我来说还不清楚。
  • 允许没有问题的任务继续不间断地进行。希望避免处理接收到异常的任务的任何副作用。

1 个答案:

答案 0 :(得分:4)

未捕获的异常附加到任务对象,可以通过Task.exception() method从该对象检索。 self.event_loop.create_task(...)调用返回任务对象,因此您需要收集这些对象以检查异常。

如果您希望在发生异常时重新安排任务的时间,则希望在新任务中执行此操作(因为您希望它在事件循环中运行),或使用包装程序协程会捕获异常,然后再次重新运行给定的协程。

后者可能看起来像:

import traceback

def rerun_on_exception(coro, *args, **kwargs):
    while True:
        try:
            await coro(*args, **kwargs)
        except asyncio.CancelledError:
            # don't interfere with cancellations
            raise
        except Exception:
            print("Caught exception")
            traceback.print_exc()

然后在将协程作为任务进行调度时,将协程用上述协程包起来:

self.event_loop.create_task(rerun_on_exception(coroutine_uncalled, arg1value, ... kwarg1=value, ...)

例如每次出现异常时,都要传入创建协程的参数。

另一种选择是在单独的任务中使用asyncio.wait(),以便您可以在循环运行时监视异常,并决定如何在那里处理异常,然后:

def exception_aware_scheduler(*task_definitions, loop=None):
    if loop is None:
        loop = asyncio.get_event_loop()

    task_arguments = {
        loop.create_task(coro(*args, **kwargs)): (coro, args, kwargs)
        for coro, args, kwargs in task_definitions
    }
    while tasks:
        done, pending = await asyncio.wait(
            tasks.keys(), loop=loop, return_when=asyncio.FIRST_EXCEPTION
        )
        for task in done:
            if task.exception() is not None:
                print('Task exited with exception:')
                task.print_stack()
                print('Rescheduling the task\n')
                coro, args, kwargs = tasks.pop(task)
                tasks[loop.create_task(coro(*args, **kwargs))] = coro, args, kwargs

当计划中的任何一项任务由于异常而退出时,事件循环将再次asyncio.wait()进行控制,但是直到发生这种情况之前,任务可能已被取消或仅完成了工作。当某个任务由于异常而退出时,您需要一种方法来再次创建相同的协程(具有相同的参数),因此需要上面的*args, **kwargs设置。

您只计划exception_aware_scheduler(),传入您要传递的任务:

tasks = (
    (coro1, (), {}),  # no arguments
    (coro2, ('arg1', 'arg2'), {}),
    # ...
)
loop.create_task(exception_aware_scheduler(*task_definitions, loop=loop))