Skip to content

Commit 6bad5fc

Browse files
authored
Stabilize Channel.invokeOnClose (#3682)
* Stabilize Channel.invokeOnClose CompletionHandler is not used deliberately, as its contract requires some additional refinement along with `onCancelling` handler stabilization. Note that replacing functional type with the very same typealias is backwards-compatible in the current state of linkage, so we are not giving up any future opportunities. Also, fix behavioural mismatch: `CancellationException` is supplied (and always has been) to `invokeOnClose` when a channel was cancelled normally instead of `null` as documentation stated. This behaviour is aligned with other cancellation handlers and also allows the handler to distinguish whether the channel was closed or cancelled. Fixes #3358
1 parent 6427e0e commit 6bad5fc

File tree

2 files changed

+59
-13
lines changed

2 files changed

+59
-13
lines changed

kotlinx-coroutines-core/common/src/channels/Channel.kt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,33 +105,40 @@ public interface SendChannel<in E> {
105105
* If the channel is closed already, the handler is invoked immediately.
106106
*
107107
* The meaning of `cause` that is passed to the handler:
108-
* * `null` if the channel was closed or cancelled without the corresponding argument
109-
* * the cause of `close` or `cancel` otherwise.
108+
* - `null` if the channel was closed normally without the corresponding argument.
109+
* - Instance of [CancellationException] if the channel was cancelled normally without the corresponding argument.
110+
* - The cause of `close` or `cancel` otherwise.
110111
*
111-
* Example of usage (exception handling is omitted):
112+
* ### Execution context and exception safety
112113
*
114+
* The [handler] is executed as part of the closing or cancelling operation, and only after the channel reaches its final state.
115+
* This means that if the handler throws an exception or hangs, the channel will still be successfully closed or cancelled.
116+
* Unhandled exceptions from [handler] are propagated to the closing or cancelling operation's caller.
117+
*
118+
* Example of usage:
113119
* ```
114-
* val events = Channel(UNLIMITED)
120+
* val events = Channel<Event>(UNLIMITED)
115121
* callbackBasedApi.registerCallback { event ->
116122
* events.trySend(event)
123+
* .onClosed { /* channel is already closed, but the callback hasn't stopped yet */ }
117124
* }
118125
*
119-
* val uiUpdater = launch(Dispatchers.Main, parent = UILifecycle) {
120-
* events.consume {}
121-
* events.cancel()
126+
* val uiUpdater = uiScope.launch(Dispatchers.Main) {
127+
* events.consume { /* handle events */ }
122128
* }
123-
*
129+
* // Stop the callback after the channel is closed or cancelled
124130
* events.invokeOnClose { callbackBasedApi.stop() }
125131
* ```
126132
*
127-
* **Note: This is an experimental api.** This function may change its semantics, parameters or return type in the future.
133+
* **Stability note.** This function constitutes a stable API surface, with the only exception being
134+
* that an [IllegalStateException] is thrown when multiple handlers are registered.
135+
* This restriction could be lifted in the future.
128136
*
129-
* @throws UnsupportedOperationException if the underlying channel doesn't support [invokeOnClose].
137+
* @throws UnsupportedOperationException if the underlying channel does not support [invokeOnClose].
130138
* Implementation note: currently, [invokeOnClose] is unsupported only by Rx-like integrations
131139
*
132140
* @throws IllegalStateException if another handler was already registered
133141
*/
134-
@ExperimentalCoroutinesApi
135142
public fun invokeOnClose(handler: (cause: Throwable?) -> Unit)
136143

137144
/**
@@ -388,7 +395,7 @@ public interface ReceiveChannel<out E> {
388395
* The successful result represents a successful operation with a value of type [T], for example,
389396
* the result of [Channel.receiveCatching] operation or a successfully sent element as a result of [Channel.trySend].
390397
*
391-
* The failed result represents a failed operation attempt to a channel, but it doesn't necessary indicate that the channel is failed.
398+
* The failed result represents a failed operation attempt to a channel, but it doesn't necessarily indicate that the channel is failed.
392399
* E.g. when the channel is full, [Channel.trySend] returns failed result, but the channel itself is not in the failed state.
393400
*
394401
* The closed result represents an operation attempt to a closed channel and also implies that the operation has failed.

kotlinx-coroutines-core/common/test/channels/BasicOperationsTest.kt

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,45 @@ class BasicOperationsTest : TestBase() {
8585
}
8686
}
8787

88+
@Test
89+
fun testCancelledChannelInvokeOnClose() {
90+
val ch = Channel<Int>()
91+
ch.invokeOnClose { assertIs<CancellationException>(it) }
92+
ch.cancel()
93+
}
94+
95+
@Test
96+
fun testCancelledChannelWithCauseInvokeOnClose() {
97+
val ch = Channel<Int>()
98+
ch.invokeOnClose { assertIs<TimeoutCancellationException>(it) }
99+
ch.cancel(TimeoutCancellationException(""))
100+
}
101+
102+
@Test
103+
fun testThrowingInvokeOnClose() = runTest {
104+
val channel = Channel<Int>()
105+
channel.invokeOnClose {
106+
assertNull(it)
107+
expect(3)
108+
throw TestException()
109+
}
110+
111+
launch {
112+
try {
113+
expect(2)
114+
channel.close()
115+
} catch (e: TestException) {
116+
expect(4)
117+
}
118+
}
119+
expect(1)
120+
yield()
121+
assertTrue(channel.isClosedForReceive)
122+
assertTrue(channel.isClosedForSend)
123+
assertFalse(channel.close())
124+
finish(5)
125+
}
126+
88127
@Suppress("ReplaceAssertBooleanWithAssertEquality")
89128
private suspend fun testReceiveCatching(kind: TestChannelKind) = coroutineScope {
90129
reset()
@@ -124,7 +163,7 @@ class BasicOperationsTest : TestBase() {
124163
channel.trySend(2)
125164
.onSuccess { expectUnreached() }
126165
.onClosed {
127-
assertTrue { it is ClosedSendChannelException}
166+
assertTrue { it is ClosedSendChannelException }
128167
if (!kind.isConflated) {
129168
assertEquals(42, channel.receive())
130169
}

0 commit comments

Comments
 (0)