Sam
11/02/2022, 1:21 PMlaunch
allowed to cancel itself but coroutineScope
isn’t?Sam
11/02/2022, 1:21 PMcoroutineScope {
launch { cancel() }
}
but this fails:
coroutineScope {
cancel()
}
Sam
11/02/2022, 1:22 PMlaunch
and coroutineScope
create a new Job
— so why do those jobs behave differently from one another?krzysztof
11/02/2022, 3:50 PMmain
function - is it with runBlocking
/ coroutineScope
/ supervisorScope
?
If so, that’s why your whole program ends with exception (which is cancellation).
runBlocking
/ coroutineScope
/ supervisorScope
instead of cancelling the parent and children, would propagate the error up (re-throw). If those are top - level coroutines, then the error crashes the app.krzysztof
11/02/2022, 3:51 PMisScopedCoroutine
is true (which is true for coroutineScope and supervisorScope) the error is propagated to parent.Sam
11/02/2022, 4:18 PMprintln("Outer job is active: $isActive")
try {
coroutineScope {
cancel()
println("Inner scope is active: $isActive")
}
} finally {
println("Outer job is active: $isActive")
}
When I run this, I get:
Outer job is active: true
Inner scope is active: false
Outer job is active: true
Exception in thread "main" kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job=ScopeCoroutine{Cancelled}@612fc6eb
In other words, the inner scope is propagating a cancellation to the outer job, even though the outer job wasn’t cancelled. Surely that’s not supposed to happen?krzysztof
11/02/2022, 4:21 PMisCancelled
on outter job?Sam
11/02/2022, 4:23 PMprintln("Outer job is active: ${coroutineContext.job.isActive}")
println("Outer job is cancelled: ${coroutineContext.job.isCancelled}")
try {
coroutineScope {
cancel()
println("Inner scope is active: ${coroutineContext.job.isActive}")
println("Inner scope is cancelled: ${coroutineContext.job.isCancelled}")
}
} finally {
println("Outer job is active: ${coroutineContext.job.isActive}")
println("Outer job is cancelled: ${coroutineContext.job.isCancelled}")
}
produces
Outer job is active: true
Outer job is cancelled: false
Inner scope is active: false
Inner scope is cancelled: true
Outer job is active: true
Outer job is cancelled: false
Exception in thread "main" kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job=ScopeCoroutine{Cancelled}@491cc5c9
krzysztof
11/02/2022, 4:26 PMkrzysztof
11/02/2022, 4:26 PMSam
11/02/2022, 4:30 PMrunBlocking
, but I think the behaviour will be the same no matter how I run it. The cancellation exception always propagates outside the coroutineScope
even though the parent job is not cancelled.krzysztof
11/02/2022, 4:31 PMval scope = CoroutineScope(...)
scope.launch {}
inside launch, do coroutineScope with cancellation. You’ll notice that the program would exit without exception (because scope
does not re-throws).
Remember to add some delay below launch
as runBlocking is not aware of that coroutine launched of scope
Sam
11/02/2022, 4:34 PMcoroutineScope
, but won’t propagate outside of the launch
, for the same reasons that we already found. Let me see if I can make a simple example to show it.Sam
11/02/2022, 4:39 PMscope.launch {
try {
coroutineScope {
cancel()
ensureActive()
}
} catch (e: CancellationException) {
println("Caught a cancellation exception")
println("Cancelled: ${coroutineContext.job.isCancelled}")
}
}
produces
Caught a cancellation exception
Cancelled: false
whereas
scope.launch {
try {
cancel()
ensureActive()
} catch (e: CancellationException) {
println("Caught a cancellation exception")
println("Cancelled: ${coroutineContext.job.isCancelled}")
}
}
produces
Caught a cancellation exception
Cancelled: true
Both examples complete normally, without an exception, presumably because launch
never propagates CancellationException
to its parent as you showed in the code you linked to.Sam
11/02/2022, 4:40 PMjoin()
to wait for the job to complete before exiting)krzysztof
11/02/2022, 4:53 PMcoroutineScope
) you catch block says that cancelled: false
because you caught the cancellation yourself (because coroutine scope would re-throw exceptions), so it never reached the parentkrzysztof
11/02/2022, 4:56 PMcancel
triggers this chained parent cancellation through Job
instance (calling cancelParent). Your catch
block is triggered, by calling ensureActive
which throws CancellationException if coroutine is no longer activekrzysztof
11/02/2022, 4:56 PMThis method is a drop-in replacement for the following code, but with more precise exception:
if (!isActive) {
throw CancellationException()
}
Sam
11/02/2022, 5:09 PMcoroutineScope
should not throw a CancellationException
if the coroutine that invoked it has not been cancelled. It seems inconsistent with the way cancellation normally works.Sam
11/02/2022, 5:14 PMcoroutineScope
can return a value, so it has to throw if there is no value to return 💡Sam
11/02/2022, 5:15 PMasync { cancel() }.await()
in effectSam
11/02/2022, 5:17 PMkrzysztof
11/02/2022, 5:19 PM