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