Parallel execution of jobs using coroutines - kotlin

I want to execute multiple jobs in parallel using coroutines. This is the piece of code that I came up with.
I have 2 queries:
How do I ensure completion callbacks happen in the caller thread?
Code has become more like the callback pattern I used to have with
normal threads. Please suggest changes in design to achieve the
coroutines readability advantage.
class ParallelExecutor {
suspend fun <OUTPUT> execute(
jobs: List<suspend () -> OUTPUT>,
onTimeout: (jobIndex: Int) -> OUTPUT,
onFailure: (jobIndex: Int, exception: Throwable) -> OUTPUT,
onCompletion: suspend (jobIndex: Int, result: OUTPUT) -> Unit,
timeout: Long,
onFullCompletion: suspend () -> Unit = {},
invokeDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
withContext(invokeDispatcher) {
var counter = 0
val listenJobs = mutableListOf<Deferred<OUTPUT>>()
jobs.forEachIndexed { index, job ->
val listenJob = async {
try {
job()
} catch (e: Exception) {
onFailure(index, e)
}
}
listenJobs.add(listenJob)
}
listenJobs.forEachIndexed { index, job ->
launch {
val output = try {
withTimeout(timeout) {
job.await()
}
} catch (e: TimeoutCancellationException) {
onTimeout(index)
}
onCompletion(index, output)
if (++counter == listenJobs.size) {
onFullCompletion()
}
}
}
}
}
}

It seems to me that you can simplify your code quite a bit. You don't need a two-step idiom that first launches all the async jobs and then launches more jobs to await on them. You can just launch the jobs and delegate to the callbacks within the same block. That way the callbacks will naturally be invoked on the caller's dispatcher and only the job itself can be called within the changed context with the invokeDispatcher.
onFullCompletion looks like a piece of code that belongs on the caller side, below the execute call. Since execute doesn't throw any exceptions, you don't need any try-finally to get it.
suspend fun <OUTPUT> execute(
jobs: List<suspend () -> OUTPUT>,
onTimeout: (jobIndex: Int) -> OUTPUT,
onFailure: (jobIndex: Int, exception: Throwable) -> OUTPUT,
onCompletion: suspend (jobIndex: Int, result: OUTPUT) -> Unit,
timeout: Long,
invokeDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
coroutineScope {
jobs.mapIndexed { index, job ->
launch {
val output = try {
withTimeout(timeout) {
withContext(invokeDispatcher) {
job()
}
}
} catch (e: TimeoutCancellationException) {
onTimeout(index)
} catch (e: Exception) {
onFailure(index, e)
}
onCompletion(index, output)
}
}
}
}

Made some improvements that should answer your queries.
class ParallelExecutor {
suspend fun <OUTPUT> execute(
jobs: List<suspend () -> OUTPUT>,
onTimeout: (jobIndex: Int) -> OUTPUT,
onFailure: (jobIndex: Int, exception: Throwable) -> OUTPUT,
onCompletion: suspend (jobIndex: Int, result: OUTPUT) -> Unit,
timeout: Long,
invokeDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
supervisorScope {
val listenJobs = jobs.map { job ->
async(invokeDispatcher) {
withTimeout(timeout) {
job()
}
}
}
listenJobs.forEachIndexed { index, job ->
launch {
val output = try {
job.await()
} catch (e: TimeoutCancellationException) {
onTimeout(index)
} catch (e: Exception) {
onFailure(index, e)
}
onCompletion(index, output)
}
}
}
}
}
Jobs are now cancelled when timeout is reached.
Completion callbacks are now called in the caller's dispatcher.
The race condition when deciding when to call onFullCompletion has been fixed.
Removed some methods that you didn't really need.
If you feel this is more like the callback pattern, then you simply shouldn't use callbacks. Coroutines are designed such that you write this sort of code at use site with minimal boilerplate, so functions like this aren't necessary and look weird (IMHO).

Related

Replacing GlobalScope.launch with something better

