ben-manes / caffeine

A high performance caching library for Java
Apache License 2.0
15.8k stars 1.59k forks source link

Cache access with individual "max-age" values? #400

Closed lathspell closed 4 years ago

lathspell commented 4 years ago

I like to provide a Cache that works like the HTTP Header "Cache-Control: max-age=30" which would use cached results provided that they are at most 30s old and creates a new result.

So far I couldn't find something like cache.get(key, maxAge) in the API.

An alternative would be a getter that returns the cache entry with not only the object but also its meta information so that we could write

Cache<Foo> entry = cache.getEntry(key)
if (entry.insertedAt > someTimestamp) { 
    return entry.object;
} else {
    return cache.get(key);   // create entry, store it into cache and then return it 
}
ben-manes commented 4 years ago

You can set a custom expiration policy, e.g. https://github.com/ben-manes/caffeine/wiki/Eviction#time-based

If set, some additional functionality is provided through cache.policy().

Does that solve your problems?

lathspell commented 4 years ago

I don't think so. The policy is for the whole cache object whereas I was looking for a maxAge parameter per access.

I'd like to implement a REST endpoint that servers usually cached data but if the clients want, they can set a "Cache-Control: max-age=42" header which I would parse and then only serve from cache if the cached object is there no longer than 42 seconds. Else I would refresh the cache.

Pseudocode:

public class LoadingCache {
   public V getWithMaxAge(K key, long maxAge) {
       val cached = get(key)
       if (cached.insertedAt.isAfter(LocalDateTime.now().minusSeconds(maxAge))) {
            return cached.data
        } else {
            refresh(key)
            return get(key).data
        }
   }
}

.... client ...

val cache = Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build {...}

val maybeOld = cache.get(key)                 // at most 30 min cached (default)
val fresher = cache.getWithMaxAge(key, 300)   // at most 5 min cached
val recent = cache.getWithMaxAge(key, 0)      // not cached at all 
ben-manes commented 4 years ago

The policy can be per-entry if using expireAfter(Expiry), but for calculating the new duration after an operation. In that case you might read, check if it was expired, if not then calculate a new duration, and return the value. It makes sense when the external resource dictates the lifetime to abide to, rather than each caller.

In your case I guess you can do this similar to your example logic,

val value = cache.getIfPresent(key)
if (value == null) {
  return cache.get(key)
} else if (value.hasExpired(maxAge)) {
  cache.asMap().remove(key, value)
  return cache.get(key)
} else {
  return value
}
ben-manes commented 4 years ago

Note that max-age is a response header, so that fits within the expireAfter(expiry) model we support.

You might consider to set a different header, like min-fresh or max-stale, if you want client-side control over the server's caching behavior. However that can be problematic if a client is always invalidating the cache by a hostile setting, causing a cache stampede. Usually you want the resource owner to dictate, so the Expiry callback works well for those cases.

lathspell commented 4 years ago

According to the MDN page you linked "max-age" can be used in both, the request or the response!

"min-fresh"/"max-stale" have a slightly different meaning. I do not know if my cache entry is "fresh" i.e. unmodified until I retrieve it from the original source to compare it but it's that retrieval I want to minimize using a cache.

I use something like the workaround that you suggested. Just thought that it would be a nice addition if you don't know what new features you could add :)

ben-manes commented 4 years ago

Oh you're right, it is request and response 😄

Do you think there is anything smarter we could do within the cache? I think we could only perform that workaround ourselves, which means it is not super helpful. A expiration-only method would have to live under cache.policy().expireVariably(), which resolves through an Optional and loses context of being a loading cache. I think doing this as an extension in user code, like you showed, is the cleanest.

lathspell commented 4 years ago

It would be helpful if the API would at least allow the user to query the insertion-date of an object.

In my workaround I currently I do not store the object itself but a wrapper class that contains just a date and the actual data. But for the expire polices you must alreay have that date somewhere in the internal data structues.

ben-manes commented 4 years ago

We do, but it's not super friendly.

Optional<Duration> duration = cache.policy().expireVariably()
    .flatMap(policy -> policy.getExpiresAfter(key));
ben-manes commented 4 years ago

Closing because I think there is nothing more intelligent that we can offer, but please reopen if you have ideas or requests! 😄