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.