CoroutineExceptionHandler 应该如何处理 OutOfMemoryError 或其他致命错误?
What should a CoroutineExceptionHandler do with an OutOfMemoryError or other fatal error?
我正在实现自定义 Kotlin CoroutineScope that deals with receiving, handling and responding to messages over a WebSocket connection. The scope's lifecycle is tied to the WebSocket session, so it's active as long as the WebSocket is open. As part of the coroutine scope's context, I've installed a custom exception handler,如果出现未处理的错误,它将关闭 WebSocket 会话。是这样的:
val handler = CoroutineExceptionHandler { _, exception ->
log.error("Closing WebSocket session due to an unhandled error", exception)
session.close(POLICY_VIOLATION)
}
我惊讶地发现异常处理程序不仅接收异常,而且实际上会为所有未处理的可抛出对象调用,包括 Error
的子类型。我不确定我应该如何处理这些,因为我从 Java API documentation for Error
知道 "an Error
[...] 表示合理的应用程序不应尝试的严重问题赶上.
我最近 运行 遇到的一个特殊情况是 OutOfMemoryError
由于会话处理的数据量。 OutOfMemoryError
被我的 CoroutineExceptionHandler
接收到,这意味着它已被记录并且 WebSocket 会话已关闭,但应用程序继续 运行。这让我很不舒服,因为我知道 OutOfMemoryError
可以在代码执行期间的任何时候抛出,结果会使应用程序处于无法恢复的状态。
我的第一个问题是:为什么 Kotlin API 选择将这些错误传递给 CoroutineExceptionHandler
让我这个程序员来处理?
我的第二个问题是:我处理它的合适方法是什么?我至少可以想到三个选项:
- 继续做我现在正在做的事情,即关闭引发错误的 WebSocket 会话,并希望应用程序的其余部分能够恢复。正如我所说,这让我感到不舒服,尤其是当我读到像 this one, in response to a question about catching
OutOfMemoryError
in Java 这样的答案时,强烈建议不要尝试从此类错误中恢复。
- 重新抛出错误,让它传播到线程。在正常(或框架)代码中遇到
Error
的任何其他情况下,我通常都会这样做,因为它最终会导致 JVM 崩溃。但是,在我的协程范围内(与一般的多线程一样),这不是一个选项。重新抛出异常只是最终将其发送到 线程的 UncaughtExceptionHandler
,它不会对其执行任何操作。
- 启动应用程序的完全关闭。停止应用程序感觉是最安全的事情,但我想确保我完全理解其中的含义。协程是否有任何机制可以将致命错误传播到应用程序的其余部分,或者我是否需要自己编写该功能的代码? 'application-fatal' 错误的传播是否是 Kotlin 协程 API 设计者已经考虑过的,或者可能会在未来的版本中考虑的?其他多线程模型通常如何处理这些类型的错误?
为什么 Kotlin API 选择将这些错误传递给 CoroutineExceptionHandler
让我这个程序员来处理?
All exception classes in Kotlin are descendants of the class Throwable.
所以 Kotlin 文档似乎对所有类型的 Throwable
都使用术语 exception,包括 Error
.
是否应该传播协程中的异常实际上是选择协程构建器的结果(参见Exception propagation):
Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce).
如果您在 WebSocket 范围内收到未处理的异常,则表示调用链中存在 non-recoverable 问题。可恢复的异常应该在最接近的可能调用级别进行处理。因此,很自然地,您不知道如何在 WebSocket 范围内响应并指示您正在调用的代码有问题。
协程函数然后选择安全路径并取消父作业(包括取消其子作业),如 Cancellation and exceptions:
中所述
If a coroutine encounters an exception other than CancellationException, it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for structured concurrency.
我该如何处理比较合适?
无论如何:首先尝试记录它(就像你已经做的那样)。考虑提供尽可能多的诊断数据(包括堆栈跟踪)。
请记住,协程库已经为您取消了作业。在许多情况下,这就足够了。不要指望协程库能做的比这更多(现在不会,将来的版本不会)。它没有做得更好的知识。应用服务器通常提供异常处理的配置,例如如 Ktor.
除此之外,它取决于并且可能涉及启发式和 trade-offs。不要盲目地遵循“最佳实践”。您比其他人更了解您的应用程序的设计和要求。需要考虑的一些方面:
为了实现高效运营,请尽可能快速、无缝地自动恢复受影响的服务。有时简单的方法(关闭并重新启动所有 可能 受影响的东西)就足够了。
评估从未知状态恢复的影响。这只是一个很容易被注意到的小故障,还是人们的生命取决于结果?如果出现未捕获的异常:应用程序是否以释放资源和回滚事务的方式设计?依赖系统能否继续不受影响?
如果您可以控制被调用的函数,则可以为可恢复异常(仅具有暂时性和 non-damaging 效果)引入单独的异常 class(层次结构),并且区别对待。
尝试恢复部分工作的系统时,考虑分阶段的方法并处理 follow-up 故障:
- 如果仅关闭协程就足够了,就这样吧。您甚至可以保持 WebSocket 会话打开并向客户端发送重新启动指示消息。考虑 Kotlin 协程文档中关于 Supervision 的章节。
- 如果这样做不安全(或发生 follow-up 错误),请考虑关闭线程。这与分派到不同线程的协程无关,但对于没有 inter-thread 耦合的系统来说是一个合适的解决方案。
- 如果仍然不安全(或发生 follow-up 错误),请关闭整个 JVM。这一切都可能取决于异常的根本原因。
如果您的应用程序修改持久数据,请确保它是 crash-proof 设计的(例如,通过原子事务或其他自动恢复策略)。
如果您的整个应用程序的设计目标是 crash-proof,请考虑 crash-only software design 而不是(可能很复杂的)关闭程序。
在 OutOfMemoryError 的情况下,如果原因是奇点(例如,一个巨大的分配),恢复可以如上所述分阶段进行。另一方面,如果 JVM 甚至不能分配微小的位,通过 Runtime.halt()
强行终止 JVM 可能会防止级联 follow-up 错误。
我正在实现自定义 Kotlin CoroutineScope that deals with receiving, handling and responding to messages over a WebSocket connection. The scope's lifecycle is tied to the WebSocket session, so it's active as long as the WebSocket is open. As part of the coroutine scope's context, I've installed a custom exception handler,如果出现未处理的错误,它将关闭 WebSocket 会话。是这样的:
val handler = CoroutineExceptionHandler { _, exception ->
log.error("Closing WebSocket session due to an unhandled error", exception)
session.close(POLICY_VIOLATION)
}
我惊讶地发现异常处理程序不仅接收异常,而且实际上会为所有未处理的可抛出对象调用,包括 Error
的子类型。我不确定我应该如何处理这些,因为我从 Java API documentation for Error
知道 "an Error
[...] 表示合理的应用程序不应尝试的严重问题赶上.
我最近 运行 遇到的一个特殊情况是 OutOfMemoryError
由于会话处理的数据量。 OutOfMemoryError
被我的 CoroutineExceptionHandler
接收到,这意味着它已被记录并且 WebSocket 会话已关闭,但应用程序继续 运行。这让我很不舒服,因为我知道 OutOfMemoryError
可以在代码执行期间的任何时候抛出,结果会使应用程序处于无法恢复的状态。
我的第一个问题是:为什么 Kotlin API 选择将这些错误传递给 CoroutineExceptionHandler
让我这个程序员来处理?
我的第二个问题是:我处理它的合适方法是什么?我至少可以想到三个选项:
- 继续做我现在正在做的事情,即关闭引发错误的 WebSocket 会话,并希望应用程序的其余部分能够恢复。正如我所说,这让我感到不舒服,尤其是当我读到像 this one, in response to a question about catching
OutOfMemoryError
in Java 这样的答案时,强烈建议不要尝试从此类错误中恢复。 - 重新抛出错误,让它传播到线程。在正常(或框架)代码中遇到
Error
的任何其他情况下,我通常都会这样做,因为它最终会导致 JVM 崩溃。但是,在我的协程范围内(与一般的多线程一样),这不是一个选项。重新抛出异常只是最终将其发送到 线程的UncaughtExceptionHandler
,它不会对其执行任何操作。 - 启动应用程序的完全关闭。停止应用程序感觉是最安全的事情,但我想确保我完全理解其中的含义。协程是否有任何机制可以将致命错误传播到应用程序的其余部分,或者我是否需要自己编写该功能的代码? 'application-fatal' 错误的传播是否是 Kotlin 协程 API 设计者已经考虑过的,或者可能会在未来的版本中考虑的?其他多线程模型通常如何处理这些类型的错误?
为什么 Kotlin API 选择将这些错误传递给
CoroutineExceptionHandler
让我这个程序员来处理?All exception classes in Kotlin are descendants of the class Throwable.
所以 Kotlin 文档似乎对所有类型的
Throwable
都使用术语 exception,包括Error
.是否应该传播协程中的异常实际上是选择协程构建器的结果(参见Exception propagation):
Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce).
如果您在 WebSocket 范围内收到未处理的异常,则表示调用链中存在 non-recoverable 问题。可恢复的异常应该在最接近的可能调用级别进行处理。因此,很自然地,您不知道如何在 WebSocket 范围内响应并指示您正在调用的代码有问题。
协程函数然后选择安全路径并取消父作业(包括取消其子作业),如 Cancellation and exceptions:
中所述If a coroutine encounters an exception other than CancellationException, it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for structured concurrency.
我该如何处理比较合适?
无论如何:首先尝试记录它(就像你已经做的那样)。考虑提供尽可能多的诊断数据(包括堆栈跟踪)。
请记住,协程库已经为您取消了作业。在许多情况下,这就足够了。不要指望协程库能做的比这更多(现在不会,将来的版本不会)。它没有做得更好的知识。应用服务器通常提供异常处理的配置,例如如 Ktor.
除此之外,它取决于并且可能涉及启发式和 trade-offs。不要盲目地遵循“最佳实践”。您比其他人更了解您的应用程序的设计和要求。需要考虑的一些方面:
为了实现高效运营,请尽可能快速、无缝地自动恢复受影响的服务。有时简单的方法(关闭并重新启动所有 可能 受影响的东西)就足够了。
评估从未知状态恢复的影响。这只是一个很容易被注意到的小故障,还是人们的生命取决于结果?如果出现未捕获的异常:应用程序是否以释放资源和回滚事务的方式设计?依赖系统能否继续不受影响?
如果您可以控制被调用的函数,则可以为可恢复异常(仅具有暂时性和 non-damaging 效果)引入单独的异常 class(层次结构),并且区别对待。
尝试恢复部分工作的系统时,考虑分阶段的方法并处理 follow-up 故障:
- 如果仅关闭协程就足够了,就这样吧。您甚至可以保持 WebSocket 会话打开并向客户端发送重新启动指示消息。考虑 Kotlin 协程文档中关于 Supervision 的章节。
- 如果这样做不安全(或发生 follow-up 错误),请考虑关闭线程。这与分派到不同线程的协程无关,但对于没有 inter-thread 耦合的系统来说是一个合适的解决方案。
- 如果仍然不安全(或发生 follow-up 错误),请关闭整个 JVM。这一切都可能取决于异常的根本原因。
如果您的应用程序修改持久数据,请确保它是 crash-proof 设计的(例如,通过原子事务或其他自动恢复策略)。
如果您的整个应用程序的设计目标是 crash-proof,请考虑 crash-only software design 而不是(可能很复杂的)关闭程序。
在 OutOfMemoryError 的情况下,如果原因是奇点(例如,一个巨大的分配),恢复可以如上所述分阶段进行。另一方面,如果 JVM 甚至不能分配微小的位,通过
Runtime.halt()
强行终止 JVM 可能会防止级联 follow-up 错误。