Skip to content

Commit 2dd3bf0

Browse files
committed
Add ChannelResult.onClosed
* Establish clear contract on ChannelResult.isClosed * This method provides a **clear** migration from correct 'offer' usages to 'trySend'
1 parent 71df60e commit 2dd3bf0

File tree

3 files changed

+56
-3
lines changed

3 files changed

+56
-3
lines changed

kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ public final class kotlinx/coroutines/channels/ChannelKt {
635635
public static synthetic fun Channel$default (IILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel;
636636
public static synthetic fun Channel$default (ILkotlinx/coroutines/channels/BufferOverflow;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/channels/Channel;
637637
public static final fun getOrElse-WpGqRn0 (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
638+
public static final fun onClosed-WpGqRn0 (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
638639
public static final fun onFailure-WpGqRn0 (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
639640
public static final fun onSuccess-WpGqRn0 (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
640641
}

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,22 @@ public interface SendChannel<in E> {
144144
* oversee such error during code review.
145145
* * Its name was not aligned with the rest of the API and tried to mimic Java's queue instead.
146146
*
147+
* **NB** Automatic migration provides best-effort for the user experience, but requires removal
148+
* or adjusting of the code that relied on the exception handling.
149+
* The complete replacement has a more verbose form:
150+
* ```
151+
* channel.trySend(element)
152+
* .onClosed { throw it ?: ClosedSendChannelException("Channel was closed normally") }
153+
* .isSuccess
154+
* ```
155+
*
147156
* See https://github.com/Kotlin/kotlinx.coroutines/issues/974 for more context.
148157
*/
149158
@Deprecated(
150159
level = DeprecationLevel.WARNING,
151160
message = "Deprecated in the favour of 'trySend' method",
152161
replaceWith = ReplaceWith("trySend(element).isSuccess")
153-
) // Since 1.5.0
162+
) // Warning since 1.5.0
154163
public fun offer(element: E): Boolean {
155164
val result = trySend(element)
156165
if (result.isSuccess) return true
@@ -297,7 +306,7 @@ public interface ReceiveChannel<out E> {
297306
@Deprecated(level = DeprecationLevel.WARNING,
298307
message = "Deprecated in the favour of 'tryReceive'",
299308
replaceWith = ReplaceWith("tryReceive().getOrNull()")
300-
) // Since 1.5.0
309+
) // Warning since 1.5.0
301310
public fun poll(): E? {
302311
val result = tryReceive()
303312
if (result.isSuccess) return result.getOrThrow()
@@ -362,6 +371,8 @@ public interface ReceiveChannel<out E> {
362371
* E.g. when the channel is full, [Channel.trySend] returns failed result, but the channel itself is not in the failed state.
363372
*
364373
* The closed result represents an operation attempt to a closed channel and also implies that the operation has failed.
374+
* It is guaranteed that if the result is _closed_, then the target channel is either [closed for send][Channel.isClosedForSend]
375+
* or is [closed for receive][Channel.isClosedForReceive] depending on whether the failed operation was sending or receiving.
365376
*/
366377
@JvmInline
367378
public value class ChannelResult<out T>
@@ -399,12 +410,14 @@ public value class ChannelResult<out T>
399410
/**
400411
* Returns the encapsulated value if this instance represents success or `null` if it represents failed result.
401412
*/
413+
@Suppress("UNCHECKED_CAST")
402414
public fun getOrNull(): T? = if (holder !is Failed) holder as T else null
403415

404416
/**
405417
* Returns the encapsulated value if this instance represents success or throws an exception if it is closed or failed.
406418
*/
407419
public fun getOrThrow(): T {
420+
@Suppress("UNCHECKED_CAST")
408421
if (holder !is Failed) return holder as T
409422
if (holder is Closed && holder.cause != null) throw holder.cause
410423
error("Trying to call 'getOrThrow' on a failed channel result: $holder")
@@ -495,6 +508,25 @@ public inline fun <T> ChannelResult<T>.onFailure(action: (exception: Throwable?)
495508
return this
496509
}
497510

511+
/**
512+
* Performs the given [action] on the encapsulated [Throwable] exception if this instance represents [failure][ChannelResult.isFailure]
513+
* due to channel being [closed][Channel.close].
514+
* The result of [ChannelResult.exceptionOrNull] is passed to the [action] parameter.
515+
* It is guaranteed that if action is invoked, then the channel is either [closed for send][Channel.isClosedForSend]
516+
* or is [closed for receive][Channel.isClosedForReceive] depending on the failed operation.
517+
*
518+
* Returns the original `ChannelResult` unchanged.
519+
*/
520+
@OptIn(ExperimentalContracts::class)
521+
public inline fun <T> ChannelResult<T>.onClosed(action: (exception: Throwable?) -> Unit): ChannelResult<T> {
522+
contract {
523+
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
524+
}
525+
@Suppress("UNCHECKED_CAST")
526+
if (holder is ChannelResult.Closed) action(exceptionOrNull())
527+
return this
528+
}
529+
498530
/**
499531
* Iterator for [ReceiveChannel]. Instances of this interface are *not thread-safe* and shall not be used
500532
* from concurrent coroutines.

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class BasicOperationsTest : TestBase() {
1414
TestChannelKind.values().forEach { kind -> testSendReceive(kind, 20) }
1515
}
1616

17+
@Test
18+
fun testTrySendToFullChannel() = runTest {
19+
TestChannelKind.values().forEach { kind -> testTrySendToFullChannel(kind) }
20+
}
21+
1722
@Test
1823
fun testTrySendAfterClose() = runTest {
1924
TestChannelKind.values().forEach { kind -> testTrySend(kind) }
@@ -118,7 +123,7 @@ class BasicOperationsTest : TestBase() {
118123
assertTrue(channel.isClosedForSend)
119124
channel.trySend(2)
120125
.onSuccess { expectUnreached() }
121-
.onFailure {
126+
.onClosed {
122127
assertTrue { it is ClosedSendChannelException}
123128
if (!kind.isConflated) {
124129
assertEquals(42, channel.receive())
@@ -127,6 +132,21 @@ class BasicOperationsTest : TestBase() {
127132
d.await()
128133
}
129134

135+
private suspend fun testTrySendToFullChannel(kind: TestChannelKind) = coroutineScope {
136+
if (kind.isConflated || kind.capacity == Int.MAX_VALUE) return@coroutineScope
137+
val channel = kind.create<Int>()
138+
// Make it full
139+
repeat(11) {
140+
channel.trySend(42)
141+
}
142+
channel.trySend(1)
143+
.onSuccess { expectUnreached() }
144+
.onFailure { assertNull(it) }
145+
.onClosed {
146+
expectUnreached()
147+
}
148+
}
149+
130150
/**
131151
* [ClosedSendChannelException] should not be eaten.
132152
* See [https://github.com/Kotlin/kotlinx.coroutines/issues/957]

0 commit comments

Comments
 (0)