how to interrupt a delay? I want to do val job = ...
# getting-started
s
how to interrupt a delay? I want to do val job = async { while(isActive){ //do something delay(10.minutes) } } from another coroutine or java thread i want to interrupt the delay earlier to continue the loop. I know i can wrap the delay and catch cancellation exception and continue the loop, is this the way?
j
Copy code
val job = async {
  while(isActive){
     //do something
     val job = launch { delay(10.minutes) }
     // hand job off somewhere that can cancel it
     job.join() // Suspend until cancelled or delay finishes
  }
}
Would something like this work? Now the delay is wrapped as a job that can be cancelled. You might be looking for something different though.
👍 3
m
I think you need to be careful with this because the
join
could throw
CancellationException
because the job being joined or the job doing the join was cancelled. I implemented something with a
CompleteableDeferred
and
withTimeoutOrNull
.
👍 1
j
You should be able to cancel the inner job without worrying about the Exception at least inside the
async { ... }
I generally try not to think about
CancellationException
when working with coroutines as I think of it more like an internal mechanism. Mechanically it appears that
join()
can throw
CancellationException
if the parent (async) job is cancelled but generally you want a
CancellationException
to propagate so you dont worry about catching it. This accomplishes (I think) what OP wanted without messing with coroutine internals.
k
The OP wants to continue the loop, which is inside an outer coroutine. Wouldn't the cancellation exception cause the loop to be exited?
j
I should not have named both jobs
job
. The loop would be exited if you cancel the outer
val job = async {
but that makes sense since you are cancelling the parent that houses the loop. If you cancel the inner
val job = launch { ... }
it should not exit the loop
👍 1
https://pl.kotl.in/SHoRLq_Kq For a contrived example. You can see that the loop keeps running even though the
innerJob
is cancelled many times.
s
did something like that, it seems to do what i want. innerJob.join() never fails on exception when canceled from the timer even if i don't wrap the delay with try/catch. I'm trying to do the same thing in an intellij plugin service but unfortunately it doesn't work there, the join always returns immediately. maybe i have a bug somewhere or maybe its related to the CoroutineScope that intellij provides me with?
Copy code
fun main() {
    var innerJob: Job? = null
    GlobalScope.launch {

        var delay = 5.seconds
        while (isActive) {
            println("${Instant.now()} in main loop")
            delay = Random.nextLong(3L, 20L).toDuration(DurationUnit.SECONDS)
            innerJob = launch {
                println("${Instant.now()} in inner job waiting $delay")
                try {
                    delay(delay.inWholeMilliseconds)
                } catch (ce: CancellationException) {
                    println("${Instant.now()} inner job: I'm canceled '${ce.message}'")
                }
            }
            try {
                println("${Instant.now()} in main loop waiting $delay")
                innerJob?.join()
                println("${Instant.now()} in main loop done waiting")
            } catch (ce: CancellationException) {
                println("${Instant.now()} in main loop exception '${ce.message}'")
            } catch (e: Throwable) {
                println("${Instant.now()} in main loop exception '${e.message}'")
            }
        }
    }

    fixedRateTimer(initialDelay = 5000, period = 5000) {
        println("${Instant.now()} canceling inner job")
        innerJob?.cancel("canceled from timer")
    }
}
2024-07-24T162325.796503571Z in main loop 2024-07-24T162325.813692432Z in inner job waiting 13s 2024-07-24T162325.813540759Z in main loop waiting 13s 2024-07-24T162330.790275909Z canceling inner job 2024-07-24T162330.792447365Z inner job: I'm canceled 'canceled from timer' 2024-07-24T162330.795182614Z in main loop done waiting 2024-07-24T162330.795347212Z in main loop 2024-07-24T162330.795800313Z in main loop waiting 3s 2024-07-24T162330.795932191Z in inner job waiting 3s 2024-07-24T162333.796556079Z in main loop done waiting 2024-07-24T162333.796715731Z in main loop 2024-07-24T162333.796881770Z in main loop waiting 6s 2024-07-24T162333.797012143Z in inner job waiting 6s 2024-07-24T162335.790475103Z canceling inner job 2024-07-24T162335.791199526Z inner job: I'm canceled 'canceled from timer' 2024-07-24T162335.791725156Z in main loop done waiting 2024-07-24T162335.791926626Z in main loop 2024-07-24T162335.792265943Z in main loop waiting 18s 2024-07-24T162335.792447879Z in inner job waiting 18s 2024-07-24T162340.790150837Z canceling inner job 2024-07-24T162340.790821127Z inner job: I'm canceled 'canceled from timer' 2024-07-24T162340.791429887Z in main loop done waiting 2024-07-24T162340.791612322Z in main loop 2024-07-24T162340.791961466Z in main loop waiting 15s 2024-07-24T162340.792129780Z in inner job waiting 15s 2024-07-24T162345.790846644Z canceling inner job 2024-07-24T162345.791541166Z inner job: I'm canceled 'canceled from timer'
j
That code seems to do what I would expect. It cancels the
innerJob
after 5 seconds which cancels the delay progressing the loop. You really shouldn't catch
CancellationException
like that though. Doing so prevents coroutines from working correctly. It essentially swallows quick cancellation which means that calls to
cancel()
might not be be cooperative. Back to your case though. What about the code above doesn't work as you expect? If you are using a
CoroutineScope
provided by something else that could be getting cancelled which would cancel all child jobs. Again, this is expected given how structured concurrency works. Coroutine Scopes are often tied to lifecycles like the Android activity lifecycle which will cancel the scope when an activity is stopped. Catching the
CancellationException
can be really problematic because it can break that mechanism which exists to prevent code from needlessly executing after an activity stops. You see this sort of mistake often with network calls where people catch the generic
Exception
that might come from a HTTP call and then cancelling that scope/job wont prevent things like deserialization and processing from occurring on a cancelled in flight network request. https://betterprogramming.pub/the-silent-killer-thats-crashing-your-coroutines-9171d1e8f79b Goes into a lot more depth
s
Thank for the info and the help. I implemented it in my plugin and it works fine!