I often find myself having to load a global resource asynchronously, where it only needs to be loaded once, but it could potentially fail. An example is the delta with the server time, so that I can implement an NTP. For those cases, I implemented a
DeferredCache
with an
await
method which when called:
- if no coroutine to load the resource has been launched, it starts a new one
- if a coroutine to load the resource has finished successfully, it returns its result
- if a coroutine to load the the resource has finished with an error, it restarts a new one
- if a coroutine to load the resource has been launched and is still active, it awaits its result
My current implementation looks like this
class DeferredCache<T: Any>(
private val coroutineScope: CoroutineScope = GlobalScope,
private val block: suspend CoroutineScope.() -> T
) {
private var deferredValue: Deferred<T>? = null
private val deferredValueLock = Mutex()
private val usableDeferredValue: Deferred<T>?
get() = deferredValue?.takeUnless { it.isCompletedExceptionally }
suspend fun await(): T {
val deferredValue = usableDeferredValue ?: deferredValueLock.withLock {
usableDeferredValue ?: coroutineScope.async(NonCancellable, block = block).also { deferredValue = it }
}
return deferredValue.await()
}
}
Can you think of a more optimal solution? Anyone running into this scenario as well? Might it be worth adding a “standard” way to do this?