Say I want to start an asynchronous background tas...
# coroutines
v
Say I want to start an asynchronous background task that should not be waited for in the current suspending function, what is the appropriate way? I was under the impression
GlobalScope.launch { /* do the background task */ }
is the appropriate way. I'm just a bit uncertain as I have to opt-in to
DelicateCoroutinesApi
and the AI-Reviewer suggests to use a
CoroutineScope
instead. It more details are interesting, when a request for X is done, X is taken from cache or calculated and put to cache. After X was requested, it is quite likely, that Y and Z are requested next, so I want to trigger asynchronously in the background that Y and Z are calculated and put to the cache. But this should not delay the returning of X.
p
One solution is to pass coroutine scope of top most needed outer function as parameter.
r
In most cases, the segment of your application that cares about these values owns a
CoroutineScope
that it uses to launch coroutines without blocking to wait for the result.
c
I would create a
CoroutineScope
as a class attribute of the
Cache
class and
launch
into that. That way, no one has to wait for it, but a user can still kill the cache and be sure there's no work left somewhere else. That will particularly be useful for testing, where you can ensure everything started by the cache is killed before the test terminates.
๐Ÿ’ฏ 1
โ˜๐Ÿผ 1
๐Ÿ‘ 1
v
If you don't mind having a look, it is this: https://github.com/typesafegithub/github-workflows-kt/blob/1368edf0e00b151b0e3e3ea[โ€ฆ]hub/typesafegithub/workflows/jitbindingserver/ArtifactRoutes.kt Do you mean, I would just add
Copy code
private val prefetchScope = CoroutineScope(...)
to the file and then use
prefetchScope
instead of
GlobalScope
? And if so, what would
...
be?
<http://Dispatchers.IO|Dispatchers.IO>
? (Sorry, I'm not that used to working with coroutines at that level yet.)
At least that seems to still do what I wanted. ๐Ÿ™‚ Just not sure whether that is what you meant, nor how it makes it better ๐Ÿ˜„
r
The main benefit of a normal
CoroutineScope
over
GlobalScope
is lifecycle management / cancelability. For example in mobile apps you don't care about coroutines that are fetching data for a certain view once the user navigates away from the view, so those coroutines should be canceled at that point. Looks like your context is a ktor web server? If the cache is long-lived along with the whole server process, then you may not have any need to cancel the pre-fetch coroutine(s), in which case global scope would be functionally equivalent. But It'd probably still be better practice to create a non-global coroutine scope wrapped in a
Cache
class that has your
prefetchBindingArtifacts
function declared as a method. Encapsulation like that should help with unit tests and such.
v
Ah, I see, thanks. Yeah, here it is a cache over the full lifetime of the JVM. So this is the "right" way to do it, right? https://github.com/typesafegithub/github-workflows-kt/pull/1908/files Including the use of
<http://Dispatchers.IO|Dispatchers.IO>
.
r
I personally would wrap the scope and related functions into a class, rather than all being top-level in the file, for the sake of testing. But it's functionally all the same, yes. As for IO dispatcher, that depends on how exactly
bindingsCache
works. If I understand correctly, the IO dispatcher is only relevant if you're calling non-suspend functions that you know primarily wait on IO rather than doing CPU work.
โž• 1
v
Thanks. I'm not going to reactor the code design, it's not my project. :-) Currently it's only doing suspend functions and those do network calls if I got it right.
๐Ÿ‘ 1
r
Regarding the IO dispatcher, after digging into your codebase more, I've found the definition of
ActionCoords.buildVersionArtifacts
and I see that it is not a suspend function. This means that your dispatcher decision does simply depend on whether that function does more IO-bound work (loading files from disk, network requests, etc.) or CPU-bound work (transforming data, computing checksums, etc.)
v
It is not "my" codebase, just contributing ๐Ÿ™‚ What it does is downloading 4-8 files, and generating some text from it which is then in the cache. The generation should not be super-complex, so I'd say the main "effort" is downloading the 4-8 files.
๐Ÿ‘ 1
d
Another problem with
GlobalScope
is that it's more difficult to control what happens if the coroutines started in it throw an exception: you have to do
launch(CoroutineExceptionHandler { _, e -> /* something */ })
each time you launch a coroutine. With a custom scope, you can write
CoroutineScope(CoroutineExceptionHandler { _, e -> /* something */ })
once and define the strategy for all its coroutines. Also, if one coroutine fails, the other coroutines running in the same scope get cancelled. To prevent that, use
CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, e -> /* something */ })
.
GlobalScope
uses a
SupervisorJob()
, too.