bboss.org.jgroups.blocks.Cache Maven / Gradle / Ivy
The newest version!
package bboss.org.jgroups.blocks;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import bboss.org.jgroups.annotations.Experimental;
import bboss.org.jgroups.annotations.ManagedAttribute;
import bboss.org.jgroups.annotations.ManagedOperation;
import bboss.org.jgroups.annotations.Unsupported;
import bboss.org.jgroups.logging.Log;
import bboss.org.jgroups.logging.LogFactory;
/**
* Simple cache which maintains keys and value. A reaper can be enabled which periodically evicts expired entries.
* Also, when the cache is configured to be bounded, entries in excess of the max size will be evicted on put().
* @author Bela Ban
* @version $Id: Cache.java,v 1.14 2009/05/13 13:06:54 belaban Exp $
*/
@Experimental
@Unsupported
public class Cache {
private static final Log log=LogFactory.getLog(Cache.class);
private final ConcurrentMap> map=new ConcurrentHashMap>();
private ScheduledThreadPoolExecutor timer=new ScheduledThreadPoolExecutor(1);
private Future task=null;
private final AtomicBoolean is_reaping=new AtomicBoolean(false);
private Set change_listeners=new HashSet();
/** The maximum number of keys, When this value is exceeded we evict older entries, until we drop below this
* mark again. This effectively maintains a bounded cache. A value of 0 means don't bound the cache.
*/
@ManagedAttribute(writable=true)
private int max_num_entries=0;
public int getMaxNumberOfEntries() {
return max_num_entries;
}
public void setMaxNumberOfEntries(int max_num_entries) {
this.max_num_entries=max_num_entries;
}
public void addChangeListener(ChangeListener l) {
change_listeners.add(l);
}
public void removeChangeListener(ChangeListener l) {
change_listeners.remove(l);
}
@ManagedAttribute
public int getSize() {
return map.size();
}
@ManagedAttribute
public boolean isReapingEnabled() {
return task != null && !task.isCancelled();
}
/** Runs the reaper every interval ms, evicts expired items */
@ManagedOperation
public void enableReaping(long interval) {
if(task != null)
task.cancel(false);
task=timer.scheduleWithFixedDelay(new Reaper(), 0, interval, TimeUnit.MILLISECONDS);
}
@ManagedOperation
public void disableReaping() {
if(task != null) {
task.cancel(false);
task=null;
}
}
@ManagedOperation
public void start() {
if(timer == null)
timer=new ScheduledThreadPoolExecutor(1);
}
@ManagedOperation
public void stop() {
if(timer != null)
timer.shutdown();
timer=null;
}
/**
*
* @param key
* @param val
* @param caching_time Number of milliseconds to keep an entry in the cache. -1 means don't cache (if reaping
* is enabled, we'll evict an entry with -1 caching time), 0 means never evict. In the latter case, we can still
* evict an entry with 0 caching time: when we have a bounded cache, we evict in order of insertion no matter
* what the caching time is.
*/
@ManagedOperation
public V put(K key, V val, long caching_time) {
if(log.isTraceEnabled())
log.trace("put(" + key + ", " + val + ", " + caching_time + ")");
Value value=new Value(val, caching_time);
Value retval=map.put(key, value);
if(max_num_entries > 0 && map.size() > max_num_entries) {
boolean rc=is_reaping.compareAndSet(false, true);
if(rc) {
if(log.isTraceEnabled())
log.trace("reaping: max_num_entries=" + max_num_entries + ", size=" + map.size());
timer.execute(new Runnable() {
public void run() {
if(max_num_entries > 0) {
try {
if(map.size() > max_num_entries) {
evict(); // see if we can gracefully evict expired items
}
if(map.size() > max_num_entries) {
// still too many entries: now evict entries based on insertion time: oldest first
int diff=map.size() - max_num_entries; // we have to evict diff entries
SortedMap tmp=new TreeMap();
for(Map.Entry> entry: map.entrySet()) {
tmp.put(entry.getValue().insertion_time, entry.getKey());
}
Collection vals=tmp.values();
for(K k: vals) {
if(diff-- > 0) {
Value v=map.remove(k);
if(log.isTraceEnabled())
log.trace("evicting " + k + ": " + v.value);
}
else
break;
}
}
if(log.isTraceEnabled())
log.trace("done reaping (size=" + map.size() + ")");
}
finally {
is_reaping.set(false);
}
}
}
});
}
}
return retval != null? retval.value : null;
}
@ManagedOperation
public V get(K key) {
if(log.isTraceEnabled())
log.trace("get(" + key + ")");
Value val=map.get(key);
if(val == null)
return null;
if(val.timeout == -1 ||
(val.timeout > 0 && val.timeout < System.currentTimeMillis())) {
map.remove(key);
return null;
}
return val.value;
}
/**
* This method should not be used to add or remove elements ! It was just added because ReplCacheDemo
* requires it for its data model
* @return
*/
public ConcurrentMap> getInternalMap() {
return map;
}
public Value getEntry(K key) {
if(log.isTraceEnabled())
log.trace("getEntry(" + key + ")");
return map.get(key);
}
public V remove(K key) {
if(log.isTraceEnabled())
log.trace("remove(" + key + ")");
Value val=map.remove(key);
return val != null? val.value : null;
}
public Set>> entrySet() {
return map.entrySet();
}
@ManagedOperation
public String toString() {
StringBuilder sb=new StringBuilder();
for(Map.Entry> entry: map.entrySet()) {
Value val=entry.getValue();
sb.append(entry.getKey()).append(": ").append(entry.getValue().getValue());
sb.append(" (expiration_time: ");
long expiration_time=val.getTimeout();
if(expiration_time <= 0)
sb.append(expiration_time);
else {
sb.append(new Date(expiration_time));
}
sb.append(")\n");
}
return sb.toString();
}
public String dump() {
StringBuilder sb=new StringBuilder();
for(Map.Entry> entry: map.entrySet()) {
sb.append(entry.getKey()).append(": ");
V val=entry.getValue().getValue();
if(val != null) {
if(val instanceof byte[])
sb.append(" (" + ((byte[])val).length).append(" bytes)");
else
sb.append(val);
}
sb.append("\n");
}
return sb.toString();
}
private void evict() {
boolean evicted=false;
for(Iterator>> it=map.entrySet().iterator(); it.hasNext();) {
Map.Entry> entry=it.next();
Value val=entry.getValue();
if(val != null) {
if(val.timeout == -1 || (val.timeout > 0 && System.currentTimeMillis() > val.insertion_time + val.timeout)) {
if(log.isTraceEnabled())
log.trace("evicting " + entry.getKey() + ": " + entry.getValue().value);
it.remove();
evicted=true;
}
}
}
if(evicted)
notifyChangeListeners();
}
private void notifyChangeListeners() {
for(ChangeListener l: change_listeners) {
try {
l.changed();
}
catch(Throwable t) {
if(log.isErrorEnabled())
log.error("failed notifying change listener", t);
}
}
}
public static class Value implements Externalizable {
private V value;
private long insertion_time=System.currentTimeMillis();
/** When the value can be reaped (in ms) */
private transient long timeout;
private static final long serialVersionUID=-3445944261826378608L;
public Value(V value, long timeout) {
this.value=value;
this.timeout=timeout;
}
public Value() {
}
public V getValue() {return value;}
public long getInsertionTime() {return insertion_time;}
public long getTimeout() {return timeout;}
public void writeExternal(ObjectOutput out) throws IOException {
out.writeLong(timeout);
out.writeObject(value);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
insertion_time=System.currentTimeMillis();
timeout=in.readLong();
value=(V)in.readObject();
}
}
private class Reaper implements Runnable {
public void run() {
evict();
}
}
public interface ChangeListener {
void changed();
}
}