https://kotlinlang.org logo
Title
b

bbaldino

09/23/2020, 5:13 PM
I've got an app that only uses coroutines in one spot (where I'm using ktor) and I want to fire-and-forget a set of requests (I don't want to block the caller on the responses, I just log when responses are received or when they time out). I have an existing IO thread pool for tasks like this. I was originally doing (simplified):
fun my update() = runBlocking {
    subscribers.forEach { url -> 
        launch(myIoPool.asCoroutineDispatcher()) {
            try {
                val resp = client.postJson(...)
                <http://logger.info|logger.info>("Got response...")
            } catch (e: HttpRequestTimeoutException) {
                logger.error("Timed out...")
            }            
        }
}
But I've now realized this is blocking until all the calls to
launch
finish. What's the best way to fire-and-forget here? I think I've seen I can do this with
GlobalScope.launch
(and getting rid of the
runBlocking
), but from what I've read it seems like it's rarely a good idea to use that.
a

araqnid

09/23/2020, 5:17 PM
GlobalScope.launch
does basically what you need here - the scope is intended to cancel things that are still ongoing when the scope ends. You might consider that if your app has “start” and “stop” concepts, then that defines a scope which would be more precise than GlobalScope.
b

bbaldino

09/23/2020, 5:17 PM
Ok, yeah from what I read the concern with
GlobalScope
is the leaking, but this particular task basically runs for the lifetime of the app, so maybe it's not a worry?
If this class had its own start and stop methods, what would be the way to do that? Have the class create its own scope to use instead of
GlobalScope
?
a

araqnid

09/23/2020, 6:57 PM
Yes, you can just call
CoroutineScope()
to create a new standalone scope (usually just a property initialiser) and then
scope.cancel()
in your stop/close method
My own experience is that launching things without allowing for housekeeping to clean them up often comes back to bite eventually.
☝️ 1
b

bbaldino

09/23/2020, 7:25 PM
Ok, I'll play with that. Thanks Steve!
g

gildor

09/24/2020, 4:24 AM
rarely a good idea to use that
Yes, specifically because it can be used in cases as yours, global fire and forget background tasks Replacing GlobalScope with own scope can be better, but only if you really have some code which cancels it, otherwise it would be exactly the same
b

bbaldino

09/24/2020, 4:29 AM
Ok good to know. I don't ever cancel this (as of now), but using a custom scope with a hook to pass something else in the constructor worked out well since I've got a good hook for tests.
g

gildor

09/24/2020, 5:01 AM
I think such code as yours should be a bit improved, to be sctructured concurrency friendly and more flexible as for end user and for usages in tests:
suspend fun myUpdate(dispatcher: CoroutineDispatcher = myIoPool.asCoroutineDispatcher()) {
    coroutineScope {
        subscribers.forEach {
            launch(dispatcher) {
                try {
                    val resp = client.postJson(...)
                    <http://logger.info|logger.info>("Got response...")
                } catch (e: HttpRequestTimeoutException) {
                    logger.error("Timed out...")
                }
            }
        }
    }
}
So now this operation is more correct it cancellable, follows structured concurrency and can be used in different contexts depending on case:
fun myBlockingUpdate() = runBlocking {
    myUpdate()
}

fun myBackgroundUpdate(): Job {
    GlobalScope.launch {
        myUpdate()
    }
}

suspend fun mySuspendUpdate() {
    <http://logger.info|logger.info>("Before update")
    myUpdate()
    <http://logger.info|logger.info>("After update")
}
b

bbaldino

09/24/2020, 3:50 PM
Thanks, Andrey. In the case of
myBackgroundUpdate
there, will there be two thread transitions? Calling thread -> GlobalScope thread -> io pool thread? Trying to make sure I'm understanding it correctly.
a

araqnid

09/24/2020, 8:34 PM
Looks like that suggestion assumes that
myUpdate()
is just a suspend function to call. You can do
GlobalScope.launch(myIoPool.asCoroutineDispatcher()) { }
to specify the scope and override coroutine context (i.e. the dispatcher here) in one go