Skip to content

Refreshing values with a RemovalListener on a BoundedLocalCache with direct executor causes values to be refreshed twice #872

@Hagmar

Description

@Hagmar

Thank you for the great library!

I have a setup where I use a cache to store the result of a network request that can possibly be very slow. Certain user interactions are blocking on having the value, so the goal is to avoid cache misses at all costs. To achieve this, I use a removal listener that checks whether the removed entry was evicted or explicitly removed, and if so calls refresh to reload the entry. The actual size of the cache is very small, so there are no memory concerns with preventing entries from being removed.

I'm having an issue after a recent bump from 3.1.2 to 3.1.3, which I believe to have been introduced by this change: 68fbff8. After that change, the removeNode call causes values to be evicted and refreshed, followed by the subsequent remove call which removes and refreshes values a second time.

Here's an example test that works on version 3.1.2 but not on 3.1.3:

import static org.assertj.core.api.Assertions.assertThat;

import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.junit.jupiter.api.Test;

public class CacheTest {
    private final LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .removalListener(this::listenRemoval)
            .executor(MoreExecutors.directExecutor())
            .recordStats()
            .build(new CacheLoader<>() {
                @Override
                public @Nullable Integer load(Integer _key) {
                    return 1;
                }
            });

    private void listenRemoval(Integer key, Integer _value, RemovalCause cause) {
        // We don't want to reload if the value was just replaced
        if (cause.wasEvicted() || cause == RemovalCause.EXPLICIT) {
            cache.refresh(key);
        }
    }

    @Test
    public void testCacheEvictionRefresh() {
        cache.get(1);
        cache.invalidateAll();
        assertThat(cache.stats().loadCount()).isEqualTo(2);
    }
}

Note that this is only an issue when using a direct executor or very fast async refresh, where the value is refreshed and re-added to the cache between removeNode and remove.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions