001 /* 002 @license.text@ 003 */ 004 package biz.hammurapi.cache; 005 006 import java.lang.ref.ReferenceQueue; 007 import java.lang.ref.SoftReference; 008 import java.util.HashMap; 009 import java.util.HashSet; 010 import java.util.Iterator; 011 import java.util.LinkedList; 012 import java.util.Map; 013 import java.util.Set; 014 import java.util.Timer; 015 import java.util.TimerTask; 016 import java.util.WeakHashMap; 017 018 import biz.hammurapi.config.Component; 019 import biz.hammurapi.config.ConfigurationException; 020 import biz.hammurapi.metrics.MeasurementCategory; 021 import biz.hammurapi.util.Acceptor; 022 023 024 /** 025 * Memory sensitive cache. Uses soft references to cache objects. 026 * @author Pavel Vlasov 027 * @version $Revision: 1.7 $ 028 */ 029 public class MemoryCache extends AbstractProducer implements Cache, Component { 030 protected Map cache=new HashMap(); 031 protected Map reverseCache=new WeakHashMap(); 032 033 private ReferenceQueue queue=new ReferenceQueue(); 034 protected Producer producer; 035 private Cache fallBack; 036 private MeasurementCategory measurementCategory; 037 038 private void addMeasurement(String name, double value, long time) { 039 if (measurementCategory!=null) { 040 measurementCategory.addMeasurement(name, value, time); 041 } 042 } 043 044 private void addMeasurement(String name, double value) { 045 if (measurementCategory!=null) { 046 measurementCategory.addMeasurement(name, value, 0); 047 } 048 } 049 050 private class CacheReference extends SoftReference { 051 052 private Object key; 053 private long expires; 054 private long time; 055 056 /** 057 * @param referent 058 * @param q 059 * @param expires 060 */ 061 public CacheReference(Object key, Object value, long time, long expires) { 062 super(value, queue); 063 this.key=key; 064 this.expires=expires; 065 this.time=time; 066 } 067 068 /** 069 * @param string 070 * @param i 071 * @param l 072 */ 073 public void addMeasurement(String name, double value) { 074 MemoryCache.this.addMeasurement(name, value); 075 } 076 } 077 078 /** 079 * Used as entry if fallBack cache is used. 080 * @author Pavel Vlasov 081 * @version $Revision: 1.7 $ 082 */ 083 private class FallBackEntry { 084 Object value; 085 private long expires; 086 private long time; 087 private Object key; 088 089 FallBackEntry(Object key, Object value, long time, long expires) { 090 this.key=key; 091 this.value=value; 092 this.expires=expires; 093 this.time=time; 094 } 095 096 protected void finalize() throws Throwable { 097 if (fallBack.isActive() && (expires<=0 || System.currentTimeMillis()<expires)) { 098 fallBack.put(key, value, time, expires); 099 } 100 101 super.finalize(); 102 } 103 } 104 105 private static class ReferenceThread extends Thread { 106 private ReferenceQueue queue; 107 private Map cache; 108 109 private ReferenceThread(ReferenceQueue queue, Map cache) { 110 this.queue=queue; 111 this.cache=cache; 112 setDaemon(true); 113 setName("Removes cleared entries"); 114 start(); 115 } 116 117 public void run() { 118 try { 119 while (true) { 120 CacheReference cacheReference = (CacheReference) queue.remove(); 121 Object key=cacheReference.key; 122 synchronized (cache) { 123 cache.remove(key); 124 } 125 cacheReference.addMeasurement("garbage collected", 1); 126 } 127 } catch (InterruptedException e) { 128 return; 129 } 130 } 131 } 132 133 private Thread referenceThread=new ReferenceThread(queue, cache); 134 135 private static class JanitorTask extends TimerTask { 136 private Map cache; 137 private MeasurementCategory measurementCategory; 138 private Map reverseCache; 139 140 private void addMeasurement(String name, double value, long time) { 141 if (measurementCategory!=null) { 142 measurementCategory.addMeasurement(name, value, time); 143 } 144 } 145 146 private JanitorTask(Map cache, Map reverseCache, MeasurementCategory measurementCategory) { 147 this.cache=cache; 148 this.reverseCache=reverseCache; 149 this.measurementCategory=measurementCategory; 150 } 151 152 public void run() { 153 long now=System.currentTimeMillis(); 154 int expired=0; 155 synchronized (cache) { 156 Iterator it=new LinkedList(cache.keySet()).iterator(); 157 while (it.hasNext()) { 158 Object key=it.next(); 159 CacheReference cacheReference = (CacheReference) cache.get(key); 160 long expires = (cacheReference).expires; 161 if (expires>0 && expires<now) { 162 cache.remove(key); 163 reverseCache.remove(cacheReference.get()); 164 expired++; 165 } 166 } 167 addMeasurement("size", cache.size(), now); 168 } 169 170 if (expired>0) { 171 addMeasurement("expired", expired, now); 172 } 173 } 174 } 175 176 private TimerTask janitorTask; 177 178 /** 179 * Default cache cleanup interval. 180 */ 181 public static final long CLEANUP_INTERVAL=60000; 182 183 /** 184 * Constructs cache with default cleanup interval (1 minute). 185 * @param producer Producer 186 * @param fallBack Fallback cache 187 * @param measurementCategory Measurement category to report cache statistics 188 */ 189 public MemoryCache(Producer producer, Cache fallBack, MeasurementCategory measurementCategory) { 190 this(producer, fallBack, measurementCategory, null, CLEANUP_INTERVAL); 191 } 192 193 /** 194 * Constructs cache with default cleanup interval (1 minute). 195 * @param producer Producer 196 * @param fallBack Fallback cache 197 * @param measurementCategory Measurement category to report cache statistics 198 * @param cleanupInterval Interval between removals of expired entries. 199 * @param timer Timer which will invoke cleanup tasks, if it is null then a new timer is created internally. 200 * Use this parameter to share timers between multiple caches in order to reduce number of threads in the 201 * application. 202 */ 203 public MemoryCache(Producer producer, Cache fallBack, MeasurementCategory measurementCategory, Timer timer, long cleanupInterval) { 204 super(); 205 206 this.producer=producer; 207 if (producer!=null) { 208 producer.addCache(this); 209 } 210 211 this.fallBack=fallBack; 212 if (fallBack!=null) { 213 addCache(fallBack); 214 } 215 216 this.measurementCategory=measurementCategory; 217 this.timer=timer; 218 219 this.cleanupInterval=cleanupInterval; 220 } 221 222 private boolean isOwnTimer; 223 private Timer timer; 224 private long cleanupInterval; 225 226 public void put(Object key, Object value, long time, long expirationTime) { 227 checkShutdown(); 228 synchronized (cache) { 229 cache.put(key, new CacheReference(key, toValue(key, value, time, expirationTime), time, expirationTime)); 230 reverseCache.put(value, key); 231 addMeasurement("put", 1, time); 232 } 233 } 234 235 /** 236 * @param key 237 * @param value 238 * @param expirationTime 239 * @return 240 */ 241 private Object toValue(Object key, Object value, long time, long expirationTime) { 242 return fallBack==null ? value : new FallBackEntry(key, value, time, expirationTime); 243 } 244 245 private Object fromValue(Object object) { 246 if (object instanceof FallBackEntry) { 247 return ((FallBackEntry) object).value; 248 } 249 return object; 250 } 251 252 public Entry get(Object key) { 253 checkShutdown(); 254 synchronized (cache) { 255 CacheReference cr=(CacheReference) cache.get(key); 256 long now = System.currentTimeMillis(); 257 addMeasurement("get", 1, now); 258 boolean doFallBack=true; 259 if (cr!=null) { 260 if ((cr.expires>0 && cr.expires<now) || cr.get()==null) { 261 cache.remove(key); 262 reverseCache.remove(fromValue(cr.get())); 263 doFallBack=false; 264 } else { 265 final Object o=fromValue(cr.get()); 266 final long et=cr.expires; 267 final long time=cr.time; 268 return new Entry() { 269 270 public long getExpirationTime() { 271 return et; 272 } 273 274 public long getTime() { 275 return time; 276 } 277 278 public Object get() { 279 return o; 280 } 281 282 }; 283 } 284 } 285 286 if (doFallBack && fallBack!=null) { 287 Entry entry = fallBack.get(key); 288 if (entry!=null) { 289 cache.put(key, new CacheReference(key, toValue(key, entry.get(), entry.getTime(), entry.getExpirationTime()), entry.getTime(), entry.getExpirationTime())); 290 reverseCache.put(entry.get(), key); 291 return entry; 292 } 293 } 294 295 if (key instanceof ProducingKey) { 296 Entry entry = ((ProducingKey) key).get(); 297 cache.put(key, new CacheReference(key, toValue(key, entry.get(), entry.getTime(), entry.getExpirationTime()), entry.getTime(), entry.getExpirationTime())); 298 reverseCache.put(entry.get(),key); 299 addMeasurement("produce", 1, now); 300 return entry; 301 } 302 303 if (producer!=null) { 304 Entry entry = producer.get(key); 305 cache.put(key, new CacheReference(key, toValue(key, entry.get(), entry.getTime(), entry.getExpirationTime()), entry.getTime(), entry.getExpirationTime())); 306 reverseCache.put(entry.get(),key); 307 addMeasurement("produce", 1, now); 308 return entry; 309 } 310 311 return null; 312 } 313 } 314 315 public void clear() { 316 synchronized (cache) { 317 cache.clear(); 318 reverseCache.clear(); 319 320 if (fallBack!=null) { 321 fallBack.clear(); 322 } 323 } 324 } 325 326 public void remove(Object key) { 327 synchronized (cache) { 328 CacheReference cr = (CacheReference) cache.remove(key); 329 if (cr!=null) { 330 reverseCache.remove(fromValue(cr.get())); 331 } 332 } 333 addMeasurement("remove", 1); 334 onRemove(key); 335 } 336 337 public void remove(Acceptor acceptor) { 338 synchronized (cache) { 339 Iterator it=cache.keySet().iterator(); 340 int removed=0; 341 while (it.hasNext()) { 342 Object key=it.next(); 343 if (acceptor.accept(key)) { 344 Entry entry=(Entry) cache.get(key); 345 if (entry!=null) { 346 reverseCache.remove(fromValue(entry.get())); 347 } 348 it.remove(); 349 removed++; 350 onRemove(key); 351 } 352 } 353 if (removed>0) { 354 addMeasurement("remove", removed); 355 } 356 } 357 } 358 359 private boolean shutDown=false; 360 361 private void checkShutdown() { 362 if (shutDown) { 363 throw new IllegalStateException("Shut down"); 364 } 365 } 366 367 public void stop() { 368 if (!shutDown) { 369 cache.clear(); 370 reverseCache.clear(); 371 referenceThread.interrupt(); 372 if (janitorTask!=null) { 373 janitorTask.cancel(); 374 } 375 if (isOwnTimer && timer!=null) { 376 timer.cancel(); 377 } 378 shutDown=true; 379 } 380 } 381 382 protected void finalize() throws Throwable { 383 if (isActive()) { 384 stop(); 385 } 386 super.finalize(); 387 } 388 389 public Set keySet() { 390 HashSet ret = new HashSet(); 391 synchronized (cache) { 392 ret.addAll(cache.keySet()); 393 } 394 395 if (producer!=null) { 396 Set pkeys=producer.keySet(); 397 if (pkeys!=null) { 398 ret.addAll(pkeys); 399 } 400 } 401 402 if (fallBack!=null) { 403 Set fkeys=fallBack.keySet(); 404 if (fkeys!=null) { 405 ret.addAll(fkeys); 406 } 407 } 408 409 return ret; 410 } 411 412 public boolean isActive() { 413 return !shutDown; 414 } 415 416 /** 417 * Creates timer if neccessary, creates cleanup task and schedules it. 418 */ 419 public void start() throws ConfigurationException { 420 if (timer==null) { 421 timer=new Timer(true); 422 isOwnTimer=true; 423 } 424 425 janitorTask=new JanitorTask(cache, reverseCache, measurementCategory); 426 timer.schedule(janitorTask, cleanupInterval, cleanupInterval); 427 } 428 429 public void setOwner(Object owner) { 430 // TODO Nothing 431 } 432 } 433 434