Skip to content
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

Update CancellableContinuation documentation #4268

Merged
merged 2 commits into from
Nov 8, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 89 additions & 17 deletions kotlinx-coroutines-core/common/src/CancellableContinuation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,97 @@ import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*

// --------------- cancellable continuations ---------------

/**
* Cancellable continuation. It is _completed_ when resumed or cancelled.
* When the [cancel] function is explicitly invoked, this continuation immediately resumes with a [CancellationException] or
* Cancellable [continuation][Continuation] is a thread-safe continuation primitive with the support of
* an asynchronous cancellation.
*
* Cancellable continuation can be [resumed][Continuation.resumeWith], but unlike regular [Continuation],
* it also might be [cancelled][CancellableContinuation.cancel] explicitly or [implicitly][Job.cancel] via a parent [job][Job].
*
* If the continuation is cancelled successfully, it resumes with a [CancellationException] or
* the specified cancel cause.
*
* An instance of `CancellableContinuation` is created by the [suspendCancellableCoroutine] function.
* ### Usage
*
* An instance of `CancellableContinuation` can only be obtained by the [suspendCancellableCoroutine] function.
* The interface itself is public for use and private for implementation.
*
* A typical use of this function is to suspend a coroutine while waiting for a result
* from a callback or external source of values:
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
*
* ```
* suspend fun <T> CompletableFuture<T>.await(): T = suspendCancellableCoroutine { c ->
* val future = this
* future.whenComplete { result, throwable ->
* if (throwable != null) {
* // Resume continuation with an exception if an external source failed
* c.resumeWithException(throwable)
* } else {
* // Resume continuation with a value if it was computed
* c.resume(result)
* }
* }
* // Cancel the computation if the continuation itself was cancelled because a caller of 'await' is cancelled
* c.invokeOnCancellation { future.cancel(true) }
* }
* ```
*
* ### Thread-safety
*
* Instances of [CancellableContinuation] are thread-safe and can be safely shared across multiple threads.
* [CancellableContinuation] allows concurrent invocations of [cancel] and [resume] pair, guaranteing
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
* that only one of these operations will succeed.
* Concurrent invocations of [resume] methods lead to a [IllegalStateException] and considered a programmatic error.
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
*
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
* ### Prompt cancellation guarantee
*
* Cancellable continuation provides a **prompt cancellation guarantee**.
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
*
* If the [Job] of the coroutine that obtained a cancellable continuation was cancelled while this continuation was suspended it will not resume
* successfully, even if [CancellableContinuation.resume] was already invoked but not yet executed.
*
* The cancellation of the coroutine's job is generally asynchronous with respect to the suspended coroutine.
* The suspended coroutine is resumed with a call to its [Continuation.resumeWith] member function or to the
* [resume][Continuation.resume] extension function.
* However, when the coroutine is resumed, it does not immediately start executing but is passed to its
* [CoroutineDispatcher] to schedule its execution when the dispatcher's resources become available for execution.
* The job's cancellation can happen before, after, and concurrently with the call to `resume`. In any
* case, prompt cancellation guarantees that the coroutine will not resume its code successfully.
*
* If the coroutine was resumed with an exception (for example, using [Continuation.resumeWithException] extension
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
* function) and cancelled, then the exception thrown by the `suspendCancellableCoroutine` function is determined
* by what happened first: exceptional resume or cancellation.
*
* ### Resuming with a closeable resource
*
* [CancellableContinuation] provides the capability for working with values that represent a resource that should be
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
* closed. For that, it proved `resume(value: R, onCancellation: ((cause: Throwable, value: R, context: CoroutineContext) -> Unit)`
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
* function that guarantees either the given `value` will be successfully returned from the corresponding `suspend` function
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
* or that `onCancellation` will be invoked with the supplied value:
*
* Cancellable continuation has three states (as subset of [Job] states):
* ```
* continuation.resume(resourceToResumeWith) { _, resourceToClose, _
* // Will be invoked if the continuation is cancelled while being dispatched
* resourceToClose.close()
* }
* ```
*
* #### Continuation states
*
* Cancellable continuation has three observable states:
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
*
* | **State** | [isActive] | [isCompleted] | [isCancelled] |
* | ----------------------------------- | ---------- | ------------- | ------------- |
* | _Active_ (initial state) | `true` | `false` | `false` |
* | _Resumed_ (final _completed_ state) | `false` | `true` | `false` |
* | _Canceled_ (final _completed_ state)| `false` | `true` | `true` |
*
* Invocation of [cancel] transitions this continuation from _active_ to _cancelled_ state, while
* invocation of [Continuation.resume] or [Continuation.resumeWithException] transitions it from _active_ to _resumed_ state.
*
* A [cancelled][isCancelled] continuation implies that it is [completed][isCompleted].
* For a detailed description of each state, see the corresponding properties' documentation.
*
* Invocation of [Continuation.resume] or [Continuation.resumeWithException] in _resumed_ state produces an [IllegalStateException],
* but is ignored in _cancelled_ state.
* A successful invocation of [cancel] transitions continuation from _active_ to _cancelled_ state, while
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
* invocation of [Continuation.resume] or [Continuation.resumeWithException] transitions it from _active_ to _resumed_ state.
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
*
* Possible state transitions diagram:
* ```
* +-----------+ resume +---------+
* | Active | ----------> | Resumed |
Expand All @@ -45,18 +111,23 @@ import kotlin.coroutines.intrinsics.*
@SubclassOptInRequired(InternalForInheritanceCoroutinesApi::class)
public interface CancellableContinuation<in T> : Continuation<T> {
/**
* Returns `true` when this continuation is active -- it has not completed or cancelled yet.
* Returns `true` when this continuation is active -- it was created,
* but not yet [resumed][Continuation.resumeWith] or [cancelled][CancellableContinuation.cancel].
*
* This state implies that [isCompleted] and [isCancelled] are `false`.
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
*/
public val isActive: Boolean

