Is there some way to hook into each suspend/yield ...
# coroutines
m
Is there some way to hook into each suspend/yield point so a check can be performed to potentially cancel the current Job? My use case is implementing an Android ContentProvider’s blocking query() method, which takes a CancellationSignal as one (amongst others) argument. At the moment, I have to pass the CancellationSignal down through the code and perform checks at appropriate points, but it would be better if the check could be done via some kind of onSuspend callback.
z
you could implement a custom
ContinuationInterceptor
that wraps your dispatcher.
👍 1
But a better option would be to register an
OnCancelListener
and cancel your Job when the coroutine is cancelled.
m
Can you explain a little more about that listener please?
Oh I see - CancellationSignal.OnCancelListener
☝️ 1
z
Yep. That's the only way cancellation will immediately take effect if you get cancelled while suspended.
m
Wouldn’t
ContinuationInterceptor
also work?
z
The interceptor only gets invoked when a continuation object is created for suspension. If cancellation signal fires during suspension, the coroutine won't get cancelled until your interceptor sees it (on resumption). The whole point of cancellation is to stop long-running tasks before they finish, and the interceptor approach doesn't do that. I only suggested it because I didn't realize there was a callback API - it was a terrible suggestion lol.
m
I thought when a Job is cancelled the actual cancellation doesn’t kick in until a suspension point (or yield) is encountered. And that this is why we should call yield() in some loops etc. And that these points are the same points where the continuation object comes into play. Which part am I getting wrong here?
Ah, maybe the distinction between interceptor and continuation object?
z
That’s not strictly true. Cancelling a Job does a few things: 1) Atomically sets a flag that the job is cancelled. After that point, anyone with access to that Job can see that it’s cancelled by calling
isActive/isCancelled
or calling
ensureActive()
, regardless of suspension – e.g. tight loops might call
ensureActive()
to cooperatively participate in cancellation. 2) Synchronously executes any cancellation logic registered via
invokeOnCancellation
(on Jobs or on `CancellableContinuation`s) – this logic and not guaranteed to be on any particular thread. 3) Immediately cancel all child jobs recursively, as part of structured concurrency. So there might be coroutines that don’t suspend at all, but still participate in cancellation. Cancellation and suspension are related, but cancellation involves a lot more than just resuming suspended coroutines, so cancellation should be propagated as quickly as possible.
👍 1
So your
query
implementation could look something like:
Copy code
override fun query(
  uri: Uri,
  projection: Array<String>,
  queryArgs: Bundle,
  cancellationSignal: CancellationSignal
): Cursor = runBlocking {
  val job = coroutineContext[Job]!!
  cancellationSignal.setOnCancelListener { job.cancel() }

  // your code
}
👍 1
Additionally, this PR (https://github.com/Kotlin/kotlinx.coroutines/pull/1972) introduces a method which allows regular JVM blocking methods (IO or synchronization) to participate in cancellation.
m
Excellent, thank you @Zach Klippenstein (he/him) [MOD] That helps me a lot 🙏