https://kotlinlang.org logo
#coroutines
Title
# coroutines
c

CLOVIS

08/15/2021, 9:29 AM
What is the correct way to implement a cache? I'm thinking of something like:
Copy code
val cache = HashMap<Id, Deferred<Value>>()
val lock = Semaphore(1)
val cacheJob = SupervisorJob()
val cacheScope = CoroutineScope(cacheJob)

suspend fun get(id: Id) {
  val r = lock.withPermit {
    if (cache[id] == null) {
      val c = CompletableDeferred()
      cacheScope.launch {
        c.complete(getElementFromNetwork(id))
      }
      c
    } else cache[id]
  }

  r.await()
}
I think that's generally safe, but I'm not sure if it will correctly handle all cases (the ID is invalid, ...)
d

Dominaezzz

08/15/2021, 9:36 AM
Mind using triple back ticks instead of single ones? Makes it easier to read.
You don't need
CompletableDeferred
here and you've leaked it by not cancelling it.
c

CLOVIS

08/15/2021, 11:37 AM
Ah, I thought it didn't work on mobile. It should be better now.
How can this work without CompletableDeferred? Where is the leak?
d

Dominaezzz

08/15/2021, 11:38 AM
Actually, nevermind that for now.
cache
will always be empty.
c

CLOVIS

08/15/2021, 11:40 AM
Ah, indeed:
Copy code
val cache = HashMap<Id, Deferred<Value>>()
val lock = Semaphore(1)
val cacheJob = SupervisorJob()
val cacheScope = CoroutineScope(cacheJob)

suspend fun get(id: Id) {
  val r = lock.withPermit {
    if (cache[id] == null) {
      val c = CompletableDeferred()
      cacheScope.launch {
        c.complete(getElementFromNetwork(id))
      }
      cache[id] = c
    }
    cache[id]
  }

  r.await()
}
👍 1
d

Dominaezzz

08/15/2021, 11:42 AM
What's wrong with
cache[id] = cacheScope.async { getElementFromNetwork(id) }
?
(Then you don't need
CompletableDeferred
)
c

CLOVIS

08/15/2021, 1:48 PM
@Dominaezzz if two coroutines
get
the same ID, two requests are fired to the server (and the result of the last to terminate will override the other)
Maybe the cache map would be better as:
val cache = HashMap<Id, Pair<StateFlow<Boolean>, StateFlow<Value>>>
This way, the UI can subscribe to either the boolean (whether the value is currently being downloaded) and/or the actual value This way, it's also possible to get the current value even when another coroutine is updating it
d

Dominaezzz

08/15/2021, 2:09 PM
You have the lock so you won't get two requests
e

ephemient

08/15/2021, 6:06 PM
Copy code
val cache = ConcurrentHashMap<Id, Deferred<Value>>()
suspend fun get(id: Id) = coroutineScope {
    cache.compute(id) { _, deferred ->
        if (deferred == null || try {
            deferred.getCompletionExceptionOrNull()
        } catch (_: IllegalStateException) {
            null
        } != null) {
            async(start = CoroutineStart.ATOMIC) {
                getElementFromNetwork(id)
            }
        } else deferred
    }.await()
}
that would effectively give you a per-key lock instead of a global lock
d

Dominaezzz

08/15/2021, 6:15 PM
But if the first caller cancels, other waiting callers never get their values.
e

ephemient

08/15/2021, 6:22 PM
any callers waiting on the same request will see a cancellation, yes, but any subsequent callers will start a new request
if that's a concern you could add a retry in there
c

CLOVIS

08/15/2021, 11:00 PM
With that version however, you can't see the old value if another coroutine is currently downloading
I guess having a map of StateFlow is the solution here?
d

Dominaezzz

08/16/2021, 10:36 AM
Old value? Didn't know invalidation was involved.
c

CLOVIS

08/16/2021, 8:23 PM
I might want to force a refresh, for example if I know that the value is outdated
10 Views