A technical deep-dive into the nasty things that c...
# feed
s
A technical deep-dive into the nasty things that can happen to an application when cancellation exceptions go rogue. Featuring "Schrödinger’s Coroutine" 😁🐱 https://betterprogramming.pub/the-silent-killer-thats-crashing-your-coroutines-9171d1e8f79b
🐈‍⬛ 3
👍 4
s
Interesting read. But I don't quite understand "_*rogue*_" CancellationException. If code you call throws a rogue one (meaning one that should not have been thrown at all), then that is a bug/issue in the code you call. Then the code you call should be fixed by removing the throwing of such rogue CancellationException. The calling code should not worry about this and should be able to just rethrow any CancellationException, because all of them should be non-rogue. Maybe I'm misunderstanding the "rogue" part of this, though...
s
Thanks for reading! Perhaps I can find some more examples to include. My contention is that there are quite a lot of things that throw a cancellation exception for reasons unrelated to job cancellation. Like
await
and
receive
, but also various Java APIs. Not bugs per se, just other ways of using the same exception supertype.
s
Hmmm... That this would mean you would need to always bracket any code within try-catch block and add that special code (ensureActive()). If you don't do that, you always would run the risk of ignoring and not fixing a rogue CancellationException.
s
I agree!
I guess I decided to stop short of recommending that every suspend call be surrounded by try/catch 😄. But your point is definitely valid.
s
😁 I'm of the opinion to never add "fixes" around the code you call. Fix the actual code that is called instead. Unless there is no other way around it 😀 Still, a good point and wondering what the Kotlin designers think about such rogue CancellationExceptions...
e
send/receive must throw something on a closed channel, as they return Unit which doesn't signal anything. CancellationException is a natural fit since it makes the typical loop work out correctly. if you want to continue on close, use trySend/receiveCatching.
f
The solution with catch Throwable + ensure active may swallow exceptions, as the catch block on a cancelled coroutine would throw CancellationException before the error is handled. This is not always desirable AFAIS.
s
Thanks for reading, @ephemient! We can distinguish between a closed channel (sender has gone away) and a cancelled channel (receiver has gone away). The send/receive functions will only throw
CancellationException
in the latter case. When the channel is simply closed, they instead throw dedicated “closed channel” exceptions that don’t extend
CancellationException
, which I think is strongly preferable. Actually I find concept of a “cancelled” channel to be unnecessary and even destructive now that we have structured concurrency. Your suggestion of using trySend/receiveCatching as a workaround is a nice solution, thank you 🙇
Great observation about suppressing other errors @franztesca, thank you! My gut feeling is that exceptions thrown during or after coroutine cancellation are fair game and that we shouldn’t worry too much about dropping them. But I agree that the added
ensureActive
check does change the behaviour there. I’ll have a think about it and see if I can come up with some examples and/or mitigations.
o
What about having these
Copy code
suspend fun Throwable.cancelsThisCoroutine() = this is CancellationException && !coroutineContext.isActive

suspend fun Throwable.propagateCancellation() {
    if (cancelsThisCoroutine()) {
        throw this
    }
}
and use the latter like so?
Copy code
try {
    // ...
} catch (throwable: Throwable) {
    throwable.propagateCancellation()
    // log exception
}
s
Nice suggestion for ensuring that a non-cancellation exception doesn’t get suppressed and replaced by a cancellation exception 👍. I guess a similar approach would be just to wrap the call to
ensureActive
in an additional type check:
Copy code
if (throwable is CancellationException) {
    coroutineContext.ensureActive()
}
Slightly different in behaviour but much the same result. Personally I’m still hesitating as to whether I think having the cancellation suppress other errors is a problem or not… perhaps it varies from case to case.
o
Re-throwing instead of resorting to
ensureActive()
preserves the original
CancellationException
. Otherwise you'd not only lose the original message but also the list of suppressed exceptions attached to the original `CancellationException`: Exceptions aggregation
blob think smart 1
s
Thanks for that docs link, I think there is some nuance in the behaviour there that I wasn’t aware of! 👌
👍 1