Choosing coroutineScope vs supervisorScope for a multi-step deployment pipeline without silent sibling cancellation
0 reputation · 01 Dec 2022, 17:13 UTC
0 reputation · 01 Dec 2022, 17:13 UTC
I'm structuring a Kotlin deployment pipeline as a set of concurrent rollout steps (config push, service restart, health check) using kotlinx.coroutines. The documented behavior is clear in isolation: a failing child in a regular coroutineScope cancels all siblings and propagates the exception, while supervisorScope lets siblings finish and requires each child to handle its own failure.
What's unresolved for me is the failure semantics I actually want. Fail-fast cancellation sounds right when steps share mutable state, but some steps (e.g., draining connections) should complete even if an unrelated step fails. I also need failures to be visible in logs: I understand async children defer exceptions until await(), and CancellationException is never reported to a CoroutineExceptionHandler, so a cancelled deployment could look like a clean shutdown.
Assume a recent stable kotlinx.coroutines release; I'll verify against the exact deployed version.
My questions:
supervisorScope around independent steps inside an outer fail-fast coroutineScope a sound pattern, or does it produce surprising propagation?async children whose results I may not always await, what is the intended way to guarantee their exceptions reach logs?CoroutineExceptionHandler at the supervisor level reliably catch launch-child failures without masking cancellation?26525 reputation · 01 Dec 2022, 20:27 UTC
async blocks.
supervisorScope inside an outer coroutineScope is a sound pattern for mixed coupling. The outer coroutineScope acts as the 'fail-fast' boundary: if a critical top-level step fails, the entire pipeline cancels. However, by wrapping independent steps in a supervisorScope, you ensure that a failure in one independent step does not cancel its siblings within that supervisor.
```kotlin
coroutineScope {
// Critical step: if this fails, the whole pipeline cancels
launch { pushConfig() }
// Independent steps: failure here doesn't stop the others
supervisorScope {
launch { restartService() }
launch { drainConnections() } // This completes even if restartService fails
}
}
```
async children defer their exceptions until await() is called. If you do not await the result, the exception is effectively "swallowed" until the scope completes. To guarantee these reach your logs without manual awaiting:
launch instead of async if you don't need a return value.async, wrap the internal logic in a try-catch and log immediately.CoroutineExceptionHandler to the launch that wraps the async block.CoroutineExceptionHandler (CEH) at the supervisor level will reliably catch launch-child failures, but it **will not** catch CancellationException. This is intended behavior: CancellationException is used for normal flow control. To distinguish between a clean shutdown and a failure, you must check the exception type in a finally block or use a invokeOnCompletion handler.
Assumption: This assumes you are using kotlinx-coroutines-1.6.0 or later, where structured concurrency and exception propagation rules are stable.
Use comments to ask for clarification. Post a solution as an answer.
26,525 reputation · 02 Dec 2022, 03:50 UTC
supervisorScope inside a coroutineScope, the outer scope acts as a global fail-fast boundary. If a critical top-level step fails, the cancellation propagates downward and shuts down the entire pipeline. However, the inner supervisorScope allows independent steps—like draining connections—to complete even if a sibling rollout step fails.
Regarding async visibility: if you do not call await(), the exception is effectively "swallowed" until the scope completes. To ensure these reach logs without manual awaiting, consider using launch for side-effect tasks or wrapping the internal async logic in a try-catch block for immediate logging. Note that a CoroutineExceptionHandler at the supervisor level will catch direct launch failures but will not intercept CancellationException.