Hello, does anybody know why the first timeout cor...
# announcements
x
Hello, does anybody know why the first timeout coroutine is not being cancelled when the second one gets a results and forces all children cancellation of the context?
Copy code
class Test(val callbackWork: CustomCallbackWork) : CoroutineScope {
    private var job = Job()
    override val coroutineContext: CoroutineContext
        get() = <http://Dispatchers.IO|Dispatchers.IO> + job

    fun start() : Flow<Unit> {
        return channelFlow {
            launch {
                delay(5000)
                logger().d("After delay, isActive: $isActive")
                close()
            }

            launch {
                callbackWork.apply {
                    onSuccess = {
                        offer(Unit)
                        coroutineContext.cancelChildren()
                    }
                    onError = {
                        offer(Unit)
                        coroutineContext.cancelChildren()
                    }
                }
            }
        }
    }
}
The log is printed (obviously displaying
isActive=true
) after 5 seconds even though
onSuccess
is triggered before that delay. However, if I get the reference of the first Job and cancel it below, it works perfectly. What's wrong in the code?
z
Probably you're only cancelling the coroutine context of the second launch, because
coroutineContext
is resolving to the extension on the scope instance that is the receiver of the launch lambda. Store a reference to the scope of the callbackFlow before calling launch, to resolve to the right one and make it clear which scope you're referencing, then cancel that.
I'm also not sure why your class implements CoroutineScope, it's not using it at all (and in fact implementing CoroutineScope directly is discouraged, since it leaks private concerns into your public API).
x
I also tried that without success. I ensured
val scope = this
in the first line of the
start()
function and invoked
scope.coroutineContext.cancelChildren()
within the callbacks. However the first coroutine isn't still cancelled and prints the log.
z
If it's the first line of the start function, this will refer to your Test class (whose coroutine context you're never actually using), not the actual scope of the callbackFlow. It should be the first line after
callbackFlow {
x
You're right @Zach Klippenstein (he/him) [MOD], I didn't notice creating a
channelFlow { }
would run in a new producer scope which is different from the one of the
Test
scope. In fact, if I move the scope to the first line and then use
scope.launch { }
below, it also works because it's now referring to the same scope.
As you pointed out, I guess creating a new scope here is pointless since
channelFlow
will return the contest where it's being collected, which is pretty much reasonable, right?
z
Channel flow doesn't return the context, it passes the collecting context as the receiver to its lambda. But yes, it's not only reasonable, it's one of the most important features of flow's design since it preserves structured concurrency.
That said, calling launch outside of the flow in this case might actually be what you want to do, depending on if you want the timeout to be scoped to your class or collectors of the flow. Eg what should happen in these cases:
Copy code
val test = Test(…)
val flow = test.start()
flow.launchIn(scope)
flow.launchIn(scope)

delay(5000)
test.start().collect { }
x
All crystal clear, thanks a lot for your help.