CoroutineExceptionHandler 应该如何处理 OutOfMemoryError 或其他致命错误?

Sam*_*Sam 5 error-handling jvm out-of-memory kotlin kotlin-coroutines

我正在实现一个自定义 Kotlin CoroutineScope,用于处理通过 WebSocket 连接接收、处理和响应消息。该作用域的生命周期与 WebSocket 会话相关,因此只要 WebSocket 打开,它就处于活动状态。作为协程作用域上下文的一部分,我安装了一个自定义异常处理程序,如果存在未处理的错误,该处理程序将关闭 WebSocket 会话。是这样的:

val handler = CoroutineExceptionHandler { _, exception -> 
    log.error("Closing WebSocket session due to an unhandled error", exception)
    session.close(POLICY_VIOLATION)
}
Run Code Online (Sandbox Code Playgroud)

我惊讶地发现异常处理程序不仅接收异常,而且实际上为所有未处理的可抛出对象(包括Error. 我不确定应该如何处理这些,因为我从Java API 文档Error中知道“一个Error[...] 表明合理的应用程序不应尝试捕获的严重问题”

我最近遇到的一个特殊情况是OutOfMemoryError由于会话处理的数据量所致。OutOfMemoryErrormy 收到了,这CoroutineExceptionHandler意味着它已被记录并且 WebSocket 会话已关闭,但应用程序继续运行。这让我感到不舒服,因为我知道OutOfMemoryError在代码执行期间的任何时候都可以抛出 an ,从而导致应用程序处于不可恢复的状态。

我的第一个问题是:为什么 Kotlin API 选择将这些错误传递给CoroutineExceptionHandler我(程序员)来处理?

我的第二个问题是:我处理它的适当方法是什么?我至少可以想到三个选择:

  1. 继续执行我现在正在执行的操作,即关闭引发错误的 WebSocket 会话,并希望应用程序的其余部分能够恢复。正如我所说,这让我感到不舒服,特别是当我阅读这样的答案时,这是针对有关OutOfMemoryError在 Java 中捕获的问题的回答,该问题强烈建议不要尝试从此类错误中恢复。
  2. 重新抛出错误,让它传播到线程。这就是我在遇到Error正常(或框架)代码的任何其他情况下通常会做的事情,因为它最终会导致 JVM 崩溃。不过,在我的协程范围内(与一般的多线程一样),这不是一个选择。重新抛出异常最终只会将其发送到线程 UncaughtExceptionHandler的,而线程不会对其执行任何操作。
  3. 启动应用程序的完全关闭。停止应用程序感觉是最安全的事情,但我想确保我完全理解其含义。协程是否有任何机制可以将致命错误传播到应用程序的其余部分,或者我需要自己编写该功能的代码吗?Kotlin 协程 API 设计者是否已考虑或可能在未来版本中考虑传播“应用程序致命”错误?其他多线程模型通常如何处理此类错误?

Oli*_* O. 4

  1. 为什么 Kotlin API 选择将这些错误传递给CoroutineExceptionHandler我(程序员)来处理?

    关于异常的 Kotlin 文档指出:

    Kotlin 中的所有异常类都是 Throwable 类的后代。

    因此,Kotlin 文档似乎对所有类型的异常都使用了术语“异常”Throwable,包括Error.

    协程中的异常是否应该传播实际上是选择协程构建器的结果(参见异常传播):

    协程构建器有两种类型:自动传播异常(启动和执行者)或将异常暴露给用户(异步和生成)。

    如果您在 WebSocket 范围内收到未处理的异常,则表明调用链中存在不可恢复的问题。可恢复的异常预计在最接近的可能调用级别进行处理。因此,您很自然地不知道如何在 WebSocket 范围内做出响应,并且表明您正在调用的代码存在问题。

    然后,协程函数选择安全路径并取消父作业(包括取消其子作业),如取消和例外中所述:

    如果协程遇到 CancellationException 以外的异常,它会取消带有该异常的父级。此行为无法重写,用于为结构化并发提供稳定的协程层次结构。

  2. 我的处理方式合适吗?

    无论如何:尝试先记录它(就像您已经做的那样)。考虑提供尽可能多的诊断数据(包括堆栈跟踪)。

    请记住,协程库已经为您取消了作业。在许多情况下,这已经足够了。不要指望协程库能做更多的事情(不是现在,也不是将来的版本)。它不具备做得更好的知识。应用程序服务器通常提供异常处理的配置,例如在Ktor中。

    除此之外,这取决于,并且可能涉及启发法和权衡。不要盲目遵循“最佳实践”。您比其他人更了解您的应用程序的设计和要求。需要考虑的一些方面:

    • 为了高效运营,尽可能快速、无缝地自动恢复受影响的服务。有时,简单的方法(关闭并重新启动可能受影响的所有内容)就足够了。

    • 评估从未知状态恢复的影响。这只是一个很容易注意到的小故障,还是人们的生命取决于结果?如果出现未捕获的异常:应用程序的设计方式是否可以释放资源并回滚事务?依赖系统可以不受影响地继续运行吗?

    • 如果您可以控制调用的函数,则可以为可恢复的异常(仅具有暂时性和非破坏性的效果)引入单独的异常类(层次结构),并以不同的方式对待它们。

    • 当尝试恢复部分工作的系统时,请考虑分阶段方法并处理后续故障:

      • 如果仅关闭协程就足够了,那么就这样吧。您甚至可以保持 WebSocket 会话打开并向客户端发送重新启动指示消息。请考虑Kotlin 协程文档中有关监督的章节。
      • 如果这样做不安全(或者发生后续错误),请考虑关闭该线程。这与调度到不同线程的协程无关,但对于没有线程间耦合的系统来说是一个正确的解决方案。
      • 如果仍然不安全(或者发生后续错误),请关闭整个 JVM。这一切可能取决于异常的根本原因。
    • 如果您的应用程序修改持久性数据,请确保它在设计上是防崩溃的(例如通过原子事务或其他自动恢复策略)。

    • 如果整个应用程序的设计目标是防崩溃,请考虑仅崩溃的软件设计,而不是(可能很复杂)的关闭程序。

    • 在发生 OutOfMemoryError 的情况下,如果原因是奇点(例如一个巨大的分配),恢复可以如上所述分阶段进行。另一方面,如果 JVM 甚至无法分配微小的位,则通过强制终止 JVMRuntime.halt()可能会防止级联后续错误。