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 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) }