Let's say a request started a long calculation, but ready to wait no longer than X seconds for the result. But instead of interrupting the calculation I would like it to continue in parallel until completion.
The first condition ("wait no longer than") is satisfied by withTimeoutOrNull
function.
What is the standard (idiomatic Kotlin) way to continue the computation and execute some final action when the result is ready?
Example:
fun longCalculation(key: Int): String { /* 2..10 seconds */}
// ----------------------------
cache = Cache<Int, String>()
suspend fun getValue(key: Int) = coroutineScope {
val value: String? = softTimeoutOrNull(
timeout = 5.seconds(),
calculation = { longCalculation() },
finalAction = { v -> cache.put(key, v) }
)
// return calculated value or null
}
CodePudding user response:
This is a niche enough case that I don't think there's a consensus on an idiomatic way to do it.
Since you want the work to continue in the background even if the current coroutine is resuming, you need a separate CoroutineScope to launch that background work, rather than using the coroutineScope
builder that launches the coroutine as a child coroutine of the current one. That outer scope will determine the lifetime of the coroutines that it launches (cancelling them if it is cancelled). Typically, if you're using coroutines, you already have a scope on hand that's associated with the lifecycle of the current class.
I think this would do what you're describing. The current coroutine can wrap a Deferred.await()
call in withTimeoutOrNull
to see if it can wait for the other coroutine (that's not a child coroutine since it was launched directly from an outer CoroutineScope) without interfering with it.
suspend fun getValue(key: Int): String? {
val deferred = someScope.async {
longCalculation(key)
.also { cache.put(key, it) }
}
return withTimeoutOrNull(5000) { deferred.await() }
}
Here's a generalized version:
/**
* Launches a coroutine in the specified [scope] to perform the given [calculation]
* and [finalAction] with that calculation's result. Returns the result of the
* calculation if it is available within [timeout].
*/
suspend fun <T> softTimeoutOrNull(
scope: CoroutineScope,
timeout: Duration,
calculation: suspend () -> T,
finalAction: suspend (T) -> Unit = { }
): T? {
val deferred = scope.async {
calculation().also { finalAction(it) }
}
return withTimeoutOrNull(timeout) { deferred.await() }
}