vishal
vishal

Reputation: 71

Use old/stale cache value if cache refresh is failed using caffine with spring

I have a requirement to use old/stale cache value if cache refresh is failed. I have gone through the many examples on internet and decided to try with caffeine cache to implement this requirements(more specifically using refreshAfterWrite feature). I am able to cache the data using caffeine but not able to get old/stale value when cache refresh(rest api is failed) is failed. I have used SimpleCacheManager(org.springframework.cache.support.SimpleCacheManager) and CaffeineCache.

**simpleCacheManager bean code** :

    <bean id="simpleCacheManager" class="org.springframework.cache.support.SimpleCacheManager">
        <property name="caches" ref="caffeineCaches" />
    </bean>

**caffeine cache** :

    @Bean
    public Collection<? extends org.springframework.cache.Cache> caffeineCaches(){
        Collection<CaffeineCache> caffeineCaches = new ArrayList<>();
        caffeineCaches.add(new CaffeineCache("pbdBankRegistry",
                Caffeine.newBuilder()
                        .refreshAfterWrite(2, TimeUnit.MINUTES)
                        .initialCapacity(1)
                        .maximumSize(2)
                        .recordStats()
                        .build(new CustomCacheLoader()),false));
        return caffeineCaches;
    }


  **CacheLoader Code :**

    public class CustomCacheLoader implements CacheLoader<Object, Object> {
    @Nullable
    @Override
    public Object load(Object o) throws Exception {
        return null;
    }

    @Override
    public Map loadAll(Set keys) throws Exception {
        return CacheLoader.super.loadAll(keys);
    }

    @Override
    public CompletableFuture asyncLoad(Object key, Executor executor) throws Exception {
        return CacheLoader.super.asyncLoad(key, executor);
    }

    @Override
    public CompletableFuture<? extends Map> asyncLoadAll(Set keys, Executor executor) throws Exception {
        return CacheLoader.super.asyncLoadAll(keys, executor);
    }

    @Nullable
    @Override
    public Object reload(Object key, Object oldValue) throws Exception {
        return CacheLoader.super.reload(key, oldValue);
    }

    @Override
    public CompletableFuture asyncReload(Object key, Object oldValue, Executor executor) throws Exception {
        return CacheLoader.super.asyncReload(key, oldValue, executor);
    }

I think i have to call my api from reload method or from build method of Caffeine. but i am not sure. I am new with the cache so here need your help to implement this code.

Note : My application is on spring 4.3.3.RELEASE version.

Upvotes: 2

Views: 1826

Answers (1)

phhu
phhu

Reputation: 1982

Something like the below worked for me in a similar case: rather than supplying a Function to .build as per the examples at https://github.com/ben-manes/caffeine/wiki/Refresh, pass in an object that implements CacheLoader, and override both the load and reload methods.

/**
 * @see https://www.javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/latest/com.github.benmanes.caffeine/com/github/benmanes/caffeine/cache/CacheLoader.html#reload(K,V)
 */ 
class CacheLoader_keepOldIf<K,V> implements CacheLoader<K,V> {
  private Function<K,V> loadFn;
  private Function<V,Boolean> testNewFn;
  CacheLoader_keepOldIf(
    Function<V,Boolean> testNewFn, 
    Function<K,V> loadFn
  ){
    this.loadFn = loadFn;
    this.testNewFn = testNewFn; 
  }
  @Override
  public @Nullable V reload(K key, V oldValue) throws Exception {
    V newValue = loadFn.apply(key);
    return testNewFn.apply(newValue) ? oldValue : newValue;
  } 
  @Override
  public @Nullable V load(K key) throws Exception {
    return loadFn.apply(key);
  }
} 

LoadingCache<String, Object> myCache = Caffeine.newBuilder()
    .refreshAfterWrite(5, TimeUnit.SECONDS) 
    .build(new CacheLoader_keepOldIf<String,Object>(
        newValue -> newValue==null, 
        key -> someExpensiveCallReturningNullOnFailure()
    ))
;

This code seems needlessly verbose just to override the reload method: some of this is doubtless my fault, and some possibly Java's. But it did the job: if my DB call fails, it returns null, and the previously cached value is used. It would presumably also be possible to specify a default value in the load method in case the first call to populate the cache fails.

(As per Ben Manes' comment on the question, I think the override of load in the example in the question should return something instead of null to fix the original issue).

Upvotes: 0

Related Questions