Monday, December 7, 2009

Thread-safe MapCache with Reaper

Sometimes when writing middle-ware applications there's a need for a simple thread safe parameterized mem map cache with eviction reaper, generally non locking on retrieval and with internal wrapper class. By reading the latest java.util.concurrent api doc there is a rich list of collections built with concurrency in mind, I could not find one with an eviction reaper built-in so today I assembled one and here's a reference implementation I'd like to share.
package com.martin.cache;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Externalizable;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
 
/**
 * Thread safe simple parameterized map cache with eviction reaper, 
 * generally non locking on retrieval + internal <V> wrapper class.
 * @author MartinZ 12/07/2009
 * @param K key type
 * @param V value type
 */ 
public final class MapCache< K , V > {
   
  private class Reaper implements Runnable {
    public void run() {
      evict();
    }
  }

  public interface EvictionListener< K > {
    void evicted( K key );
  }
     
  /////////////////////////////////////////////////////////////////////
  public static final long DEFAULT_EVICT_INTERVAL_MS = (30 * 1000);
  public static final long DEFULT_15MINUTES_CACHE_MS = (15 * 60 * 1000); 
  ///////////////////////////////////////////////////////////////////// 
  
  private final ConcurrentHashMap< K , Value<V> > map;
  private final ScheduledThreadPoolExecutor timer;
  private final HashSet<EvictionListener<K>> listeners;
    private final long interval;
    
  public MapCache() {
      this(DEFAULT_EVICT_INTERVAL_MS);
  }
  
  public MapCache( Long tm ) {
    interval  = (tm==null)?DEFAULT_EVICT_INTERVAL_MS:tm;
    map       = new ConcurrentHashMap<K,Value<V>>();
    timer     = new ScheduledThreadPoolExecutor(1);
    listeners = new HashSet<EvictionListener<K>>();
  }
 
  public void addEvictionListener(EvictionListener<K> l) {
    listeners.add(l);
  }
  public void removeEvictionListener(EvictionListener<K> l) {
    listeners.remove(l);
  }

  public int getSize() {
    return map.size();
  }

  public void clear() {
    map.clear();
  }
  
  public void start() {
    timer.scheduleWithFixedDelay(
       new Reaper(),0,interval,TimeUnit.MILLISECONDS
    );
  }

  public void stop() {
    timer.shutdown();
  }

  /**   
   * @param key != null
   * @param val != null
   * uses default 15 minute time to live since last get   
   */  
  public V put( K key , V val ) {
    return put( key , val , DEFULT_15MINUTES_CACHE_MS );
  }  
  
  /**   
   * @param key != null
   * @param val != null
   * @param time ms to keep an entry alive. 0 = never evict.    
   */  
  public V put( K key , V val , long tm ) {
    if(key == null || val == null ) return null;
    Value<V> value  = new Value<V>( val , tm );
    Value<V> retval = map.put( key , value );
    return (retval != null ? retval.value : null);
  }

  public V get(K key) {
    if(key == null) return null;
    Value<V> val = map.get(key);
    if(val == null) return null;    
    val.refreshTimestamp();
    return val.value;
  }

  public Value<V> getEntry( K key ) {
    if(key == null) return null;
    Value<V> val = map.get(key);
    if(val == null) return null;
    val.refreshTimestamp();
    return val;
  }

  public V remove(K key) {
    Value<V> val = map.remove(key);
    return (val != null ? val.value : null);
  }

  public Set<Map.Entry<K,Value<V>>> entrySet() {
    return map.entrySet();
  }
 
  private void evict() {
    for( Iterator<Map.Entry<K,Value<V>>> it = 
       map.entrySet().iterator(); it.hasNext();) {
       Map.Entry<K,Value<V>> entry = it.next();
       Value<V> val = entry.getValue();
       if(val != null) {
        if( (val.timeout > 0) && 
            (System.currentTimeMillis() > 
              (val.insertion + val.timeout))) {
            it.remove(); // rm from iterator
            notifyListeners( entry.getKey() );
        }
       }
    }
  }

  private void notifyListeners( K key ) {
    for(EvictionListener<K> l: listeners) {
      try { l.evicted(key);
      } catch(Throwable t) { }
    }
  }

  public String toString() {
    StringBuilder sb=new StringBuilder();
    for(Map.Entry<K,Value<V>> entry: map.entrySet()) {
      Value<V> val=entry.getValue();
      sb.append(entry.getKey()).append(": ");
      sb.append(entry.getValue().getValue());
      sb.append(" (expiration tm.ms: ");
      sb.append(val.getTimeout()).append(")\n");
    }
    return sb.toString();
  }  
  
    ///////////////////// internal wrapper ////////////////////  
  public static class Value< V > implements Externalizable {
    private V value;
    private long timeout;
    private transient long insertion=System.currentTimeMillis();
    private static final long serialVersionUID = 1100000000001L;
    
    public Value( V value , long timeout ) {
      this.value   = value;
      this.timeout = timeout;
    }

    public V getValue()            { return value;     }
    public long getTimeout()       { return timeout;   }
    public long getInsertionTime() { return insertion; }
    
    public void refreshTimestamp() {
      if( timeout > 0 )
      insertion=System.currentTimeMillis();
    }
    
    public void writeExternal(ObjectOutput out) 
    throws IOException {
      out.writeLong(timeout);
      out.writeObject(value);
    }

    @SuppressWarnings("unchecked")
    public void readExternal(ObjectInput in) 
    throws IOException,ClassNotFoundException {
      timeout   = in.readLong();
      value     = (V) in.readObject();
      insertion = System.currentTimeMillis();      
    }
  }
}


2 comments:

Frank said...

Martin,

I liked your post, but the code was really tough to read.

regards,
-Frank

Martin Zoldano said...

I agree, the SyntaxHighlighter I normally use choked with [Generics] so I had to use a simpler one. cheers