2

I'm trying to use caffeine and spring-boot-starter-cache to implement the following caching logic:

  1. If expiration time has passed and condition (That requires computation and I/O) is evaluated to TRUE then force fetch the data and update cache.
  2. If expiration time has passed and condition (That requires computation and I/O) is evaluated to FALSE then don't invalidate the cache data and retrieve the value from cache.
  3. If expiration time has not passed then retrieve the value from cache.

I worked according to this guide: https://www.baeldung.com/spring-boot-caffeine-cache

I tried all sort of approaches using @CachePut, @CacheEvict and @Cacheable on the getter method of the object I;m caching but the core issue is that I need to condition the eviction with both an expiration time and another logic but these annotations cannot control whether to evict or not... Perhaps this can be done using a Scheduler?

Eyal Ringort
  • 601
  • 6
  • 19

2 Answers2

3

I think you are looking for refreshAfterWrite and override CacheLoader.reload(K, V). Here is the post that explain the details: https://github.com/ben-manes/caffeine/wiki/Refresh

Implementation for your case would be something like:

@Log4j2
@Configuration
public class CacheConfig {

    @Bean
    public Cache<String,Item> caffeineConfig() {
        return Caffeine.newBuilder()
            .refreshAfterWrite(10, TimeUnit.SECONDS)
            .build(new ConditionalCacheLoader<>(this::shouldReload, this::load));
    }

    private Item load(String key){
        //load the item
        return null;
    }

    private boolean shouldReload(Item oldValue){
        //your condition logic here
        return true;
    }


    private static class Item{
         // the item value can contain any data
    }

    private static class ConditionalCacheLoader<K,V> implements CacheLoader<K,V>{
        private final Predicate<V> shouldReload;

        private final Function<K,V> load;

        protected ConditionalCacheLoader(Predicate<V> shouldReload, Function<K, V> load) {
            this.shouldReload = shouldReload;
            this.load = load;
        }

        @Override
        public V load(K key) throws Exception{
            return load.apply(key);
        }

        @Override
        public V reload(K key, V oldValue) throws Exception {
            if (shouldReload.test(oldValue)){
                return load(key);
            }else {
                return oldValue;
            }
        }
    }
}
1

It seems that you are not using caffeine as a cache system.

In this case using a custom class and holding data in simple Map will be better because it gives you a lot more flexibility.

Here is a skeleton

public class SpecialCache {
    private Map<String, SpecialCache.Entry> map = new HashMap<>();

    private boolean specialCondition(String key) {
        ...
    }

    private Object fetch(String key) {
       ...
    }


    public Object get(String key) {
        SpecialCache.Entry entry = map.get(key);
        if (entry.getExpiringEpoch() > System.currentTimeMillis()) {
            if (specialCondition(key)) {
                Object data = fetch(key);
                entry.setExpiringEpoch(...);
                entry.setData(data);
                return data;
            } else {
                return entry.getData();
            } 
        } else {
            return entry.getData();
        }
    }

    @Data
    public static class Entry {
        private long expiringEpoch;
        private Object data;
    }
}

IN the example I added specialCondition and fetch methods in the cache. You can also pass the methods as lambda functions to the get method to have more flexibility.

The code must be completed with, for example you need to add:

  • check for non existing keys in the cache
  • method to populate the cache (a put? )
Davide Lorenzo MARINO
  • 26,420
  • 4
  • 39
  • 56