Exerosis
06/14/2022, 9:11 AMcontext(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.Joffrey
06/14/2022, 9:22 AMsimultaneously
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.Exerosis
06/14/2022, 9:49 AMscope.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:
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.Exerosis
06/14/2022, 9:51 AMval 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:
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)