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; } }