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