package org.xorm.cache;
import org.xorm.InterfaceManagerFactory;
import org.xorm.datastore.Row;
import org.xorm.datastore.Table;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
/**
* An LRU Cache with a hard and soft reference limit. Objects that exceed the
* hard limit are stored as soft references, and objects that exceed the soft
* limit are discarded from the cache. The hard limit and soft limit are
* additive, in that hard limit is the number of objects to store with hard
* references, and the soft limit is the number of objects to store with soft
* references, exclusive of the hard limit. Hence, a hard limit of 10, and soft
* limit of 20 would create a (possible) cache size of 30. Since items stored
* as soft references are subject to collection by the Garbage collector, the
* soft reference cache will often be smaller than the limit set on it. It is
* possible to also configure the cache to behave as only a soft or hard cache
* by simply configuring the hard and soft limit appropriately. See the
* additional documentation below about how to configure this.
* This class uses a referenceQueue to insure that values removed from the
* This class can also be configured to log its statistics to its logger
* (defined as Logger.getLogger("org.xorm.cache.LRUCache"))
*
* Normally, this class would be used by adding the following properties to the properties files given to xorm at startup:
* <ul><li>org.xorm.cache.DataCacheClass=org.xorm.cache.LRUCache</li></ul>
* The following properties will override the default values in this class:
* <ul><li>org.xorm.cache.LRUCache.hardSize=<non-negative intValue></li>
* <li>org.xorm.cache.LRUCache.softSize=<intValue></li>
* <li>org.xorm.cache.LRUCache.logInterval=<intValue></li></ul>
* See setProperties for a description of the meaning of these properties.
*
* @author Harry Evans
*/
public class LRUCache implements DataCache {
public static final int DEFAULT_HARD_SIZE = 500;
public static final int DEFAULT_SOFT_SIZE = -1;
public static final long DEFAULT_LOG_INTERVAL = -1;
private Map hardMap;
private Map softMap;
private int hardSize;
private int softSize;
private RowCacheKeyFactory ckFactory;
private ReferenceQueue referenceQueue;
private static final Logger logger = Logger.getLogger("LRUCache");
private long lastLogTime;
private long logInterval;
private int hits;
private int misses;
private final Object semaphore;
/**
* Construct a LRUCache. This method is provided primarily for use when
* programatically assigning the cache that XORM will use. The default
* constructor is usually the one that gets used.
* @param hardSize The number of objects to keep hard references to. If
* this number is greater than 0, keep references to that number of objects
* with hard references. Objects that are discarded from the hard cache
* will be put into soft cache, if soft cache does not have a size of 0.
* If this value is 0, do not create a hard cache. An exception is thrown
* if this value is less than 0.
* @param softSize The number of objects to keep soft references to. If
* this number is greater than 0, keep references to that number of objects
* with soft references. Objects that are discarded from the soft cache,
* either through lack of space, or through garbage collection, are
* discarded completely from the cache. If this value is 0, do not create a
* soft cache. If this value is less than 0, the number of references
* stored will be limited to the amount of memory in the system, and how
* aggressive the garbage collector is.
* @param aLogInterval the amount of time in milliseconds to log usage
* statistics to the logger ("org.xorm.cache.LRUCache"). If this value is
* 0, always log. If this value is a negative number, never log. If this
* value is positive, log whenever that number of milliseconds has elapsed
* and the cache is accessed. Logging is done on a best effort basis, and
* does not use a background thread. Therefore, if the cache is not
* accessed for time greater than the log interval, no values will be
* logged.
* @throws RuntimeException if the hardSize is less than 0, or the
* hardSize and softSize are both 0
*/
public LRUCache(int hardSize, int softSize, long aLogInterval) {
semaphore = new Object();
init(hardSize, softSize, aLogInterval);
}
/**
* Construct an LRUCache using DEFAULT_HARD_SIZE, DEFAULT_SOFT_SIZE, and
* DEFAULT_LOG_INTERVAL. This constructor is provided to enable reflective
* instantiation of the cache. It is normally used in combination with a
* call to setProperties, which will result in the cache extracting
* (possibly) different values than the default with which to run.
*/
public LRUCache() {
semaphore = new Object();
init(DEFAULT_HARD_SIZE, DEFAULT_SOFT_SIZE, DEFAULT_LOG_INTERVAL);
}
/**
* method used to acutally set the values for the size of the caches, and
* the log interval. Called by the constructors, and by setProperties.
* Note, that whenever this method is called with good parameters, the
* entire cache is dumped.
*/
private void init(int hardSize, int softSize, long aLogInterval) {
if(hardSize <= 0 && softSize == 0) {
String message = "HardSize and SoftSize cannot both 0 or less in size: hardSize: " + hardSize + " softSize: " + softSize;
logger.severe(message);
throw new RuntimeException(message);
}
synchronized(semaphore) {
ckFactory = new RowCacheKeyFactory();
if(softSize != 0) {
softMap = new LRUMap(softSize, this);
}
if(hardSize > 0) {
if(softMap != null) {
hardMap = new LRUMap(hardSize, this);
} else {
hardMap = new LRUMap(hardSize, this);
}
}
referenceQueue = new ReferenceQueue();
logInterval = aLogInterval;
lastLogTime = System.currentTimeMillis();
this.hardSize = hardSize;
this.softSize = softSize;
hits = 0;
misses = 0;
}
}
/**
* This method is called by XORM after reflectively instantiating the class.
* This method looks for the following properties:
* <ul><li>org.xorm.cache.LRUCache.hardSize</li>
* <li>org.xorm.cache.LRUCache.softSize</li>
* <li>org.xorm.cache.LRUCache.logInterval</li></ul>
* Any property that isn't found will have the class default value assigned
* to it.
* @param props The Properties object (possibly) containing values to use
* for hardSize, softSize, and logInterval.
*/
public void setProperties(Properties props) {
String hardSize = props.getProperty("org.xorm.cache.LRUCache.hardSize", String.valueOf(DEFAULT_HARD_SIZE));
String softSize = props.getProperty("org.xorm.cache.LRUCache.softSize", String.valueOf(DEFAULT_SOFT_SIZE));
String logInterval = props.getProperty("org.xorm.cache.LRUCache.logInterval", String.valueOf(LRUCache.DEFAULT_LOG_INTERVAL));
//ensureCache(cacheKey, hardSize, softSize);
int hardValue = DEFAULT_HARD_SIZE;
int softValue = DEFAULT_SOFT_SIZE;
long logValue = DEFAULT_LOG_INTERVAL;
StringBuffer message = new StringBuffer();
try {
hardValue = Integer.parseInt(hardSize);
} catch(NumberFormatException e) {
message.append("Property name: org.xorm.cache.LRUCache.hardSize\nProperty value: ").append(hardSize).append("\n");
}
try {
softValue = Integer.parseInt(softSize);
} catch(NumberFormatException e) {
message.append("Property name: org.xorm.cache.LRUCache.softSize\nProperty value: ").append(softSize).append("\n");
}
try {
logValue = Long.parseLong(logInterval);
} catch(NumberFormatException e) {
message.append("Property name: org.xorm.cache.LRUCache.logInterval\nProperty value: ").append(logInterval).append("\n");
}
if(message.length() > 0) {
throw new RuntimeException("The following properties require numeric values, but were not parsable:\n" + message.toString() + "Cache is usable, but is using only default values.");
} else {
init(hardValue, softValue, logValue);
}
}
public void setFactory(InterfaceManagerFactory factory) {
// not needed
}
public void add(Row row) {
addInternal(row);
serviceStaleEntries();
}
/**
* Internal method to handle just adding a row to the cache, without any
* stale entry servicing done by the public methods.
* @param row The Row object to add.
*/
private void addInternal(Row row) {
Object key = ckFactory.getCacheKey(row);
synchronized(semaphore) {
if(hardMap != null) {
hardMap.put(key, row);
} else {
softMap.put(key, wrapValue(key, row));
}
}
// We need to flag that this instance of the Row is cached.
// That way if the row needs to be modified, the caller will
// know that it should be cloned first.
row.setCached(true);
}
/**
* Optional method to add a collection of objects to the cache all at once.
* Not currently used by xorm or defined by the xorm interface.
* @param c A Collection of rows to be added to the cache. The Rows will be
* added in Iterator order.
*/
public void addAll(Collection c) {
for(Iterator it = c.iterator(); it.hasNext();) {
//don't make it service stale entries every add, just after all the adds
addInternal((Row)it.next());
}
serviceStaleEntries();
}
/* methods for possible future use
public boolean containsKey(Object key) {
serviceStaleEntries();
if(hardMap != null && hardMap.containsKey(key)) {
return true;
}
return (softMap != null && softMap.containsKey(key));
}
public boolean containsValue(Object o) {
serviceStaleEntries();
//TODO maybe figure out how to fix this?
throw new UnsupportedOperationException();
}
public boolean invalidate(Object key) {
serviceStaleEntries();
if(hardMap != null && hardMap.containsKey(key)) {
hardMap.remove(key);
return true;
} else if(softMap != null && softMap.containsKey(key)) {
softMap.remove(key);
return true;
}
return false;
}
*/
/**
* Retrieves a cloned copy of the Row from the cache with the
* matching primary key.
*/
public Row get(Table table, Object aPrimaryKey) {
serviceStaleEntries();
Object realKey = ckFactory.makeRowPrimaryKey(table, aPrimaryKey);
if(realKey != null) {
Object value = get(realKey);
if(value != null) {
// We're no longer going to clone cached rows.
// Instead we should have setCached(true) already.
// The caller will use that to determine if a
// clone is necessary.
//return (Row)((Row)value).clone();
return (Row)value;
}
}
return null;
}
/**
* Retrieves an object from the cache. Used internally to retrieve real
* values instead of clones.
*/
private Object get(Object key) {
Object value = null;
synchronized(semaphore) {
if(hardMap != null) {
value = hardMap.get(key);
}
if(value == null && softMap != null) {
Object wrappedValue = softMap.get(key);
if(wrappedValue != null) {
value = unwrapValue(wrappedValue);
if(hardMap != null) {
softMap.remove(key);
hardMap.put(key, value);
}
}
}
}
if(value == null) {
misses++;
} else {
hits++;
}
log(false);
return value;
}
/**
* Optional method for use by external classes to force logging of the cache
* usage statistics regardless of the value of logInterval.
*/
public void log() {
log(true);
}
/**
* Internal method to handle when usgae statictics should actually be
* logged.
* @param force If true, log regardless of other considerations.
*/
private void log(boolean force) {
if(force || logInterval == 0 || System.currentTimeMillis() < (lastLogTime + logInterval)) {
String msg = new StringBuffer("[LRUCache] hardSize: ")
.append(hardSize).append(" softSize: ").append(softSize)
.append(" hits: ").append(hits).append(" misses: ")
.append(" misses: ").append(" logInterval: ")
.append(logInterval).toString();
logger.fine(msg);
lastLogTime = System.currentTimeMillis();
}
}
/**
* Optional method to remove a value from the cache. Since all objects removed are Rows, this method should not be called directly in the near future.
*/
public Object removeValue(Object value) {
Object key = ckFactory.getCacheKey(value);
return removeKey(key);
}
/**
* Method that removes a record from the cache by key.
*/
private Object removeKey(Object key) {
serviceStaleEntries();
synchronized(semaphore) {
if(hardMap != null && hardMap.containsKey(key)) {
return hardMap.remove(key);
} else if(softMap != null && softMap.containsKey(key)) {
return unwrapValue(softMap.remove(key));
}
}
return null;
}
public void remove(Row row) {
removeValue(row);
// I don't know if this will make a difference, but once
// a row leaves the cache we should flip the cached flag
// back to false.
row.setCached(false);
}
/**
* Method called by LRUMap when a value is removed from a cache, to
* (possibly) place it in a new cache. This only occurrs (currently) when
* the hard map and soft map are both defined, and the hard map must bump a
* value into the soft map. Dropping the value from the old map is handled
* by the calling map implementation.
* @param aMap the map the value is currently contained in.
* @param eldestEntry the value that is being bumped out, due to space
* limitations.
*/
private void dumpMapValue(LRUMap aMap, Map.Entry eldestEntry) {
if(aMap == hardMap && softMap != null) {
synchronized(semaphore) {
Object key = eldestEntry.getKey();
Object value = eldestEntry.getValue();
Object ref = wrapValue(key, value);
softMap.put(key, ref);
}
}
}
/**
* Method to wrap values in soft references for use in the soft map.
*/
private Object wrapValue(Object key, Object value) {
return new KeyedSoftReference(value, referenceQueue, key);
}
/**
* Method used to extract the Row value from a keyed soft reference, when it
* is being extracted from the softMap.
*/
private Object unwrapValue(Object value) {
if(value == null) {
return value;
}
if(value instanceof KeyedSoftReference) {
KeyedSoftReference ksr = (KeyedSoftReference)value;
return ksr.get();
} else {
return value;
}
}
/**
* Method to clean up soft referneces that have been flagged for collection
* and removal. This avoids the memory leaks that are often found in soft
* and weak reference implementations, such as ThreadLocal variables.
*/
private void serviceStaleEntries() {
Object stale = null;
synchronized(semaphore) {
while ((stale = referenceQueue.poll()) != null) {
KeyedSoftReference ksr = (KeyedSoftReference)stale;
Object key = ksr.getKey();
softMap.remove(key);
}
}
}
/**
* A class to take advantage of the LRU properties of LinkedHashMap. This
* implemeation will service the removal of old entries, once the maxSize
* value equals the number of contained values, by calling the dumpMapValue
* method of the LRUCache parent provided at construction.
* @author Harry Evans
*/
static class LRUMap extends java.util.LinkedHashMap {
private int maxSize;
private LRUCache parent;
/**
* Construct a new LRUMap.
* @param aMaxSize the maximum number of values to hold onto before
* calling parent.dumpMapValue(). If aMaxSize is greater than 0, than a
* cache size over this value will result in the eldest entry being
* removed, and dumpMapValue() being called. If 0 or less, dumpMapValue
* will never be called.
* @param aParent the LRUCache parent of this map to which it dumps
* values if full.
*/
public LRUMap(int aMaxSize, LRUCache aParent) {
super(16, 0.75f, true);
maxSize = aMaxSize;
parent = aParent;
}
/**
* Override the default( noop) behaviour of this method. Checks if
* maxSize is greater than 0 and size() is greater than maxSize, and if
* so, calls parent.dumpMapValue, and returns true, so that the value is
* removed.
*/
protected boolean removeEldestEntry(java.util.Map.Entry eldestEntry) {
boolean remove = ((maxSize > 0) ? (size() > maxSize) : false);
if(remove) {
parent.dumpMapValue(this, eldestEntry);
}
return remove;
}
}
}