Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Different behavior from expireAfter and expireAfterWrite #1745

Closed
glasser opened this issue Aug 1, 2024 · 4 comments
Closed

Different behavior from expireAfter and expireAfterWrite #1745

glasser opened this issue Aug 1, 2024 · 4 comments

Comments

@glasser
Copy link

glasser commented Aug 1, 2024

(I apologize — this is not a great bug report in that it is not a self-contained reproduction, and it's in Kotlin. But perhaps there's something blatantly obvious we are doing wrong.)

We have a cache that we set up with this configuration:

Caffeine.newBuilder().let { builder ->
	builder.maximumSize(10_000)
	builder.expireAfterWrite(10.minutes)
	builder.recordStats()
	builder.buildAsync<K, Box<V>> { k, _ -> coroutineScope.future { Box(loader(k)) } }
}

(10.minutes uses a Kotlin extension function that returns a Duration for 10 minutes.)

We use this cache in an app that calls get() on the cache once a minute for a few hundred keys (the same keys each minute). We see that the underlying loader is called on every key once every ten minutes.

We want to spread this out, so we want to jitter the expiration. So we do this:

// See https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.random/-random/next-long.html
fun randomDurationNanos(min: Duration, max: Duration) =
	kotlin.random.Random.nextLong(min.toNanos(), max.toNanos())

fun expireRandomDurationAfterCreate(min: Duration, max: Duration) = object : Expiry<String, Boolean> {
	override fun expireAfterCreate(key: String, value: Boolean, currentTime: Long) =
		randomDurationNanos(min, max)

	override fun expireAfterUpdate(key: String, value: Boolean, currentTime: Long, currentDuration: Long) =
		currentDuration

	override fun expireAfterRead(key: String, value: Boolean, currentTime: Long, currentDuration: Long) =
		currentDuration
}

Caffeine.newBuilder().let { builder ->
	builder.maximumSize(10_000)
	builder.expireAfter(expireRandomDurationAfterCreate(5.minutes, 15.minutes))
	builder.scheduler(Scheduler.systemScheduler())
	builder.recordStats()
	builder.buildAsync<K, Box<V>> { k, _ -> coroutineScope.future { Box(loader(k)) } }
}

When we run this, we expect to see our loader called for each key every 5 to 15 minutes, but instead it seems to never call it after the initial call.

Are we confused about how to implement Expiry? We are modeling this after Expiry.creating. We are using Caffeine v3.10.0 (we should upgrade, but Expiry.creating does not appear to be in any released version anyway).

@ben-manes
Copy link
Owner

Offhand it looks fine to me as well. If you could work on a reproducer, e.g. using Ticker to fake time, that might help determine the cause. By chance maybe you are impacted by Kotlin/kotlinx.coroutines#4156?

@glasser
Copy link
Author

glasser commented Aug 1, 2024

Thanks, I will try to do a real reproduction. I don't think it's that bug (we use .await() not .asDeferred()) but it's possible!

@ben-manes
Copy link
Owner

One difference is that expireAfterWrite is reset on both create and update, whereas your translation was for only creation. The equivalent would be to also have expireAfterUpdate return randomDurationNanos, but that doesn't sound related to your observation of the entry never expiring.

You might also be interested in refreshAfterWrite and the coalescing bulkloader example. This way you can hide the latency penalty when a popular item expires by reloading it early and batch those requires for efficiency.

@glasser
Copy link
Author

glasser commented Aug 1, 2024

As is typically the case when somebody opens an issue with a random snippet of unedited code, the problem was not in anything I showed you — or it was sort of there but not really correctly. My caches are all created with values of type Box<V> (so I can store nulls in them), eg Box<Boolean> in this case, but I was implementing Expiry<String, Boolean> instead of Expiry<String, Box<Boolean>>. I'm not exactly sure why things still typechecked, but implementing the correct type seems to lead to it actually working.

@glasser glasser closed this as completed Aug 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants