Skip to content

Tweak CancellableContinuation.invokeOnCancellation contract for consistency #1915

Closed
@elizarov

Description

@elizarov

Consider the following code that uses suspendCancellableCoroutine:

val result = suspendCancellableCoroutiune<Unit> { cont: CancellableContinuation<Unit> ->
   // call later, asynchronously
   cont.resume(Unit)
}

This call to suspendCancellableCoroutiune will not necessarily produce result == Unit. It may also end up throwing CancellationException. While indistinguishable from the outside, internally there are two conceptually different cases when CancellationExcption could be produced:

  • Case A: The coroutine running this code may get canceled before the call to cont.resume(Unit).
  • Case B: The coroutine running this code may get canceled after the call to cont.resume(Unit), but before the continuation gets dispatched to the caller.

Note, that in a general case the caller of cont.resume(Unit) cannot distinguish these two cancellation cases either, because coroutine cancellation is asynchronous and may race with the call to cont.resume(Unit). The latter does not provide any indication to the caller whether cancellation or resumption came first. There's only an internal API called tryResume that can be used to find it out.

Now there are two ways to install a handler that gets invoked on cancellation:

They are currently not consistent between themselves with respect as to which cancellation cases they react to:

  • cont.invokeOnCancelltion { ... } continuation handler only gets invoked in case A of cancellation. If resume happened before cancellation, but the coroutine was canceled later (case B), then the continuation cancellation handler is not called.
  • cont.resume(Unit) { ... } cancellation handler gets invoked in both cases.

The proposal is to make them consistent so that both are invoked in both cases. While this is technically a breaking change, it is not clear what kind of correctly working code this kind of change can break, since the difference in behavior would only manifest in cases when cont.resume was called after cancellation, but there is no way to figure out whether cancel or resume was first anyway.

There is a motivation beyond consistency, too. This inconsistency turned out while working on #1813. When the prototype without atomic cancellation was implemented, the implementation of Semaphore stoped to work correctly in case of acquire cancellation. The root of the problem is that with atomic cancellation the case B was not possible and there was no need to worry about it. However, making the proposed change to invokeOnCancellation (so that it is called in case B) fixed Semaphore. This leads us to believe that this change may also eliminate some cancel/resume races that currently exist in 3rd party code, too, because 3rd party code did not have access to atomic cancellation so it did not have any means to distinguish two cases of cancellation and was likely incorrect in racing scenarios.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions