001: package org.xorm.cache;
002:
003: import org.xorm.InterfaceManagerFactory;
004: import org.xorm.datastore.Row;
005: import org.xorm.datastore.Table;
006: import java.lang.ref.ReferenceQueue;
007: import java.lang.ref.SoftReference;
008: import java.util.Collection;
009: import java.util.Iterator;
010: import java.util.LinkedHashMap;
011: import java.util.Map;
012: import java.util.Properties;
013: import java.util.logging.Logger;
014:
015: /**
016: * An LRU Cache with a hard and soft reference limit. Objects that exceed the
017: * hard limit are stored as soft references, and objects that exceed the soft
018: * limit are discarded from the cache. The hard limit and soft limit are
019: * additive, in that hard limit is the number of objects to store with hard
020: * references, and the soft limit is the number of objects to store with soft
021: * references, exclusive of the hard limit. Hence, a hard limit of 10, and soft
022: * limit of 20 would create a (possible) cache size of 30. Since items stored
023: * as soft references are subject to collection by the Garbage collector, the
024: * soft reference cache will often be smaller than the limit set on it. It is
025: * possible to also configure the cache to behave as only a soft or hard cache
026: * by simply configuring the hard and soft limit appropriately. See the
027: * additional documentation below about how to configure this.
028: * This class uses a referenceQueue to insure that values removed from the
029: * This class can also be configured to log its statistics to its logger
030: * (defined as Logger.getLogger("org.xorm.cache.LRUCache"))
031: *
032: * Normally, this class would be used by adding the following properties to the properties files given to xorm at startup:
033: * <ul><li>org.xorm.cache.DataCacheClass=org.xorm.cache.LRUCache</li></ul>
034: * The following properties will override the default values in this class:
035: * <ul><li>org.xorm.cache.LRUCache.hardSize=<non-negative intValue></li>
036: * <li>org.xorm.cache.LRUCache.softSize=<intValue></li>
037: * <li>org.xorm.cache.LRUCache.logInterval=<intValue></li></ul>
038: * See setProperties for a description of the meaning of these properties.
039: *
040: * @author Harry Evans
041: */
042: public class LRUCache implements DataCache {
043:
044: public static final int DEFAULT_HARD_SIZE = 500;
045: public static final int DEFAULT_SOFT_SIZE = -1;
046: public static final long DEFAULT_LOG_INTERVAL = -1;
047:
048: private Map hardMap;
049: private Map softMap;
050: private int hardSize;
051: private int softSize;
052: private RowCacheKeyFactory ckFactory;
053: private ReferenceQueue referenceQueue;
054: private static final Logger logger = Logger.getLogger("LRUCache");
055: private long lastLogTime;
056: private long logInterval;
057: private int hits;
058: private int misses;
059:
060: private final Object semaphore;
061:
062: /**
063: * Construct a LRUCache. This method is provided primarily for use when
064: * programatically assigning the cache that XORM will use. The default
065: * constructor is usually the one that gets used.
066: * @param hardSize The number of objects to keep hard references to. If
067: * this number is greater than 0, keep references to that number of objects
068: * with hard references. Objects that are discarded from the hard cache
069: * will be put into soft cache, if soft cache does not have a size of 0.
070: * If this value is 0, do not create a hard cache. An exception is thrown
071: * if this value is less than 0.
072: * @param softSize The number of objects to keep soft references to. If
073: * this number is greater than 0, keep references to that number of objects
074: * with soft references. Objects that are discarded from the soft cache,
075: * either through lack of space, or through garbage collection, are
076: * discarded completely from the cache. If this value is 0, do not create a
077: * soft cache. If this value is less than 0, the number of references
078: * stored will be limited to the amount of memory in the system, and how
079: * aggressive the garbage collector is.
080: * @param aLogInterval the amount of time in milliseconds to log usage
081: * statistics to the logger ("org.xorm.cache.LRUCache"). If this value is
082: * 0, always log. If this value is a negative number, never log. If this
083: * value is positive, log whenever that number of milliseconds has elapsed
084: * and the cache is accessed. Logging is done on a best effort basis, and
085: * does not use a background thread. Therefore, if the cache is not
086: * accessed for time greater than the log interval, no values will be
087: * logged.
088: * @throws RuntimeException if the hardSize is less than 0, or the
089: * hardSize and softSize are both 0
090: */
091: public LRUCache(int hardSize, int softSize, long aLogInterval) {
092: semaphore = new Object();
093: init(hardSize, softSize, aLogInterval);
094: }
095:
096: /**
097: * Construct an LRUCache using DEFAULT_HARD_SIZE, DEFAULT_SOFT_SIZE, and
098: * DEFAULT_LOG_INTERVAL. This constructor is provided to enable reflective
099: * instantiation of the cache. It is normally used in combination with a
100: * call to setProperties, which will result in the cache extracting
101: * (possibly) different values than the default with which to run.
102: */
103: public LRUCache() {
104: semaphore = new Object();
105: init(DEFAULT_HARD_SIZE, DEFAULT_SOFT_SIZE, DEFAULT_LOG_INTERVAL);
106: }
107:
108: /**
109: * method used to acutally set the values for the size of the caches, and
110: * the log interval. Called by the constructors, and by setProperties.
111: * Note, that whenever this method is called with good parameters, the
112: * entire cache is dumped.
113: */
114: private void init(int hardSize, int softSize, long aLogInterval) {
115: if (hardSize <= 0 && softSize == 0) {
116: String message = "HardSize and SoftSize cannot both 0 or less in size: hardSize: "
117: + hardSize + " softSize: " + softSize;
118: logger.severe(message);
119: throw new RuntimeException(message);
120: }
121: synchronized (semaphore) {
122: ckFactory = new RowCacheKeyFactory();
123: if (softSize != 0) {
124: softMap = new LRUMap(softSize, this );
125: }
126: if (hardSize > 0) {
127: if (softMap != null) {
128: hardMap = new LRUMap(hardSize, this );
129: } else {
130: hardMap = new LRUMap(hardSize, this );
131: }
132: }
133: referenceQueue = new ReferenceQueue();
134: logInterval = aLogInterval;
135: lastLogTime = System.currentTimeMillis();
136: this .hardSize = hardSize;
137: this .softSize = softSize;
138: hits = 0;
139: misses = 0;
140: }
141: }
142:
143: /**
144: * This method is called by XORM after reflectively instantiating the class.
145: * This method looks for the following properties:
146: * <ul><li>org.xorm.cache.LRUCache.hardSize</li>
147: * <li>org.xorm.cache.LRUCache.softSize</li>
148: * <li>org.xorm.cache.LRUCache.logInterval</li></ul>
149: * Any property that isn't found will have the class default value assigned
150: * to it.
151: * @param props The Properties object (possibly) containing values to use
152: * for hardSize, softSize, and logInterval.
153: */
154: public void setProperties(Properties props) {
155: String hardSize = props.getProperty(
156: "org.xorm.cache.LRUCache.hardSize", String
157: .valueOf(DEFAULT_HARD_SIZE));
158: String softSize = props.getProperty(
159: "org.xorm.cache.LRUCache.softSize", String
160: .valueOf(DEFAULT_SOFT_SIZE));
161: String logInterval = props.getProperty(
162: "org.xorm.cache.LRUCache.logInterval", String
163: .valueOf(LRUCache.DEFAULT_LOG_INTERVAL));
164: //ensureCache(cacheKey, hardSize, softSize);
165: int hardValue = DEFAULT_HARD_SIZE;
166: int softValue = DEFAULT_SOFT_SIZE;
167: long logValue = DEFAULT_LOG_INTERVAL;
168: StringBuffer message = new StringBuffer();
169: try {
170: hardValue = Integer.parseInt(hardSize);
171: } catch (NumberFormatException e) {
172: message
173: .append(
174: "Property name: org.xorm.cache.LRUCache.hardSize\nProperty value: ")
175: .append(hardSize).append("\n");
176: }
177: try {
178: softValue = Integer.parseInt(softSize);
179: } catch (NumberFormatException e) {
180: message
181: .append(
182: "Property name: org.xorm.cache.LRUCache.softSize\nProperty value: ")
183: .append(softSize).append("\n");
184: }
185: try {
186: logValue = Long.parseLong(logInterval);
187: } catch (NumberFormatException e) {
188: message
189: .append(
190: "Property name: org.xorm.cache.LRUCache.logInterval\nProperty value: ")
191: .append(logInterval).append("\n");
192: }
193: if (message.length() > 0) {
194: throw new RuntimeException(
195: "The following properties require numeric values, but were not parsable:\n"
196: + message.toString()
197: + "Cache is usable, but is using only default values.");
198: } else {
199: init(hardValue, softValue, logValue);
200: }
201: }
202:
203: public void setFactory(InterfaceManagerFactory factory) {
204: // not needed
205: }
206:
207: public void add(Row row) {
208: addInternal(row);
209: serviceStaleEntries();
210: }
211:
212: /**
213: * Internal method to handle just adding a row to the cache, without any
214: * stale entry servicing done by the public methods.
215: * @param row The Row object to add.
216: */
217: private void addInternal(Row row) {
218: Object key = ckFactory.getCacheKey(row);
219: synchronized (semaphore) {
220: if (hardMap != null) {
221: hardMap.put(key, row);
222: } else {
223: softMap.put(key, wrapValue(key, row));
224: }
225: }
226: // We need to flag that this instance of the Row is cached.
227: // That way if the row needs to be modified, the caller will
228: // know that it should be cloned first.
229: row.setCached(true);
230: }
231:
232: /**
233: * Optional method to add a collection of objects to the cache all at once.
234: * Not currently used by xorm or defined by the xorm interface.
235: * @param c A Collection of rows to be added to the cache. The Rows will be
236: * added in Iterator order.
237: */
238: public void addAll(Collection c) {
239: for (Iterator it = c.iterator(); it.hasNext();) {
240: //don't make it service stale entries every add, just after all the adds
241: addInternal((Row) it.next());
242: }
243: serviceStaleEntries();
244: }
245:
246: /* methods for possible future use
247: public boolean containsKey(Object key) {
248: serviceStaleEntries();
249: if(hardMap != null && hardMap.containsKey(key)) {
250: return true;
251: }
252: return (softMap != null && softMap.containsKey(key));
253: }
254: public boolean containsValue(Object o) {
255: serviceStaleEntries();
256: //TODO maybe figure out how to fix this?
257: throw new UnsupportedOperationException();
258: }
259: public boolean invalidate(Object key) {
260: serviceStaleEntries();
261: if(hardMap != null && hardMap.containsKey(key)) {
262: hardMap.remove(key);
263: return true;
264: } else if(softMap != null && softMap.containsKey(key)) {
265: softMap.remove(key);
266: return true;
267: }
268: return false;
269: }
270:
271: */
272: /**
273: * Retrieves a cloned copy of the Row from the cache with the
274: * matching primary key.
275: */
276: public Row get(Table table, Object aPrimaryKey) {
277: serviceStaleEntries();
278: Object realKey = ckFactory
279: .makeRowPrimaryKey(table, aPrimaryKey);
280: if (realKey != null) {
281: Object value = get(realKey);
282: if (value != null) {
283: // We're no longer going to clone cached rows.
284: // Instead we should have setCached(true) already.
285: // The caller will use that to determine if a
286: // clone is necessary.
287: //return (Row)((Row)value).clone();
288: return (Row) value;
289: }
290: }
291: return null;
292: }
293:
294: /**
295: * Retrieves an object from the cache. Used internally to retrieve real
296: * values instead of clones.
297: */
298: private Object get(Object key) {
299: Object value = null;
300: synchronized (semaphore) {
301: if (hardMap != null) {
302: value = hardMap.get(key);
303: }
304: if (value == null && softMap != null) {
305: Object wrappedValue = softMap.get(key);
306: if (wrappedValue != null) {
307: value = unwrapValue(wrappedValue);
308: if (hardMap != null) {
309: softMap.remove(key);
310: hardMap.put(key, value);
311: }
312: }
313: }
314: }
315: if (value == null) {
316: misses++;
317: } else {
318: hits++;
319: }
320: log(false);
321: return value;
322: }
323:
324: /**
325: * Optional method for use by external classes to force logging of the cache
326: * usage statistics regardless of the value of logInterval.
327: */
328: public void log() {
329: log(true);
330: }
331:
332: /**
333: * Internal method to handle when usgae statictics should actually be
334: * logged.
335: * @param force If true, log regardless of other considerations.
336: */
337: private void log(boolean force) {
338: if (force
339: || logInterval == 0
340: || System.currentTimeMillis() < (lastLogTime + logInterval)) {
341: String msg = new StringBuffer("[LRUCache] hardSize: ")
342: .append(hardSize).append(" softSize: ").append(
343: softSize).append(" hits: ").append(hits)
344: .append(" misses: ").append(" misses: ").append(
345: " logInterval: ").append(logInterval)
346: .toString();
347: logger.fine(msg);
348: lastLogTime = System.currentTimeMillis();
349: }
350: }
351:
352: /**
353: * 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.
354: */
355: public Object removeValue(Object value) {
356: Object key = ckFactory.getCacheKey(value);
357: return removeKey(key);
358: }
359:
360: /**
361: * Method that removes a record from the cache by key.
362: */
363: private Object removeKey(Object key) {
364: serviceStaleEntries();
365: synchronized (semaphore) {
366: if (hardMap != null && hardMap.containsKey(key)) {
367: return hardMap.remove(key);
368: } else if (softMap != null && softMap.containsKey(key)) {
369: return unwrapValue(softMap.remove(key));
370: }
371: }
372: return null;
373: }
374:
375: public void remove(Row row) {
376: removeValue(row);
377: // I don't know if this will make a difference, but once
378: // a row leaves the cache we should flip the cached flag
379: // back to false.
380: row.setCached(false);
381: }
382:
383: /**
384: * Method called by LRUMap when a value is removed from a cache, to
385: * (possibly) place it in a new cache. This only occurrs (currently) when
386: * the hard map and soft map are both defined, and the hard map must bump a
387: * value into the soft map. Dropping the value from the old map is handled
388: * by the calling map implementation.
389: * @param aMap the map the value is currently contained in.
390: * @param eldestEntry the value that is being bumped out, due to space
391: * limitations.
392: */
393: private void dumpMapValue(LRUMap aMap, Map.Entry eldestEntry) {
394: if (aMap == hardMap && softMap != null) {
395: synchronized (semaphore) {
396: Object key = eldestEntry.getKey();
397: Object value = eldestEntry.getValue();
398: Object ref = wrapValue(key, value);
399: softMap.put(key, ref);
400: }
401: }
402: }
403:
404: /**
405: * Method to wrap values in soft references for use in the soft map.
406: */
407: private Object wrapValue(Object key, Object value) {
408: return new KeyedSoftReference(value, referenceQueue, key);
409: }
410:
411: /**
412: * Method used to extract the Row value from a keyed soft reference, when it
413: * is being extracted from the softMap.
414: */
415: private Object unwrapValue(Object value) {
416: if (value == null) {
417: return value;
418: }
419: if (value instanceof KeyedSoftReference) {
420: KeyedSoftReference ksr = (KeyedSoftReference) value;
421: return ksr.get();
422: } else {
423: return value;
424: }
425: }
426:
427: /**
428: * Method to clean up soft referneces that have been flagged for collection
429: * and removal. This avoids the memory leaks that are often found in soft
430: * and weak reference implementations, such as ThreadLocal variables.
431: */
432: private void serviceStaleEntries() {
433: Object stale = null;
434: synchronized (semaphore) {
435: while ((stale = referenceQueue.poll()) != null) {
436: KeyedSoftReference ksr = (KeyedSoftReference) stale;
437: Object key = ksr.getKey();
438: softMap.remove(key);
439: }
440: }
441: }
442:
443: /**
444: * A class to take advantage of the LRU properties of LinkedHashMap. This
445: * implemeation will service the removal of old entries, once the maxSize
446: * value equals the number of contained values, by calling the dumpMapValue
447: * method of the LRUCache parent provided at construction.
448: * @author Harry Evans
449: */
450: static class LRUMap extends java.util.LinkedHashMap {
451: private int maxSize;
452: private LRUCache parent;
453:
454: /**
455: * Construct a new LRUMap.
456: * @param aMaxSize the maximum number of values to hold onto before
457: * calling parent.dumpMapValue(). If aMaxSize is greater than 0, than a
458: * cache size over this value will result in the eldest entry being
459: * removed, and dumpMapValue() being called. If 0 or less, dumpMapValue
460: * will never be called.
461: * @param aParent the LRUCache parent of this map to which it dumps
462: * values if full.
463: */
464: public LRUMap(int aMaxSize, LRUCache aParent) {
465: super (16, 0.75f, true);
466: maxSize = aMaxSize;
467: parent = aParent;
468: }
469:
470: /**
471: * Override the default( noop) behaviour of this method. Checks if
472: * maxSize is greater than 0 and size() is greater than maxSize, and if
473: * so, calls parent.dumpMapValue, and returns true, so that the value is
474: * removed.
475: */
476: protected boolean removeEldestEntry(
477: java.util.Map.Entry eldestEntry) {
478: boolean remove = ((maxSize > 0) ? (size() > maxSize)
479: : false);
480: if (remove) {
481: parent.dumpMapValue(this, eldestEntry);
482: }
483: return remove;
484: }
485: }
486: }
|