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.
- If not, new value is loaded via
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.
4 Answers
Answers 1
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
- As your
getIndex()
method does not have a parameters it is stored in the cache for keySimpleKey.EMPTY
- The code assumes that IndexService is in the
hello
package.
Answers 2
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; } } }
Answers 3
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.
Answers 4
I would use a Guava LoadingCache in your index service, like shown in the code sample below:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(1, TimeUnit.MINUTES) .build( new CacheLoader<Key, Graph>() { public Graph load(Key key) { // no checked exception return getGraphFromDatabase(key); } public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) { if (neverNeedsRefresh(key)) { return Futures.immediateFuture(prevGraph); } else { // asynchronous! ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() { public Graph call() { return getGraphFromDatabase(key); } }); executor.execute(task); return task; } } });
You can create an async reloading cache loader by calling Guava's method:
public abstract class CacheLoader<K, V> { ... public static <K, V> CacheLoader<K, V> asyncReloading( final CacheLoader<K, V> loader, final Executor executor) { ... } }
The trick is to run the reload operation in a separate thread, using a ThreadPoolExecutor for example:
- On first call, the cache is populated by the load() method, thus it may take some time to answer,
- On subsequent calls, when the value needs to be refreshed, it's being computed asynchronously while still serving the stale value. It will serve the updated value once the refresh has completed.
0 comments:
Post a Comment