I'm refactoring the following bit of code that's wrapping a CompletableFuture API into something that can be used with Coroutines, but it's using GlobalScope.launch { ... } which is discouraged:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cf = CompletableFuture<T>()
try {
this.connectionPool.inTransaction { connection ->
GlobalScope.launch {
try {
cf.complete(f(connection))
} catch (e: Throwable) {
cf.completeExceptionally(e)
}
}
cf
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cf.completeExceptionally(e)
}
return cf.await()
}
Getting rid of the CompletableFuture and replacing it with CompletableDeferred is the easy part:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cdf = CompletableDeferred<T>()
try {
connectionPool.inTransaction { connection ->
GlobalScope.launch {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cdf.completeExceptionally(e)
}
return cdf.await()
}
The inTransaction API expects a CompletableFuture, I'm assuming this is for backwards compatibility with Java
override fun <A> inTransaction(f: (Connection) -> CompletableFuture<A>):
CompletableFuture<A> =
objectPool.use(configuration.executionContext) { it.inTransaction(f) }
Since I'm outside a CoroutineScope, I can't just call launch { ... } and since f is a suspend function, that section needs to be inside a CoroutineScope
Wrapping the connectionPool.inTransaction inside a coroutineScope in order to replace the GlobalScope locks up when it runs ...
try {
coroutineScope {
connectionPool.inTransaction { connection ->
launch {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
}
} catch (e: Throwable) {
Similarly with
try {
coroutineScope {
connectionPool.inTransaction { connection ->
async {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
}
} catch (e: Throwable) {
Adding some good ol' println debugging:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cdf = CompletableDeferred<T>()
println("1")
try {
println("2")
coroutineScope {
println("3")
connectionPool.inTransaction { connection ->
println("4")
launch {
println("5")
try {
println("6")
cdf.complete(f(connection))
println("7")
} catch (e: Throwable) {
println("8")
cdf.completeExceptionally(e)
println("9")
}
}
println("10")
cdf.asCompletableFuture()
}
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cdf.completeExceptionally(e)
}
println("11")
return cdf.await()
}
Outputs:
1
2
3
11
4
10
followed by a stacktrace that the query timed out
timeout query item <postgres-connection-2> after 73751 ms and was not cleaned by connection as it should, will destroy it - timeout is 30000
This means the code inside the launch is never executing, similarly with async, I'm assuming some thread is getting blocked somewhere.
Replacing the GlobalScope.launch with CoroutineScope(Dispatchers.IO).launch { ... } works (similarly with Dispatchers.Unconfined)), but is this the correct solution here?
If CompletableFuture is thread-blocking, then this is probably better than the GlobalScope.launch solution ...
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
val cdf = CompletableDeferred<T>()
try {
connectionPool.inTransaction { connection ->
CoroutineScope(Dispatchers.IO).launch {
try {
cdf.complete(f(connection))
} catch (e: Throwable) {
cdf.completeExceptionally(e)
}
}
cdf.asCompletableFuture()
}
} catch (e: Throwable) {
log.error(e.message ?: "", e)
cdf.completeExceptionally(e)
}
return cdf.await()
}
Any suggestions on what the correct way is to get rid of that GlobalScope.launch?
It's pretty complicated, but I believe the cause of launch()/async() not executing is that their parent coroutineScope() already finished at this point in time. Notice "11" happens before "4", meaning that you invoked launch() after going out of coroutineScope(). Which makes sense, because inTransaction() starts asynchronous operation, so it returns immediately, without waiting for the inner code. To fix this, you just need to move cdf.await() inside coroutineScope().
Another thing that concerns me is that you await on completable that you created by yourself and not the one returned from inTransaction(). Note that it may be a totally different CompletableFuture and in that case you actually return before the operation completes.
Also, I'm not sure if this manual exception handling for completable is really necessary. async() already performs exception handling and wraps the result as CompleteableDeferred, then it is converted to CompletableFuture which also wraps exceptions. The only thing we have to do is to replace coroutineScope() with supervisorScope(). Otherwise, async() would automatically signal coroutineScope() to fail, so the exception handling would totally bypass inTransaction() function.
Try this code:
suspend fun <T> transaction(f: suspend (Connection) -> T): T {
return supervisorScope {
connectionPool.inTransaction { connection ->
async { f(connection) }.asCompletableFuture()
}.await()
}
}

How to create a polling mechanism with kotlin coroutines?

I am trying to create a polling mechanism with kotlin coroutines using sharedFlow and want to stop when there are no subscribers and active when there is at least one subscriber. My question is, is sharedFlow the right choice in this scenario or should I use channel. I tried using channelFlow but I am unaware how to close the channel (not cancel the job) outside the block body. Can someone help? Here's the snippet.
fun poll(id: String) = channelFlow {
while (!isClosedForSend) {
try {
send(repository.getDetails(id))
delay(MIN_REFRESH_TIME_MS)
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
invokeOnClose { Timber.e("channel flow closed.") }
}
}
You can use SharedFlow which emits values in a broadcast fashion (won't emit new value until the previous one is consumed by all the collectors).
val sharedFlow = MutableSharedFlow<String>()
val scope = CoroutineScope(Job() + Dispatchers.IO)
var producer: Job()
scope.launch {
val producer = launch() {
sharedFlow.emit(...)
}
sharedFlow.subscriptionCount
.map {count -> count > 0}
.distinctUntilChanged()
.collect { isActive -> if (isActive) stopProducing() else startProducing()
}
fun CoroutineScope.startProducing() {
producer = launch() {
sharedFlow.emit(...)
}
}
fun stopProducing() {
producer.cancel()
}
First of all, when you call channelFlow(block), there is no need to close the channel manually. The channel will be closed automatically after the execution of block is done.
I think the "produce" coroutine builder function may be what you need. But unfortunately, it's still an experimental api.
fun poll(id: String) = someScope.produce {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val channel = poll("hello")
channel.receive()
channel.cancel()
}
The produce function will suspended when you don't call the returned channel's receive() method, so there is no need to delay.
UPDATE: Use broadcast for sharing values across multiple ReceiveChannel.
fun poll(id: String) = someScope.broadcast {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val broadcast = poll("hello")
val channel1 = broadcast.openSubscription()
val channel2 = broadcast.openSubscription()
channel1.receive()
channel2.receive()
broadcast.cancel()
}

After a coroutine scope is canceled, can it still be used again?

When we have a coroutine scope, when it is canceled, can it be used again?
e.g. for the below, when I have scope.cancel, the scope.launch no longer work
#Test
fun testingLaunch() {
val scope = MainScope()
runBlocking {
scope.cancel()
scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}.join()
println("Finished")
}
}
Similarly, when we have scope.cancel before await called,
#Test
fun testingAsync() {
val scope = MainScope()
runBlocking {
scope.cancel()
val defer = scope.async {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
defer.await()
println("Finished")
}
}
It will not execute. Instead, it will crash with
kotlinx.coroutines.JobCancellationException: Job was cancelled
; job=SupervisorJobImpl{Cancelled}#39529185
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1579)
at kotlinx.coroutines.CoroutineScopeKt.cancel(CoroutineScope.kt:217)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default(CoroutineScope.kt:215)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:241)
at |b|b|b(Coroutine boundary.|b(|b)
at kotlinx.coroutines.DeferredCoroutine.await$suspendImpl(Builders.common.kt:101)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:254)
Caused by: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelled}#39529185
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1579)
at kotlinx.coroutines.CoroutineScopeKt.cancel(CoroutineScope.kt:217)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default(CoroutineScope.kt:215)
at com.example.coroutinerevise.CoroutineExperiment$testingAsync$1.invokeSuspend(CoroutineExperiment.kt:241)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
Is it true, a canceled coroutine scope cannot be used for launch or async anymore?
Instead of using CoroutineScope for cancelling all of the launched jobs in it, you may want to use the underlying CoroutineContext with its cancelChildren() method which doesn't affect the Job state (which is not true for plain cancel() method) and allows to continue launching new coroutines after being invoked.
Following up on #Alex Bonel response.
For example you have a method like
fun doApiCall() {
viewModelScope.launch {
// do api call here
}
}
You can call doApiCall() again and again
viewModelScope.coroutineContext.cancelChildren()
doApiCall()
Calling doApiCall() will not have any effect.
viewModelScope.coroutineContext.cancel()
doApiCall() // would not call
I am using that in compose and it is slightly different.
Define coroutine scope in compose
val coroutineScope = rememberCoroutineScope()
Launch coroutine
coroutineScope.launch(Dispatchers.Main) { //Perform your operation }
Cancel all coroutines that belong to coroutineScope
coroutineScope.coroutineContext.cancelChildren()
Related to lifecycleScope comment above, I'm not sure how common cancellation like this would be in practice? fwiw something like following will work:
val scope = MainScope()
runBlocking {
val job1 = scope.launch {
try {
println("Start Launch 1")
delay(200)
println("End Launch 1")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
job1.cancel()
val job2 = scope.launch {
try {
println("Start Launch 2")
delay(200)
println("End Launch 2")
} catch (e: CancellationException) {
println("Cancellation Exception")
}
}
job2.join()
println("Finished")
}
This particular example will print
Start Launch 1
Cancellation Exception
Start Launch 2
End Launch 2
Finished
I needed to collect the event just once and then stop listening. I came up with this solution:
/**
* Collect the value once and stop listening (one-time events)
*/
suspend fun <T> Flow<T>.collectOnce(action: suspend (value: T) -> Unit) {
try {
coroutineScope {
collectLatest {
action(it)
this#coroutineScope.cancel()
}
}
} catch (e: CancellationException) { }
}
You can use it like this:
viewModelScope.launch {
myStateFlow.collectOnce {
// Code to run
}
}

Catching an error of a coroutine launch call

in the following code:
private fun executeCognitoRequest(result: MethodChannel.Result, block: suspend CoroutineScope.() -> Any?) {
try {
CoroutineScope(Dispatchers.Default).launch {
val requestResult = block()
withContext(Dispatchers.Main) {
result.success(requestResult)
}
}
} catch (exception: Exception) {
val cognitoErrorType = CognitoErrorType.getByException(exception)
result.error(cognitoErrorType.code, null, null)
}
}
if the call to block throws, will it be caught?
It will be caught, but the problem with your code is that you violate the principles of structured concurrency and launch a coroutine in the GlobalScope. So if you test your code from a main function like this:
fun main() {
runBlocking {
executeCognitoRequest(MethodChannel.Result()) {
funThatThrows()
}
}
}
the whole program will end before the coroutine has completed execution.
This is how you should write your function:
private fun CoroutineScope.executeCognitoRequest(
result: MethodChannel.Result,
block: suspend CoroutineScope.() -> Any?
) {
try {
launch(Dispatchers.IO) {
val requestResult = block()
withContext(Dispatchers.Main) {
result.success(requestResult)
}
}
} catch (exception: Exception) {
val cognitoErrorType = CognitoErrorType.getByException(exception)
result.error(cognitoErrorType.code, null, null)
}
}
Now your function is an extension on CoroutineScope and launch is automatically called with that receiver. Also, for blocking IO calls you shouldn't use the Default but the IO dispatcher.
However, I find your higher-level design weird, you start from blocking code and turn it into async, callback-oriented code. Coroutines are there to help you get rid of callbacks.

Vert.x way to write coroutine blocking code

I do have the project that uses both coroutines and Vert.x.
I'm trying to write a wrapper function to run blocking code on vertx worker thread pool
Something like:
suspend inline fun <T> executeOnWorkerThread(crossinline block: () -> T) =
withContext(**Vertx-Worker-ThreadPool**) {
block()
}
So it may be used like
suspend fun usage(obj: Any): String = executeOnWorkerThread {
try {
// blocking code
} catch (e: Exception) {
// Exception handling
}
}
But this is not vert.x way. And I couldn't find the way to extract thread pool out of vert.x
suspend fun <T> awaitBlockingUnordered(block: () -> T): T {
return awaitResult { handler ->
val ctx = Vertx.currentContext()
ctx.executeBlocking<T>(
{ fut -> fut.complete(block()) },
false,
{ ar -> handler.handle(ar) }
)
}
}