异步异常处理在使用 ensure_future 时有效,但在使用 run_until_complete 时无效

Async exception handling works when using ensure_future but not when using run_until_complete

我正在使用 aiologger 进行异步日志记录,并编写了两个函数来覆盖默认异常处理程序:

from aiologger import Logger as AioLogger
from aiologger.levels import LogLevel
import asyncio

logger = AioLogger.with_default_handlers(name='test', level=LogLevel.NOTSET,
                                         loop=asyncio.get_event_loop())

def excepthook(exc_type, exc_value, exc_traceback):
    print('excepthook called.')
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return
    logger.error(f'Uncaught Exception: {exc_type.__name__}')

def asyncio_exception_handler(loop, context):
    print('asyncio_exception_handler called.')
    exception = context.get('exception', None)
    if exception:
        exc_info = (type(exception), exception, exception.__traceback__)
        if issubclass(exception.__class__, KeyboardInterrupt):
            sys.__excepthook__(*exc_info)
            return
        logger.error(f'Uncaught Exception: {exc_info[0].__name__}')
    else:
        logger.error(context['message'])

然后,我用我提供的异常处理程序覆盖了异常处理程序:

import sys

sys.excepthook = excepthook
asyncio.get_event_loop().set_exception_handler(asyncio_exception_handler)

最后,我写了一个简单的代码来测试功能:

async def main():
    raise RuntimeError('Uncaught Test')

loop = asyncio.get_event_loop()
asyncio.ensure_future(main())
loop.run_forever()

这按预期工作,输出为:

asyncio_exception_handler called.
Uncaught Exception: RuntimeError
^Cexcepthook called.
Traceback (most recent call last):
  File "examples/sample.py", line 110, in <module>
    loop.run_forever()
  File "/usr/lib/python3.8/asyncio/base_events.py", line 570, in run_forever
    self._run_once()
  File "/usr/lib/python3.8/asyncio/base_events.py", line 1823, in _run_once
    event_list = self._selector.select(timeout)
  File "/usr/lib/python3.8/selectors.py", line 468, in select
    fd_event_list = self._selector.poll(timeout, max_ev)
KeyboardInterrupt

(异常后进程保持打开状态,我必须发送KeyboardInterrupt终止它。)


但是,如果我将 asyncio.ensure_future(main()) 替换为 loop.run_until_complete(main()),一切都会变得疯狂,应用程序会在没有任何日志的情况下退出:

$ python main.py 
excepthook called.
$

令人困惑的部分是,在这种情况下,我的 excepthook 函数被执行而不是 asyncio_exception_handler。我的看法是,以某种方式使用 loop.run_until_complete() 会将代码视为非异步,因此调用 logger.error() 创建异步任务没有任何效果。


当我的代码是 运行 loop.run_until_complete() 时,我如何设法使用我的异常处理程序来记录未捕获的异常?我提供的两个场景有什么区别?我对 asyncio 不是很好,我可能在这里遗漏了一些琐碎的笔记。

AioLogger 是一个异步日志记录框架,它依赖于 运行 的事件循环。当您从 ensure_future 引发时,您的事件循环仍然是 运行ning,直到您按下 Ctrl+C,这就是您看到日志的原因。另一方面,当您使用 run_until_complete(main()) 时,run_until_complete 之后的事件循环不会 运行 引发任何事件,因此 excepthook 安排的日志消息将被删除。

要解决此问题,您可以在记录器调用 excepthook 后 运行 类似 asyncio.sleep(0.001) 的内容以确保日志消息通过:

def excepthook(exc_type, exc_value, exc_traceback):
    print('excepthook called.')
    if issubclass(exc_type, KeyboardInterrupt):
        sys.__excepthook__(exc_type, exc_value, exc_traceback)
        return
    logger.error(f'Uncaught Exception: {exc_type.__name__}')
    # sleep a bit to give the log a chance to flush
    loop.run_until_complete(asyncio.sleep(0.001))

一般来说,asyncio 异常处理程序是最后的错误报告手段,而不是您的业务逻辑应该依赖的东西。当您调用 run_until_complete() 时,没有理由将异常传递给 asyncio 异常处理程序,因为异常会传播到 run_until_complete() 的调用者,后者可以按通常的方式处理它。 (在你的例子中,调用 sys.excepthook 是因为错误传播到顶层。)当你调用 ensure_future 时,你会得到一个 Future 实例(或其 Task 子类的实例) 您可以 await 或调用 result()exception() 来获取异常。如果你这样做 none 并且只允许未来被 GC 销毁,它会将异常传递给异步处理程序以记录它。