I've setup Coroutine Scopes inside our Presenter c...
# coroutines
s
I've setup Coroutine Scopes inside our Presenter class, where every child coroutine has one of 2 parent coroutines. I'm seeing some really odd behavior around `CoroutineExceptionHandler`s, where after we receive a single exception, every child coroutine immediately fails with the same exception again. Code snippets on thread.
From our base presenter:
Copy code
protected open val coroutineExceptionHandler: CoroutineExceptionHandler? = null

  private var _mountScope = ComponentCoroutineScope()
  private var _viewScope = ComponentCoroutineScope()

  protected val mountScope: CoroutineScope
    get() = _mountScope
  protected val viewScope: CoroutineScope
    get() = _viewScope

  private inner class ComponentCoroutineScope : CoroutineScope {
    private val baseJob = Job()

    val isActive: Boolean = baseJob.isActive

    override val coroutineContext: CoroutineContext
      get() {
        val job = Job(parent = baseJob) + Dispatchers.Main
        return coroutineExceptionHandler?.let { job + it } ?: job
      }

    fun cancel() {
      baseJob.cancel()
    }
  }
In a child presenter class:
Copy code
override val coroutineExceptionHandler: CoroutineExceptionHandler?
    get() = ApiCoroutineExceptionHandler(apiExceptionHandler)
g
Yes, this is expected behavior
s
What do I do to work around it then? I find this undesirable.
g
Use SupervisorJob instead (also see docs)
s
which docs specifically? I've read them all a few times now at different times
😁 1
g
About SupervisorJob
There are additional details about this behavior
s
ok. thx. Looks like you saved me again @gildor
I literally just read this page again about exception handling. I didn't even see this supervision section
g
👍
s
@gildor So I just confirmed that fixed it. This seems like dangerous undesirable behavior to me. There are several things like this around coroutines, i.e. when a coroutine is cancelled, all it's child coroutines halt at their next suspension point. I find these dangerous and undesirable.
I kind of feel like I've exchanged one set of concurrency problems, and received a whole host of new ones that I'm not familiar with. RxJava was at least consistent, but I feel like coroutines aren't.
2
u
I tend to agree.
s
@spierce7 I don’t agree. E.g. you have a Coroutine that is busy downloading a bunch of data in parallel and the Coroutine reports the result of the downloads back to the caller (e.g. combining/meshing the downloaded results), then when one download fails or cancels, all should fail/cancel. This is where the
Job
comes in handy if it is tied to the Coroutine-Scope’s context. If you don’t want all other siblings to fail, use
SupervisorJob
. Not sure if I like the name, though 🙂
u
I think it depends on where you use coroutines most frequently in your daily work. If you work a lot inside architectures that use presenters or similar constructs than this can seem like a counter-intuitive default-behavior.
But it's probably also caused by the rather drastic change introduced by structured concurrency.
s
@streetsofboston I disagree. i.e. For something as that takes as long as network traffic, what I really want is for the other requests to continue processing in the background so that I can cache their results. All I want is for their results to not not trigger a callback in the coroutine I've cancelled.
By cancelling all child coroutines at their next suspension point, all you've really done is create the possibility of ending up with inconsistent state somewhere downstream in a stateful object where one of your child coroutines went.
g
This is not exactly "default behavior", you always choose between Job and SupervisionJob
s
If I call GlobalScope.launch, I'm defaulted into this behavior.
2
s
But if the siblings are not canceled, where do they report their results? It depends. Use Job or SupervisorJob on your use-case. That Job is a default, that is arguable, I agree.
s
To "opt out" I have to do something special.
g
Yeah, you right, Job is "more default", because it creating if scope doesn't have Job by default (like GlobalScope which doesn't have any Job
s
To be clear, Supervisor job doesn't stop child coroutines from being cancelled on their next suspension point. That's another issue entirely that you can only solve by creating new coroutine scopes everywhere you want their to be a barrier.
Are there any other uses for Supervisor job other than ensuring there isn't a bidirectional relationship between exceptions?
g
Not sure what you mean by "doesn't stop child coroutines from being cancelled". SupervisorJob is not cancelled if one of child coroutines is cancelled. Maybe you could show an example
Just for context there is a related issue about cancellation semantics (also mentioned a few other related discussions) https://github.com/Kotlin/kotlinx.coroutines/issues/763
d
The thing you may need @spierce7 is CoroutineStart.ATOMIC
For operations that should not be cancelled halfway
In my opinion it is confusing what changes you need to make to opt out of the default behaviour, which is pretty much to destroy everything when an exception occurs somewhere. If it's cumbersome, you can consider creating your own coroutine builders, that crossinline your coroutine body into a try/catch, and handle the exceptions your way.
s
@Dico how sure are you that atomic does what I'm advocating for?
d
Read the docs and judge for yourself
Maybe mix it with
NonCancellable