Skip to content

Reconsider what happens when await() gets cancelled #4329

Open
@dkhalanskyjb

Description

In kotlinx.coroutines, we have two types of functions called await():

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 from future.asDeferred().await(): one cancels the computation when await() is cancelled, the other one doesn't. It's inconsistent, and not in a way that's unavoidable when translating concepts across ecosystems: if Future.get gets interrupted, the whole computation doesn't get aborted, so this seems like kotlinx.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.

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.

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions