Shalom
07/23/2024, 10:58 PMJames Yox
07/23/2024, 11:28 PMval 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.Michael Krussel
07/24/2024, 11:22 AMjoin
could throw CancellationException
because the job being joined or the job doing the join was cancelled.
I implemented something with a CompleteableDeferred
and withTimeoutOrNull
.James Yox
07/24/2024, 3:40 PMasync { ... }
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.Klitos Kyriacou
07/24/2024, 3:52 PMJames Yox
07/24/2024, 3:55 PMjob
. 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 loopJames Yox
07/24/2024, 4:04 PMinnerJob
is cancelled many times.Shalom
07/24/2024, 4:35 PMfun 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'James Yox
07/24/2024, 5:39 PMinnerJob
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 depthShalom
07/25/2024, 8:53 AM