View Javadoc
1   package com.randomnoun.common;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   
7   import java.util.*;
8   
9   import org.apache.log4j.Logger;
10  
11  /**
12   * Most-recently used cache class. 
13   * 
14   * <p>You probably want to use something in the guava Collections library these days instead.
15   * 
16   * <p>This class can be used to implement serverside data caches. The constructor
17   * takes three parameters: maximum size, expiry time (in milliseconds) and a 
18   * 'RetrievalCallback' object (defined as an interface in this class). 
19   * 
20   * <p>As items are put into this object, they are timestamped; if a key is applied
21   * to retrieve an object that has 'expired', then this object will return null
22   * (if no callback has been supplied), or will execute the callback to refresh
23   * the value. A callback can (optionally) be supplied with every get() invocation
24   * so that this object knows precisely how to generate the requested object. (This
25   * works since it is usually computationally cheaper to generate the Callback object
26   * than to generate the result of the callback, which generally needs to fetch
27   * information from a database).
28   * 
29   * <p>There are two possible expiry mechanisms: either an item is expired from the
30   * time it is entered into the cache, or expired from the time it is last retrieved
31   * from the cache. The former makes sense for 'dynamic' data that may change over time
32   * (e.g. rowcounts); the latter makes sense for 'static' data that is just used to
33   * cache large objects that don't need to be in memory at all times 
34   * (e.g. message definitions). By default this object will operate as per 'static'
35   * rules; the other method can be selected by calling the setDynamicCaching() after
36   * construction.
37   *
38   * <p><b>Implementation notes</b>
39   *    
40   * <p>It may be possible to extend this class to use WeakReferences, so that it can
41   * shrink in size as memory constraints within the VM become increased. This is 
42   * unlikely to be useful in practice, however, since this is probably an indication
43   * of a memory leak or over-utilised server. It may make more sense to just reduce the
44   * maximum size in these cases. Performance tuning would be required to know 
45   * for sure.
46   * 
47   * <p>Also note that if an object exceeds it's expiryTime, it is *not* automatically
48   * removed from the cache. This would involve having an additional Timer thread
49   * running through these objects and scanning for expired objects, which is unlikely
50   * to be useful in practice. An additional method could be implemented to trigger these
51   * cleanups if this becomes necessary.
52   * 
53   * @author knoxg
54   * 
55   *
56   */
57  
58  // this probably isn't as efficient as it could be (it contains a mruList AND a 
59  // HashMap of expiryTimes, which I'm sure is overkill. To do this properly
60  // would involve extending Map.Entry to include a timestamp value, however,
61  // which would require a fair amount of re-engineering. (would need to subclass 
62  // Map.Entry, and use "this.putAll()" to insert subclasses into this instance. 
63  // you'd still need a way of keeping a sorted list of timestamped entries anyway,
64  // now that I think about it (to choose the next expiry candidate), so this may not 
65  // be so bad after all. 
66  public class MRUCache<K, V>
67      extends HashMap<K, V>
68  {
69      
70      /** Generated serialVersionUID */
71      private static final long serialVersionUID = -6826049000932880050L;
72  
73  	/** Logger instance for this class */
74      public static Logger logger = Logger.getLogger(MRUCache.class);
75      
76      /** The callback interface. Caches should provide one of these classes to 
77       * perform any time-consuming tasks required to generate values in this cache.
78       * If the time taken to generate a value isn't time-consuming, then perhaps
79       * you shouldn't be using a cache to store them.
80       */
81      public static interface RetrievalCallback<K, V> {
82          
83          /** Dynamically generates the value of an entity in a cache
84           * 
85           * @param key The key used to reference the cached object
86           * 
87           * @return The newly generated (or updated) cached object
88           */ 
89          public V get(K key);
90      }
91      
92      /** If set to true, expires data from the time it was <i>entered</i> into the cache,
93       * rather than the time if was <i>fetched</i> from the cache. Suitable when we 
94       * wish to apply a strict expiry time for dynamically-updating data, e.g.
95       * rowcounts.
96       */
97      private boolean dynamicCaching = false;
98  
99      /** 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 &lt;= 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 &lt;= 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 &lt;=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 &lt;=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 }