首页 > 解决方案 > CoroutineExceptionHandler 应该如何处理 OutOfMemoryError 或其他致命错误?

问题描述

我正在实现一个自定义的 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)
}

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

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

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

我的第二个问题,直接从那开始,是:什么是我处理它的合适方式?我能想到至少三个选项:

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

标签: kotlinerror-handlingjvmout-of-memorykotlin-coroutines

解决方案


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

    Kotlin 文档异常状态:

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

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

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

    协程构建器有两种形式:自动传播异常(启动和演员)或向用户公开它们(异步和生产)。

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

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

    如果协程遇到除 CancellationException 以外的异常,它会取消其父级并出现该异常。此行为不能被覆盖,并用于为结构化并发提供稳定的协程层次结构。

  2. 我处理它的合适方法是什么?

    在任何情况下:尝试先记录它(就像你已经做的那样)。考虑提供尽可能多的诊断数据(包括堆栈跟踪)。

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

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

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

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

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

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

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

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

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


推荐阅读