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
006package fc.util.cache;  
007
008import java.text.*;
009import java.util.*;
010import fc.io.*;
011import fc.util.*;
012
013/**
014Memory resident cache of objects. Allows to set expire times for contained
015objects after which those expired objects are removed. By default, references
016to expired objects are not retained in the cache, such that those object may
017be garbage collected as needed by the garbage collector (if those objects
018becomes unreachable from the rest of the program).
019<p>
020The size of this cache is unbounded. To use a bounded cache, use 
021the {@link BoundedCache} class. 
022<p>
023Expired items are reaped via a background high priority thread that sleeps
024for intervals between expired item checks. After every reap operation, the
025reaper thread sleeps for 120 seconds (by default). This implies that the
026minimum expire time for any item will be 120 seconds. This sleep time can
027be changed via the {@link #setReapInterval} method.
028<p>
029ThreadSafety: This class <b>is</b> thread safe and can be used by multiple 
030threads concurrently.
031
032@author   hursh jain
033@version  1.0 12/29/2001
034**/
035public final class MemoryCache implements Cache
036{
037
038/** Useful constant of 1 second (in milliseconds) */
039public static final long ONE_SEC = 1000;
040
041/** Useful constant of 1 second (in milliseconds) */
042public static final long ONE_SECOND = ONE_SEC;
043
044/** Useful constant of five seconds (in milliseconds) */
045public static final long FIVE_SEC = 5 * ONE_SEC;
046
047/** Useful constant of five seconds (in milliseconds) */
048public static final long FIVE_SECOND  = FIVE_SEC;
049
050/** Useful constant of ten seconds (in milliseconds) */
051public static final long TEN_SEC  = 10 * ONE_SEC;
052
053/** Useful constant of ten seconds (in milliseconds) */
054public static final long TEN_SECOND = TEN_SEC;
055
056/** Useful constant of 30 seconds (in milliseconds) */
057public static final long THIRTY_SEC  = 30 * ONE_SEC;
058
059/** Useful constant of 30 seconds (in milliseconds) */
060public static final long THIRTY_SECOND  = THIRTY_SEC;
061
062/** Useful constant for cache expiry time of 1 minute (in milliseconds) */
063public static final long ONE_MIN  = 1  * 60  * 1000;
064
065/** Useful constant for cache expiry time of 2 minutes (in milliseconds) */
066public static final long TWO_MIN = 2  * 60  * 1000;
067
068/** Useful constant for cache expiry time of five minutes (in milliseconds) */
069public static final long FIVE_MIN = 5  * 60  * 1000;
070
071/** Useful constant for cache expiry time of ten minutes (in milliseconds) */
072public static final long TEN_MIN = 10 * 60 * 1000;
073
074/** Useful constant for cache expiry time of thirty minutes (in milliseconds) */
075public static final long THIRTY_MIN = 30 * 60 * 1000;
076
077/** Useful constant for cache expiry time of one hour (in milliseconds) */
078public static final long ONE_HOUR = 60 * ONE_MIN;
079
080/** Useful constant for cache expiry time of two hours (in milliseconds) */
081public static final long TWO_HOUR = 2  * ONE_HOUR;
082
083/** Useful constant for cache expiry time of four hours (in milliseconds) */
084public static final long FOUR_HOUR  = 4  * ONE_HOUR;
085
086/** Useful constant for cache expiry time of eight hours (in milliseconds) */
087public static final long EIGHT_HOUR = 8  * ONE_HOUR;
088
089final   String        myName;
090final   Map         cache;
091volatile  boolean       closed = false;
092volatile  long        defaultTTL;
093volatile  long        reapInterval;
094      CacheReaperThread   reaperThread;
095final   Log         log;
096
097/**
098Instantiates 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**/
108public 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/**
122Creates a memory cache with a system-assigned logger and name and
123a default ttl of 30 minutes.
124*/
125public 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/** 
135Sets the minimum time between expired item checks.
136**/
137public void setReapInterval(final long interval) {
138  reapInterval = interval;
139  }
140
141/** 
142Sets the cache reaper thread priority. By default the reaper runs at MAX_PRIORITY - 1 but
143if there are a lot of low priority caches, setting this to a lower priority could be useful.
144**/
145public void setReapThreadPriority(final int priority) {
146  reaperThread.setPriority(priority);
147  }
148
149
150public 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
168public 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
185public Map getAll() {
186  return cache;
187  }
188
189public 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
207public 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
221public Object put(Object key, Object item)
222  {
223  Argcheck.isfalse(isClosed(), "Memory cache has been closed");
224  return put(key, item, getDefaultTTL());
225  }
226
227public 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
239public 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
251public 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/**
267Closes this cache, which makes all items in the cache unavailable. Any items
268needed 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,
271because it releases resources, including any internal threads spawned and used
272by this implementation.
273**/
274public void close()
275  {
276  reaperThread.stopThread();
277  this.closed = true;
278  log.info("*** cache:[", myName, "] closed. ***"); 
279  }
280
281/**
282Returns true if this cache has been closed, false otherwise.
283**/
284public boolean isClosed()
285  {
286  return this.closed;
287  }
288
289/**
290Sets the default TTL in milliseconds.
291*/
292public void setDefaultTTL(long millis) {
293  defaultTTL = millis;
294  }
295
296/**
297Gets the default TTL in milliseconds.
298*/
299public long getDefaultTTL() {
300  return defaultTTL;
301  }
302
303public void clear() {
304  //don't clear in the middle of reaper thread or getAll iteration
305  synchronized (cache) { 
306    cache.clear();
307    }
308  }
309
310public 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
322private class CacheItem
323{
324final Object  key;
325final Object  val;
326final long    startTime;
327    long    expire;
328    long    lastAccessTime;
329
330CacheItem(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
339boolean hasExpired() 
340  {
341  if (this.expire == -1) {
342    return false;
343    }
344  else {
345    return (calcTimeLeft() <= 0);
346    }
347  }
348
349void 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  
357void expireNow()
358  {
359  this.expire = 0;
360  }
361  
362long expireTimeLeft()
363  {
364  if (this.expire == -1) {
365    return 0;
366    }
367  else return calcTimeLeft();
368  }
369
370long calcTimeLeft()
371  {
372  return (this.expire + this.startTime) - System.currentTimeMillis();
373  }
374
375Object getValue()
376  {
377  this.lastAccessTime = System.currentTimeMillis();
378  return this.val;
379  }
380
381/**
382Returns a description but the exact details of said description
383are unspecified and subject to change. However, the following
384may be regarded as typical:
385<tt>name; key=value; expired=true/false</tt>
386**/
387public 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
396final class CacheReaperThread extends Thread
397{
398volatile boolean stop = false;
399
400public 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  
408public 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
429public 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
436final 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
455public static void main(String[] args)
456  {
457  new Test(new Args(args));
458  }
459
460static 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