`runBlocking`is so easy and evil to use. How do I ...
# coroutines
d
`runBlocking`is so easy and evil to use. How do I avoid it in top level methods which are below
main
or in
koin
module definitions or class initializers/constructors? Example from
koin
init (as you'll see I'm questioning the original author's overuse and non-trivial computations)
Copy code
single {
                val redis: RedisConnection = get(CORE_REDIS_CONNECTION)
                val redisHealth = HealthDetail(
                    url = redis.uris.first().toString(),
                    healthy = runCatching {
                        // ugh, this is ugly. too much work in koin and unclear what the coroutine context is.
                        // this may deadlock coroutines as this won't release the context until it completes.
                        runBlocking(<http://Dispatchers.IO|Dispatchers.IO>) {
                            redis.withSuspendConnection { connection ->
                                connection.ping()
                            }
                        }
                        true
                    }.getOrDefault(false),
                    responseTimeMs = 0
                )

                Health(database = get(MYSQL), stage = Stage.fromEnvironment(), redisHealth = redisHealth)
            }
c
The framework should provide a way to initialize data in a suspending fashion. Maybe ask in #C67HDJZ2N.
Deep down, the idea is that the entire callstack from the root scope to your initializer should be
suspend
. If you put Koin in that, you must make sure it can
suspend
.
👍 1
If you can control
Health
, another option is to make the
Health.redisHealth
a
Deferred<HealthDetail>
and initialize it lazily from
GlobalScope
;
Copy code
single {
    // …
    val redisHealth = GlobalScope.async(<http://Dispatchers.IO|Dispatchers.IO>, start = LAZY) {
        HealthDetail(
            url = redis.uris.first().toString()
            healthy = redis.withSuspendConnection { it.ping() }
        ),
        responseTimeMs = 0
    }

    Health(…, redisHealth = redisHealth)
}
Here,
GlobalScope
is ok because the job is started lazily (so no leak happens if it's never initialized) and it returns a
Deferred
(so the caller can control cancellation).
z
The framework should provide a way to initialize data in a suspending fashion.
I don't have many strong feelings about DI frameworks but I probably wouldn't want my DI framework to grow an async loading, repository-ish appendage. Dependency injection can be complex enough of a job as-is, and async loading can also get quite nuanced. This looks to me like it wants a service class that's responsible for making the async calls, give it whatever suspending/flow/other API it needs, then just inject a singleton of that everywhere and leave the consumers to call the loading API as necessary.
initialize it lazily from
GlobalScope
GlobalScope
is also definitely something to avoid, if you're using it to avoid
runBlocking
your code isn't much better.
2
👍 1
c
Dependency injection can be complex enough of a job as-is, and async loading can also get quite nuanced.
IMO, if you're going to use a DI framework instead of using regular variables, I hope that it solves the complex issues of loading things. Otherwise, I don't really see the point.
@Zach Klippenstein (he/him) [MOD] do you have other ideas of how to solve this?
k
Not about DI, but one thing I’d love to see is runBlocking marked as delicate. There are some pretty big foot-guns available for people who use runBlocking from within a coroutine that are difficult to avoid in a sufficiently large codebase (imagine a suspend function which calls a regular function which calls runBlocking). Marking this as delicate would help flag this as potentially dangerous in code review.
👀 1
c
Personally I'd like to see a
lazyAsync {}
where the block is computed in the scope of the first coroutine to await it (so no need to specify a scope at creation time).
👎🏾 1
z
do you have other ideas of how to solve this?
I gave one
if you're going to use a DI framework instead of using regular variables, I hope that it solves the complex issues of loading things. Otherwise, I don't really see the point.
Again, I completely disagree. DI frameworks imo mostly only have value in huge codebases with huge dependency graphs where manually doing constructor injection gets untenable. That's their job, and imo should be their only job. Libraries for loading things (Picasso, Coil, Store) are quite complex to solve their own problems, I wouldn't want to conflate those two jobs. Glide is a counterexample of a library that does both (image loading + DI), and ew.
2
d
I do think this code is improperly delegating system readiness/health check to the DI initialization. I should probably get the team that built it to do it further down the stack or in the context where there's a clear coroutine context, but they'll think I'm being pedantic and they have 🔥 to fight...
k
I don't think you really ever want network connections happening when you initialize DI, which it seems like you are. I think it touches somewhat lightly on this. https://publicobject.com/2019/06/10/value-objects-service-objects-and-glue/
☝🏻 1
☝️ 1
☝🏾 1