Description
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.Task
by 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.Task
if 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.get
gets interrupted, the whole computation doesn't get aborted, so this seems likekotlinx.coroutines
invention.- 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
Deferred
fail 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.