Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Cache refreshing obsolete values

In a Spring-based application I have a service which performs the calculation of some Index. Index is relatively expensive to calculate (say, 1s) but relatively cheap to check for actuality (say, 20ms). Actual code does not matter, it goes along the following lines:

public Index getIndex() {
    return calculateIndex();
}

public Index calculateIndex() {
    // 1 second or more
}

public boolean isIndexActual(Index index) {
    // 20ms or less
}

I'm using Spring Cache to cache the calculated index via @Cacheable annotation:

@Cacheable(cacheNames = CacheConfiguration.INDEX_CACHE_NAME)
public Index getIndex() {
    return calculateIndex();
}

We currently configure GuavaCache as cache implementation:

@Bean
public Cache indexCache() {
    return new GuavaCache(INDEX_CACHE_NAME, CacheBuilder.newBuilder()
            .expireAfterWrite(indexCacheExpireAfterWriteSeconds, TimeUnit.SECONDS)
            .build());
}

@Bean
public CacheManager indexCacheManager(List<Cache> caches) {
    SimpleCacheManager cacheManager = new SimpleCacheManager();
    cacheManager.setCaches(caches);
    return cacheManager;
}

What I also need is to check if cached value is still actual and refresh it (ideally asynchronously) if it is not. So ideally it should go as follows:

  • When getIndex() is called, Spring checks if there is a value in the cache.
    • If not, new value is loaded via calculateIndex() and stored in the cache
    • If yes, the existing value is checked for actuality via isIndexActual(...).
      • If old value is actual, it is returned.
      • If old value is not actual, it is returned, but removed from the cache and loading of the new value is triggered as well.

Basically I want to serve the value from the cache very fast (even if it is obsolete) but also trigger refreshing right away.

What I've got working so far is checking for actuality and eviction:

@Cacheable(cacheNames = INDEX_CACHE_NAME)
@CacheEvict(cacheNames = INDEX_CACHE_NAME, condition = "target.isObsolete(#result)")
public Index getIndex() {
    return calculateIndex();
}

This checks triggers eviction if the result is obsolete and returns the old value immediately even if it is the case. But this does not refresh the value in the cache.

Is there a way to configure Spring Cache to actively refresh obsolete values after eviction?

Update

Here's a MCVE.

public static class Index {

    private final long timestamp;

    public Index(long timestamp) {
        this.timestamp = timestamp;
    }

    public long getTimestamp() {
        return timestamp;
    }
}

public interface IndexCalculator {
    public Index calculateIndex();

    public long getCurrentTimestamp();
}

@Service
public static class IndexService {
    @Autowired
    private IndexCalculator indexCalculator;

    @Cacheable(cacheNames = "index")
    @CacheEvict(cacheNames = "index", condition = "target.isObsolete(#result)")
    public Index getIndex() {
        return indexCalculator.calculateIndex();
    }

    public boolean isObsolete(Index index) {
        long indexTimestamp = index.getTimestamp();
        long currentTimestamp = indexCalculator.getCurrentTimestamp();
        if (index == null || indexTimestamp < currentTimestamp) {
            return true;
        } else {
            return false;
        }
    }
}

Now the test:

@Test
public void test() {
    final Index index100 = new Index(100);
    final Index index200 = new Index(200);

    when(indexCalculator.calculateIndex()).thenReturn(index100);
    when(indexCalculator.getCurrentTimestamp()).thenReturn(100L);
    assertThat(indexService.getIndex()).isSameAs(index100);
    verify(indexCalculator).calculateIndex();
    verify(indexCalculator).getCurrentTimestamp();

    when(indexCalculator.getCurrentTimestamp()).thenReturn(200L);
    when(indexCalculator.calculateIndex()).thenReturn(index200);
    assertThat(indexService.getIndex()).isSameAs(index100);
    verify(indexCalculator, times(2)).getCurrentTimestamp();
    // I'd like to see indexCalculator.calculateIndex() called after
    // indexService.getIndex() returns the old value but it does not happen
    // verify(indexCalculator, times(2)).calculateIndex();


    assertThat(indexService.getIndex()).isSameAs(index200);
    // Instead, indexCalculator.calculateIndex() os called on
    // the next call to indexService.getIndex()
    // I'd like to have it earlier
    verify(indexCalculator, times(2)).calculateIndex();
    verify(indexCalculator, times(3)).getCurrentTimestamp();
    verifyNoMoreInteractions(indexCalculator);
}

I'd like to have the value refreshed shortly after it was evicted from the cache. At the moment it is refreshed on the next call of getIndex() first. If the value would have been refreshed right after eviction, this would save me 1s later on.

I've tried @CachePut, but it also does not get me the desired effect. The value is refreshed, but the method is always executed, no matter what condition or unless are.

The only way I see at the moment is to call getIndex() twice(second time async/non-blocking). But that's kind of stupid.

like image 274
lexicore Avatar asked Feb 20 '17 11:02

lexicore


3 Answers

I would say the easiest way of doing what you need is to create a custom Aspect which will do all the magic transparently and which can be reused in more places.

So assuming you have spring-aop and aspectj dependencies on your class path the following aspect will do the trick.

@Aspect
@Component
public class IndexEvictorAspect {

    @Autowired
    private Cache cache;

    @Autowired
    private IndexService indexService;

    private final ReentrantLock lock = new ReentrantLock();

    @AfterReturning(pointcut="hello.IndexService.getIndex()", returning="index")
    public void afterGetIndex(Object index) {
        if(indexService.isObsolete((Index) index) && lock.tryLock()){
            try {
                Index newIndex = indexService.calculateIndex();
                cache.put(SimpleKey.EMPTY, newIndex);
            } finally {
                lock.unlock();
            }
        }
    }
}