/**
* Returns `true` when this continuation has completed for any reason. A cancelled continuation
* is also considered complete.
* Returns `true` when this continuation was completed -- [resumed][Continuation.resumeWith] or
* [cancelled][CancellableContinuation.cancel].
*
* This state implies that [isActive] is `false`.
*/
public val isCompleted: Boolean

/**
* Returns `true` if this continuation was [cancelled][cancel].
* Returns `true` if this continuation was [cancelled][CancellableContinuation.cancel].
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Do we need to introduce such a fix throughout the documentation of every class/interface referring to its other members? I hope not: if just writing [cancel] is problematic, to me, this looks like a bug in our documentation tooling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be more explicit, I can revert that if you believe it's important.
I.e. it could've been some top-level cancel, too much of a context to process at once

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's just a stylistic choice (that no one except the ones reading pure KDoc text will see?), it's not important to me either way, but if this is needed for the link to correctly resolve, that's alarming.

*
* It implies that [isActive] is `false` and [isCompleted] is `true`.
*/
Expand Down Expand Up @@ -124,6 +195,7 @@ public interface CancellableContinuation<in T> : Continuation<T> {
/**
* Cancels this continuation with an optional cancellation `cause`. The result is `true` if this continuation was
* cancelled as a result of this invocation, and `false` otherwise.
* [cancel] might return `false` when the continuation was either [resumed][resume] or already [cancelled][cancel].
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
*/
public fun cancel(cause: Throwable? = null): Boolean

Expand Down Expand Up @@ -243,7 +315,7 @@ internal fun <T> CancellableContinuation<T>.invokeOnCancellation(handler: Cancel
/**
* Suspends the coroutine like [suspendCoroutine], but providing a [CancellableContinuation] to
* the [block]. This function throws a [CancellationException] if the [Job] of the coroutine is
* cancelled or completed while it is suspended.
* cancelled or completed while it is suspended, or if [CancellableContinuation.cancel] is invoked.
*
* A typical use of this function is to suspend a coroutine while waiting for a result
* from a single-shot callback API and to return the result to the caller.
Expand Down