The default behavior of children coroutines in Kotlin loosely corresponds to "one-for-all" supervision behavior -- when one crashed child kills all siblings (and parent, too). There is no automatic restart in Kotlin, but that is completely different topic. The default in Kotlin is chosen this way, because this is a good default for a use-case of parallel decomposition when one large job is decomposed it smaller jobs that work in parallel. It also makes coroutines ephemeral -- a coroutine can always delegate a piece of its work to a child without an outside world noticing it.
However, sometimes, children coroutines are independend, but are still children in the sense that parent cancellation has to cancel them, too, however, their crash should not kill parent and all sibling coroutines, but shall be handled independently. Currently, it requires a bit of boilerplate to write:
launch { // child
try {
doSomething() // child's job
} catch(e: Throwable) {
// handle failure
}
}
In a particular project, the above boilerplate can be readily incapsulated into a function together with project-specific error-handling code:
fun CoroutineScope.launchAndHandle(suspend block: CoroutineScope.() -> Unit) {
launch { // child
try {
block()
} catch(e: Throwable) {
// handle failure
}
}
}
However, these kinds of functions cannot be readily provided by kotlinx.coroutines, because they are not composable -- one needs to define such function for each type of coroutine builder.
Instead, the proposal is to add a new CoroutineScope builder function, so that one can write:
__independentCoroutineScope {
launch { // child
doSomething() // child's job
}
}
The __independentCoroutineScope function (the good actual name is to be found) creates a CoroutineScope of a special kind that makes all the coroutines launched directly from inside of it to have this special behavior -- they are cancelled when the parent is cancelled, but their exceptions are handled independently by an installed CoroutineExceptionException which can be inherited from a parent or can be specified in this builder explicitly:
__independentCoroutineScope(CoroutineExceptionHandler { .... }) {
launch { // child
doSomething() // child's job
}
}
Hmmm. isolated { ... }?
Naming things is hard.
@elizarov Do you think it would make sense to be able to configure it as the default of a given CoroutineScope?
Like this for instance:
class MyUiComponent : CoroutineScope {
override val coroutineContext = Job() + Dispatchers.UI
override val isolatedChidren = true // this may be `false` by default if not overriden
fun handleAction() {
launch { /* this coroutine is a child which won't crash the parent in case of failure */ }
}
}
Or maybe as a context element: coroutineContext = job + dispatcher + IsolatedChildren.
The motivation I have are UI components for which I want all children to be isolated. That way they will be cancelled at the end of the life of the component, but the component won't crash just because of an unexpected failure in an action. So I'd like to be able to specify that some given scopes launch children isolated by default.
@jcornaz We'll use a special implementation of Job for that scope (one that does not cancel all children on a crash of one of them), so another way to expose this feature is to provide a separate __IsolatedJob() constructor, in your example:
override val coroutineContext = __IsolatedJob() + Dispatchers.UI
Tentative names are supervisorScope { ... } for a scope that would handle exceptions of its children independently and SupervisorJob() for an explicit job object constructor with the same properties.
Is it correct that another way to think about this is: a regular scope (non-supervisor) is effectively just a supervisor scope that cancels/fails itself on child failure?
a regular scope (non-supervisor) is effectively just a supervisor scope that cancels/fails itself on child failure?
Nope. With supervisor, all children handle their exceptions independently (either via CoroutineExceptionHandler or default mechanism).
Without supervisor, behaviour depends on the parent type. For launch-like scope all exception will be aggregated and only one of them will be reported, for async it will be just aggregated and stored as value etc.
Good. Structured concurrency is the supervision tree :-).
For a long term evolution, I propose to support supervisor strategy with Akka or Erlang style, it's proofed.
Also I propose Kotlin support the argument reference,similar Java 8 method reference,or limited implicit parameters in Scala(implicit parameters is good parts in past years).
Both described as comments there:
https://medium.com/@elizarov/structured-concurrency-722d765aa952
There is a example code for switching AllForOneStrategy or OneForOneStrategy :
https://medium.com/@qihui.sun/there-maybe-indentation-hell-for-example-4728b982978e
// here use argument reference ::coroutineContext
fun async(::coroutineContext :SupervisorStrategy) {
// implementation
}
if most people think聽:: is too easy to abuse, we could denote longer modifiers here, such as by field聽:
// here use argument reference or limit implicit parameter in Scala
fun async(coroutineContext :SupervisorStrategy by field) {
// implementation
}
Most helpful comment
Tentative names are
supervisorScope { ... }for a scope that would handle exceptions of its children independently andSupervisorJob()for an explicit job object constructor with the same properties.