Several things to note

  1. As your getIndex() method does not have a parameters it is stored in the cache for key SimpleKey.EMPTY
  2. The code assumes that IndexService is in the hello package.
like image 139
Babl Avatar answered Oct 02 '22 17:10

Babl


Something like the following could refresh the cache in the desired way and keep the implementation simple and straightforward.

There is nothing wrong about writing clear and simple code, provided it satisfies the requirements.

@Service
public static class IndexService {
    @Autowired
    private IndexCalculator indexCalculator;

    public Index getIndex() {
        Index cachedIndex = getCachedIndex();

        if (isObsolete(cachedIndex)) {
            evictCache();
            asyncRefreshCache();
        }

        return cachedIndex;
    }

    @Cacheable(cacheNames = "index")
    public Index getCachedIndex() {
        return indexCalculator.calculateIndex();
    }

    public void asyncRefreshCache() {
        CompletableFuture.runAsync(this::getCachedIndex);
    }

    @CacheEvict(cacheNames = "index")
    public void evictCache() { }

    public boolean isObsolete(Index index) {
        long indexTimestamp = index.getTimestamp();
        long currentTimestamp = indexCalculator.getCurrentTimestamp();

        if (index == null || indexTimestamp < currentTimestamp) {
            return true;
        } else {
            return false;
        }
    }
}
like image 26
Thomas Kabassis Avatar answered Oct 02 '22 16:10

Thomas Kabassis


EDIT1:

The caching abstraction based on @Cacheable and @CacheEvict will not work in this case. Those behaviour is following: during @Cacheable call if the value is in cache - return value from the cache, otherwise compute and put into cache and then return; during @CacheEvict the value is removed from the cache, so from this moment there is no value in cache, and thus the first incoming call on @Cacheable will force the recalculation and putting into cache. The use @CacheEvict(condition="") will only do the check on condition wether to remove from cache value during this call based on this condition. So after each invalidation the @Cacheable method will run this heavyweight routine to populate cache.

to have the value beign stored in the cache manager, and updated asynchronously, I would propose to reuse following routine:

@Inject
@Qualifier("my-configured-caching")
private Cache cache; 
private ReentrantLock lock = new ReentrantLock();

public Index getIndex() {
    synchronized (this) {
        Index storedCache = cache.get("singleKey_Or_AnythingYouWant", Index.class); 
        if (storedCache == null ) {
             this.lock.lock();
             storedCache = indexCalculator.calculateIndex();
             this.cache.put("singleKey_Or_AnythingYouWant",  storedCache);
             this.lock.unlock();
         }
    }
    if (isObsolete(storedCache)) {
         if (!lock.isLocked()) {
              lock.lock();
              this.asyncUpgrade()
         }
    }
    return storedCache;
}

The first construction is sycnhronized, just to block all the upcoming calls to wait until the first call populates cache.

then the system checks wether the cache should be regenerated. if yes, single call for asynchronous update of the value is called, and the current thread is returning the cached value. upcoming call once the cache is in state of recalculation will simply return the most recent value from the cache. and so on.

with solution like this you will be able to reuse huge volumes of memory, of lets say hazelcast cache manager, as well as multiple key-based cache storage and keep your complex logic of cache actualization and eviction.

OR IF you like the @Cacheable annotations, you can do this following way:

@Cacheable(cacheNames = "index", sync = true)
public Index getCachedIndex() {
    return new Index();
}

@CachePut(cacheNames = "index")
public Index putIntoCache() {
    return new Index();
}

public Index getIndex() {
    Index latestIndex = getCachedIndex();

    if (isObsolete(latestIndex)) {
        recalculateCache();
    }

    return latestIndex;
}

private ReentrantLock lock = new ReentrantLock();

@Async
public void recalculateCache() {
    if (!lock.isLocked()) {
        lock.lock();
        putIntoCache();
        lock.unlock();
    }
}

Which is almost the same, as above, but reuses spring's Caching annotation abstraction.

ORIGINAL: Why you are trying to resolve this via caching? If this is simple value (not key-based, you can organize your code in simpler manner, keeping in mind that spring service is singleton by default)

Something like that:

@Service
public static class IndexService {
    @Autowired
    private IndexCalculator indexCalculator;

    private Index storedCache; 
    private ReentrantLock lock = new ReentrantLock();

    public Index getIndex() {
        if (storedCache == null ) {
             synchronized (this) {
                 this.lock.lock();
                 Index result = indexCalculator.calculateIndex();
                 this.storedCache = result;
                 this.lock.unlock();
             }
        }
        if (isObsolete()) {
             if (!lock.isLocked()) {
                  lock.lock();
                  this.asyncUpgrade()
             }
        }
        return storedCache;
    }

    @Async
    public void asyncUpgrade() {
        Index result = indexCalculator.calculateIndex();
        synchronized (this) {
             this.storedCache = result;
        }
        this.lock.unlock();
    }

    public boolean isObsolete() {
        long currentTimestamp = indexCalculator.getCurrentTimestamp();
        if (storedCache == null || storedCache.getTimestamp() < currentTimestamp) {
            return true;
        } else {
            return false;
        }
    }
}

i.e. first call is synchronized and you have to wait until the results are populated. Then if stored value is obsolete the system will perform asynchronous update of the value, but the current thread will receive the stored "cached" value.

I had also introduced the reentrant lock to restrict single upgrade of stored index at time.

like image 35
Ilya Dyoshin Avatar answered Oct 02 '22 17:10

Ilya Dyoshin