Skip to content

Commit 390be6f

Browse files
authored
Rework CompletionHandler to avoid subclassing a functional type (#4010)
Workaround for KT-64075 Also, thoroughly document the contracts imposed on the callbacks provided to `invokeOnCompletion` and `invokeOnCancellation`.
1 parent f86cbc7 commit 390be6f

15 files changed

+190
-191
lines changed

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ public final class kotlinx/coroutines/CancellableContinuationKt {
8787
public final class kotlinx/coroutines/ChildContinuation {
8888
public final field child Lkotlinx/coroutines/CancellableContinuationImpl;
8989
public fun <init> (Lkotlinx/coroutines/CancellableContinuationImpl;)V
90-
public synthetic fun invoke (Ljava/lang/Object;)Ljava/lang/Object;
9190
public fun invoke (Ljava/lang/Throwable;)V
9291
}
9392

@@ -1332,12 +1331,11 @@ public abstract interface class kotlinx/coroutines/selects/SelectClause1 : kotli
13321331
public abstract interface class kotlinx/coroutines/selects/SelectClause2 : kotlinx/coroutines/selects/SelectClause {
13331332
}
13341333

1335-
public class kotlinx/coroutines/selects/SelectImplementation : kotlinx/coroutines/selects/SelectBuilder, kotlinx/coroutines/selects/SelectInstanceInternal {
1334+
public class kotlinx/coroutines/selects/SelectImplementation : kotlinx/coroutines/CancelHandler, kotlinx/coroutines/selects/SelectBuilder, kotlinx/coroutines/selects/SelectInstanceInternal {
13361335
public fun <init> (Lkotlin/coroutines/CoroutineContext;)V
13371336
public fun disposeOnCompletion (Lkotlinx/coroutines/DisposableHandle;)V
13381337
public fun doSelect (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
13391338
public fun getContext ()Lkotlin/coroutines/CoroutineContext;
1340-
public synthetic fun invoke (Ljava/lang/Object;)Ljava/lang/Object;
13411339
public fun invoke (Ljava/lang/Throwable;)V
13421340
public fun invoke (Lkotlinx/coroutines/selects/SelectClause0;Lkotlin/jvm/functions/Function1;)V
13431341
public fun invoke (Lkotlinx/coroutines/selects/SelectClause1;Lkotlin/jvm/functions/Function2;)V

kotlinx-coroutines-core/common/src/Await.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private class AwaitAll<T>(private val deferreds: Array<out Deferred<T>>) {
6767
val deferred = deferreds[i]
6868
deferred.start() // To properly await lazily started deferreds
6969
AwaitAllNode(cont).apply {
70-
handle = deferred.invokeOnCompletion(asHandler)
70+
handle = deferred.invokeOnCompletion(handler = this)
7171
}
7272
}
7373
val disposer = DisposeHandlersOnCancel(nodes)
@@ -79,11 +79,11 @@ private class AwaitAll<T>(private val deferreds: Array<out Deferred<T>>) {
7979
// it is already complete while handlers were being installed -- dispose them all
8080
disposer.disposeAll()
8181
} else {
82-
cont.invokeOnCancellation(handler = disposer.asHandler)
82+
cont.invokeOnCancellation(handler = disposer)
8383
}
8484
}
8585

86-
private inner class DisposeHandlersOnCancel(private val nodes: Array<AwaitAllNode>) : CancelHandler() {
86+
private inner class DisposeHandlersOnCancel(private val nodes: Array<AwaitAllNode>) : CancelHandler {
8787
fun disposeAll() {
8888
nodes.forEach { it.handle.dispose() }
8989
}

kotlinx-coroutines-core/common/src/CancellableContinuation.kt

+19-11
Original file line numberDiff line numberDiff line change
@@ -122,28 +122,27 @@ public interface CancellableContinuation<in T> : Continuation<T> {
122122

123123
/**
124124
* Registers a [handler] to be **synchronously** invoked on [cancellation][cancel] (regular or exceptional) of this continuation.
125-
* When the continuation is already cancelled, the handler is immediately invoked
126-
* with the cancellation exception. Otherwise, the handler will be invoked as soon as this
127-
* continuation is cancelled.
125+
* When the continuation is already cancelled, the handler is immediately invoked with the cancellation exception.
126+
* Otherwise, the handler will be invoked as soon as this continuation is cancelled.
128127
*
129128
* The installed [handler] should not throw any exceptions.
130129
* If it does, they will get caught, wrapped into a [CompletionHandlerException] and
131130
* processed as an uncaught exception in the context of the current coroutine
132131
* (see [CoroutineExceptionHandler]).
133132
*
134-
* At most one [handler] can be installed on a continuation. Attempt to call `invokeOnCancellation` second
135-
* time produces [IllegalStateException].
133+
* At most one [handler] can be installed on a continuation.
134+
* Attempting to call `invokeOnCancellation` a second time produces an [IllegalStateException].
136135
*
137136
* This handler is also called when this continuation [resumes][Continuation.resume] normally (with a value) and then
138137
* is cancelled while waiting to be dispatched. More generally speaking, this handler is called whenever
139138
* the caller of [suspendCancellableCoroutine] is getting a [CancellationException].
140139
*
141-
* A typical example for `invokeOnCancellation` usage is given in
140+
* A typical example of `invokeOnCancellation` usage is given in
142141
* the documentation for the [suspendCancellableCoroutine] function.
143142
*
144-
* **Note**: Implementation of `CompletionHandler` must be fast, non-blocking, and thread-safe.
145-
* This `handler` can be invoked concurrently with the surrounding code.
146-
* There is no guarantee on the execution context in which the `handler` will be invoked.
143+
* **Note**: Implementations of [CompletionHandler] must be fast, non-blocking, and thread-safe.
144+
* This [handler] can be invoked concurrently with the surrounding code.
145+
* There is no guarantee on the execution context in which the [handler] will be invoked.
147146
*/
148147
public fun invokeOnCancellation(handler: CompletionHandler)
149148

@@ -201,6 +200,15 @@ public interface CancellableContinuation<in T> : Continuation<T> {
201200
public fun resume(value: T, onCancellation: ((cause: Throwable) -> Unit)?)
202201
}
203202

203+
/**
204+
* A version of `invokeOnCancellation` that accepts a class as a handler instead of a lambda, but identical otherwise.
205+
* This allows providing a custom [toString] instance that will look better during debugging.
206+
*/
207+
internal fun <T> CancellableContinuation<T>.invokeOnCancellation(handler: CancelHandler) = when (this) {
208+
is CancellableContinuationImpl -> invokeOnCancellationInternal(handler)
209+
else -> throw UnsupportedOperationException("third-party implementation of CancellableContinuation is not supported")
210+
}
211+
204212
/**
205213
* Suspends the coroutine like [suspendCoroutine], but providing a [CancellableContinuation] to
206214
* the [block]. This function throws a [CancellationException] if the [Job] of the coroutine is
@@ -373,9 +381,9 @@ internal fun <T> getOrCreateCancellableContinuation(delegate: Continuation<T>):
373381
*/
374382
@InternalCoroutinesApi
375383
public fun CancellableContinuation<*>.disposeOnCancellation(handle: DisposableHandle): Unit =
376-
invokeOnCancellation(handler = DisposeOnCancel(handle).asHandler)
384+
invokeOnCancellation(handler = DisposeOnCancel(handle))
377385

378-
private class DisposeOnCancel(private val handle: DisposableHandle) : CancelHandler() {
386+
private class DisposeOnCancel(private val handle: DisposableHandle) : CancelHandler {
379387
override fun invoke(cause: Throwable?) = handle.dispose()
380388
override fun toString(): String = "DisposeOnCancel[$handle]"
381389
}

kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt

+44-21
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,12 @@ internal open class CancellableContinuationImpl<in T>(
234234
}
235235
}
236236

237-
private fun callCancelHandler(handler: CompletionHandler, cause: Throwable?) =
237+
private fun callCancelHandler(handler: InternalCompletionHandler, cause: Throwable?) =
238238
/*
239239
* :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
240240
* because we play type tricks on Kotlin/JS and handler is not necessarily a function there
241241
*/
242-
callCancelHandlerSafely { handler.invokeIt(cause) }
242+
callCancelHandlerSafely { handler.invoke(cause) }
243243

244244
fun callCancelHandler(handler: CancelHandler, cause: Throwable?) =
245245
callCancelHandlerSafely { handler.invoke(cause) }
@@ -343,7 +343,7 @@ internal open class CancellableContinuationImpl<in T>(
343343
// Install the handle
344344
val handle = parent.invokeOnCompletion(
345345
onCancelling = true,
346-
handler = ChildContinuation(this).asHandler
346+
handler = ChildContinuation(this)
347347
)
348348
_parentHandle.compareAndSet(null, handle)
349349
return handle
@@ -390,10 +390,9 @@ internal open class CancellableContinuationImpl<in T>(
390390
invokeOnCancellationImpl(segment)
391391
}
392392

393-
public override fun invokeOnCancellation(handler: CompletionHandler) {
394-
val cancelHandler = makeCancelHandler(handler)
395-
invokeOnCancellationImpl(cancelHandler)
396-
}
393+
override fun invokeOnCancellation(handler: CompletionHandler) = invokeOnCancellation(CancelHandler.UserSupplied(handler))
394+
395+
internal fun invokeOnCancellationInternal(handler: CancelHandler) = invokeOnCancellationImpl(handler)
397396

398397
private fun invokeOnCancellationImpl(handler: Any) {
399398
assert { handler is CancelHandler || handler is Segment<*> }
@@ -461,9 +460,6 @@ internal open class CancellableContinuationImpl<in T>(
461460
error("It's prohibited to register multiple handlers, tried to register $handler, already has $state")
462461
}
463462

464-
private fun makeCancelHandler(handler: CompletionHandler): CancelHandler =
465-
if (handler is CancelHandler) handler else InvokeOnCancel(handler)
466-
467463
private fun dispatchResume(mode: Int) {
468464
if (tryResume()) return // completed before getResult invocation -- bail out
469465
// otherwise, getResult has already commenced, i.e. completed later or in other thread
@@ -625,19 +621,46 @@ private object Active : NotCompleted {
625621
}
626622

627623
/**
628-
* Base class for all [CancellableContinuation.invokeOnCancellation] handlers to avoid an extra instance
629-
* on JVM, yet support JS where you cannot extend from a functional type.
624+
* Essentially the same as just a function from `Throwable?` to `Unit`.
625+
* The only thing implementors can do is call [invoke].
626+
* The reason this abstraction exists is to allow providing a readable [toString] in the list of completion handlers
627+
* as seen from the debugger.
628+
* Use [UserSupplied] to create an instance from a lambda.
629+
* We can't avoid defining a separate type, because on JS, you can't inherit from a function type.
630+
*
631+
* @see InternalCompletionHandler for a very similar interface, but used for handling completion and not cancellation.
630632
*/
631-
internal abstract class CancelHandler : CancelHandlerBase(), NotCompleted
632-
633-
// Wrapper for lambdas, for the performance sake CancelHandler can be subclassed directly
634-
private class InvokeOnCancel( // Clashes with InvokeOnCancellation
635-
private val handler: CompletionHandler
636-
) : CancelHandler() {
637-
override fun invoke(cause: Throwable?) {
638-
handler.invoke(cause)
633+
internal interface CancelHandler : NotCompleted {
634+
/**
635+
* Signals cancellation.
636+
*
637+
* This function:
638+
* - Does not throw any exceptions.
639+
* Violating this rule in an implementation leads to [handleUncaughtCoroutineException] being called with a
640+
* [CompletionHandlerException] wrapping the thrown exception.
641+
* - Is fast, non-blocking, and thread-safe.
642+
* - Can be invoked concurrently with the surrounding code.
643+
* - Can be invoked from any context.
644+
*
645+
* The meaning of `cause` that is passed to the handler is:
646+
* - It is `null` if the continuation was cancelled directly via [CancellableContinuation.cancel] without a `cause`.
647+
* - It is an instance of [CancellationException] if the continuation was _normally_ cancelled from the outside.
648+
* **It should not be treated as an error**. In particular, it should not be reported to error logs.
649+
* - Otherwise, the continuation had cancelled with an _error_.
650+
*/
651+
fun invoke(cause: Throwable?)
652+
653+
/**
654+
* A lambda passed from outside the coroutine machinery.
655+
*
656+
* See the requirements for [CancelHandler.invoke] when implementing this function.
657+
*/
658+
class UserSupplied(private val handler: (cause: Throwable?) -> Unit) : CancelHandler {
659+
/** @suppress */
660+
override fun invoke(cause: Throwable?) { handler(cause) }
661+
662+
override fun toString() = "CancelHandler.UserSupplied[${handler.classSimpleName}@$hexAddress]"
639663
}
640-
override fun toString() = "InvokeOnCancel[${handler.classSimpleName}@$hexAddress]"
641664
}
642665

643666
// Completed with additional metadata

kotlinx-coroutines-core/common/src/CompletionHandler.common.kt

+55-27
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,71 @@
11
package kotlinx.coroutines
22

3-
import kotlinx.coroutines.internal.*
4-
53
/**
64
* Handler for [Job.invokeOnCompletion] and [CancellableContinuation.invokeOnCancellation].
75
*
8-
* Installed handler should not throw any exceptions. If it does, they will get caught,
9-
* wrapped into [CompletionHandlerException], and rethrown, potentially causing crash of unrelated code.
10-
*
11-
* The meaning of `cause` that is passed to the handler:
12-
* - Cause is `null` when the job has completed normally.
13-
* - Cause is an instance of [CancellationException] when the job was cancelled _normally_.
6+
* The meaning of `cause` that is passed to the handler is:
7+
* - It is `null` if the job has completed normally or the continuation was cancelled without a `cause`.
8+
* - It is an instance of [CancellationException] if the job or the continuation was cancelled _normally_.
149
* **It should not be treated as an error**. In particular, it should not be reported to error logs.
15-
* - Otherwise, the job had _failed_.
10+
* - Otherwise, the job or the continuation had _failed_.
11+
*
12+
* A function used for this should not throw any exceptions.
13+
* If it does, they will get caught, wrapped into [CompletionHandlerException], and then either
14+
* - passed to [handleCoroutineException] for [CancellableContinuation.invokeOnCancellation]
15+
* and, for [Job] instances that are coroutines, [Job.invokeOnCompletion], or
16+
* - for [Job] instances that are not coroutines, simply thrown, potentially crashing unrelated code.
17+
*
18+
* Functions used for this must be fast, non-blocking, and thread-safe.
19+
* This handler can be invoked concurrently with the surrounding code.
20+
* There is no guarantee on the execution context in which the function is invoked.
1621
*
1722
* **Note**: This type is a part of internal machinery that supports parent-child hierarchies
1823
* and allows for implementation of suspending functions that wait on the Job's state.
1924
* This type should not be used in general application code.
20-
* Implementations of `CompletionHandler` must be fast and _lock-free_.
2125
*/
26+
// TODO: deprecate. This doesn't seem better than a simple function type.
2227
public typealias CompletionHandler = (cause: Throwable?) -> Unit
2328

24-
// We want class that extends LockFreeLinkedListNode & CompletionHandler but we cannot do it on Kotlin/JS,
25-
// so this expect class provides us with the corresponding abstraction in a platform-agnostic way.
26-
internal expect abstract class CompletionHandlerBase() : LockFreeLinkedListNode {
27-
abstract fun invoke(cause: Throwable?)
28-
}
29+
/**
30+
* Essentially the same as just a function from `Throwable?` to `Unit`.
31+
* The only thing implementors can do is call [invoke].
32+
* The reason this abstraction exists is to allow providing a readable [toString] in the list of completion handlers
33+
* as seen from the debugger.
34+
* Use [UserSupplied] to create an instance from a lambda.
35+
* We can't avoid defining a separate type, because on JS, you can't inherit from a function type.
36+
*
37+
* @see CancelHandler for a very similar interface, but used for handling cancellation and not completion.
38+
*/
39+
internal interface InternalCompletionHandler {
40+
/**
41+
* Signals completion.
42+
*
43+
* This function:
44+
* - Does not throw any exceptions.
45+
* For [Job] instances that are coroutines, exceptions thrown by this function will be caught, wrapped into
46+
* [CompletionHandlerException], and passed to [handleCoroutineException], but for those that are not coroutines,
47+
* they will just be rethrown, potentially crashing unrelated code.
48+
* - Is fast, non-blocking, and thread-safe.
49+
* - Can be invoked concurrently with the surrounding code.
50+
* - Can be invoked from any context.
51+
*
52+
* The meaning of `cause` that is passed to the handler is:
53+
* - It is `null` if the job has completed normally.
54+
* - It is an instance of [CancellationException] if the job was cancelled _normally_.
55+
* **It should not be treated as an error**. In particular, it should not be reported to error logs.
56+
* - Otherwise, the job had _failed_.
57+
*/
58+
fun invoke(cause: Throwable?)
2959

30-
internal expect val CompletionHandlerBase.asHandler: CompletionHandler
60+
/**
61+
* A lambda passed from outside the coroutine machinery.
62+
*
63+
* See the requirements for [InternalCompletionHandler.invoke] when implementing this function.
64+
*/
65+
class UserSupplied(private val handler: (cause: Throwable?) -> Unit) : InternalCompletionHandler {
66+
/** @suppress */
67+
override fun invoke(cause: Throwable?) { handler(cause) }
3168

32-
// More compact version of CompletionHandlerBase for CancellableContinuation with same workaround for JS
33-
internal expect abstract class CancelHandlerBase() {
34-
abstract fun invoke(cause: Throwable?)
69+
override fun toString() = "InternalCompletionHandler.UserSupplied[${handler.classSimpleName}@$hexAddress]"
70+
}
3571
}
36-
37-
internal expect val CancelHandlerBase.asHandler: CompletionHandler
38-
39-
// :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension,
40-
// because we play type tricks on Kotlin/JS and handler is not necessarily a function there
41-
internal expect fun CompletionHandler.invokeIt(cause: Throwable?)
42-
43-
internal inline fun <reified T> CompletionHandler.isHandlerOf(): Boolean = this is T

kotlinx-coroutines-core/common/src/Exceptions.common.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package kotlinx.coroutines
22

33
/**
4-
* This exception gets thrown if an exception is caught while processing [CompletionHandler] invocation for [Job].
4+
* This exception gets thrown if an exception is caught while processing [InternalCompletionHandler] invocation for [Job].
55
*
66
* @suppress **This an internal API and should not be used from general code.**
77
*/

0 commit comments

Comments
 (0)