package com.socialize.cache;
import com.socialize.log.SocializeLogger;
import java.util.*;
/**
* Simple cache object backed by a synchronized map which allows a TTL (Time To Live) for objects in cache.
* @author Jason Polites
*/
public class TTLCache<K extends Comparable<K>, E extends ICacheable<K>> {
public static int DEFAULT_CACHE_COUNT = 1000;
private TreeMap<Key<K>, TTLObject<K, E>> objects;
private Map<K, Key<K>> keys;
private SocializeLogger logger;
private boolean debug = false;
private boolean extendOnGet = true;
private long reapCycle = 60000L; // 1 minute
private long defaultTTL = 60 * 60 * 1000; // 1 hour
private int maxCapacity = -1;
private long maxCapacityBytes = -1;
private long currentSizeInBytes = 0;
/**
* If true the cache will not accept new additions if the max bytes would be exceeded.
* If false, over sized objects are removed during reap.
*/
private boolean hardByteLimit = false;
private ICacheEventListener<K, E> eventListener;
protected ICacheableFactory<K, E> objectFactory;
private static Timer reapTimer;
private Reaper reaper;
private boolean reaping = false;
protected class Reaper extends TimerTask {
public void run() {
reap();
}
}
public TTLCache() {
this(10, DEFAULT_CACHE_COUNT);
}
public TTLCache(int initialCapacity) {
this(initialCapacity, DEFAULT_CACHE_COUNT);
}
public TTLCache(int initialCapacity, int maxCapacity) {
super();
this.maxCapacity = maxCapacity;
objects = makeMap();
keys = new HashMap<K, Key<K>>(initialCapacity);
// Start reaper
startReaper();
}
/**
* Empties the cache and destroys all persistent states.
*/
public void destroy() {
stopReaper();
clear(true);
}
/**
* Empties the case and should remove any persistent states.
*/
public void clear() {
clear(false);
}
public void pause() {
stopReaper();
}
public void resume() {
startReaper();
}
protected synchronized void clear(boolean destroy) {
keys.clear();
Collection<TTLObject<K, E>> values = objects.values();
for (TTLObject<K, E> ttlObject : values) {
ttlObject.getObject().onRemove(destroy);
}
objects.clear();
currentSizeInBytes = 0;
}
public void setReapCycle(long milliseconds) {
reapCycle = milliseconds;
startReaper();
}
protected synchronized void startReaper() {
stopReaper();
if(reapCycle > 0) {
reaper = new Reaper();
if(reapTimer == null) {
reapTimer = new Timer("CacheReaper", true); // Daemon so we auto-exit on shutdown
}
reapTimer.schedule(reaper, reapCycle, reapCycle);
}
}
protected synchronized void stopReaper() {
if(reaper != null) {
reaper.cancel();
reaper = null;
}
if(reapTimer != null) {
reapTimer.purge();
}
reaping = false;
}
/**
* Adds an object to cache with the given time-to-live
* @param strKey
* @param object
* @param ttl
*/
public boolean put(K strKey, E object, long ttl) {
return put(strKey, object, ttl, false);
}
/**
* Adds an object to cache that optionally lives forever.
* @param strKey
* @param object
* @param eternal
* @return
*/
public boolean put(K strKey, E object, boolean eternal) {
return put(strKey, object, defaultTTL, eternal);
}
/**
* Adds an eternal object to cache. Eternal objects will never expire, unless they commit suicide.
* @param strKey
* @param object
*/
public boolean put(K strKey, E object) {
return put(strKey, object, defaultTTL, (defaultTTL <= 0));
}
/**
* Adds an object to cache with the given Time To Live in milliseconds
* @param k
* @param object
* @param ttl milliseconds
* @param eternal
*/
protected synchronized boolean put(K k, E object, long ttl, boolean eternal) {
// Check the key map first
if(exists(k)) {
TTLObject<K, E> ttlObject = getTTLObject(k);
Key<K> key = keys.get(k);
key.setTime(System.currentTimeMillis());
ttlObject.setEternal(eternal);
ttlObject.extendLife(ttl);
ttlObject.setObject(object);
if(eventListener != null) {
eventListener.onPut(object);
}
return true;
}
else {
TTLObject<K, E> t = new TTLObject<K, E>(object, k, ttl);
t.setEternal(eternal);
long addedSize = object.getSizeInBytes();
long newSize = currentSizeInBytes + addedSize;
boolean oversize = false;
oversize = (hardByteLimit && maxCapacityBytes > 0 && newSize > maxCapacityBytes);
if(!oversize) {
Key<K> key = new Key<K>(k, System.currentTimeMillis());
keys.put(k, key);
objects.put(key, t);
t.getObject().onPut(k);
// Increment size
currentSizeInBytes = newSize;
if(eventListener != null) {
eventListener.onPut(object);
}
return true;
}
}
return false;
}
protected TTLObject<K, E> getTTLObject(K strKey) {
TTLObject<K, E> obj = null;
// Look for the key
Key<K> key = keys.get(strKey);
if(key != null) {
obj = objects.get(key);
}
return obj;
}
/**
* Ignores proxy and always returns raw object
* @param strKey
* @return
*/
public synchronized E getRaw(K strKey) {
TTLObject<K, E> obj = getTTLObject(strKey);
if(obj != null && !isExpired(obj)) {
return obj.getObject();
}
return null;
}
/**
* Gets an object from cache. Returns null if the object does not exist, or has expired.
* @param key
* @return
*/
public synchronized E get(K key) {
TTLObject<K, E> obj = getTTLObject(key);
if(obj != null && !isExpired(obj)) {
if(extendOnGet) {
extendTTL(key);
}
if(eventListener != null) {
eventListener.onGet(obj.getObject());
}
obj.getObject().onGet();
return obj.getObject();
}
else if(obj != null) {
// Expired
destroy(obj.getKey());
obj = null;
}
if (obj == null) {
if(objectFactory != null) {
E object = objectFactory.create(key);
if(object != null) {
if(!put(key, object) && logger != null) {
// We couldn't put this record.. just log a warning
logger.warn("Failed to put object into cache. Cache size exceeded");
}
}
return object;
}
}
return null;
}
/**
* Returns the internal values of the cache.
* <br/>
* Proxy objects are returned if gets are proxied within the cache.
* @return
*/
public Collection<E> values() {
Collection<E> values = null;
Collection<TTLObject<K, E>> ttls = objects.values();
if(ttls != null) {
values = new ArrayList<E>(ttls.size());
for (TTLObject<K, E> t : ttls) {
if(!isExpired(t)) {
values.add(t.getObject());
}
}
}
return values;
}
/**
* Returns true if the object with the given key resides in the cache.
* @param k
* @return true if the object with the given key resides in the cache.
*/
public boolean exists(K k) {
Key<K> key = keys.get(k);
if(key != null) {
return objects.get(key) != null;
}
return false;
}
public boolean keyExists(K k) {
return keys.get(k) != null;
}
/**
* Removes an object from cache. If the object maintains a persistent state.
* @param key
*/
public E remove(K key) {
return remove(key, false);
}
/**
* Destroys an object in cache. This differs from remove() such that any persistent state associated with the object should also be removed.
* @param key
*/
public E destroy(K key) {
return remove(key, true);
}
public synchronized E remove(K strKey, boolean destroy) {
Key<K> key = keys.get(strKey);
TTLObject<K, E> removed = null;
if(key != null) {
removed = objects.remove(key);
if(removed != null) {
currentSizeInBytes -= removed.getObject().getSizeInBytes();
removed.getObject().onRemove(destroy);
}
keys.remove(strKey);
}
if(removed != null) {
return removed.getObject();
}
return null;
}
/**
* Extends the ttl of the object with the given key with the current system time.
* @param strKey
*/
public synchronized void extendTTL(K strKey) {
TTLObject<K, E> object = getTTLObject(strKey);
if(object != null) {
object.setLifeExpectancy(System.currentTimeMillis() + object.getTtl());
}
}
/**
* @return Returns the debug.
*/
public boolean isDebug() {
return debug;
}
/**
* @param debug The debug to set.
*/
public void setDebug(boolean debug) {
this.debug = debug;
}
public boolean isExpired(TTLObject<K, E> object) {
E o = object.getObject();
if(o instanceof ISuicidal) {
ISuicidal<K> s = (ISuicidal<K>) o;
if(s.isDead()) {
return true;
}
}
return !object.isEternal() && object.getLifeExpectancy() <= System.currentTimeMillis();
}
public boolean doReap() {
return reap();
}
protected synchronized boolean reap() {
if(!reaping) {
int reaped = 0;
try {
reaping = true;
if(eventListener != null) {
eventListener.onReapStart();
}
int size = objects.size();
if(size > 0) {
Set<Key<K>> localKeys = objects.keySet();
TreeMap<Key<K>, TTLObject<K, E>> newMap = makeMap();
long time = System.currentTimeMillis();
boolean ok = true;
TTLObject<K, E> object = null;
for (Key<K> key : localKeys) {
object = objects.get(key);
if(object != null) {
ok = true;
if(object.getObject() instanceof ISuicidal) {
ISuicidal<K> s = (ISuicidal<K>) object.getObject();
if(s.isDead()) {
size--;
if(debug && logger != null) {
String msg = "Object [" +
object.getObject().toString() +
"] has comitted suicide and will be purged from cache [" +
size +
"] objects remain";
logger.debug(msg);
}
ok = false;
}
}
if(ok) {
if(object.isEternal() || object.getLifeExpectancy() >= time) {
// Save
newMap.put(key, object);
}
else {
ok = false;
size--;
if(debug && logger != null) {
String msg = "Object [" +
object.getObject().toString() +
"] with ttl of [" +
object.getTtl() +
"] has expired and will be purged from cache [" +
size +
"] objects remain";
logger.debug(msg);
}
}
}
if(!ok) {
if(logger != null && logger.isDebugEnabled()) {
logger.debug("Removing with key [" +
key +
"] during reap");
}
keys.remove(key.getKey());
currentSizeInBytes -= object.getObject().getSizeInBytes();
reaped++;
object.getObject().onRemove(true);
}
}
else {
if(logger != null) {
logger.warn("No object found with key [" +
key +
"]");
objects.remove(key);
}
}
}
// Now check for over size
size = newMap.size();
if((maxCapacity > 0 && size > maxCapacity) || (maxCapacityBytes > 0 && currentSizeInBytes > maxCapacityBytes)) {
// To many objects.. start trimming
if(debug && logger != null) {
String msg = "TTLCache has count of [" +
size +
"], size of [" +
currentSizeInBytes +
"] bytes which exceeds maximum of [" +
maxCapacity +
"], [" +
maxCapacityBytes +
"] bytes. Excess items will be trimmed";
logger.debug(msg);
}
Key<K> key = null;
while((maxCapacity > 0 && size > maxCapacity) || (maxCapacityBytes > 0 && currentSizeInBytes > maxCapacityBytes)) {
key = newMap.firstKey();
if(debug && logger != null) {
String msg = "Removing item with key [" +
key +
"] from cache";
logger.debug( msg);
}
TTLObject<K, E> removed = newMap.remove(key);
keys.remove(key.getKey());
size = newMap.size();
currentSizeInBytes -= removed.getObject().getSizeInBytes();
removed.getObject().onRemove(true);
reaped++;
if(debug && logger != null) {
String msg = "[" +
size +
"] objects remain with size of [" +
currentSizeInBytes +
"]";
logger.debug(msg);
}
}
}
// Swap
objects = newMap;
}
}
finally {
if(eventListener != null) {
eventListener.onReapEnd(reaped);
}
reaping = false;
}
return true;
}
return false;
}
protected TreeMap<Key<K>, TTLObject<K, E>> makeMap() {
return new TreeMap<Key<K>, TTLObject<K, E>>();
}
public int size() {
return objects.size();
}
public long sizeInBytes() {
return currentSizeInBytes;
}
public boolean isExtendOnGet() {
return extendOnGet;
}
public void setExtendOnGet(boolean extendOnGet) {
this.extendOnGet = extendOnGet;
}
public long getMaxCapacityBytes() {
return maxCapacityBytes;
}
public void setMaxCapacityBytes(long maxCapacityBytes) {
this.maxCapacityBytes = maxCapacityBytes;
}
public ICacheEventListener<K, E> getEventListener() {
return eventListener;
}
public void setEventListener(ICacheEventListener<K, E> eventListener) {
this.eventListener = eventListener;
}
/**
* Extends the maximum capacity.
* @param extension
*/
public void extendMax(int extension) {
maxCapacity += extension;
}
/**
* @return the maxCapacity
*/
public int getMaxCapacity() {
return maxCapacity;
}
public ICacheableFactory<K, E> getObjectFactory() {
return objectFactory;
}
public void setObjectFactory(ICacheableFactory<K, E> constructor) {
this.objectFactory = constructor;
}
public long getDefaultTTL() {
return defaultTTL;
}
public void setDefaultTTL(long defaultTTL) {
this.defaultTTL = defaultTTL;
}
public boolean isHardByteLimit() {
return hardByteLimit;
}
public void setHardByteLimit(boolean hardByteLimit) {
this.hardByteLimit = hardByteLimit;
}
public SocializeLogger getLogger() {
return logger;
}
public void setLogger(SocializeLogger logger) {
this.logger = logger;
}
}