Solution: Correct mistakes with cancellation
There are two changes that need to be made:
- We should not block suspending function unless we set a dispatcher that can be used for blocking operations. This is because suspending functions should not be blocking (they should be dispatcher-agnostic). To correct it, we should wrap the whole function with
withContextwith a dispatcher that can be used for blocking operations, likeDispatchers.IO. - We should use
yieldbetween two blocking operations, to allow cancellation and redispatching between them. - We should wrap
revertUnfinishedTransactionswith the blockwithContext(NonCancellable)so it (a suspending function) can be executed (otherwise a suspension would throw an exception in cancelled coroutine). - We should rethrow the
CancellationException, because the functions that callupdateUsermight also want to specify what to do when the operation is cancelled.
Notice, that using async here is not useful, because all operations must be called sequentially.
suspend fun updateUser() = withContext(Dispatchers.IO) { val user = readUser() // blocking yield() val settings = readUserSettings(user.id) // blocking try { updateUserInDatabase(user, settings) // suspending } catch (e: CancellationException) { withContext(NonCancellable) { revertUnfinishedTransactions() // suspending } throw e } }
There are three changes that need to be made:
- We should use a dispatcher that can be used for blocking operations, like
Dispatchers.IO, because we have blocking operationsreadTextanddelete. - We should make sure we rethrow the
CancellationException. - We should use
yieldbetween a blocking and CPU operations, to allow cancellation between them (though this is not such important, ascalculateSignatureis likely lightweight, and there is a suspending call straight after it).
Notice, that using async here is not useful, because all operations must be called sequentially.
suspend fun sendSignature(file: File) = withContext(Dispatchers.IO) { try { val content = file.readText() yield() val signature = calculateSignature(content) sendSignature(signature) // suspending } catch (e: CancellationException) { throw e } catch (e: Exception) { println("Error while sending signature: ${e.message}") e.printStackTrace() } finally { file.delete() } }
There is one change that needs to be made:
- We should rethrow the
CancellationException.
suspend fun trySendUntilSuccess() { var success = false do { try { send() success = true } catch (e: CancellationException) { throw e } catch (e: Exception) { println("Error while sending: ${e.message}") e.printStackTrace() } } while (!success) }
That example is especially interesting, because if we do not rethrow the CancellationException, in case of cancellation, this function will be in an infinite loop. You can use the following code to see it yourself:
fun main(): Unit = runBlocking { val job = launch { trySendUntilSuccess() } delay(100) job.cancelAndJoin() } suspend fun send() { println("Sending...") delay(1000) }