Skip to content

Unexpected cache misses with expireAfterWrite using multiple keys #30

@ybayk

Description

@ybayk

We just discovered that while observing a lot of cache misses in our logs.
I isolated the problem down to simple unit test. However, it is intermittent, and is not reproducible if is run under gradle testing. So I put multiple attempts inside a loop and also recommend using built-in Eclipse tester to reproduce that. I am also able to reproduce as a simple app with main() on Linux and Mac.

The unit tests are provided below. There are two scenarios. One test that uses only one cache key works fine, another test that uses the same logic but with two keys fails on unexpected cache miss.

package com.thomsonreuters.onep.pito.utils;

import static org.junit.Assert.assertEquals;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

import org.junit.Test;

import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;

public class CaffeineTest {

  private static final int ATTEMPTS = 100;
  private static final int TTL = 100;
  private static final int EPSILON = 10;

  @Test
  public void testWithCoupleOfKeys() throws Exception {
    testExpiration(true);
  }

  @Test
  public void testSameWithSingleKey() throws Exception {
    testExpiration(false);
  }


  private void testExpiration(boolean withPairOfKey) throws Exception {
    final ConcurrentMap<String,String> source = new ConcurrentHashMap<>();
    final ConcurrentMap<String,Date> lastLoad = new ConcurrentHashMap<>();

    class Loader implements CacheLoader<String,String> {

      private void reportCacheMiss(String key) {
        Date now = new Date();
        Date last = lastLoad.get(key);
        System.out.print(new SimpleDateFormat("hh:MM:ss.SSS").format(new Date()) + ": cache miss for key: " + key);
        if (last != null) {
          System.out.println(" -  " + (now.getTime() - last.getTime()) + "ms after last load");
        } else {
          System.out.println();
        }
        lastLoad.put(key, now);
      }

      @Override
      public String load(String key) {
        reportCacheMiss(key);
        return source.get(key);
      }

      @Override
      public CompletableFuture<String> asyncLoad(String key, Executor executor) {
        reportCacheMiss(key);
        return CompletableFuture.completedFuture(source.get(key));
      }
    }    

    for (int i = 0; i < ATTEMPTS; i++) {
      System.out.println("Attempt #" + i);

      //initial value
      source.put("foo", "foo0");
      if (withPairOfKey) {
        source.put("bar", "bar0");
      }

      lastLoad.clear();
      AsyncLoadingCache<String,String> cache = Caffeine.newBuilder()
          .expireAfterWrite(TTL, TimeUnit.MILLISECONDS)
          .buildAsync(new Loader());

      assertEquals("should serve initial value", "foo0", cache.get("foo").get());
      if (withPairOfKey) {
        assertEquals("should serve initial value", "bar0", cache.get("bar").get());
      }

      //first update
      source.put("foo", "foo1");
      if (withPairOfKey) {
        source.put("bar", "bar1");
      }

      assertEquals("should serve cached initial value", "foo0", cache.get("foo").get());
      if (withPairOfKey) {
        assertEquals("should serve cached initial value", "bar0", cache.get("bar").get());
      }
      Thread.sleep(EPSILON); //sleep for less than expiration
      assertEquals("should still serve cached initial value", "foo0", cache.get("foo").get());
      if (withPairOfKey) {
        assertEquals("should still serve cached initial value", "bar0", cache.get("bar").get());
      }
      Thread.sleep(TTL + EPSILON); //sleep until expiration
      assertEquals("should now serve first updated value", "foo1", cache.get("foo").get());
      if (withPairOfKey) {
        assertEquals("should now serve first updated value", "bar1", cache.get("bar").get());
      }

      //second update
      source.put("foo", "foo2");
      if (withPairOfKey) {
        source.put("bar", "bar2");
      }

      assertEquals("should still serve cached first updated value", "foo1", cache.get("foo").get());
      if (withPairOfKey) {
        assertEquals("should still serve cached first updated value", "bar1", cache.get("bar").get());
      }
      Thread.sleep(EPSILON); //sleep for less than expiration
//it never fails here for the first key
      assertEquals("should still serve cached first updated value", "foo1", cache.get("foo").get());
      if (withPairOfKey) {
//it fails here after a few attempts because of cache miss
        assertEquals("should still serve cached first updated value", "bar1", cache.get("bar").get());
      }
    }
  }
}

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