Inserting back into cache in RemovalListener.onRemoval

535 views
Skip to first unread message

stev...@exabeam.com

unread,
Jan 16, 2017, 6:12:16 PM1/16/17
to guava-discuss
Hello,

I have a use case where some of the keys in my cache needs different expireAfterWrite timeouts.

So what I'm doing is associating each key with an integer indicating how many time it has expired (essentially an expireAfterWrite multiplier) then in my RemovalListener I check to see if it has truly expired (the value reached 0) and if it hasn't I put it back into the cache. This should guarantee that each key would only expire after some multiple of the expireAfterWrite timeout.

I wrote a short (Scala) test case for it and it seems to work just fine. Any thoughts on this approach and any potential pitfalls? One gotcha that I can think of is that if the multiplier was large enough, it might accrue enough delay to "add one" to the multiplier but that's acceptable for me.

object foo extends App {

 
case class Value(v: Int) {
   
def countdown: Value = copy(v - 1)
 
}

 
private def removalListener: RemovalListener[String, Value] = {
   
new RemovalListener[String, Value] {
     
override def onRemoval(notification: RemovalNotification[String, Value]): Unit = {
        notification
.getCause match {
         
case RemovalCause.EXPIRED =>
            notification
.getValue.countdown match {
             
case Value(0) => println(s"${java.lang.System.currentTimeMillis()} => ${notification.getKey} truly expired")
             
case x@Value(_) => cache.put(notification.getKey, x)
           
}
         
case _ => /* Disregard other forms of removal */
       
}
     
}
   
}
 
}

  val cache
: Cache[String, Value] = CacheBuilder.newBuilder()
   
.expireAfterWrite(1, TimeUnit.SECONDS)
   
.removalListener(removalListener)
   
.build[String, Value]()

  println
(s"START=${java.lang.System.currentTimeMillis()}")
  cache
.put("foo", Value(5))
  cache
.put("bar", Value(1))
 
while (cache.size() != 0) {
    cache
.cleanUp()
   
Thread sleep 1000
 
}
  println
(s"END=${java.lang.System.currentTimeMillis()}")
}

Thanks!

Ben Manes

unread,
Jan 16, 2017, 6:30:04 PM1/16/17
to guava-discuss, stev...@exabeam.com
There is a potential visibility race, which may be fine but you should be aware of.

The removalListener is called after the hash table has been updated and is not protected by any exclusive read or write locks. This means that between the hash table removal and the listener's write, the entry will not be present. A thread may observe this and whether that is benign depends on your usage. This will be seen when a reader accesses an expired entry and a not found response is retuned. While Guava will clean the entry up and call your listener, it already determined the result and will return the cache miss.

Guava's Cache wasn't designed for variable expiration or users to override its decision of what to evict. That was mostly because a cache is transient, volatile, recomputable data so it tended to not matter too much. There are workarounds, but they can be a little ugly due to that design expectation.

Cheers.

stev...@exabeam.com

unread,
Jan 16, 2017, 6:34:11 PM1/16/17
to guava-discuss, stev...@exabeam.com
Ben, this is harmless in my use case but good to know! Thanks!


On Monday, January 16, 2017 at 3:12:16 PM UTC-8, stev...@exabeam.com wrote:
Reply all
Reply to author
Forward
0 new messages