Daniel
10/16/2020, 4:35 PMgetTile
, which may need to call renewAccessToken
. If I make ten parallel calls to getTile
, and I need to renew the token, one call should do the renewing, and the rest should block until it is done.
I'm wondering if I want something that emits a stream of (mostly-identical) access tokens, which getTile pulls the latest access token from. The stream then chooses whether to renew or present a stale valuenanodeath
10/16/2020, 4:55 PMgetTile
synchronized?Daniel
10/16/2020, 5:23 PM```class AccessTokenCache(private val renewAsync: suspend () -> Deferred<AccessTokenResponse>) {
private var cache: AccessToken? = null
private val mutex = Mutex()
private var currentRequest: Deferred<AccessTokenResponse>? = null
suspend fun get(): AccessTokenResponse {
val cached = cache
return if (cached != null) {
Either.right(cached)
} else {
mutex.withLock {
Timber.i("Acquired lock")
val request = currentRequest
if (request != null) {
request.await()
} else {
val request = renewAsync()
currentRequest = request
request.await()
.map {
cache = it
it
}
}
}
}
}
}
nanodeath
10/16/2020, 5:33 PMnanodeath
10/16/2020, 5:34 PMDaniel
10/16/2020, 5:35 PMMarc Knaup
10/16/2020, 5:40 PMDaniel
10/16/2020, 5:40 PMMarc Knaup
10/16/2020, 5:42 PMDaniel
10/16/2020, 5:42 PMMarc Knaup
10/16/2020, 5:43 PMcurrentRequest
🙂Marc Knaup
10/16/2020, 5:43 PMMarc Knaup
10/16/2020, 5:43 PMMarc Knaup
10/16/2020, 5:43 PMMarc Knaup
10/16/2020, 5:43 PMMarc Knaup
10/16/2020, 5:43 PMDaniel
10/16/2020, 5:44 PMDaniel
10/16/2020, 5:44 PMcurrentRequest
? Failing to do so was a bugMarc Knaup
10/16/2020, 5:45 PMDaniel
10/16/2020, 5:46 PMMarc Knaup
10/16/2020, 5:46 PMcurrentRequest
.
Call 2 comes in but is blocked at the beginning of the function because the mutex is locked.
Call 1 finishes, unsets currentRequest
and returns the result.
Call 2 resumes because the mutex is available. It checks the currentRequest
which is not longer set and thus starts a new request.
await()
will likely never be called.Daniel
10/16/2020, 5:47 PMget
will be called in the uncached case multiple times, and should make only one request for a new token, but give that token to every callerMarc Knaup
10/16/2020, 5:48 PMcurrentRequest
.Daniel
10/16/2020, 5:54 PMclass AccessTokenCache(private val renew: suspend () -> AccessTokenResponse) {
private var cache: AccessToken? = null
private val mutex = Mutex()
suspend fun get(): AccessTokenResponse {
val cached = cache
if (cached != null) {
Timber.i("Got cached without lock")
return Either.right(cached)
}
mutex.withLock {
val cached = cache
return if (cached != null) {
Timber.i("Acquired lock, got cached")
Either.right(cached)
} else {
Timber.i("Acquired lock, renewing")
renew().map {
cache = it
it
}
}
}
}
}
Marc Knaup
10/16/2020, 5:56 PMmap
does but looks better now.Daniel
10/16/2020, 5:58 PMDaniel
10/16/2020, 5:58 PMPetter Måhlén
10/19/2020, 5:54 AM@Volatile
to ensure that the field is safely published, I’d say (otherwise, you’ll be getting unnecessary attempts at acquiring the lock). Also, do you ever clear the cached entry? (if you do, then without volatile, that may fail, and other threads than the clearing one might see the old value). You’re effectively doing this: https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java