kotlinx.coroutines
kotlinx.coroutines copied to clipboard
Reconsider what happens when `await()` gets cancelled
In kotlinx.coroutines, we have two types of functions called await():
- Multiple consumers can call
await()independently: this is the case for JS Promise, Deferred, the reactive integrations, andcom.google.android.gms.tasks.Taskby default. - If a consumer is who calls
await()is cancelled, the entity gets cancelled: this is the idea behind bidirectional cancellation in https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.future/await.html and https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/await.html, andcom.google.android.gms.tasks.Taskif you pass a cancellation token.
A way to share (for example) a ListenableFuture among several consumers is to convert it to a Deferred and call await() on that.
There are a few things that bother me about this.
future.await()behaves differently fromfuture.asDeferred().await(): one cancels the computation whenawait()is cancelled, the other one doesn't. It's inconsistent, and not in a way that's unavoidable when translating concepts across ecosystems: ifFuture.getgets interrupted, the whole computation doesn't get aborted, so this seems likekotlinx.coroutinesinvention.- Why do these two operations that behave significantly differently share the same name? The data structure that is used is orthogonal to whether it's single-shot. If the second kind of
await()was called something different (for example,consume()), we could- For things that currently only support consuming, support the non-obvious step of first converting it to
Deferred. - For things that currently only support awaiting, have what acts as a limited version of intersections of coroutine scopes. The latter is a wider issue than just cancelling an operation if one of two parents gets cancelled, but the idea of having a
Deferredfail if either the only consumer fails or the component computing the value does seems like a notable special case.
- For things that currently only support consuming, support the non-obvious step of first converting it to
https://youtrack.jetbrains.com/issue/KTIJ-17464/Warn-on-unstructured-no-CoroutineScope-protection-gap-between-getting-a-future-and-await-call-on-it also mentions that for the consuming await(), this pattern is incorrect:
val future = foo()
something() // can throw
future.await() // .consume()
If await() is never called, the computation leaks.
This leads to another problem that I think is worth considering: future.asDeferred() does not allow integrating into structured concurrency. If it did, then this problem would be avoided using this cleaner API:
val future = foo().asDeferred(currentCoroutineContext().job)
future.await()
The IDE inspection would then be limited to suggesting to pass something to asDeferred when it's called in a suspend context.