I'm using a Caffeine cache with the following configuration:
datesCache = Caffeine.newBuilder()
.maximumSize(1000L)
.expireAfterWrite(1, TimeUnit.HOURS)
.writer(new CacheWriter<String, Map<String, List<ZonedDateTime>>>() {
@Override
public void write(@NonNull final String key, @NonNull final Map<String, List<ZonedDateTime>> datesList) {
CompletableFuture.runAsync(() -> copyToDatabase(key, datesList), Executors.newCachedThreadPool());
}
@Override
public void delete(@NonNull final String key, @Nullable final Map<String, List<ZonedDateTime>> datesList,
@NonNull final RemovalCause removalCause) {
System.out.println("Cache key " + key + " got evicted from due to " + removalCause);
}
})
.scheduler(Scheduler.forScheduledExecutorService(Executors.newSingleThreadScheduledExecutor()))
.removalListener((key, dateList, removalCause) -> {
LOG.info("Refreshing cache key {}.", key);
restoreKeys(key);
})
.build();
I'm using a CacheWriter
to copy the records to distributed database upon writes to the cache if the values satisfy certain conditions. Also, upon eviction I'm using a RemovalListener
to call a backend service with the evicted key to keep the records up-to-date.
In order to make this work, I also had to initialize the cache upon booting up the service and I use the put
method to insert the values in the cache. I retrieve values from the cache using datesCache.get(key, datesList -> callBackendService(key))
just in case the request is for a key that I didn't get upon initialization.
The API that leverages this cache has periods of very heavy use, and it seems that for some reason the records were getting evicted (in every request?) because the code in the RemovalListener
and the CacheWriter
got executed every few milliseconds, eventually creating over 25,000 threads and making the service error out.
Can anybody tell me if I'm doing something deadly wrong? Or something painful obvious that is wrong? I feel like I'm deeply misunderstanding Caffeine.
The goal is to have the records in the cache refresh every 1 hr and upon refresh, get the new values from the backend API and persist them in a database if they satisfy certain conditions.
The cache entered in an infinite loop because the RemovalListener
is async and therefore, upon heavy load, the cache values were being replaced by a request to an expired key before the RemovalListener
could actually refresh the cache.
Therefore the values will:
REPLACED
removal causeRemovalListener
Solution:
Evaluate the RemovalCause
in the RemovalListener
to ignore REPLACED
keys. Method wasEvicted()
can be used or compare the enum values themselves.