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 (producer!=null) {
296 Entry entry = producer.get(key);
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 return null;
304 }
305 }
306
307 public void clear() {
308 synchronized (cache) {
309 cache.clear();
310 reverseCache.clear();
311
312 if (fallBack!=null) {
313 fallBack.clear();
314 }
315 }
316 }
317
318 public void remove(Object key) {
319 synchronized (cache) {
320 CacheReference cr = (CacheReference) cache.remove(key);
321 if (cr!=null) {
322 reverseCache.remove(fromValue(cr.get()));
323 }
324 }
325 addMeasurement("remove", 1);
326 onRemove(key);
327 }
328
329 public void remove(Acceptor acceptor) {
330 synchronized (cache) {
331 Iterator it=cache.keySet().iterator();
332 int removed=0;
333 while (it.hasNext()) {
334 Object key=it.next();
335 if (acceptor.accept(key)) {
336 Entry entry=(Entry) cache.get(key);
337 if (entry!=null) {
338 reverseCache.remove(fromValue(entry.get()));
339 }
340 it.remove();
341 removed++;
342 onRemove(key);
343 }
344 }
345 if (removed>0) {
346 addMeasurement("remove", removed);
347 }
348 }
349 }
350
351 private boolean shutDown=false;
352
353 private void checkShutdown() {
354 if (shutDown) {
355 throw new IllegalStateException("Shut down");
356 }
357 }
358
359 public void stop() {
360 if (!shutDown) {
361 cache.clear();
362 reverseCache.clear();
363 referenceThread.interrupt();
364 if (janitorTask!=null) {
365 janitorTask.cancel();
366 }
367 if (isOwnTimer && timer!=null) {
368 timer.cancel();
369 }
370 shutDown=true;
371 }
372 }
373
374 protected void finalize() throws Throwable {
375 if (isActive()) {
376 stop();
377 }
378 super.finalize();
379 }
380
381 public Set keySet() {
382 HashSet ret = new HashSet();
383 synchronized (cache) {
384 ret.addAll(cache.keySet());
385 }
386
387 if (producer!=null) {
388 Set pkeys=producer.keySet();
389 if (pkeys!=null) {
390 ret.addAll(pkeys);
391 }
392 }
393
394 if (fallBack!=null) {
395 Set fkeys=fallBack.keySet();
396 if (fkeys!=null) {
397 ret.addAll(fkeys);
398 }
399 }
400
401 return ret;
402 }
403
404 public boolean isActive() {
405 return !shutDown;
406 }
407
408 /**
409 * Creates timer if neccessary, creates cleanup task and schedules it.
410 */
411 public void start() throws ConfigurationException {
412 if (timer==null) {
413 timer=new Timer(true);
414 isOwnTimer=true;
415 }
416
417 janitorTask=new JanitorTask(cache, reverseCache, measurementCategory);
418 timer.schedule(janitorTask, cleanupInterval, cleanupInterval);
419 }
420
421 public void setOwner(Object owner) {
422 // TODO Nothing
423 }
424 }
425
426