Skip to content

Cache returns already stale value when refreshing #1337

@gissuebot

Description

@gissuebot

Original issue created by harmathdenes on 2013-03-14 at 05:34 PM


When using a LoadingCache with refreshAfterWrite, if the given CacheLoader.reload() is asynchronous and returns too fast, then the following scenario occurs indeterministically:
0. The value already exist in the cache but is eligible for refresh.

  1. A call to get() starts a refresh, the asynchronous reload() returns a future that is already done when Cache checks this and therefore returns synchronously with the result.
    1a. Meanwhile the future's callback starts to put the new value into the cache.
  2. An immediate second call to get() occurs before 1a. would complete, therefore returns the value described in 0.

The following test case reproduces the problem after some iterations:

import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Assert;
import org.junit.Test;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

public class TooFastRefresh {
    private ListeningExecutorService EXECUTOR = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());

@Test
public void test() throws Exception {
    final AtomicInteger counter = new AtomicInteger();

    LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
    .refreshAfterWrite(10, TimeUnit.MILLISECONDS)
    .build(new CacheLoader<String, Integer>() {
        @Override
        public Integer load(String key) {
            return counter.incrementAndGet();
        }

        @Override
        public ListenableFuture<Integer> reload(final String key, Integer prev) {
            return EXECUTOR.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    return load(key);
                }
            });
        }
    });

    cache.get("");
    for(int i=0; i<50; i++) {
        Thread.sleep(20);
        Integer first = cache.get("");
        Integer second = cache.get("");
        Assert.assertEquals(first, second);
    }

    EXECUTOR.shutdown();
}

}

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions