I have a coroutine that keeps sending packets to a remote host every second to tell that host that a local action is ongoing. Once the action is finished, a specific “action done” packet must be sent. Pseudo code:
try {
while (true) {
delay(1000)
sendPacket(generateActionIsOngoingPacket())
}
} finally {
sendPacket(generateActionIsFinishedPacket())
}
That way, as soon as my local action is done, I can stop the repeated packet sending simply via cancelling the coroutine job.
However, in sendPacket
, a withContext(Dispatchers.IO)
block is used, since sending packet is an IO operation. And, due to the prompt cancellation guarantee, once the coroutine is cancelled, withContext
will not actually run its block - instead, it itself will cancel. So, no “action is finished” packet is sent.
One way to handle this is to use NonCancellable
:
try {
while (true) {
delay(1000)
sendPacket(generateActionIsOngoingPacket())
}
} finally {
withContext(NonCancellable) {
sendPacket(generateActionIsFinishedPacket())
}
}
This works. The “action is finished” packet is sent. Cancelling the job does what I expect.
However, I am not sure that this is the best approach. NonCancellable
has a big potential to cause a lot of problems due to uncancellable code.
Also, at a deeper level, another question comes up: In this context, what does cancelling actually signify? By that I mean that cancelling may be done for multiple reasons. Perhaps it is simply because the local action was finished in an orderly fashion. But perhaps it is cancelled because somewhere else, a fatal error was detected, and now the whole system is shutting down. In the latter case, it would perhaps not be a good idea to try to send that “action is finished” packet - after all, what if IO is also affected by that fatal error? What if it hangs indefinitely?
Some refinements to my solution I was thinking of are:
- Make it a requirement for
sendPacket
implementations that they must timeout after a few seconds if the underlying IO is unresponsive. This at least prevents permanently hangingNonCancellable
blocks. - Write a custom subclass of
Job
, and add an extra function like “finish”. That one would internally callcancel
, but would pass a subclass ofCancellationException
. In the code above, it would then check for that exception.
It would look like this:
class CustomJob : Job {
fun finish() = cancel(MyCustomCancellationExceptionSubclass())
}
// [...]
try {
while (true) {
delay(1000)
sendPacket(generateActionIsOngoingPacket())
}
} catch (_: MyCustomCancellationExceptionSubclass) {
withContext(NonCancellable) {
sendPacket(generateActionIsFinishedPacket())
}
}
That way, it would only send that “action is finished” packet if the coroutine is cancelled via the finished
call. In non-orderly shutdown cases (like when a fatal error occurs), it would be regular “cancel it all now” behavior.
Thoughts? Are my reservations about NonCancellable
correct here?