Guava CacheBuilder Cache RemovalListener onRemoval no se llama cada vez que se elimina la entrada

I have created a Guava CacheBuilder based cache with on expiry of 5 seconds if key is not written to. Have added a removalListener to it which prints the key/value pair being removed. What I have observed is that the onRemoval method of the listener gets called only the first time. It doesn't get called the second time an entry is removed. (The actual removal happens. Just the onRemoval method of the removalListener doesn't get called).

Am I doing something wrong? Can somebody help? Thanks in advance. Here's my code:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;


public class TestCacheBuilder {

  public static void main(String[] args) {
    try {
      new TestCacheBuilder();
    }catch (Exception e){      
     e.printStackTrace(); 
    }
  }

  public TestCacheBuilder() {

    Cache<String, String> myCache = CacheBuilder.newBuilder()
        .expireAfterWrite(5, TimeUnit.SECONDS)
        .removalListener(new RemovalListener<String, String>() {
          public void onRemoval(RemovalNotification<String, String> removal) {
            System.out.println("removal: "+removal.getKey()+"/"+removal.getValue());
          }          
        })
        .build();


    Map<String, String> inMap = myCache.asMap();

    inMap.put("MyKey", "FirstValue");

    System.out.println("Initial Insert: "+inMap);

    //Wait 16 seconds

    try {
      Thread.sleep(4000);
    } catch(InterruptedException ex) {
        Thread.currentThread().interrupt();
    }

    System.out.println("After 4 seconds: " + inMap);

    inMap.put("MyKey", "SecondValue");

    try {
      Thread.sleep(1000);
    } catch(InterruptedException ex) {
        Thread.currentThread().interrupt();
    }

    System.out.println("After 1 more second: " + inMap);

    try {
      Thread.sleep(4000);
    } catch(InterruptedException ex) {
        Thread.currentThread().interrupt();
    }


    System.out.println("After 4 more seconds: " + inMap);

  }

}

La salida es la siguiente:

Initial Insert: {MyKey=FirstValue}
After 4 seconds: {MyKey=FirstValue}
removal: MyKey/FirstValue
After 1 more second: {MyKey=SecondValue}
After 4 more seconds: {}

preguntado el 28 de agosto de 12 a las 11:08

Aside from the actual question: you can enhance your tests by using the Ticker to simulate a clock that you can manipulate at your will (mire aquí). That will speed up your tests and makes them resilient to subtle timing problems. -

Thanks Pyranja. Will definitely check that out ! -

3 Respuestas

The removal doesn't actually happen directly: Guava does not have its own cleaning thread to remove expired entries. The removals happen usually when there's a write in the same segment of the cache, or during a read when a certain amount of time has passed (to amortize the cost of the removal). However, while the entry is still there, it's seen as expired, which is why it's not printed.

Citando CacheBuilder's javadoc:

Si se solicita expireAfterWrite o expireAfterAccess, las entradas pueden desalojarse en cada modificación de caché, en accesos ocasionales a caché o en llamadas a Cache.cleanUp(). Las entradas caducadas se pueden contar en Cache.size(), pero nunca serán visibles para las operaciones de lectura o escritura.

respondido 09 mar '18, 14:03

Thanks Frank. Cache.cleanUp() seems to do the trick. Just wondering it will have any performance implications if used often. The problem that I am facing is that I need the message when the cache entry gets removed as the system needs to do other things based on the removal. - Kiran

@Kiran it depends on the access pattern of your cache. If it's called a lot, you'll add some contention when running Cache.cleanUp(). - Frank Pavageau

@Kiran: The more busy is your cache, the more clean up happens automatically, so during the rush hours you could probably switch off the manual cleanUp. OTOH, I believe to have seen that a single call to cleanUp did not remove all garbage. If this is really true, maybe you should consult the Guava guys. - maaartinus

The logic is that cleanup happens automatically on occasional cache reads, on every cache write, or whenever Cache.cleanUp() is called. In your tests, I'd go ahead and call cleanUp(), but it shouldn't be necessary in production. - Luis Wassermann

My rule of thumb: call Cache.cleanUp() liberally in tests; but avoid calling Cache.cleanUp() in production. The only exception I'm aware if is extremely low throughput caches with very large objects. After all, if your cache is actually being used, it will be cleaned up fast enough. - freír

As an alternative to Guava Cache puedes usar Cafeína, "a high performance, near optimal caching library based on Java 8".

Tiene "Google Guava inspired API" and performs eviction periodically, so it may fit your use case better (if you're worried about memory not being freed semi-automatically). See Eviction wiki page:

Expiration is performed with periodic maintenance during writes and occasionally during reads. Scheduling and firing of an expiration event is executed in amortized O(1) time.

respondido 11 mar '18, 08:03

If expireAfterWrite or expireAfterAccess is requested, entries may be evicted on each cache modification, on occasional cache accesses, or on calls to Cache.cleanUp(). Expired entries may be counted in Cache.size(), but will never be visible to read or write operations.

In the long run, this will end-up eating all heap memory or lead to memory starvation for other modules which are not using any cache.

Respondido 20 ago 14, 04:08

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.