Description
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 tocont.resume(Unit)
. The latter does not provide any indication to the caller whether cancellation or resumption came first. There's only an internal API calledtryResume
that can be used to find it out.
Now there are two ways to install a handler that gets invoked on cancellation:
cont.invokeOnCancellation { ... }
(stable) — continuation cancellation handler.cont.resume(Unit) { ... }
(experimental) — resume cancellation handler.
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. Ifresume
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.