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