Search code examples
javacachingjava-8caffeine-cache

Caffeine combining both scheduler and executor service


I am using caffeine in the following configuration:

    Cache<String, String> cache = Caffeine.newBuilder()
                .executor(newWorkStealingPool(15))
                .scheduler(createScheduler())
                .expireAfterWrite(10, TimeUnit.SECONDS)
                .maximumSize(MAXIMUM_CACHE_SIZE)
                .removalListener(this::onRemoval)
                .build();


    private Scheduler createScheduler() {
        return forScheduledExecutorService(newSingleThreadScheduledExecutor());
    }

will I be correct to assume that onRemoval method will be executed on the newWorkStealingPool(15) ForkJoinPool, and the scheduler will be invoked only to find the expired entries that needs to be evicted?

meaning it will go something like this:

  1. single thread scheduler is invoked (every ~ 1 second)
  2. find all the expired entries to be evicted
  3. execute onRemoval for each of the evicted entries in the newWorkStealingPool(15) define in the cache builder?

I didn't found documentation that explains this behavior, so I am asking here

Tnx


Solution

  • Your assumption is close, except that it is slightly more optimized in practice.

    1. Cache reads and writes are performed on the underlying hash table and appended to internal ring buffers.
    2. When the buffers reach thresholds then a task is submitted to Caffeine.executor to call Cache.cleanUp.
    3. When this maintenance cycle runs (under a lock),
      • The buffers are drained and the events replayed against the eviction policies (e.g. LRU reordering)
      • Any evictable entry is discarded and a task is submitted to Caffeine.executor to call RemovalListener.onRemoval.
      • The duration until the next entry will expire is calculated and submitted to the scheduler. This is guarded by a pacer so avoid excessive scheduling by ensuring that ~1s occurs between scheduled tasks.
    4. When the scheduler runs, a task is submitted to Caffeine.executor to call Cache.cleanUp (see #3).

    The scheduler does the minimal amount of work and any processing is deferred to the executor. That maintenance work is cheap due to using O(1) algorithms so it may occur often based on the usage activity. It is optimized for small batches of work, so the enforced ~1s delay between scheduled calls helps capture more work per invocation. If the next expiration event is in the distant future then the scheduler won't run until then, though calling threads may trigger a maintenance cycle due to their activity on the cache (see #1,2).