001 // Copyright (c) 2001 Hursh Jain (http://www.mollypages.org)
002 // The Molly framework is freely distributable under the terms of an
003 // MIT-style license. For details, see the molly pages web site at:
004 // http://www.mollypages.org/. Use, modify, have fun !
005
006 package fc.util.cache;
007
008 import java.text.*;
009 import java.util.*;
010 import fc.io.*;
011 import fc.util.*;
012
013 /**
014 Memory resident cache of objects. Allows to set expire times for contained
015 objects after which those expired objects are removed. By default, references
016 to expired objects are not retained in the cache, such that those object may
017 be garbage collected as needed by the garbage collector (if those objects
018 becomes unreachable from the rest of the program).
019 <p>
020 The size of this cache is unbounded. To use a bounded cache, use
021 the {@link BoundedCache} class.
022 <p>
023 Expired items are reaped via a background high priority thread that sleeps
024 for intervals between expired item checks. After every reap operation, the
025 reaper thread sleeps for 120 seconds (by default). This implies that the
026 minimum expire time for any item will be 120 seconds. This sleep time can
027 be changed via the {@link #setReapInterval} method.
028 <p>
029 ThreadSafety: This class <b>is</b> thread safe and can be used by multiple
030 threads concurrently.
031
032 @author hursh jain
033 @version 1.0 12/29/2001
034 **/
035 public final class MemoryCache implements Cache
036 {
037
038 /** Useful constant of 1 second (in milliseconds) */
039 public static final long ONE_SEC = 1000;
040
041 /** Useful constant of 1 second (in milliseconds) */
042 public static final long ONE_SECOND = ONE_SEC;
043
044 /** Useful constant of five seconds (in milliseconds) */
045 public static final long FIVE_SEC = 5 * ONE_SEC;
046
047 /** Useful constant of five seconds (in milliseconds) */
048 public static final long FIVE_SECOND = FIVE_SEC;
049
050 /** Useful constant of ten seconds (in milliseconds) */
051 public static final long TEN_SEC = 10 * ONE_SEC;
052
053 /** Useful constant of ten seconds (in milliseconds) */
054 public static final long TEN_SECOND = TEN_SEC;
055
056 /** Useful constant of 30 seconds (in milliseconds) */
057 public static final long THIRTY_SEC = 30 * ONE_SEC;
058
059 /** Useful constant of 30 seconds (in milliseconds) */
060 public static final long THIRTY_SECOND = THIRTY_SEC;
061
062 /** Useful constant for cache expiry time of 1 minute (in milliseconds) */
063 public static final long ONE_MIN = 1 * 60 * 1000;
064
065 /** Useful constant for cache expiry time of 2 minutes (in milliseconds) */
066 public static final long TWO_MIN = 2 * 60 * 1000;
067
068 /** Useful constant for cache expiry time of five minutes (in milliseconds) */
069 public static final long FIVE_MIN = 5 * 60 * 1000;
070
071 /** Useful constant for cache expiry time of ten minutes (in milliseconds) */
072 public static final long TEN_MIN = 10 * 60 * 1000;
073
074 /** Useful constant for cache expiry time of thirty minutes (in milliseconds) */
075 public static final long THIRTY_MIN = 30 * 60 * 1000;
076
077 /** Useful constant for cache expiry time of one hour (in milliseconds) */
078 public static final long ONE_HOUR = 60 * ONE_MIN;
079
080 /** Useful constant for cache expiry time of two hours (in milliseconds) */
081 public static final long TWO_HOUR = 2 * ONE_HOUR;
082
083 /** Useful constant for cache expiry time of four hours (in milliseconds) */
084 public static final long FOUR_HOUR = 4 * ONE_HOUR;
085
086 /** Useful constant for cache expiry time of eight hours (in milliseconds) */
087 public static final long EIGHT_HOUR = 8 * ONE_HOUR;
088
089 final String myName;
090 final Map cache;
091 volatile boolean closed = false;
092 volatile long defaultTTL;
093 volatile long reapInterval;
094 CacheReaperThread reaperThread;
095 final Log log;
096
097 /**
098 Instantiates this class with the specified name and logger.
099
100 @param name String denoting the name for this object
101 @param logger a reference to a {@link fc.io.Log}
102 @param default_ttl the default time to live for objects in the cache (in milliseconds)
103 (objects may appear to live for greater than the ttl if the the reaper
104 interval time is greater than this value). Also note, that this is the
105 <i>default</i> time and that the {@link #put(Object,Object,long) put}
106 method allows a different ttl for each individual object.
107 **/
108 public MemoryCache(Log log, String name, long default_ttl_millis)
109 {
110 if (log == null)
111 log = Log.getDefault();
112
113 this.log = log;
114 this.myName = name;
115 this.defaultTTL = default_ttl_millis;
116 this.reapInterval = 120 * 1000;
117 this.cache = Collections.synchronizedMap(new HashMap());
118 this.reaperThread = new CacheReaperThread();
119 }
120
121 /**
122 Creates a memory cache with a system-assigned logger and name and
123 a default ttl of 30 minutes.
124 */
125 public MemoryCache()
126 {
127 this(null,
128 "MemoryCache/created@"
129 + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
130 .format(new Date()),
131 30 * 60);
132 }
133
134 /**
135 Sets the minimum time between expired item checks.
136 **/
137 public void setReapInterval(final long interval) {
138 reapInterval = interval;
139 }
140
141 /**
142 Sets the cache reaper thread priority. By default the reaper runs at MAX_PRIORITY - 1 but
143 if there are a lot of low priority caches, setting this to a lower priority could be useful.
144 **/
145 public void setReapThreadPriority(final int priority) {
146 reaperThread.setPriority(priority);
147 }
148
149
150 public boolean containsKey(Object key)
151 {
152 Argcheck.isfalse(isClosed(), "Memory cache has been closed");
153
154 final CacheItem item = (CacheItem) cache.get(key);
155
156 if (item == null) {
157 return false;
158 }
159
160 if (item.hasExpired()) {
161 return false;
162 }
163
164 return true;
165 }
166
167
168 public Object get(Object key)
169 {
170 Argcheck.isfalse(isClosed(), "Memory cache has been closed");
171
172 final CacheItem item = (CacheItem) cache.get(key);
173
174 if (item == null)
175 return null;
176
177 if (item.hasExpired()) {
178 return null;
179 }
180 else {
181 return item.getValue();
182 }
183 }
184
185 public Map getAll() {
186 return cache;
187 }
188
189 public long getTimeLeft(Object key)
190 {
191 final CacheItem item = (CacheItem) cache.get(key);
192
193 if (item == null)
194 return 0;
195
196 if (item.expire == -1)
197 return -1;
198
199 final long time = item.calcTimeLeft();
200
201 if (time <= 0)
202 return 0;
203
204 return time;
205 }
206
207 public Object put(Object key, Object val, long expiry)
208 {
209 Argcheck.isfalse(isClosed(), "Memory cache has been closed");
210 Argcheck.istrue(expiry >= 0 || expiry == -1, "Illegal value [" + expiry + "] for the expiry argument, need the value to be >=0 or -1");
211
212
213 final CacheItem cacheitem = new CacheItem(key, val, expiry);
214 final Object item = cache.put(key, cacheitem);
215 if (item != null) { //return previous item
216 return ((CacheItem)item).getValue();
217 }
218 return null;
219 }
220
221 public Object put(Object key, Object item)
222 {
223 Argcheck.isfalse(isClosed(), "Memory cache has been closed");
224 return put(key, item, getDefaultTTL());
225 }
226
227 public long expireTimeLeft(Object key)
228 {
229 Argcheck.isfalse(isClosed(), "Memory cache has been closed");
230 final Object item = cache.get(key);
231 if (item == null) {
232 return 0;
233 }
234 else {
235 return ((CacheItem)item).expireTimeLeft();
236 }
237 }
238
239 public void expire(Object key)
240 {
241 Argcheck.isfalse(isClosed(), "Memory cache has been closed");
242 final Object item = cache.get(key);
243 if (item != null) {
244 final CacheItem citem = (CacheItem)item;
245 if ( ! citem.hasExpired() ) { //test not needed but conceptually clean
246 citem.expireNow(); //expire now
247 }
248 }
249 }
250
251 public void extend(Object key, long renewTime)
252 {
253 Argcheck.isfalse(isClosed(), "Memory cache has been closed");
254 Argcheck.istrue(renewTime >= 0 || renewTime == -1, "Illegal value [" + renewTime + "] for the renewTime argument, need the value to be >=0 or -1");
255 final Object item = cache.get(key);
256 if (item != null) {
257 final CacheItem citem = (CacheItem)item;
258 if (citem != null) {
259 if ( ! citem.hasExpired() ) {
260 citem.renew(renewTime);
261 }
262 }
263 }
264 }
265
266 /**
267 Closes this cache, which makes all items in the cache unavailable. Any items
268 needed for later should be taken out of the cache before closing it.
269 <p>
270 <b>Note:</b>, it is a good idea to close the cache once it's not needed,
271 because it releases resources, including any internal threads spawned and used
272 by this implementation.
273 **/
274 public void close()
275 {
276 reaperThread.stopThread();
277 this.closed = true;
278 log.info("*** cache:[", myName, "] closed. ***");
279 }
280
281 /**
282 Returns true if this cache has been closed, false otherwise.
283 **/
284 public boolean isClosed()
285 {
286 return this.closed;
287 }
288
289 /**
290 Sets the default TTL in milliseconds.
291 */
292 public void setDefaultTTL(long millis) {
293 defaultTTL = millis;
294 }
295
296 /**
297 Gets the default TTL in milliseconds.
298 */
299 public long getDefaultTTL() {
300 return defaultTTL;
301 }
302
303 public void clear() {
304 //don't clear in the middle of reaper thread or getAll iteration
305 synchronized (cache) {
306 cache.clear();
307 }
308 }
309
310 public String toString()
311 {
312 final int size = cache.size();
313 String temp = this.myName + "; contains ";
314 temp += (size == Integer.MAX_VALUE) ?
315 Integer.toString(size) : Integer.toString(size) + " (or greater)";
316 temp += " items; ";
317 temp += isClosed() ? "cache is closed. " : "cache is open.";
318 return temp;
319 }
320
321 //stores key,val pairs along with expiry data, never expires if expiry is -1
322 private class CacheItem
323 {
324 final Object key;
325 final Object val;
326 final long startTime;
327 long expire;
328 long lastAccessTime;
329
330 CacheItem(final Object key, final Object val, long expire)
331 {
332 this.key = key;
333 this.val = val;
334 this.expire = expire;
335 this.startTime = System.currentTimeMillis();
336 this.lastAccessTime = 0;
337 }
338
339 boolean hasExpired()
340 {
341 if (this.expire == -1) {
342 return false;
343 }
344 else {
345 return (calcTimeLeft() <= 0);
346 }
347 }
348
349 void renew(long time)
350 {
351 if (this.expire == -1) {
352 this.expire = 0; //so that we don't lose 1 millisec between -1 and 0
353 }
354 this.expire = this.expire + time; //this also works if time == -1
355 }
356
357 void expireNow()
358 {
359 this.expire = 0;
360 }
361
362 long expireTimeLeft()
363 {
364 if (this.expire == -1) {
365 return 0;
366 }
367 else return calcTimeLeft();
368 }
369
370 long calcTimeLeft()
371 {
372 return (this.expire + this.startTime) - System.currentTimeMillis();
373 }
374
375 Object getValue()
376 {
377 this.lastAccessTime = System.currentTimeMillis();
378 return this.val;
379 }
380
381 /**
382 Returns a description but the exact details of said description
383 are unspecified and subject to change. However, the following
384 may be regarded as typical:
385 <tt>name; key=value; expired=true/false</tt>
386 **/
387 public String toString()
388 {
389 return "(MemoryCache.CacheItem: Expired=" + hasExpired() + ";<" + this.key + ">=<" + this.val + ">)";
390 }
391
392 } //~inner class CacheItem
393
394 //instantiate/start only 1 reaper thread per cache. This could also have
395 //been achieved using java.util.Timer/TimerTask
396 final class CacheReaperThread extends Thread
397 {
398 volatile boolean stop = false;
399
400 public CacheReaperThread()
401 {
402 setName("CacheReaperThread->["+myName+"]");
403 setPriority(Thread.MAX_PRIORITY - 1); //does it's work asap while it's awake.
404 setDaemon(true);
405 start();
406 }
407
408 public void run()
409 {
410 log.bug("CacheReaperThread for cache:[", myName, "] started...");
411 while (! stop)
412 {
413 try {
414 synchronized(this)
415 {
416 // log.bug("reaperthread: begin sleep for: ", reapInterval, " milliseconds");
417 this.wait(reapInterval);
418 }
419 }
420 catch (InterruptedException e) {
421 //e.printStackTrace();
422 break;
423 }
424 log.bug("CacheReaperThread for cache:[", myName, "] now running reapCache()");
425 reapCache();
426 }
427 }
428
429 public void stopThread()
430 {
431 this.stop = true;
432 //might be waiting for the reapInterval duration so also interrupt the thread
433 this.interrupt();
434 }
435
436 final void reapCache()
437 {
438 log.bug("waiting for memorycache object lock");
439 synchronized (cache)
440 {
441 log.bug("acquired lock for cache, now running reap loop");
442 final Iterator it = cache.entrySet().iterator();
443 while (it.hasNext()) {
444 CacheItem citem = (CacheItem)(((Map.Entry) it.next()).getValue());
445 if (citem.hasExpired()) {
446 log.bug("Reaping: ", citem);
447 it.remove();
448 }
449 }
450 }
451 }
452
453 } //~inner class CacheReaperThread
454
455 public static void main(String[] args)
456 {
457 new Test(new Args(args));
458 }
459
460 static private class Test
461 {
462 Test(Args myargs)
463 {
464 try {
465 Cache mycache = new MemoryCache();
466 ((MemoryCache)mycache).setReapInterval(5 * 1000);
467
468 ((MemoryCache)mycache).log.setLevel(myargs.get("log", "debug"));
469
470 mycache.put("key1", "val1");
471 mycache.put(null, "val2");
472 mycache.put("key3", null);
473 mycache.put(null, null);
474 System.out.println("key1 = " + mycache.get("key1"));
475 System.out.println("key2 = " + mycache.get("key2"));
476 System.out.println("key3 = " + mycache.get("key3"));
477 int num = 10;
478 System.out.println("adding: " + num + " key,val pairs");
479 for (int n = 0; n < num; n++) {
480 mycache.put("key" + n, "val" + n);
481 }
482 System.out.println("finished adding. cache now contains the following");
483 for (int n = 0; n < num; n++) {
484 System.out.println("key"+n + "=" + mycache.get("key" + n));
485 }
486 System.out.println("Now expiring every other item");
487 for (int n = 0; n < num; n++) {
488 if (n % 2 == 0)
489 continue;
490 mycache.expire("key"+n);
491 }
492
493 System.out.println("Expire times for cache entries:");
494 for (int n = 0; n < num; n++) {
495 System.out.println("key"+n + "=" + mycache.getTimeLeft(("key"+n)));
496 }
497
498 System.out.println("Sleeping for 30 seconds...give reaper time to do it's thing.");
499 Thread.currentThread().sleep(30*1000); //give reaper thread time to work
500
501 System.out.println("Expiring finished, cache now contains the following (expired keys should return null)");
502 for (int n = 0; n < num; n++) {
503 System.out.println("key"+n + "=" + mycache.get("key" + n));
504 }
505
506 System.out.println("Expire time for 'key1' is: " + mycache.getTimeLeft("key1"));
507 System.out.println("Expire time for 'key2' is: " + mycache.getTimeLeft("key2"));
508
509 System.out.println("closing cache");
510 mycache.close();
511 System.out.println("mycache.toString() = " + mycache);
512 System.out.println("the following should throw an Exception");
513 mycache.put("foo", "bar");
514 }
515 catch (Exception e) {
516 e.printStackTrace();
517 }
518 } //~init
519 } //~class test
520
521 } //~MemoryCache