为什么 io.ktor.websocket.WebSocketSession.send 在 tcp 连接关闭时抛出 CancellationException?

问题描述 投票:0回答:1

如果会话被tcp连接或peer关闭,则会抛出CancellationException,这对协程的流程有非常显着的影响,并且可能非常危险。

我期望抛出 ChannelClosedException 或类似的异常。

据我了解,当用户有明确意图时,通常会抛出 CancellationException:

  • 范围.取消,作业.取消
  • 有超时
  • 延迟.等待
  • 父子协程取消传播

现在,普通的发送函数会抛出它,那么是否还有更多这样的函数可能会抛出 CancellationException ?这会使程序的异常检查变得非常复杂。更有可能的是问题没有被认识到。

    @Test
    fun testChannelSendCatchCancellationException(): Unit = runBlocking {
        val server = MockWebServer()
        server.start()
        val serverUrl = server.url("/").toString().replaceFirst("http", "ws")
        println(serverUrl)

        val response = MockResponse().withWebSocketUpgrade(object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                val request = server.takeRequest()
                println(request)
                webSocket.close(3000, "goodbye")
            }
        })
        server.enqueue(response)

        val session = HttpClient { install(WebSockets) }.webSocketSession(serverUrl)
        launch {
            try {
                delay(500L)
                try {
                    session.send(Frame.Text("Hello, world"))
                    // Some other codes need to call when success
                } catch (e: Exception) {
                    // MY QUESTION: how to differentiate between a user's cancellation and a send failure?
                    // For withTimeout, it throws TimeoutCancellationException, differed from CancellationException
                    println("session tx caught: $e")
                    // Some other codes need to call when failed
                }
            } catch (e: CancellationException) {
                println("tx coroutine caught cancellation: $e")
                throw e
            } catch (e: Exception) {
                println("tx coroutine caught exception: $e")
                // it prints: tx coroutine caught: java.util.concurrent.CancellationException: Channel was cancelled
            }
        }
        delay(200L)
        server.close()
        delay(200L)
    }

如果send抛出与CancellationException不同的异常,则更容易处理,否则代码非常冗余。因为我需要区分是用户取消,还是正常发送失败。

kotlin-coroutines kotlin-coroutine-channel
1个回答
-2
投票

我将这个问题解释为如何处理抛出不属于协程框架一部分的 CancellationException 的情况。这是很有可能的,因为在 JVM 上,

kotlin.coroutines.cancellation.CancellationException
只是
java.util.concurrent.CancellationException
的类型别名。这可能被与协程无关的各种其他框架使用。

关于您的具体情况,请跳至最后一部分。

一般建议是将 Java 代码的 CancellationException 的错误处理与协程相关的错误处理隔离开来。这是可能的,因为 Kotlin 中的协程是合作的:如果您不希望您的协程被取消,没有什么可以阻止您忽略取消请求。

这意味着您必须主动检查取消请求,并且只有当您想满足取消请求时才应该抛出 CancellationException。例如,

delay
就是这样做的。因此,当您调用其他 suspend 函数时,它们可能会抛出 CancellationException ,表明它们想要配合并取消当前协程的执行。这是一个例外 - 本着合作的精神 - 你应该重新抛出。协程框架正常运行所必需的

另一方面,当 Java 函数(不挂起)抛出 CancellationException 时,该异常具有其他含义。您不需要重新抛出它,因为它并不意味着控制协程的流程。您甚至可能不会在协程中“成为”,因为 Java 函数无法通过将自身声明为挂起来强制执行这一点。这意味着您可以安全地捕获源自非挂起 Java 代码的 CancellationException。您甚至可以在协程中执行此操作,而不会冒捕获与协程相关的 CancellationException 的风险,因为后者只能从“挂起”函数中抛出,而您的非挂起 Java 函数则不然。 在 Ktor 的 WebSocketSession::send

抛出 CancellationException 的特定情况下,情况略有不同。该函数是一个挂起函数,您不知道异常的原因。可能是内部调用
ensureActive()

来配合协程取消,也可能是由关闭的 websocket 连接或完全不同的原因引起的。

关键是,你不知道。由于该函数被明确标记为挂起,因此唯一安全的途径是将异常解释为控制协程的方法。这意味着您不应该在不确定原因是什么的情况下捕获它(或者至少重新抛出它)。您唯一知道的是您无法发送帧并且当前的协程应该停止。
您可能不同意 Ktor 团队在连接关闭时抛出 CancellationException 的决定,但也有一些论据支持它。这是文档对此的说明:

如果传出通道已经关闭,则可能会抛出异常,因此无法传输任何消息。关闭帧后发送的帧可以被默默忽略。

如果将 websocket 调用封装在协程中,那么协程被取消应该没什么关系,重要的是要知道消息无法发送。当 websocket 连接仍然打开时,您可以简单地重试,启动一个新的协程。

© www.soinside.com 2019 - 2024. All rights reserved.