just "played around" with a similar "challenge".
I wanted to "split" the result Flow of (possibly many) producers in a round-robin manner to multiple asynchronous consumers.
Achieving that:
- the producer produces a flow "in the background"
- one / many consumers consume chunks of the producers outcome in a round-robin fashion
(also "in the background")
- while all of this happens, you can continue to "do something" until all data is produced AND consumed
To achieve the above I used MutableSharedFlow
and after the producer (and consumers) finished "their" flows finally had to be canceled/finished.
I doubt that my result is in any way optimal or even a "good" solution, but as I have spend a "significant" time on it (learning) I thought I might share it here as well:
package scratch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okio.*
import kotlin.random.Random
data class Packet(val nr: Int, val cmd: String, val line: String, val isFinal: Boolean = false)
class TerminatedException(message: String, cause: Throwable? = null) : Exception(message, cause)
/** possible multiple producers sending to one MutableSharedFlow</br>
* multiple consumers fed round-robin from the one SharedFlow of all producers */
fun main(@Suppress("UNUSED_PARAMETER") args: Array<String>) {
println("===============================")
println("==== multipleConsumers() ====")
println("===============================")
showcaseMultipleConsumers()
println("\nmompl...\n")
Thread.sleep(2000L)
println("===============================")
println("===== singleConsumer() ======")
println("===============================")
showcaseSingleConsumer()
println("main ended.")
}
private fun showcaseMultipleConsumers() {
runBlocking {
// launch producer with SharedFlow to emit to
val producerFlow = MutableSharedFlow<Packet>()
launch(Dispatchers.IO) {
producer(name = "producer", cmd = "/Users/hoffi/gitRepos/scratch/scratch/some.sh", producerFlow)
}
// launch concurrent consumers with each its own SharedFlow
val consumerCount = 3
val mapOfFlows = mutableMapOf<Int, MutableSharedFlow<Packet>>()
for (i in 1..consumerCount) {
mapOfFlows[i - 1] = MutableSharedFlow()
launch(Dispatchers.IO) {
consume(i.toString(), mapOfFlows[i - 1]!!)
}
}
println("finished launching $consumerCount consumers.")
// round-robin emit from the producerFlow to the existing MutableSharedFlows
var i = 0
try {
producerFlow.buffer().collect() { packet ->
if (packet.isFinal) {
println("producer: final packet received"); throw TerminatedException("'producer' terminated")
}
mapOfFlows[i++ % consumerCount]!!.emit(packet)
}
} catch (e: TerminatedException) {
println(e.message)
}
println("round-robbin load-balancing finished.")
// finally send terminal packet to all consumer's MutableSharedFlows
for (flow in mapOfFlows.values) {
flow.emit(Packet(-1, "final", "final", true))
}
// might do something here _after_ the process has finished and its output is load-balanced to consumers
// but consuming of process output has not finished yet.
println("end of runBlocking ...")
}
}
/** coroutine consuming from given Flow (which is a MutableSharedFlow!) */
suspend fun consume(name: String, flow: Flow<Packet>) {
try {
flow.buffer().collect { packet ->
if (packet.isFinal) { println("$name: final packet received") ; throw TerminatedException("'$name' terminated") }
println("%5d in c%s: %s".format(packet.nr, name, packet.line))
delay(Random.nextLong(50L, 550L))
}
} catch(e: TerminatedException) {
println("consumer: ${e.message}")
}
}
/** coroutine emitting to given producer's MutableSharedFlow */
suspend fun producer(name: String, cmd: String, producerFlow: MutableSharedFlow<Packet>) {
val process = ProcessBuilder("\\s".toRegex().split(cmd))
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.redirectErrorStream(true)
.start() // non-blocking asynchronous start process in the background
val inputBuffer = process.inputStream.source().buffer()
var i = 0
while (true) {
val line = inputBuffer.readUtf8Line() ?: break
producerFlow.emit(Packet(++i, cmd, line))
}
producerFlow.emit(Packet(-1, "final", "final", true))
println("producer function ended")
}
// =====================================================================================================================
// =====================================================================================================================
// =====================================================================================================================
private fun showcaseSingleConsumer() {
runBlocking {
val flow: Flow<Packet> = singleProducer(name = "producer", cmd = "/Users/hoffi/gitRepos/scratch/scratch/some.sh")
launch(Dispatchers.IO) {
singleConsumer(name = "consumer", flow)
}
// do something here, while the process is executed
// and the consumer is consuming the process's output
println("end of runBlocking ...")
}
}
/** no suspend needed as a flow { ... } implicitly "runs in a coroutine" */
fun singleProducer(name: String, cmd: String) = flow {
val process = ProcessBuilder("\\s".toRegex().split(cmd))
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.redirectErrorStream(true)
.start() // non-blocking asynchronous start process in the background
val inputBuffer = process.inputStream.source().buffer()
var i = 0
while (true) {
val line = inputBuffer.readUtf8Line() ?: break
emit(Packet(++i, cmd, line))
}
println("producer function ended")
}
suspend fun singleConsumer(name: String, flow: Flow<Packet>) {
flow.buffer().collect { packet ->
println("%5d in c%s: %s".format(packet.nr, name, packet.line))
delay(Random.nextLong(30L, 150L))
}
}
some.sh
shellscript used as "data source":
#!/bin/bash
# some.sh
echo "first line"
echo -e "second line\nand third line"
echo -n "fourth line without newline"
sleep 2
echo
echo -e "fifth line after sleep\nand sixth line"
echo -e "some stderr\nand stderr has two lines" >&2
for i in {1..25}; do
echo "and loop line $i"
done
echo -n "last line"