javacaffeine

Caffeine java cache - how to load new value first and then return new one instead of old value, if refreshAfterWrite expired


I'm trying to use caffeine cache and have a problem:

Let's say cache is empty and I query for a value, it uses loader and loads a new value into the cache, after 2 days have passed I query for the same value and I get OLD value first then refresh is initiated on a separate thread and the new value is loaded if loading is possible.

Caffeine.newBuilder()
        .refreshAfterWrite(5, TimeUnit.MINUTES)
        .expireAfterWrite(3, TimeUnit.DAYS)
        .build(loader);

What I want to archive is - try to refresh first and return the new value first if possible, if something goes wrong only then return the old one. How can I archive this? How to implement simple, future proof, neat implementation, without workarounds? That would be awesome!

Edit: Would this solution work properly?

boolean needsSyncRefresh = cache.policy().expireAfterWrite()
                .flatMap(stringBigDecimalExpiration -> stringBigDecimalExpiration.ageOf(key))
                .filter(age -> age.toMinutes() < 0 || age.toMinutes() > REFRESH_AFTER_MINUTES)
                .isPresent();

V value = cache.get(key);

if (needsSyncRefresh) {
    return cache.asMap().computeIfPresent(key, (k, oldValue) -> {
        if (oldValue.equals(value)) {
            try {
                return loader(key);
            } catch (Exception e) {
                //handle error
            }
        }
        return oldValue;
    });
}
return value;

Solution

  • I don't think using CacheWriter is necessary here. Instead inspect the metadata to determine if a refresh is needed.

    The simplest might be to run a scheduled task that triggers a refresh early. For example,

    var policy = cache.policy().expireAfterWrite().get();
    for (var key : cache.asMap().keySet()) {
      boolean refresh = policy.ageOf(key)
          .filter(age -> age.toDays() >= 2)
          .isPresent();
      if (refresh) {
        cache.refresh(key);
      }
    }
    

    A slightly more complicated version does this at retrieval by checking if the retrieved value is stale and performing a new computation, e.g.

    V value = cache.get(key, this::loadValue);
    
    var policy = cache.policy().expireAfterWrite().get();
    boolean refresh = policy.ageOf(key)
        .filter(age -> age.toDays() >= 2)
        .isAbsent();
    if (!refresh) {
      return value;
    }
    return cache.asMap().compute((key, oldValue) -> {
      if ((oldValue != null) && (oldValue != value)) {
        return oldValue; // reloaded by another thread
      }
      try {
        return loadValue(key);
      } catch (Exception e) {
        logger.error("Failed to load {}", key, e);
        return oldValue;
      }
    });