Kotlinx.coroutines: Provide a way to cancel a running coroutine immediately

Created on 19 Aug 2019  路  6Comments  路  Source: Kotlin/kotlinx.coroutines

When using blocking IO or computations the coroutine won't be cancelled until the operation is complete meaning finally and invokeOnCompletion blocks can be called long after the parent job is cancelled.

Here's an example:

class MyPresenter {
  fun loadStuff() {
    scope.launch {
      view.showProgress()
      try {
        performNetworkRequest()
      } catch(e: Exception) {
        view.hideProgress()
      }
    }
  }
}

The example above might crash if the performNetworkRequest isn't cancelled before the view is detached from the presenter.

Currently the only way around this is to either use suspendCancellableCoroutine and perform a context switch to suspend the coroutine or to use invokeOnCompletion(onCancelling = true) (which is marked internal).

Perhaps a special context could be added to indicate that the result of an operation should be ignored when cancelling.

question

Most helpful comment

On the JVM, there's no generic way to force cancellation for any blocking operation. Some APIs have the ability to cancel, others you can work around with thread interruptions. But it's up to the integration with your particular blocking API to handle actually cancelling the blocking operation. If you're using Retrofit's coroutine integration, network requests will automatically be cancelled when the coroutine is cancelled.

Even if you can't actually cancel the operation, as long as the network request suspends the current coroutine (which it will if it changes context), the cancellation exception should be getting thrown from that function when it finishes at least. Catch and rethrow CancellationException, or check if e is CancellationException, before updating view state again. In general, swallowing CancellationExceptions (or generic Exceptions) is usually not a great idea unless you're really sure that's what you want to do.

All 6 comments

On the JVM, there's no generic way to force cancellation for any blocking operation. Some APIs have the ability to cancel, others you can work around with thread interruptions. But it's up to the integration with your particular blocking API to handle actually cancelling the blocking operation. If you're using Retrofit's coroutine integration, network requests will automatically be cancelled when the coroutine is cancelled.

Even if you can't actually cancel the operation, as long as the network request suspends the current coroutine (which it will if it changes context), the cancellation exception should be getting thrown from that function when it finishes at least. Catch and rethrow CancellationException, or check if e is CancellationException, before updating view state again. In general, swallowing CancellationExceptions (or generic Exceptions) is usually not a great idea unless you're really sure that's what you want to do.

I understand that the action itself won鈥檛 actually be cancelled, but what I want is that if the coroutine is suspended waiting a result from another dispatcher that the suspended coroutine be resumed instantly. This might not be possible for reasons I don鈥檛 understand but the problem is real. It would at least be nice if there was a non internal way to get notified instantly when the job is cancelled

Ah, I see. The calling function can't be resumed eagerly because the callee could still catch the cancellation and swallow it or re-throw, and that would need to get reported to the caller. Another way you could do this is just wrap the network request in an async/await:

try {
  async { performNetworkRequest() }.await()
}

It's a bit more concise than the other options, and the caller will get resumed immediately since the actual blocking work is being done in a separate coroutine.

@ansman If you want to run an non-cancellable operation in such a way that cancellation does not wait for it completion, then you can be explicit about the fact that cancelling it would leave a "leftover" code that is still working after cancellation, by writing it like this:

GlobalScope.async { performNetworkRequest() }.await()

It looks like it is going to do exactly what you want.

The good thing about writing it like that is that the reader of the code will immediately see what is the danger of this code -- upon cancellation it leaks a background task, so if you do it again and again (like user constantly hitting "retry" button) you'll run out of resources. Does it help?

I still want to mark the job as cancelled, I just don't want to wait. Zach's suggestion works but will fail the parent job unless it's a supervisor job but that is manageable I suppose.

I created this function now that would fix this. Thanks for the suggestion.

suspend inline fun <R> withCancellableContext(
    context: CoroutineContext,
    crossinline block: CoroutineScope.() -> R
): R {
    val job = GlobalScope.async(context) {
        runCatching(block)
    }
    val result = try {
        job.await()
    } catch (e: CancellationException) {
        job.cancel()
        throw e
    }
    return result.getOrThrow()
}
Was this page helpful?
0 / 5 - 0 ratings