View Javadoc
1   /*
2    * $Source$
3    * $Revision$
4    *
5    * Copyright (C) 2000 William Chesters
6    *
7    * Part of Melati (http://melati.org), a framework for the rapid
8    * development of clean, maintainable web applications.
9    *
10   * Melati is free software; Permission is granted to copy, distribute
11   * and/or modify this software under the terms either:
12   *
13   * a) the GNU General Public License as published by the Free Software
14   *    Foundation; either version 2 of the License, or (at your option)
15   *    any later version,
16   *
17   *    or
18   *
19   * b) any version of the Melati Software License, as published
20   *    at http://melati.org
21   *
22   * You should have received a copy of the GNU General Public License and
23   * the Melati Software License along with this program;
24   * if not, write to the Free Software Foundation, Inc.,
25   * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
26   * GNU General Public License and visit http://melati.org to obtain the
27   * Melati Software License.
28   *
29   * Feel free to contact the Developers of Melati (http://melati.org),
30   * if you would like to work out a different arrangement than the options
31   * outlined here.  It is our intention to allow Melati to be used by as
32   * wide an audience as possible.
33   *
34   * This program is distributed in the hope that it will be useful,
35   * but WITHOUT ANY WARRANTY; without even the implied warranty of
36   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
37   * GNU General Public License for more details.
38   *
39   * Contact details for copyright holder:
40   *
41   *     William Chesters <williamc At paneris.org>
42   *     http://paneris.org/~williamc
43   *     Obrechtstraat 114, 2517VX Den Haag, The Netherlands
44   */
45  
46  package org.melati.poem.util;
47  
48  import java.lang.ref.ReferenceQueue;
49  import java.lang.ref.SoftReference;
50  import java.util.Enumeration;
51  import java.util.Hashtable;
52  import java.util.Vector;
53  
54  import org.melati.poem.PoemException;
55  
56  /**
57   * A store whose capacity has a guaranteed lower limit but whose upper limit 
58   * is bounded by the amount of memory available to the JVM. 
59   * The lower limit can be adjusted after cache creation.  
60   * 
61   * Elements added to the cache once the cache size exceeds the lower limit 
62   * (the cache is full) force the least recently used (LRU) item off the held list.
63   * Items which are removed from the held list are not deleted but 
64   * converted to soft references: available to be retrieved if they 
65   * are not garbage collected first.  
66   * 
67   * The cache cannot contain nulls, as there would be no mechanism to 
68   * distinguish between a held null value and an unheld value.
69   * 
70   * There is no mechanism for invalidating the cache, reducing its size 
71   * to zero mearly allows all its members to be garbage collected.
72   * 
73   * Ideally, having been created, objects remain in the cache; however this  
74   * is impracticable when the possible size of the sum of cached objects 
75   * exceeds available memory. 
76   * 
77   * The performance of the cache depends upon the pattern of accesses to it. 
78   * An LRU cache such as this assumes a normal distribution of accesses.
79   *  
80   * The state of the cache can be monitored by using the reporting 
81   * objects, see for example org.melati.admin.Status.
82   * 
83   */
84  public final class Cache {
85  
86   /** A <code>key:value</code> pair. */
87    private interface Node {
88      /** The key. */
89      Object key();
90      /** The value. */
91      Object value();
92    }
93  
94   /** A <code>node</code> in the cache, which is guaranteed to be there. */
95    private static class HeldNode implements Node {
96      Object key;
97      Object value;
98      HeldNode nextMRU = null; // Next Most Recently Used node
99      HeldNode prevMRU = null; // Previous Most Recently Used node
100 
101     HeldNode(Object key, Object value) {
102       this.key = key;
103       this.value = value;
104     }
105 
106     synchronized void putBefore(HeldNode nextMRU_P) {
107 
108       //
109       // Before:
110       //
111       //   11 -A-> 00 -B-> 22      33 -E-> 44
112       //   11 <-C- 00 <-D- 22      33 <-F- 44
113       //
114       // After:
115       //
116       //   11 -G-> 22              33 -I-> 00 -J-> 44
117       //   11 <-H- 22              33 <-K- 00 <-L- 44
118       //
119       // What has to happen:
120       //
121       //   A => G if 1 exists
122       //   B => J
123       //   C => K
124       //   D => H if 2 exists
125       //   E => I if 3 exists
126       //   F => L if 4 exists
127       //
128 
129       if (this.nextMRU != null)                   // 2 exists
130         this.nextMRU.prevMRU = prevMRU;           // D => H using C
131 
132       if (prevMRU != null)                        // 1 exists
133         prevMRU.nextMRU = this.nextMRU;           // A => G using B
134 
135       if (nextMRU_P != null) {                    // 4 exists
136         if (nextMRU_P.prevMRU != null)            // 3 exists 
137           // this should never happen as nextMRU_P is always null or 
138           // theMRU which always has a null prevMRU  
139           nextMRU_P.prevMRU.nextMRU = this;       // E => I
140         prevMRU = nextMRU_P.prevMRU;              // C => K using F
141         nextMRU_P.prevMRU = this;                 // F => L
142       }
143       else
144         prevMRU = null;                           // C => K
145       this.nextMRU = nextMRU_P;                   // B => J
146     }
147 
148     /**
149      * {@inheritDoc}
150      * @see org.melati.poem.util.Cache.Node#key()
151      */
152     public Object key() {
153       return key;
154     }
155 
156     /**
157      * {@inheritDoc}
158      * @see org.melati.poem.util.Cache.Node#value()
159      */
160     public Object value() {
161       return value;
162     }
163 
164     /**
165      * {@inheritDoc}
166      * @see java.lang.Object#toString()
167      */
168     public String toString() {
169       StringBuffer ret = new StringBuffer();
170       if(prevMRU == null) 
171         ret.append("null");
172       else 
173         ret.append(prevMRU.key());
174       ret.append(">>"+ key + "=" + value + ">>");
175       if(nextMRU == null) 
176         ret.append("null");
177       else 
178         ret.append(nextMRU.key());
179       return  ret.toString();
180     }
181     
182     
183   }
184 
185  /** A {@link SoftReference} node; that is one which can be reclaimed
186   *  by the Garbage Collector before it throws an out of memory error. 
187   */
188   private static class DroppedNode extends SoftReference<Object> implements Node {
189 
190     Object key;
191 
192     /**
193      * Constructor.
194      * @param key cache key
195      * @param value Cache object
196      * @param queue reference queue
197      */
198     DroppedNode(Object key, Object value, ReferenceQueue<Object> queue) {
199       super(value, queue);
200       this.key = key;
201     }
202 
203     /**
204      * {@inheritDoc}
205      * @see org.melati.poem.util.Cache.Node#key()
206      */
207     public Object key() {
208       return key;
209     }
210 
211     /**
212      * {@inheritDoc}
213      * @see org.melati.poem.util.Cache.Node#value()
214      */
215     public Object value() {
216       return this.get();
217     }
218   }
219 
220   private Hashtable<Object, Object> table = new Hashtable<Object, Object>();
221   private HeldNode theMRU = null, theLRU = null;
222   private int heldNodes = 0;
223   private int maxSize;
224   private int collectedEver = 0;
225 
226   private ReferenceQueue<Object> collectedValuesQueue = new ReferenceQueue<Object>();
227 
228   // invariants:
229   //   if theMRU != null, theMRU.prevMRU == null
230   //   if theLRU != null, theLRU.nextMRU == null
231   //   following theMRU gives you the same elements as following theLRU
232   //       in the opposite order
233   //   everything in the[ML]RU is in table as a HeldNode, and visa versa.
234   //   heldNodes == length of the[ML]RU
235 
236   /**
237    * Thrown if one or more problems are discovered with cache consistency.
238    */
239   public class InconsistencyException extends PoemException {
240 
241     /**serialVersionUID */
242     private static final long serialVersionUID = 1832694552964508864L;
243     
244     public Vector<Object> problems;
245 
246     /** Constructor. */
247     public InconsistencyException(Vector<Object> probs) {
248       this.problems = probs;
249     }
250 
251     /**
252      * {@inheritDoc}
253      */
254     public String getMessage() {
255       return EnumUtils.concatenated("\n", problems.elements());
256     }
257   }
258 
259   /**
260    * @return a Vector of problematic nodes 
261    */
262   private Vector<Object> invariantBreaches() {
263     Vector<Object> probs = new Vector<Object>();
264 
265     if (theMRU != null && theMRU.prevMRU != null)
266       probs.addElement("theMRU.prevMRU == " + theMRU.prevMRU);
267     if (theLRU != null && theLRU.nextMRU != null)
268       probs.addElement("theLRU.nextMRU == " + theLRU.nextMRU);
269 
270     Object[] held = new Object[heldNodes];
271     Hashtable<HeldNode, Boolean> heldHash = new Hashtable<HeldNode, Boolean>();
272 
273     int countML = 0;
274     for (HeldNode n = theMRU; n != null; n = n.nextMRU, ++countML) {
275       if (table.get(n.key()) != n)
276         probs.addElement("MRU list check: table.get(" + n + ".key()) == " + table.get(n.key()));
277       if (countML < heldNodes)
278         held[countML] = n;
279       heldHash.put(n, Boolean.TRUE);
280     }
281 
282     if (countML != heldNodes)
283       probs.addElement(countML + " nodes in MRU->LRU not " + heldNodes);
284 
285     Hashtable<Object,HeldNode> keys = new Hashtable<Object,HeldNode>();
286     int countLM = 0;
287     for (HeldNode n = theLRU; n != null; n = n.prevMRU, ++countLM) {
288       HeldNode oldn = (HeldNode)keys.get(n.key());
289       if (oldn != null)
290         probs.addElement("key " + n.key() + " duplicated in " + n + " and " +
291                          oldn);
292       keys.put(n.key(), n);
293 
294       if (table.get(n.key()) != n)
295         probs.addElement("LRU list check: table.get(" + n + ".key()) == " + table.get(n.key()));
296       if (countLM < heldNodes) {
297         int o = heldNodes - (1 + countLM);
298         if (n != held[o])
299           probs.addElement("lm[" + countLM + "] == " + n + " != ml[" +
300                            o + "] == " + held[o]);
301       }
302     }
303 
304     for (Enumeration<Object> nodes = table.elements(); nodes.hasMoreElements();) {
305       Node n = (Node)nodes.nextElement();
306       if (n instanceof HeldNode && !heldHash.containsKey(n))
307         probs.addElement(n + " in table but not MRU->LRU");
308     }
309 
310     if (countLM != heldNodes)
311       probs.addElement(countLM + " nodes in LRU->MRU not " + heldNodes);
312 
313     return probs;
314   }
315 
316   /**
317    * This is too costly to run in production.
318    */
319   private void assertInvariant() {
320     boolean debug = false;
321     if (debug) { 
322       Vector<Object> probs = invariantBreaches();
323       if (probs.size() != 0) {
324        throw new InconsistencyException(probs);
325       }
326     }
327   }
328 
329   /**
330    * Constructor with maximum size.
331    * @param maxSize maximum size cache may grow to 
332    */
333   public Cache(int maxSize) {
334     setSize(maxSize);
335   }
336 
337   /**
338    * Set maximum size of Cache.
339    * @param maxSize maximum size cache may grow to 
340    */
341   public void setSize(int maxSize) {
342     if (maxSize < 0)
343       throw new IllegalArgumentException();
344     this.maxSize = maxSize;
345   }
346 
347   /**
348    * Get maximum size of Cache.
349    */
350   public int getSize() {
351     return maxSize;
352   }
353 
354   /**
355    * Check whether the garbage collector has actually reclaimed any of 
356    * our {@link SoftReference}s, should it have done so it will have 
357    * placed the reference on the collected queue, this entry is then 
358    * removed from the backing store.
359    */
360   private synchronized void checkForGarbageCollection() {
361     DroppedNode collected;
362     while ((collected = (DroppedNode)collectedValuesQueue.poll()) != null) {
363       table.remove(collected.key());
364       ++collectedEver;
365     }
366   }
367 
368   /**
369    * Reduce number of units held in the cache, without changing 
370    * its size.
371    * 
372    * This is intended for cache maintenance, enabling only the 
373    * most frequently used items to remain in the cache, whilst 
374    * the others are dropped; where dropped means converted to a soft reference. 
375    * 
376    * @param maxSize_P maximum number of units to hold 
377    */
378   public synchronized void trim(int maxSize_P) {
379     checkForGarbageCollection();  
380 
381     HeldNode n = theLRU;
382     while (n != null && heldNodes > maxSize_P) {
383       HeldNode nn = n.prevMRU;
384       n.putBefore(null);
385       table.put(n.key, new DroppedNode(n.key, n.value, collectedValuesQueue));
386       --heldNodes;
387       n = nn;
388     }
389 
390     if (n == null) {
391       theLRU = null;
392       theMRU = null;
393     } else
394       theLRU = n;
395 
396     assertInvariant();
397   }
398 
399   /**
400    * Remove from cache.
401    * 
402    * If key is not in the cache as a HeldNode then does nothing.
403    * 
404    * @param key cache key field
405    */
406   public synchronized void delete(Object key) {
407     Node n = (Node)table.get(key);
408     if (n == null)
409       return;
410 
411     if (n instanceof HeldNode) {
412       HeldNode h = (HeldNode)n;
413 
414       if (theLRU == h)
415         theLRU = h.prevMRU;
416       if (theMRU == h)
417         theMRU = h.nextMRU;
418 
419       h.putBefore(null);
420 
421       --heldNodes;
422     }
423 
424     table.remove(key);
425 
426     assertInvariant();
427   }
428 
429   /**
430    * Add an Object to the cache. 
431    * 
432    * @param key the Object to use as a lookup
433    * @param value the Object to put in the cache
434    */
435   public synchronized void put(Object key, Object value) {
436     if (key == null || value == null)
437       throw new NullPointerException();
438 
439     trim(maxSize);
440 
441     if (maxSize == 0)  {
442       table.put(key, new DroppedNode(key, value, collectedValuesQueue));
443     } else {
444       HeldNode node = new HeldNode(key, value);
445 
446       Object previous = table.put(key, node);
447       if (previous != null) {
448         // Return cache to previous state
449         table.put(key, previous);
450         throw new CacheDuplicationException("Key already in cache for key=" + key + ", value="+value);
451       }
452 
453       node.putBefore(theMRU);
454       theMRU = node;
455       if (theLRU == null) theLRU = node;
456 
457       ++heldNodes;
458 
459       assertInvariant();
460     }
461   }
462 
463   /**
464    * Return an object from the Cache, null if not found.
465    * @param key the cache key
466    * @return the object or null if not in cache
467    */
468   public synchronized Object get(Object key) {
469 
470     checkForGarbageCollection();
471 
472     Node node = (Node)table.get(key);
473     if (node == null)
474       return null;
475     else {
476       HeldNode held;
477       if (node instanceof HeldNode) {
478         held = (HeldNode)node;
479         if (held != theMRU) {
480           if (held == theLRU)
481             theLRU = held.prevMRU;
482           held.putBefore(theMRU);
483           theMRU = held;
484         }
485       } else {  // It is a DroppedNode ie SoftReference - which may be mangled
486         if (node.value() == null)   
487           // This seems actually to happen
488           // which means that the value has been collected since
489           // the call to checkForGarbageCollection() above
490           return null;
491         held = new HeldNode(key, node.value());
492         table.put(key, held);
493         ++heldNodes;
494         held.putBefore(theMRU);
495         theMRU = held;
496         if (theLRU == null)
497           theLRU = held;
498         trim(maxSize);
499       }
500 
501       assertInvariant();
502       return held.value;
503     }
504   }
505 
506   /**
507    * Apply function to all items in cache, only ignoring items 
508    * where the value has been garbage collected.
509    * 
510    * @param f Procedure to apply to all members of cache
511    */
512   public synchronized void iterate(Procedure f) {
513     checkForGarbageCollection();
514     for (Enumeration<Object> n = table.elements(); n.hasMoreElements();) {
515       Object value = ((Node)n.nextElement()).value();
516       if (value != null) 
517         // Value could conceivably have been nulled since call to checkForGarbageCollection above
518         f.apply(value);
519     }
520   }
521 
522   /**
523    * Report on the status of the cache.
524    * @return  an Enumeration of report lines
525    */
526   public Enumeration<Object> getReport() {
527     return new ConsEnumeration<Object>("" + 
528         maxSize + " maxSize, " + 
529         theMRU + " theMRU, " +
530         theLRU + " theLRU, " + 
531         collectedEver + " collectedEver",
532         new ConsEnumeration<Object>(
533                heldNodes + " held, " + table.size() + " total ",
534                invariantBreaches().elements()));
535   }
536 
537   /**
538    * A class which enables reporting upon the state of the <code>Cache</code>.
539    */
540   public final class Info {
541 
542     private Info() {}
543 
544     /**
545      * @return an Enumeration of objects held
546      */
547     public Enumeration<Object> getHeldElements() {
548       checkForGarbageCollection();
549       return new MappedEnumeration<Object, Object>(
550           new FilteredEnumeration<Object>(table.elements()) {
551             public boolean isIncluded(Object o) {
552               return o instanceof HeldNode;
553             }
554           }) {
555         public Object mapped(Object o) {
556           return ((Node)o).value();
557         }
558       };
559     }
560 
561     /**
562      * @return an Enumeration of elements dropped from the cache
563      */
564     public Enumeration<Object> getDroppedElements() {
565       checkForGarbageCollection();
566       return new MappedEnumeration<Object, Object>(
567           new FilteredEnumeration<Object>(table.elements()) {
568             public boolean isIncluded(Object o) {
569               return o instanceof DroppedNode;
570             }
571           }) {
572         public Object mapped(Object o) {
573           return ((Node)o).value();
574         }
575       };
576     }
577 
578     /**
579      * @return the report 
580      */
581     public Enumeration<Object> getReport() {
582       return Cache.this.getReport();
583     }
584     
585     
586   }
587 
588   /**
589    * @return a new Info object
590    */
591   public Info getInfo() {
592     return new Info();
593   }
594 
595   /**
596    * Dump to Syserr.
597    */
598   public void dumpAnalysis() {
599     for (Enumeration<Object> l = getReport(); l.hasMoreElements();)
600       System.err.println(l.nextElement());
601   }
602   
603   /**
604    * Output to syserr.
605    */
606   public void dump() { 
607     System.err.println("Keys: " + table.size());
608     System.err.println("maxSize: " + maxSize);
609     System.err.println("theMRU: " + theMRU);
610     System.err.println("theLRU: " + theLRU);
611     System.err.println("heldNodes: " + heldNodes);
612     System.err.println("collectedEver: " + collectedEver);
613     Enumeration<Object> e = table.keys();
614     while(e.hasMoreElements()) { 
615       Object k = e.nextElement();
616       System.err.print(k );
617       System.err.print(" : ");
618       System.err.println(table.get(k) );
619     }
620   }
621 }
622 
623 
624 
625