Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does iterating over a Guava LoadingCache's map-view entry set reset the access time used for expiration?

Tags:

java

guava

I have a LoadingCache<K,V> created using a CacheBuilder:

LoadingCache<K,V> myCache = CacheBuilder.newBuilder()
    .expireAfterAccess(1, TimeUnit.MINUTES)
    .maximumSize(500)
    .build(someCacheLoader);

I need to periodically iterate over all of the entries (keys and values) in the cache. I know I can accomplish that using LoadingCache#asMap(), and either:

  • iterate over that map view's Map#entrySet(), or
  • iterate over just the Map#keySet(), and explicitly Map#get(Object) the values.

In the former case:

for (Map.Entry<K, V> entry : myCache.asMap().entrySet()) {
    K key = entry.getKey();
    V value = entry.getValue();

    doSomeWorkOn(key, value);
}

Will this update the access time of every entry in the cache? I've read the JavaDoc for CacheBuilder#expireAfterAccess(long, TimeUnit) pretty closely, but have found it to be ambiguous/unclear in this case:

Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after the entry's creation, the most recent replacement of its value, or its last access. Access time is reset by all cache read and write operations (including Cache.asMap().get(Object) and Cache.asMap().put(K, V)), but not by operations on the collection-views of Cache.asMap.

Clearly, the second way of iterating that I mentioned does reset the access time, but I'd like to know what the behavior is using the first way.

like image 718
Matt Ball Avatar asked Jan 17 '13 03:01

Matt Ball


People also ask

How Does Guava cache work?

The Guava Cache is an incremental cache, in the sense that when you request an object from the cache, it checks to see if it already has the corresponding value for the supplied key. If it does, it simply returns it (assuming it hasn't expired).

How does LoadingCache work?

Interface LoadingCache<K,V> A semi-persistent mapping from keys to values. Values are automatically loaded by the cache, and are stored in the cache until either evicted or manually invalidated. Implementations of this interface are expected to be thread-safe, and can be safely accessed by multiple concurrent threads.

Is Guava cache concurrent?

Guava's cache is built on top of Java 5's ConcurrentHashMap with a default concurrency level of 4. This setting is because that hash table is segmented into multiple smaller tables, so more segments allows for higher concurrency at a cost of a larger memory footprint.

What is a cache loader?

A CacheLoader is an interface which specifies load and loadAll methods with a variety of parameters. CacheLoaders come from JCache, but are a frequently requested feature, so they have been incorporated into the core Ehcache classes and can be configured in ehcache. xml.


1 Answers

I would interpret this:

(...) but not by operations on the collection-views of Cache.asMap

to refer to entrySet, keySet, and values. Those are the three collection views of a Map. Therefore utilizing them should not result in an access.

Here are JUnit (+Mockito) tests that show the behaviour in each case. Reading the value through entrySet or values does not prevent the entry from being removed (nor does reading the key from entrySet or keySet). Reading using asMap().get() does count as an access, as the docs specify.

Setup

private Ticker ticker = Mockito.mock(Ticker.class);

@SuppressWarnings({"unchecked"})
private RemovalListener<String, String> removalListener = Mockito.mock(RemovalListener.class);

private Cache<String, String> cache = CacheBuilder.newBuilder()
            .expireAfterAccess(5, TimeUnit.SECONDS)
            .removalListener(removalListener)
            .ticker(ticker)
            .build();

entrySet

@Test
public void testEntrySetAccessDoesNotCountAsAccess() {
    //write
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(0));
    cache.put("foo", "bar");

    //read
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(4));
    cache.asMap().entrySet().iterator().next().getValue();
    cache.asMap().entrySet().iterator().next().getKey();

    //maintenance
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(6));
    cache.cleanUp();

    verify(removalListener).onRemoval(Mockito.<RemovalNotification<String,String>>any());
}

keySet

@Test
public void testKeySetAccessDoesNotCountAsAccess() {
    //write
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(0));
    cache.put("foo", "bar");

    //read
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(4));
    cache.asMap().keySet().iterator().next();

    //maintenance
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(6));
    cache.cleanUp();

    verify(removalListener).onRemoval(Mockito.<RemovalNotification<String,String>>any());
}

values

@Test
public void testValuesAccessDoesNotCountAsAccess() {
    //write
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(0));
    cache.put("foo", "bar");

    //read
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(4));
    cache.asMap().values().iterator().next();

    //maintenance
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(6));
    cache.cleanUp();

    verify(removalListener).onRemoval(Mockito.<RemovalNotification<String,String>>any());
}

asMap().get()

@Test
public void testMapGetAccessCountsAsAccess() {
    //write
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(0));
    cache.put("foo", "bar");

    //read
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(4));
    cache.asMap().get("foo");

    //maintenance
    when(ticker.read()).thenReturn(TimeUnit.SECONDS.toNanos(6));
    cache.cleanUp();

    verify(removalListener, never()).onRemoval(Mockito.<RemovalNotification<String,String>>any());
}
like image 63
Mark Peters Avatar answered Oct 18 '22 11:10

Mark Peters