David Glasser
09/05/2019, 2:48 PMget()
method which launches a coroutine using CoroutineScope.future{}
. That coroutine should not be scoped to the get()
method's lifetime, because if get()
is called concurrently with the same key, both get()
calls will end up waiting on the same CompletableFuture, so cancelling the first coroutine shouldn't cause the second coroutine to fail. So I'm currently passing a CoroutineScope
into the constructor of the cache and using that to launch the `future`s.
However, my current implementation means that if any of the calls inside future{}
fail, the long-lived CoroutineScope associated with the cache is canceled, which I don't want. What I want basically is a supervisor scope nested inside the passed-in CoroutineScope, created at cache construction time. But supervisorScope
doesn't seem to be what I want — I can't call that at cache construction time because I don't want to actually wait before returning like a supervisorScope does! Is there some other API I should be using here? I feel like maybe the answer is SupervisorJob()
but I am still kinda confused about the distinction between Job and CoroutineScope — I can make a SupervisorJob(coroutineScope.coroutineContext[Job])
but I'm confused about how to extract a CoroutineScope from it that I can use for launching coroutines.SupervisorJob(cs.coroutineContext[Job]).let { job ->
object : CoroutineScope {
override val coroutineContext = cs.coroutineContext + job
}
}
but surely there's a simpler way? Also this doesn't seem to work.streetsofboston
09/05/2019, 3:00 PMDavid Glasser
09/05/2019, 3:00 PMstreetsofboston
09/05/2019, 3:01 PMDavid Glasser
09/05/2019, 3:02 PMclass InMemAsyncCache<K : Any, V>(
coroutineScope: CoroutineScope,
loader: suspend (K) -> V
) {
class Value<V>(val value: V)
private val job = SupervisorJob(coroutineScope.coroutineContext[Job])
private val nestedScope = object : CoroutineScope {
override val coroutineContext = coroutineScope.coroutineContext + job
}
private val under = Caffeine.newBuilder().let { builder ->
builder.buildAsync<K, Value<V>> { k, _ -> nestedScope.future { Value(loader(k)) } }
}
suspend fun get(key: K): V = under.get(key).await().value
}
streetsofboston
09/05/2019, 3:03 PMclass MyAsyncLoadingCache {
private val scope = CoroutineScope(SupervisorJob() + someDispatcher)
....
suspend fun getData(key: Key) : Data {
val data: Data = scope.async { ..... }.await()
return data
}
...
fun destroy() { scope.cancel() }
}
getData(someKey)
in its own scope, the getData
will be cancelled, but the code inside scope.async { .... }
won’tDavid Glasser
09/05/2019, 3:04 PMstreetsofboston
09/05/2019, 3:05 PMDominaezzz
09/05/2019, 3:05 PMclose
method.HttpClient
.streetsofboston
09/05/2019, 3:07 PMfun destroy
not enough, which can be expanded not only to cancel the scope, but do some additional clean up as well?David Glasser
09/05/2019, 3:07 PMstreetsofboston
09/05/2019, 3:08 PMDominaezzz
09/05/2019, 3:08 PMDavid Glasser
09/05/2019, 3:08 PMstreetsofboston
09/05/2019, 3:10 PMMyAsyncLoadingCache.scope
by calling MyAsyncLoadingCache.destroy()
, any caller that is awaiting the result of scope.async
will get a CancelationException
, which will cause the cancelation of the caller’s Job as well (how that is handled exactly, depends on the caller’s CoroutineScope).GlobalScope
(it doesn’t have Job
)Dominaezzz
09/05/2019, 3:11 PMDavid Glasser
09/05/2019, 3:12 PMDominaezzz
09/05/2019, 3:12 PMstreetsofboston
09/05/2019, 3:13 PMscope.async { ....}.await()
.David Glasser
09/05/2019, 3:13 PMstreetsofboston
09/05/2019, 3:16 PMDavid Glasser
09/05/2019, 3:19 PMNo function should create coroutines that outlive the function call’s lifetime, unless explicitly provided with a CoroutineScope defining the created coroutine’s lifetime.But it sounds like it should just be:
No function should create coroutines that outlive the function call’s lifetime, unless explicitly provided with a CoroutineScope defining the created coroutine’s lifetime, or the function is a method on an object with a-style method that will cancel all spawned coroutines.close()
streetsofboston
09/05/2019, 3:19 PMclass InMemAsyncCache<K : Any, V>(
loader: suspend (K) -> V,
dispatcher: CoroutineContext,
) {
class Value<V>(val value: V)
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
private val under = Caffeine.newBuilder().let { builder ->
builder.buildAsync<K, Value<V>> { ... }
}
suspend fun get(key: K): V = scope.async { under.get(key) }.await().value
fun destroy() { scope.cancel() }
}
(I’m not sure what the ‘buildAsync’ with the .future
call does….)David Glasser
09/05/2019, 3:20 PMstreetsofboston
09/05/2019, 3:21 PMbuildAsync
function is a regular blocking (non-suspending) lambda, correct?under.get(key)
is a regular blocking (non-suspending) function a well?David Glasser
09/05/2019, 3:22 PMValue
thing is just about dealing w/ the fact that Caffeine doesn't store null values and we want to)streetsofboston
09/05/2019, 3:23 PMloader
parameter does not need to be suspend
and nestedScope.future
is not necessary either….. It can all be blockingDavid Glasser
09/05/2019, 3:23 PMdispatcher: CoroutineContext = EmptyCoroutineContext
which I believe means that Dispatchers.Default will get used?streetsofboston
09/05/2019, 3:23 PMDavid Glasser
09/05/2019, 3:24 PMstreetsofboston
09/05/2019, 3:25 PMscope.future { .... }
David Glasser
09/05/2019, 3:26 PMstreetsofboston
09/05/2019, 3:26 PMDavid Glasser
09/05/2019, 3:29 PMfun close()
go through and do cache1.close(); cache2.close(); cache3.close()
etc (but actually with appropriate try/finally's so they all close even on exception). this isn't a coroutines question any more but is there a Kotlin idiom I don't know about for "this Closeable
contains a bunch of other `Closeable`s"?streetsofboston
09/05/2019, 3:29 PMfun CouroutineScope.doSomething(...)
Dominaezzz
09/05/2019, 3:31 PMstreetsofboston
09/05/2019, 3:31 PMDavid Glasser
09/05/2019, 3:33 PMstreetsofboston
09/05/2019, 3:42 PMinterface ClosableCoroutineScope : CoroutineScope, AutoClosable {
override fun close() = cancel()
companion object {
operator fun invoke(context: CoroutineContext) : ClosableCoroutineScope =
object: ClosableCoroutineScope {
override val coroutineContext = context
}
}
}
class MyCache(context: CoroutineContext): ClosableCoroutineScope by ClosableCoroutineScope(context + SupervisorJob()) {
...
fun getValue(key: Key): Data = async { ... }.await()
...
}
David Glasser
09/05/2019, 3:44 PMstreetsofboston
09/05/2019, 3:45 PMDominaezzz
09/05/2019, 3:45 PMuse
but I don't think it helps much.spand
09/10/2019, 8:20 AMstreetsofboston
10/07/2019, 8:13 PMspand
10/08/2019, 7:21 AM