-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework CompletionHandler to avoid subclassing a functional type #4010
Changes from all commits
978d494
6d8827b
b37b851
f971700
2c8f00a
651fd5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,71 @@ | ||
package kotlinx.coroutines | ||
|
||
import kotlinx.coroutines.internal.* | ||
|
||
/** | ||
* Handler for [Job.invokeOnCompletion] and [CancellableContinuation.invokeOnCancellation]. | ||
* | ||
* Installed handler should not throw any exceptions. If it does, they will get caught, | ||
* wrapped into [CompletionHandlerException], and rethrown, potentially causing crash of unrelated code. | ||
* | ||
* The meaning of `cause` that is passed to the handler: | ||
* - Cause is `null` when the job has completed normally. | ||
* - Cause is an instance of [CancellationException] when the job was cancelled _normally_. | ||
* The meaning of `cause` that is passed to the handler is: | ||
* - It is `null` if the job has completed normally or the continuation was cancelled without a `cause`. | ||
* - It is an instance of [CancellationException] if the job or the continuation was cancelled _normally_. | ||
* **It should not be treated as an error**. In particular, it should not be reported to error logs. | ||
* - Otherwise, the job had _failed_. | ||
* - Otherwise, the job or the continuation had _failed_. | ||
* | ||
* A function used for this should not throw any exceptions. | ||
* If it does, they will get caught, wrapped into [CompletionHandlerException], and then either | ||
* - passed to [handleCoroutineException] for [CancellableContinuation.invokeOnCancellation] | ||
* and, for [Job] instances that are coroutines, [Job.invokeOnCompletion], or | ||
* - for [Job] instances that are not coroutines, simply thrown, potentially crashing unrelated code. | ||
* | ||
* Functions used for this must be fast, non-blocking, and thread-safe. | ||
* This handler can be invoked concurrently with the surrounding code. | ||
* There is no guarantee on the execution context in which the function is invoked. | ||
* | ||
* **Note**: This type is a part of internal machinery that supports parent-child hierarchies | ||
* and allows for implementation of suspending functions that wait on the Job's state. | ||
* This type should not be used in general application code. | ||
* Implementations of `CompletionHandler` must be fast and _lock-free_. | ||
*/ | ||
// TODO: deprecate. This doesn't seem better than a simple function type. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we? I see this type alias (or maybe even I would interpret it as an opaque type if it was possible) as a perfect fit -- it is a functional type, but with very specific semantics that is a property of this "type", not of the callsite ( WDYT? As usual, this particular endpoint might be not the most important one, but it's nice to align our mental models here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's my reasoning behind the tl;dr: this function's contract is defined by the places that accept the function, not the other way around, so these other places should define what they expect. Also, the latest commit that fixed the explanation of what the argument means, and this type looks even more unnatural now. Long version: I know of two use cases for binding an arbitrary function to a name:
For the second use case, introducing a type alias is clearly beneficial: when you have a
This surprises me greatly: I think it's exactly the opposite. The contract on the handler is exactly the combination of the contracts of the functions that accept the handler. Here's the deciding factor for me: if the behavior of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the elaborate answer.
Indeed. It appears I just think about this one inside out -- its definition/contract is driven bu the need of its "clients" ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I got your intention correctly, you're touching on a deep concept here, called the native type theory. In essence, we can take any dynamically typed function and define its type as a function that accepts all values that don't make it misbehave. By this logic, I think the reasons There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's quite close. I don't see it fit e.g. for an arbitrary function, but here we have a multitude of functions that all accept the same kind of parameter with the same kind of constraints. These constraints are significant enough to document, and broad enough to get tired of repeating it (NB: here, I focus not on "repeating the text" but on "repeating the conceptual complexity of the entity accepting this parameter and draining our complexity budget"). It's just lightweight and specific enough so it doesn't deserve a dedicated nominal type. (please note that this discussion is not a blocker of this PR by any means) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Let's consider two scenarios separately:
I'm confident the second option is a no-go because the constraints and the behavior are actually different between The first option also seems strange to me: then, the contract on |
||
public typealias CompletionHandler = (cause: Throwable?) -> Unit | ||
|
||
// We want class that extends LockFreeLinkedListNode & CompletionHandler but we cannot do it on Kotlin/JS, | ||
// so this expect class provides us with the corresponding abstraction in a platform-agnostic way. | ||
internal expect abstract class CompletionHandlerBase() : LockFreeLinkedListNode { | ||
abstract fun invoke(cause: Throwable?) | ||
} | ||
/** | ||
* Essentially the same as just a function from `Throwable?` to `Unit`. | ||
* The only thing implementors can do is call [invoke]. | ||
* The reason this abstraction exists is to allow providing a readable [toString] in the list of completion handlers | ||
* as seen from the debugger. | ||
* Use [UserSupplied] to create an instance from a lambda. | ||
* We can't avoid defining a separate type, because on JS, you can't inherit from a function type. | ||
* | ||
* @see CancelHandler for a very similar interface, but used for handling cancellation and not completion. | ||
*/ | ||
internal interface InternalCompletionHandler { | ||
/** | ||
* Signals completion. | ||
* | ||
* This function: | ||
* - Does not throw any exceptions. | ||
* For [Job] instances that are coroutines, exceptions thrown by this function will be caught, wrapped into | ||
* [CompletionHandlerException], and passed to [handleCoroutineException], but for those that are not coroutines, | ||
* they will just be rethrown, potentially crashing unrelated code. | ||
* - Is fast, non-blocking, and thread-safe. | ||
* - Can be invoked concurrently with the surrounding code. | ||
* - Can be invoked from any context. | ||
* | ||
* The meaning of `cause` that is passed to the handler is: | ||
* - It is `null` if the job has completed normally. | ||
* - It is an instance of [CancellationException] if the job was cancelled _normally_. | ||
* **It should not be treated as an error**. In particular, it should not be reported to error logs. | ||
* - Otherwise, the job had _failed_. | ||
*/ | ||
fun invoke(cause: Throwable?) | ||
|
||
internal expect val CompletionHandlerBase.asHandler: CompletionHandler | ||
/** | ||
* A lambda passed from outside the coroutine machinery. | ||
* | ||
* See the requirements for [InternalCompletionHandler.invoke] when implementing this function. | ||
*/ | ||
class UserSupplied(private val handler: (cause: Throwable?) -> Unit) : InternalCompletionHandler { | ||
/** @suppress */ | ||
override fun invoke(cause: Throwable?) { handler(cause) } | ||
|
||
// More compact version of CompletionHandlerBase for CancellableContinuation with same workaround for JS | ||
internal expect abstract class CancelHandlerBase() { | ||
abstract fun invoke(cause: Throwable?) | ||
override fun toString() = "InternalCompletionHandler.UserSupplied[${handler.classSimpleName}@$hexAddress]" | ||
} | ||
} | ||
|
||
internal expect val CancelHandlerBase.asHandler: CompletionHandler | ||
|
||
// :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, | ||
// because we play type tricks on Kotlin/JS and handler is not necessarily a function there | ||
internal expect fun CompletionHandler.invokeIt(cause: Throwable?) | ||
|
||
internal inline fun <reified T> CompletionHandler.isHandlerOf(): Boolean = this is T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice to mention the
cause
semantics and when it's a null?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Now the abstraction of
CompletionHandler
looks even more strange.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's a mess. Luckily, it's a documented mess right now and a constant reminder we probably should change it :)