Can someone help me understand how to handle cance...
# coroutines
e
Can someone help me understand how to handle cancellation here properly?
Copy code
context(Toggled) @Base
suspend fun simultaneously(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> (Unit)
) {
    val current = currentCoroutineContext()[ContinuationInterceptor]!! + context
    val scope = object : CoroutineScope { override val coroutineContext = current }
    var task: Job? = null
    onEnabled { task = scope.launch(context) { block() } }
    onDisabled {
        println("Children: ${task?.children?.count()}")
        println("Active: ${currentCoroutineContext()[Job]?.isActive}")
        try {
            task?.cancelAndJoin(); task = null
        } catch (reason: Throwable) {
            reason.printStackTrace()
        }
        println("Active: ${currentCoroutineContext()[Job]?.isActive}")
    }
}
The issue is that onDisable callback is SOMETIMES called by code in
block
which causes cancelAndJoin() to throw cancellation exception. I figure that if I just catch it and finish letting the disable listeners get called that would be bad... since for example if another disable listener suspends execution it won't be able to return to executing state since it's job is no longer active. But I'm not sure how else I can make sure that the call which is causing cancellation is allowed to complete before it's actually done. Hopefully, that makes some sense.
j
What are you trying to achieve here on a higher level? It looks like you're fiddling with contexts more than you should be, and I wonder what makes you need this. For instance, one thing is that you drop everything from the current coroutine context apart from the dispatcher. This means that, if the caller of
simultaneously
doesn't provide a
context
, the
current
context is just a dispatcher, and in particular it doesn't have a
Job
. But also it doesn't have a
CoroutineName
or everything else that the caller would provide. That said, I don't even understand why this function is
suspend
at all here, and if it weren't, there wouldn't even be a context to get things from. Then you create your own
CoroutineScope
implementation instead of using the factory function, which means you bypass the safety that makes sure there is a
Job
in the scope - so there is none. This effectively disables structured concurrency (and automatic cancellation mechanisms) in your scope. You also use
scope.launch(context) {...}
, and I don't quite understand why not just
scope.launch {...}
. You have configured your
scope
with a specific coroutine context, and now you re-override that context with the
context
argument. This should have no effect given how the scope is setup.
e
scope.launch(context) {...}
the context provided is appended to the scope's context (overriding only if there are conflicting keys) So I could use it to add back in a CoroutineName for example, or change a different dispatcher. The removing of everything other than a dispatcher is something I probably should reconsider I agree, however, I needed to remove all of my custom context if nothing else (I was considering rebuilding the context with fold instead). The reason I remove job is because if I say:
Copy code
val myComponent = runBlocking(Dispatcher.Main) {
  Component {
    //acts like a timer while Component is enabled.
    simultaneously {
      while (isActive) {
       delay(1.seconds)
       doSomething()
      }
    }
  }
}
//somewhere else
MainJavaSystemExampleThing.onSomething {
  runBlocking(MyMainDispatcher) {
    myComponent.enable()
  }
}
I want to make sure the whole component is enabled before I let the java onSomething call return, however I do not want to wait for the Component to disable... because that would often if not always result in deadlocking. So maybe it's because I have created my dispatcher incorrectly but hopefully that makes sense.
What I'm trying to accomplish on a larger scale is a bit complicated. But basically I'm using custom context to detect listeners to containers that recursively modify the container under some circumstances for example:
Copy code
val example = MutableTable<Int, String>()
example.onChanged { key, from, to ->
  if (to > 50) example[key] = 50
}
And in this situation it would first finish calling any other listeners so they all see the change to a number above 50, then it would call them all again with the change down to 50... I need to use custom context because otherwise there is no way to tell the difference between a call like this, and an edit coming in on another thread (which would be forced to wait for the first change to complete) https://gitlab.com/ballysta/architecture/-/blob/snapshot/src/main/kotlin/com/gitlab/ballysta/architecture/Sequence.kt ^ this is responsible for that mechanism. So the reason I have to strip out my custom context in the simultaneously block is because otherwise if you say:
Copy code
Component {
  simultaneously {
    delay(10.seconds)
    disable()
  }
}
it will treat that as a "recursive" call to disable, rather than a call to disable which must wait for existing calls to complete (which is the way it must be)