001package com.randomnoun.common; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.util.*; 008 009import org.apache.log4j.Logger; 010 011/** 012 * Most-recently used cache class. 013 * 014 * <p>You probably want to use something in the guava Collections library these days instead. 015 * 016 * <p>This class can be used to implement serverside data caches. The constructor 017 * takes three parameters: maximum size, expiry time (in milliseconds) and a 018 * 'RetrievalCallback' object (defined as an interface in this class). 019 * 020 * <p>As items are put into this object, they are timestamped; if a key is applied 021 * to retrieve an object that has 'expired', then this object will return null 022 * (if no callback has been supplied), or will execute the callback to refresh 023 * the value. A callback can (optionally) be supplied with every get() invocation 024 * so that this object knows precisely how to generate the requested object. (This 025 * works since it is usually computationally cheaper to generate the Callback object 026 * than to generate the result of the callback, which generally needs to fetch 027 * information from a database). 028 * 029 * <p>There are two possible expiry mechanisms: either an item is expired from the 030 * time it is entered into the cache, or expired from the time it is last retrieved 031 * from the cache. The former makes sense for 'dynamic' data that may change over time 032 * (e.g. rowcounts); the latter makes sense for 'static' data that is just used to 033 * cache large objects that don't need to be in memory at all times 034 * (e.g. message definitions). By default this object will operate as per 'static' 035 * rules; the other method can be selected by calling the setDynamicCaching() after 036 * construction. 037 * 038 * <p><b>Implementation notes</b> 039 * 040 * <p>It may be possible to extend this class to use WeakReferences, so that it can 041 * shrink in size as memory constraints within the VM become increased. This is 042 * unlikely to be useful in practice, however, since this is probably an indication 043 * of a memory leak or over-utilised server. It may make more sense to just reduce the 044 * maximum size in these cases. Performance tuning would be required to know 045 * for sure. 046 * 047 * <p>Also note that if an object exceeds it's expiryTime, it is *not* automatically 048 * removed from the cache. This would involve having an additional Timer thread 049 * running through these objects and scanning for expired objects, which is unlikely 050 * to be useful in practice. An additional method could be implemented to trigger these 051 * cleanups if this becomes necessary. 052 * 053 * @author knoxg 054 * 055 * 056 */ 057 058// this probably isn't as efficient as it could be (it contains a mruList AND a 059// HashMap of expiryTimes, which I'm sure is overkill. To do this properly 060// would involve extending Map.Entry to include a timestamp value, however, 061// which would require a fair amount of re-engineering. (would need to subclass 062// Map.Entry, and use "this.putAll()" to insert subclasses into this instance. 063// you'd still need a way of keeping a sorted list of timestamped entries anyway, 064// now that I think about it (to choose the next expiry candidate), so this may not 065// be so bad after all. 066public class MRUCache<K, V> 067 extends HashMap<K, V> 068{ 069 070 /** Generated serialVersionUID */ 071 private static final long serialVersionUID = -6826049000932880050L; 072 073 /** Logger instance for this class */ 074 public static Logger logger = Logger.getLogger(MRUCache.class); 075 076 /** The callback interface. Caches should provide one of these classes to 077 * perform any time-consuming tasks required to generate values in this cache. 078 * If the time taken to generate a value isn't time-consuming, then perhaps 079 * you shouldn't be using a cache to store them. 080 */ 081 public static interface RetrievalCallback<K, V> { 082 083 /** Dynamically generates the value of an entity in a cache 084 * 085 * @param key The key used to reference the cached object 086 * 087 * @return The newly generated (or updated) cached object 088 */ 089 public V get(K key); 090 } 091 092 /** If set to true, expires data from the time it was <i>entered</i> into the cache, 093 * rather than the time if was <i>fetched</i> from the cache. Suitable when we 094 * wish to apply a strict expiry time for dynamically-updating data, e.g. 095 * rowcounts. 096 */ 097 private boolean dynamicCaching = false; 098 099 /** An ordered list of the objects in this cache. Objects at position 0 on the list 100 * have been the most recently accessed (i.e. objects will be expired from the 101 * end of this list). */ 102 private LinkedList<K> mruList; 103 104 /** The maximum number of elements that this cache can hold. If <= 0, then the 105 * size of the map is unlimited. */ 106 private int cacheSize; 107 108 /** A callback which can be used to populate entries in the cache that are unknown, 109 * or have expired. */ 110 private RetrievalCallback<K, V> callbackInstance; 111 112 /** A Map whose keys are elements in this cache, and who's entries are the last time 113 * that entry was 'touched' (i.e. added to or retrieved from the cache) */ 114 private Map<K, Long> lastUpdatedTimes; 115 116 /** The amount of time an entry can remain valid, measured from when the element 117 * is first added to the cache. If expiryTime is set to <= 0, then items never expire. */ 118 private int expiryTime; 119 120 /** 121 * Creates a new MRUCache object. 122 * 123 * @param cacheSize The maximum number of elements that this cache can hold. A value <=0 means 124 * that the size of this map is not limited. 125 * @param expiryTime The amount of time (in ms) an entry can remain valid, measured from when the element 126 * is first added to the cache (or last retrieved if {@link #setDynamicCaching()} is 127 * in effect. A value <=0 means that items do not have an 128 * expiry period. 129 * @param callback A callback which can be used to populate entries in the cache that are unknown, 130 * or have expired. This parameter can be set to null <b>if and only if</b> the 131 * two-parameter {@link #get(Object, RetrievalCallback)} method is used to retrieve 132 * elements from the map. 133 */ 134 public MRUCache(int cacheSize, int expiryTime, RetrievalCallback<K, V> callback) 135 { 136 mruList = new LinkedList<>(); 137 lastUpdatedTimes = new HashMap<>(); 138 this.cacheSize = cacheSize; 139 this.callbackInstance = callback; 140 this.expiryTime = expiryTime; 141 } 142 143 /** 144 * Retrieve an element from the cache. If a callback was defined in the cache 145 * constructor, then this will be used to refresh 146 * values in the cache if they are missing, or have expired. 147 * 148 * @param key key whose associated value is to be returned 149 * 150 * @return the value to which this map maps the specified key, or 151 * null if the map contains no mapping for this key. 152 */ 153 @SuppressWarnings("unchecked") 154 @Override 155 public synchronized V get(Object key) 156 { 157 return get((K) key, callbackInstance); 158 } 159 160 /** 161 * Retrieves an element from the cache. The supplied callback will be used to 162 * refresh the value in the cache if it is missing, or has expired. If the 163 * requested element is missing and the callback is set to null, then 164 * null is returned. 165 * 166 * @param key key whose associated value is to be returned 167 * @param callback a callback used to populate the cache if necessary 168 * 169 * @return the value to which this map maps the specified key, or 170 * null if the map contains no mapping for this key. 171 */ 172 public synchronized V get(K key, RetrievalCallback<K, V> callback) 173 { 174 long now = System.currentTimeMillis(); 175 176 if (!this.containsKey(key)) 177 { 178 if (callback == null) { 179 return null; 180 } else { 181 // cache is missing key; retrieve from callback and update access times 182 synchronized (this) { 183 if (cacheSize > 0 && mruList.size() >= cacheSize) { 184 this.remove(mruList.getLast()); 185 mruList.removeLast(); 186 } 187 188 super.put(key, callback.get(key)); 189 lastUpdatedTimes.put(key, Long.valueOf(now)); 190 mruList.addFirst(key); 191 } 192 } 193 194 } else { 195 196 // cache contains key 197 if (expiryTime > 0) { 198 199 // expire this element if it's old 200 Long lastUpdatedTime = (Long) lastUpdatedTimes.get(key); 201 if (lastUpdatedTime==null) { 202 logger.warn("MRUCache contains key '" + key + "' with no lastUpdatedTime set"); 203 } 204 if (lastUpdatedTime == null || (lastUpdatedTime.longValue() + expiryTime < now)) { 205 synchronized (this) { 206 if (callback == null) { 207 // no callback, we just remove it 208 super.remove(key); 209 lastUpdatedTimes.remove(key); 210 mruList.remove(key); 211 212 return null; 213 } else { 214 super.put((K) key, callback.get((K) key)); 215 lastUpdatedTimes.put(key, Long.valueOf(now)); 216 } 217 } 218 } 219 } 220 221 // we have an element and it has been retrieved, move it to the 222 // top of the mruList. (we may not necessarily want to do this; 223 // this should be a constructor preference). There are pros & cons 224 // to this approach: 225 // pros: if we just got an element, we probably don't want it expiring out 226 // of the cache 227 // cons: this moves 'stale' objects to the top of the mruList 228 // stack; this means that when we remove elements from mruList 229 // to fit in more elements, we may be expiring older elements. 230 // this should probably be a decision made by the developer, rather than 231 // this code, but I'd like to keep the interface simple. 232 if (!dynamicCaching) { 233 synchronized (this) { 234 mruList.remove(key); 235 mruList.addFirst(key); 236 } 237 } 238 } 239 240 return super.get(key); 241 } 242 243 /** 244 * Returns the object if it is in the cache and has not expired. 245 * This method will return null if the the key is not in cache, or 246 * if it is in the cache but has expired. 247 * 248 * @param key key whose associated value is to be returned 249 * 250 * @return the value to which this map maps the specified key, or 251 * null if the map contains no mapping for this key. 252 */ 253 public synchronized Object getNoCallback(Object key) { 254 if (!this.containsKey(key)) { 255 return null; 256 } 257 258 if (expiryTime > 0) { 259 if (((Long)lastUpdatedTimes.get(key)).longValue() + expiryTime < 260 System.currentTimeMillis()) 261 { 262 return null; 263 } 264 } 265 266 return super.get(key); 267 } 268 269 /** 270 * Add an element into the cache. 271 * 272 * @param key key with which the specified value is to be associated. 273 * @param value value to be associated with the specified key. 274 * 275 * @return the previous value of this map entry, if one exists, otherwise null 276 */ 277 @Override 278 public synchronized V put(K key, V value) { 279 280 synchronized (this) { 281 V lastValue = super.put(key, value); 282 lastUpdatedTimes.put(key, Long.valueOf(System.currentTimeMillis())); 283 return lastValue; 284 } 285 } 286 287 /** Enforces dynamic caching rules. When called, data will be expired from the time it 288 * is <i>entered</i> into the cache, rather than the time if is <i>fetched</i> from 289 * the cache. Suitable when we wish to apply a strict expiry time for 290 * dynamically-updating data, e.g. rowcounts. 291 */ 292 public void setDynamicCaching() { 293 dynamicCaching = true; 294 } 295}