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?17515 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.
No question comments on this page.