Job is the primary mechanism for controlling cancellation behavior of co-routines.
Job - provides another avenue of stopping work other than traditional exception flow. Separate cancellation mechanism that is not as apparent as typical exceptions, where failed co-routine cancel’s all co-routines that are related to it through Job hierarchy. (Note: Supervisor-Job is exception)
Uncaught, non-cancellation exception → Cancels Siblings + Propagates recursively through parent Job hierarchy, with each parent shutting down its children, Effectively shutting down the entire hierarchy up to the top most Job (or up to where you caught exception at error boundary).
Propagates to parent: Parent scope will rethrow the original exception that was thrown in co-routine.
Highlight
Job - provide another avenue of stopping work other than traditional exceptions. Separate cancellation mechanism that is not as apparent as typical exception flow, where failed co-routine cancel’s all co-routines that are related to it through Job hierarchy.
Each coroutine has its own Job instance.
Each Job instance is tied to it’s parent job (if it has a parent Job). This tie in enables cancellation behavior.
Example
If regular Job() were used. If Coroutine#6 throws non-CancellationException exception the entire hierarchy is gonig to be cancelled and Coroutine#1 will rethrow the exception that Coroutine#6 threw.
In the “Active” state, a job is running and doing its job. If the job
is created with a coroutine builder, this is the state where the body
of this coroutine will be executed. In this state, we can start child
coroutines. Most coroutines will start in the “Active” state. Only
those that are started lazily will start with the “New” state. These
need to be started in order for them to move to the “Active” state.
When a coroutine is executing its body, it is surely in the “Active”
state. When it is done, its state changes to “Completing”, where it
waits for its children. Once all its children are done, the job changes
its state to “Completed”, which is a terminal one. Alternatively, if a
job cancels or fails when running (in the “Active” or “Completing”
state), its state will change to “Cancelling”. In this state, we have the
last chance to do some clean-up, like closing connections or freeing
resources (we will see how to do this in the next chapter). Once this
is done, the job will move to the “Cancelled” state.
SupervisorJob is similar to a regular Job with the difference that cancellation is propagated only downwards, which means:
Children can fail independently of each other.
Does not auto-fail parent.
A failure or cancellation of a child does not cause the supervisor job to fail and does not affect its other children.
SupervisorJob() examples
Straightforward Examples
Straightforward SupervisorJob examples
SupervisorJob() added at scope level allows one child to finish, when the other child threw exception
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.asyncimport kotlinx.coroutines.delayimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessfun main(): kotlin.Unit = runBlocking { val out = Out.standard() out.info("START") try { val coroutineScope = CoroutineScope( Dispatchers.Default + SupervisorJob() ) val coRoutine1Deferred = coroutineScope.async(CoroutineName("WillFail")) { val timeMillis = 2000L out.info("This one will throw in $timeMillis ms") delay(timeMillis) val exc = MyExceptionWillThrowFromCoroutine("I-Failed-In-CoRoutine") out.warn("${Emojis.EXCEPTOIN} I am throwing [${exc::class.simpleName}/${exc.message}]! ${Emojis.EXCEPTOIN}") throw exc "ResultFromWillFail" } val coRoutine2Deferred = coroutineScope.async(CoroutineName("JustPrints")) { (0..10) .map { "a-${it}" } .forEach { out.info(it) try { delay(500) } catch (e: CancellationException) { val excMsg = e.message ?: e.toString() out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ") throw e } } } // Notice we wait on co-routine 2 first. we do that since 2nd one is not the one that throws // MyExceptionWillThrowFromCoroutine out.info("co-routine-2 result: " + coRoutine2Deferred.await()) out.info("Successfully awaited co-routine-2, now awaiting co-routine-1") out.info("co-routine-1 result: " + coRoutine1Deferred.await()) } catch (e: Exception) { out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1") exitProcess(1) } out.info("DONE - without errors on main")}class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)
Command to reproduce:
gt.sandbox.checkout.commit fc38283452b69eaf78fc \&& cd "${GT_SANDBOX_REPO}" \&& cmd.run.announce "./gradlew run --quiet"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug[INFO][elapsed: 14ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START[INFO][elapsed: 33ms][2️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-1/tid:31] This one will throw in 2000 ms[INFO][elapsed: 37ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-0[INFO][elapsed: 538ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-1[INFO][elapsed: 1039ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-2[INFO][elapsed: 1539ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-3[INFO][elapsed: 2040ms][2️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-1/tid:31] a-4[WARN][elapsed: 2068ms][3️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-2/tid:32] 💥 I am throwing [MyExceptionWillThrowFromCoroutine/I-Failed-In-CoRoutine]! 💥 [INFO][elapsed: 2541ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-5[INFO][elapsed: 3041ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-6[INFO][elapsed: 3542ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-7[INFO][elapsed: 4043ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-8[INFO][elapsed: 4543ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-9[INFO][elapsed: 5044ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-10[INFO][elapsed: 5546ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-2 result: kotlin.Unit[INFO][elapsed: 5546ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Successfully awaited co-routine-2, now awaiting co-routine-1[ERROR][elapsed: 5560ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] in main got an exception! of type=[MyExceptionWillThrowFromCoroutine] with msg=[I-Failed-In-CoRoutine] cause=[com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine: I-Failed-In-CoRoutine]. Exiting with error code 1FAILURE: Build failed with an exception.* What went wrong:Execution failed for task ':app:run'.> Process 'command '/home/nickolaykondratyev/.jdks/corretto-21.0.7/bin/java'' finished with non-zero exit value 1* Try:> Run with --stacktrace option to get the stack trace.> Run with --info or --debug option to get more log output.> Run with --scan to get full insights.> Get more help at https://help.gradle.org.BUILD FAILED in 5s
SupervisorJob() added at 'async' level of throwing sibling allows the other sibling to finish
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.asyncimport kotlinx.coroutines.delayimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessfun main(): kotlin.Unit = runBlocking { val out = Out.standard() out.info("START") try { val coroutineScope = CoroutineScope( Dispatchers.Default ) val coRoutine1Deferred = coroutineScope.async( CoroutineName("WillFail") + SupervisorJob() ) { val timeMillis = 2000L out.info("This one will throw in $timeMillis ms") delay(timeMillis) val exc = MyExceptionWillThrowFromCoroutine("I-Failed-In-CoRoutine") out.warn("${Emojis.EXCEPTOIN} I am throwing [${exc::class.simpleName}/${exc.message}]! ${Emojis.EXCEPTOIN}") throw exc "ResultFromWillFail" } val coRoutine2Deferred = coroutineScope.async(CoroutineName("JustPrints")) { (0..10) .map { "a-${it}" } .forEach { out.info(it) try { delay(500) } catch (e: CancellationException) { val excMsg = e.message ?: e.toString() out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ") throw e } } } // Notice we wait on co-routine 2 first. we do that since 2nd one is not the one that throws // MyExceptionWillThrowFromCoroutine out.info("co-routine-2 result: " + coRoutine2Deferred.await()) out.info("Successfully awaited co-routine-2, now awaiting co-routine-1") out.info("co-routine-1 result: " + coRoutine1Deferred.await()) } catch (e: Exception) { out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1") exitProcess(1) } out.info("DONE - without errors on main")}class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)
Command to reproduce:
gt.sandbox.checkout.commit 2156e50526a56711a2e8 \&& cd "${GT_SANDBOX_REPO}" \&& cmd.run.announce "./gradlew run --quiet"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug[INFO][elapsed: 15ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START[INFO][elapsed: 36ms][2️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-1/tid:31] This one will throw in 2000 ms[INFO][elapsed: 38ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-0[INFO][elapsed: 540ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-1[INFO][elapsed: 1041ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-2[INFO][elapsed: 1542ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-3[INFO][elapsed: 2042ms][2️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-1/tid:31] a-4[WARN][elapsed: 2071ms][3️⃣][⓶][coroutname:@WillFail#2][tname:DefaultDispatcher-worker-2/tid:32] 💥 I am throwing [MyExceptionWillThrowFromCoroutine/I-Failed-In-CoRoutine]! 💥 [INFO][elapsed: 2543ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-5[INFO][elapsed: 3044ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-6[INFO][elapsed: 3544ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-7[INFO][elapsed: 4045ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-8[INFO][elapsed: 4546ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-9[INFO][elapsed: 5047ms][3️⃣][⓷][coroutname:@JustPrints#3][tname:DefaultDispatcher-worker-2/tid:32] a-10[INFO][elapsed: 5548ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-2 result: kotlin.Unit[INFO][elapsed: 5549ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Successfully awaited co-routine-2, now awaiting co-routine-1[ERROR][elapsed: 5563ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] in main got an exception! of type=[MyExceptionWillThrowFromCoroutine] with msg=[I-Failed-In-CoRoutine] cause=[com.glassthought.sandbox.MyExceptionWillThrowFromCoroutine: I-Failed-In-CoRoutine]. Exiting with error code 1FAILURE: Build failed with an exception.* What went wrong:Execution failed for task ':app:run'.> Process 'command '/home/nickolaykondratyev/.jdks/corretto-21.0.7/bin/java'' finished with non-zero exit value 1* Try:> Run with --stacktrace option to get the stack trace.> Run with --info or --debug option to get more log output.> Run with --scan to get full insights.> Get more help at https://help.gradle.org.BUILD FAILED in 5s
We are able to cancel Deferred(Job) that used SupervisorJob() to be created by calling cancel() directly on deferred object
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.asyncimport kotlinx.coroutines.delayimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessfun main(): kotlin.Unit = runBlocking { val out = Out.standard() out.info("START") try { val scope = CoroutineScope( Dispatchers.Default ) val deferred = scope.async(CoroutineName("JustPrints") + SupervisorJob()) { (0..10) .map { "a-${it}" } .forEach { out.info(it) try { delay(500) } catch (e: CancellationException) { val excMsg = e.message ?: e.toString() out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ") throw e } } } out.info("delay 1500 before cancelling co-routine 1") delay(1500) out.info("Cancelling co-routine 1 - ${Emojis.CANCELLATION}") deferred.cancel() out.info("delay before deferred.await()") delay(1500) out.info("calling deferred.await()") // Notice we wait on co-routine 2 first. we do that since 2nd one is not the one that throws // MyExceptionWillThrowFromCoroutine out.info("co-routine-1 result: " + deferred.await()) } catch (e: Exception) { out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1") exitProcess(1) } out.info("DONE - without errors on main")}class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)
Command to reproduce:
gt.sandbox.checkout.commit c1640d7e2e9cfd437d54 \&& cd "${GT_SANDBOX_REPO}" \&& cmd.run.announce "./gradlew run --quiet"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug[INFO][elapsed: 15ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START[INFO][elapsed: 34ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay 1500 before cancelling co-routine 1[INFO][elapsed: 36ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] a-0[INFO][elapsed: 538ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] a-1[INFO][elapsed: 1038ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] a-2[INFO][elapsed: 1536ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Cancelling co-routine 1 - ❌[INFO][elapsed: 1538ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay before deferred.await()[WARN][elapsed: 1583ms][2️⃣][⓶][coroutname:@JustPrints#2][tname:DefaultDispatcher-worker-1/tid:31] 🫡 I have caught [JobCancellationException/DeferredCoroutine was cancelled], and rethrowing it 🫡 [INFO][elapsed: 3039ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] calling deferred.await()[ERROR][elapsed: 3040ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] in main got an exception! of type=[JobCancellationException] with msg=[DeferredCoroutine was cancelled] cause=[kotlinx.coroutines.JobCancellationException: DeferredCoroutine was cancelled; job="JustPrints#2":DeferredCoroutine{Cancelled}@3a03464]. Exiting with error code 1FAILURE: Build failed with an exception.* What went wrong:Execution failed for task ':app:run'.> Process 'command '/home/nickolaykondratyev/.jdks/corretto-21.0.7/bin/java'' finished with non-zero exit value 1* Try:> Run with --stacktrace option to get the stack trace.> Run with --info or --debug option to get more log output.> Run with --scan to get full insights.> Get more help at https://help.gradle.org.BUILD FAILED in 3s
When a coroutine has its own (independent) job, it has nearly no
relation to its parent. It only inherits other contexts, but other
results of the parent-child relationship will not apply. This causes us
to lose structured concurrency, which is a problematic situation that
should be avoided. - kotlin-coroutines-deep-dive
+Job()/+SupervisorJob() must-inherit-from-parent or it will ⚠️not respect parent cancellation⚠️
issue
Bug prone: +Job() Ignores parent cancellation
+SupervisorJob() completely unlinks from parent scope cancellation / Breaks the parent-child chain
SupervisorJob() added at async level -> cancel entire scope -> job with SupervisorJob() does NOT care about scope cancellation and keeps going - ⚠️🐛⚠️
+SupervisorJob() is likely NOT what you want.
We have to be careful with creating a brand new SupervisorJob() since that UNLINKS it from the parent/child chain. Making it ignore cancellations coming from the parent. Example below shows the issue where co-routine created with + SupervisorJob() ignores cancellation of the parent scope.
After checking this problem look at the next example of how we can keep connection of parent -> child cancellations.
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.asyncimport kotlinx.coroutines.cancelimport kotlinx.coroutines.delayimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessfun main(): kotlin.Unit = runBlocking { val out = Out.standard() out.info("START") try { val scope = CoroutineScope( Dispatchers.Default ) val deferred = scope.async(CoroutineName("DoesNotCareAboutScopeCancellation") + SupervisorJob()) { (0..10) .map { "a-${it}" } .forEach { out.info(it) try { delay(500) } catch (e: CancellationException) { val excMsg = e.message ?: e.toString() out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ") throw e } } } out.info("delay 1500 before cancelling entire scope") delay(1500) out.info("Cancelling scope - ${Emojis.CANCELLATION}") scope.cancel() out.info("delay before deferred.await()") delay(1500) out.info("calling deferred.await()") // Notice we wait on co-routine 2 first. we do that since 2nd one is not the one that throws // MyExceptionWillThrowFromCoroutine out.info("co-routine-1 result: " + deferred.await()) } catch (e: Exception) { out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1") exitProcess(1) } out.info("DONE - without errors on main")}class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)
Command to reproduce:
gt.sandbox.checkout.commit 4c71bd4d11e9f563f1da \&& cd "${GT_SANDBOX_REPO}" \&& cmd.run.announce "./gradlew run --quiet"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug[INFO][elapsed: 15ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START[INFO][elapsed: 35ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay 1500 before cancelling entire scope[INFO][elapsed: 37ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-0[INFO][elapsed: 539ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-1[INFO][elapsed: 1040ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-2[INFO][elapsed: 1537ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] Cancelling scope - ❌[INFO][elapsed: 1539ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] delay before deferred.await()[INFO][elapsed: 1540ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-3[INFO][elapsed: 2041ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-4[INFO][elapsed: 2542ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-5[INFO][elapsed: 3039ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] calling deferred.await()[INFO][elapsed: 3042ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-6[INFO][elapsed: 3543ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-7[INFO][elapsed: 4044ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-8[INFO][elapsed: 4544ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-9[INFO][elapsed: 5045ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-10[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-1 result: kotlin.Unit[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - without errors on main
+Job() has the same issue of unlinking from parent cancellations
+Job() has the same issue of unlinking from parent cancellations - ⚠️🐛⚠️
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.*import kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsfun main(): kotlin.Unit = runBlocking { val out = Out.standard() out.info("START") try { val scope = CoroutineScope( Dispatchers.Default ) val deferred = scope.async(CoroutineName("DoesNotCareAboutScopeCancellation") + Job()) { (0..10) .map { "a-${it}" } .forEach { out.info(it) try { delay(500) } catch (e: CancellationException) { val excMsg = e.message ?: e.toString() out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ") throw e } } "result-from-deferred" } out.delayedActionWithMsg( "cancelling scope", action = { scope.cancel() }, delayBeforeAction = 1500.milliseconds ) out.delayedActionWithMsg( "getting result from deferred", action = { out.info("co-routine-1 result: " + deferred.await()) }, delayBeforeAction = 1500.milliseconds ) } catch (e: Exception) { out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1") exitProcess(1) } out.info("DONE - without errors on main")}class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)
Command to reproduce:
gt.sandbox.checkout.commit 31674a09b0c92c89d928 \&& cd "${GT_SANDBOX_REPO}" \&& cmd.run.announce "./gradlew run --quiet"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug[INFO][elapsed: 15ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START[INFO][elapsed: 37ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-0[INFO][elapsed: 37ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [1.5sms delay] before_action=[cancelling scope][INFO][elapsed: 539ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-1[INFO][elapsed: 1040ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-2[INFO][elapsed: 1539ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [Done with=1.5sms delay] performing action=[cancelling scope][INFO][elapsed: 1540ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-3[INFO][elapsed: 1541ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] performed action=[cancelling scope][INFO][elapsed: 1541ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [1.5sms delay] before_action=[getting result from deferred][INFO][elapsed: 2041ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-4[INFO][elapsed: 2542ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-5[INFO][elapsed: 3042ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] [Done with=1.5sms delay] performing action=[getting result from deferred][INFO][elapsed: 3042ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-6[INFO][elapsed: 3543ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-7[INFO][elapsed: 4044ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-8[INFO][elapsed: 4544ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-9[INFO][elapsed: 5045ms][2️⃣][⓶][coroutname:@DoesNotCareAboutScopeCancellation#2][tname:DefaultDispatcher-worker-1/tid:31] a-10[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] co-routine-1 result: result-from-deferred[INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] performed action=[getting result from deferred][INFO][elapsed: 5547ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - without errors on main
Use +SupervisorJob(coroutineContext[Job]) Instead of +SupervisorJob()
Use Use +Job(coroutineContext[Job]) Instead of +Job()
+ SupervisorJob(coroutineContext[Job]) respects the cancellation of parent scope - ✅
Code
package com.glassthought.sandboximport gt.sandbox.util.output.Emojisimport gt.sandbox.util.output.Outimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.CoroutineNameimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.Jobimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.asyncimport kotlinx.coroutines.cancelimport kotlinx.coroutines.coroutineScopeimport kotlinx.coroutines.delayimport kotlinx.coroutines.launchimport kotlinx.coroutines.runBlockingimport kotlin.system.exitProcessimport kotlin.time.Duration.Companion.millisecondsfun main(): kotlin.Unit = runBlocking { val out = Out.standard() out.info("START") try { val scope = CoroutineScope( Dispatchers.Default ) val deferredLevel1 = scope.launch(CoroutineName("level-1")) { coroutineScope { val isolatedScope = CoroutineScope( coroutineContext + SupervisorJob(coroutineContext[Job]) ) val deferred = isolatedScope.async( CoroutineName("Cares about cancellation of parent scope") ) { (0..10) .map { "a-${it}" } .forEach { out.info(it) try { delay(500) } catch (e: CancellationException) { val excMsg = e.message ?: e.toString() out.warn("${Emojis.OBIDIENT} I have caught [${e::class.simpleName}/$excMsg], and rethrowing it ${Emojis.OBIDIENT} ") throw e } } } out.delayedActionWithMsg( "cancel top level scope", action = { scope.cancel("Cancelling top level scope - ${Emojis.CANCELLATION}") }, delayBeforeAction = 1500.milliseconds ) out.delayedActionWithMsg( "await on deferred", action = { // This will throw CancellationException, since we cancelled the scope out.info("deferred result=" + deferred.await()) }, delayBeforeAction = 1500.milliseconds ) } } deferredLevel1.join() } catch (e: Exception) { out.error("in main got an exception! of type=[${e::class.simpleName}] with msg=[${e.message}] cause=[${e.cause}]. Exiting with error code 1") exitProcess(1) } out.info("DONE - without errors on main")}class MyExceptionWillThrowFromCoroutine(msg: String) : RuntimeException(msg)
Command to reproduce:
gt.sandbox.checkout.commit 5cc712f733a6345c8f05 \&& cd "${GT_SANDBOX_REPO}" \&& cmd.run.announce "./gradlew run --quiet"
Recorded output of command:
Picked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debugPicked up JAVA_TOOL_OPTIONS: -Dkotlinx.coroutines.debug[INFO][elapsed: 14ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] START[INFO][elapsed: 36ms][2️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-2/tid:32] a-0[INFO][elapsed: 36ms][3️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-1/tid:31] [1.5sms delay] before_action=[cancel top level scope][INFO][elapsed: 537ms][2️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-2/tid:32] a-1[INFO][elapsed: 1038ms][2️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-2/tid:32] a-2[INFO][elapsed: 1537ms][2️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-2/tid:32] [Done with=1.5sms delay] performing action=[cancel top level scope][INFO][elapsed: 1538ms][3️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-1/tid:31] a-3[INFO][elapsed: 1540ms][2️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-2/tid:32] performed action=[cancel top level scope][INFO][elapsed: 1540ms][2️⃣][⓷][coroutname:@level-1#2][tname:DefaultDispatcher-worker-2/tid:32] [1.5sms delay] before_action=[await on deferred][WARN][elapsed: 1585ms][3️⃣][⓶][coroutname:@Cares about cancellation of parent scope#3][tname:DefaultDispatcher-worker-1/tid:31] 🫡 I have caught [CancellationException/Cancelling top level scope - �, and rethrowing it 🫡 [INFO][elapsed: 1586ms][🥇][①][coroutname:@coroutine#1][tname:main/tid:1] DONE - without errors on main
You typically do NOT need to create Job()/SupervisorJob() by hand. Look at scope-function-error-boundary instead of creating Job()/SupervisorJob() by hand.