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 withContext with a dispatcher that can be used for blocking operations, like Dispatchers.IO.
We should use yield between two blocking operations, to allow cancellation and redispatching between them.
We should wrap revertUnfinishedTransactions with the block withContext(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 call updateUser might 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 operations readText and delete.
We should make sure we rethrow the CancellationException.
We should use yield between a blocking and CPU operations, to allow cancellation between them (though this is not such important, as calculateSignature is 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)
}
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.