I've got a use case that I thought would be a good...
# coroutines
b
I've got a use case that I thought would be a good fit for structured concurrency, but I'm struggling with it and not sure if I'm missing something or trying to force a square peg into a round hole. I've got basically a class/job hiearchy, but need the following behavior: 1. If any sub-task errors, then the parent should know so it can cancel the others (this works well with structured concurrency) 2. If any sub-task finishes, then the parent should know so it can stop all the others (This has been one of the trickier parts to get working with structured concurrency) 3. Stopping the parent should stop all the children (this works well with structured concurrency) 4. Tasks need to perform non-trivial cleanup upon completion, both in the 'clean' finish and error modes, but I want to be able to do this on a class level, not just a 'task' level. That is: a class may launch several child jobs, but I don't want to run the cleanup on the completion of each of those child jobs, more on the class' "scope" level. (I even played with writing a
CoroutingScope#onFinish
helper which delayed wrapped an indefinite delay in a try/finally and called a cleanup method in the
finally
block, but it feels wrong). Of these, I think #2 is the biggest sticking point: I find myself wishing I could create a child coroutine scope but wait for any child to finish, instead of all children to finish (like
coroutineScope
and
runBlocking
do). Does that exist?
l
raceOf
from Splitties coroutines might be what you're looking for. For the part 4, it depends on where you put the cleanup code (likely in a finally block).
m
I think you can achieve this using channels. I did a test using
launch
inside
channelFlow
right now, and it seems to act the way you want. I used
first()
on the resulting flow to get the first element to finish. This is what I did:
Copy code
val tasks = // List of tasks

val parent = launch {
    channelFlow {
        tasks.forEach { task ->
            launch {
                send(task.run())
            }
        }
    }.first()
}

parent.invokeOnCompletion { 
    println("Cleanup") 
}
b
Thanks @louiscad, I'll take a look at
raceOf
. I did do work in a finally block, but since it blocks the other coroutines from starting cleanup it wasn't a great fit. Not sure the best way to get a different behavior there.
Oh, interesting @marstran...I'll have to mess with that a bit to better understand it and see how it works in the error case.
l
You can launch the cleanup in another scope (e.g. a custom one or
GlobalScope
) if the temporary leak is not an issue. @bbaldino
m
Yeah. The
channelFlow
will automatically cancel all child-jobs when
first()
returns. If something fails first, then that exception will bubble up and cause all the others to be cancelled as well.
b
@louiscad ok, I'll have to play with that.
m
I guess you can use
supervisorScope
inside your parent if you want more control of the error-handling.
b
Ah good point, I'd been avoiding it since I do want everything to shutdown when one child fails, but I didn't think of using it as a hook to do the cleanup.
l
@bbaldino Here's the link to the doc of the
raceOf
function you can use via the depends or copy pasting: https://github.com/LouisCAD/Splitties/tree/main/modules/coroutines#racing-coroutines
👍 1
b
@marstran what would be the best method if tasks can have child tasks of their own? I'd need to have the parent pass in a CoroutineScope when creating the tasks so they'd have access to it?
m
You can pass the scope created by
launch
to the task, so it can create more child-tasks on it.
Copy code
launch { task.run(this) }
b
👍
l
Usually, there's no need to explicitly pass the
CoroutineScope
as it's the receiver
b
Well, the way my code is set up is that the 'tasks' are really more first-class components which manage some state as well as launch some tasks. So it's a little bit more than just a simple task method, and I don't think having the member method be an extension of coroutinescope will work there? That would require the multiple receiver stuff, I think(?)
l
Maybe you can just make these functions suspending so they don't need a scope as a receiver (and use
coroutineScope { }
if they have child coroutines to launch)
b
Yeah, I thought about that but run into the "finish on the first one succeeding" issue with
coroutineScope
But, like you said, maybe a `raceOf`/similar helper will work there.
👍 1