如何将“协程从未被等待”处理为异常?

huangapple go评论67阅读模式
英文:

How to handle "coroutine never awaited" as an exception?

问题

关于如何忽略或解决这个问题有很多答案,但我需要的是一种能处理这种情况的解决方案,因为我们有一个庞大的项目,这种情况很可能发生,但它是静默的,因此有一段时间内不被注意到。

当发生这种情况时(某个地方的协程未被等待),我想要调用一些名为的函数,或者引发一个异常。

我尝试过添加以下代码,正如ChatGPT建议的那样,但它不起作用:

# 将警告模式设置为“错误”
import warnings
warnings.simplefilter('error', RuntimeWarning)

现在控制台只是显示:

“在:<协程对象XXX,地址0x7fbca2e4eb90>中忽略的异常”

仍然没有异常,也没有办法捕获和对这个错误做出反应。

英文:

There are many answers about how to ignore or fix this, but what I need is a solution that would handle these, because we have a huge project and something like this is bound to happen, but it's silent and therefore goes unnoticed for some time.

I want to have some function called, or an exception raised, when this happens (a coroutine somewhere wasn't awaited).

I've tried adding this, as suggested by ChatGPT, but it doesn't work:

# Set the warning mode to &#39;error&#39;
import warnings
warnings.simplefilter(&#39;error&#39;, RuntimeWarning)

All it did is now the console says:

&quot;Exception ignored in: &lt;coroutine object XXX at 0x7fbca2e4eb90&gt;

There is still no exception and no way to catch and react to this error.

答案1

得分: 1

主要问题是,仅当协程本身超出范围时才会发生这种情况,通常是在循环关闭时,程序结束时(或者在架构中包含的某个“重置阶段”例外情况下)。错误只有在代码中找到对协程的调用时,如果有对它的引用,才会被引发。由于通常会希望获得协程的返回值,忘记await只是将协程本身存储在应该包含结果的变量中,但不会引发错误。这应该很容易在 linters 和类似工具上捕捉到 - 甚至在静态类型检查中 - 但在运行时可能很难。

由于您希望在引发异常时获得“回调” - 也许我们可以看一下警告代码,看看负责在那里引发异常的代码是否可以在运行时进行修补,以使其回调而不是引发异常。普通的 Python 异常无法通过系统范围的挂钩捕获(除非在运行时根本没有处理异常的情况下进行了程序范围的挂钩 - 这不是情况,因为在这种情况下,asyncio 循环 do handle 引发的 RuntimeWarning 以打印“Exception ignored”消息。

但是警告通常不会引发 - 它们是通过调用 warnings 中的 API 进行的 - 而那是可以在运行时修改的 Python 代码。

原来,如果警告保持默认值,而不更改为将其作为错误引发,Python warnings 机制将首先将其包装在 warnings.WarningMessage 实例中。可以钩入警告模块,并用一个效果期望的回调替换这个类,并在运行时进行修补(“猴子补丁”)。

所以,您可以编写像下面这样的类,然后运行以下行来将回调框架植入警告机制:

import warnings

OriginalMessage = warnings.WarningMessage

class HookableWarningMessage(warnings.WarningMessage):
     callback_registry = {}
     def __init__(self, *args, **kw):
          super().__init__(*args, **kw)
          self._check_callback()

     def _check_callback(self):
         if not isinstance(self.message, RuntimeWarning):
              return
         for arg in self.message.args:
             for key in self.callback_registry:
                  if key in arg:
                      self.callback_registry[key](self, self.message)
     def register(self, pattern, target):
           self.callback_registry[pattern] = target

warnings.WarningMessage = HookableWarningMessage

def not_awaited_coroutine_callback(self, RuntimeWarning):
    ...<handle warning here>...

warnings.WarningMessage.register(warnings.WarningMessage, "never awaited", not_awaited_coroutine_callback)
英文:

The main problem is that this only happens when the coroutine itself goes out of scope - which is typically when the loop is shoot down, at the program ending (or, exceptionally at some "reset stage" you include in your architecture).

The error won't be raised just when a call to a co-routine is found in the code if there is a reference to it. Since one typically will want to get the return value back of a co-routine, forgetting the await just stores the co-routine itself in the variable that should contain the result, but it won't raise the error. It should be easy to catch on linters and similar tools - even static type checking - but at runtime it can be hard.

Since you want a "callback" whenever the exception is raised - maybe we can take a look at the warnings code, and see if the code responsible to raise an Exception there can be patched in runtime to make that call back instead. Ordinary Python exceptions have no way to be caught with a system-wide hook (unless a program wide hook when the exception was not handled at runtime at all - which is not the case, as the asyncio loop do handle the raised RuntimeWarning in this case to print the Exception ignored message.

But warnings are not normally raised - they are issued through calling the API in warnings - and that is Python code, which is modifiable at runtime.

Turns out that if warnings are left at default, and not changed to be raised as errors, Python warnings mechanism will first wrap it in a warnings.WarningMessage instance. It is possible to hook into the warnings module, and replace this class by one which would effect the desired callback, and patch it at runtime ("monkey patch").

So, you can code a class like the one bellow, and then run the lines following to implant a callback framework into the warnings machinery:

import warnings

OriginalMessage = warnings.WarningMessage

class HookableWarningMessage(warnings.WarningMessage):
     callback_registry = {}
     def __init__(self, *args, **kw):
          super().__init__(*args, **kw)
          self._check_callback()

     def _check_callback(self):
         if not isinstance(self.message, RuntimeWarning):
              return
         for arg in self.message.args:
             for key in self.callback_registry:
                  if key in arg:
                      self.callback_registry[key](self, self.message)
     def register(self, pattern, target):
           self.callback_registry[pattern] = target

warnings.WarningMessage = HookableWarningMessage

def not_awaited_coroutine_callback(self, RuntimeWarning):
    ...&lt;handle warning here&gt;...

warnings.WarningMessage.register(warnings.WarningMessage, &quot;never awaited&quot;, not_awaited_coroutine_callback)

huangapple
  • 本文由 发表于 2023年3月15日 20:20:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/75744618.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定