Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope for "one-for-one" supervision (children don't crash parent and siblings) #576

Closed
elizarov opened this issue Sep 17, 2018 · 7 comments

Comments

@elizarov
Copy link
Contributor

elizarov commented Sep 17, 2018

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
    }
}
@kevinherron
Copy link

Hmmm. isolated { ... }?

Naming things is hard.

@jcornaz
Copy link
Contributor

jcornaz commented Sep 18, 2018

@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.

@elizarov
Copy link
Contributor Author

elizarov commented Sep 18, 2018

@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

elizarov added a commit that referenced this issue Sep 21, 2018
Tentative implementation and name, no docs yet

Fixes #576
@elizarov
Copy link
Contributor Author

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.

@zach-klippenstein
Copy link
Contributor

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?

elizarov added a commit that referenced this issue Sep 21, 2018
Tentative implementation and name, no docs yet

Fixes #576
elizarov added a commit that referenced this issue Sep 21, 2018
Tentative implementation and name, no docs yet

Fixes #576
@qwwdfsad
Copy link
Contributor

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.

elizarov added a commit that referenced this issue Sep 22, 2018
Documentation for Job spells the difference between the regular job
and the supervisor job.

Fixes #576
elizarov added a commit that referenced this issue Sep 23, 2018
elizarov added a commit that referenced this issue Sep 23, 2018
@SolomonSun2010
Copy link

SolomonSun2010 commented Sep 24, 2018

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
}

elizarov added a commit that referenced this issue Sep 25, 2018
This change also fixes propagation of cancellation for Job() constructor.
When both Job() and SupervisorJob() are cancelled with exception (fail),
they cancel their parent, too. So we have similar behavior between:
* Job() and coroutineScope { ... }
* SupervisorJob() and supervisorScope { ... }

Fixes #576
elizarov added a commit that referenced this issue Sep 25, 2018
This change also fixes propagation of cancellation for Job() constructor.
When both Job() and SupervisorJob() are cancelled with exception (fail),
they cancel their parent, too. So we have similar behavior between:
* Job() and coroutineScope { ... }
* SupervisorJob() and supervisorScope { ... }

Fixes #576
elizarov added a commit that referenced this issue Sep 25, 2018
This change also fixes propagation of cancellation for Job() constructor.
When both Job() and SupervisorJob() are cancelled with exception (fail),
they cancel their parent, too. So we have similar behavior between:
* Job() and coroutineScope { ... }
* SupervisorJob() and supervisorScope { ... }

Fixes #576
qwwdfsad pushed a commit that referenced this issue Sep 26, 2018
This change also fixes propagation of cancellation for Job() constructor.
When both Job() and SupervisorJob() are cancelled with exception (fail),
they cancel their parent, too. So we have similar behavior between:
* Job() and coroutineScope { ... }
* SupervisorJob() and supervisorScope { ... }

Fixes #576
qwwdfsad pushed a commit that referenced this issue Sep 28, 2018
This change also fixes propagation of cancellation for Job() constructor.
When both Job() and SupervisorJob() are cancelled with exception (fail),
they cancel their parent, too. So we have similar behavior between:
* Job() and coroutineScope { ... }
* SupervisorJob() and supervisorScope { ... }

Fixes #576
elizarov added a commit that referenced this issue Sep 28, 2018
This change also fixes propagation of cancellation for Job() constructor.
When both Job() and SupervisorJob() are cancelled with exception (fail),
they cancel their parent, too. So we have similar behavior between:
* Job() and coroutineScope { ... }
* SupervisorJob() and supervisorScope { ... }

Fixes #576
elizarov added a commit that referenced this issue Sep 28, 2018
This change also fixes propagation of cancellation for Job() constructor.
When both Job() and SupervisorJob() are cancelled with exception (fail),
they cancel their parent, too. So we have similar behavior between:
* Job() and coroutineScope { ... }
* SupervisorJob() and supervisorScope { ... }

Fixes #576
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants