View Javadoc

1   package org.apache.jcs.utils.struct;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.Serializable;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashSet;
26  import java.util.Hashtable;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.NoSuchElementException;
31  import java.util.Set;
32  
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.apache.jcs.engine.control.group.GroupAttrName;
36  import org.apache.jcs.engine.stats.StatElement;
37  import org.apache.jcs.engine.stats.Stats;
38  import org.apache.jcs.engine.stats.behavior.IStatElement;
39  import org.apache.jcs.engine.stats.behavior.IStats;
40  
41  /**
42   * This is a simple LRUMap. It implements most of the map methods. It is not recommended that you
43   * use any but put, get, remove, and clear.
44   * <p>
45   * Children can implement the processRemovedLRU method if they want to handle the removal of the
46   * lest recently used item.
47   * <p>
48   * This class was abstracted out of the LRU Memory cache. Put, remove, and get should be thread
49   * safe. It uses a hashtable and our own double linked list.
50   * <p>
51   * Locking is done on the instance.
52   * <p>
53   * @author aaron smuts
54   */
55  public class LRUMap<K, V>
56      implements Map<K, V>
57  {
58      /** The logger */
59      private final static Log log = LogFactory.getLog( LRUMap.class );
60  
61      /** double linked list for lru */
62      private final DoubleLinkedList<LRUElementDescriptor<K, V>> list;
63  
64      /** Map where items are stored by key. */
65      protected Map<K, LRUElementDescriptor<K, V>> map;
66  
67      /** stats */
68      int hitCnt = 0;
69  
70      /** stats */
71      int missCnt = 0;
72  
73      /** stats */
74      int putCnt = 0;
75  
76      /** if the max is less than 0, there is no limit! */
77      int maxObjects = -1;
78  
79      /** make configurable */
80      private int chunkSize = 1;
81  
82      /**
83       * This creates an unbounded version. Setting the max objects will result in spooling on
84       * subsequent puts.
85       */
86      public LRUMap()
87      {
88          list = new DoubleLinkedList<LRUElementDescriptor<K, V>>();
89  
90          // normal hshtable is faster for
91          // sequential keys.
92          map = new Hashtable<K, LRUElementDescriptor<K, V>>();
93          // map = new ConcurrentHashMap();
94      }
95  
96      /**
97       * This sets the size limit.
98       * <p>
99       * @param maxObjects
100      */
101     public LRUMap( int maxObjects )
102     {
103         this();
104         this.maxObjects = maxObjects;
105     }
106 
107     /**
108      * This simply returned the number of elements in the map.
109      * <p>
110      * @see java.util.Map#size()
111      */
112     public int size()
113     {
114         return map.size();
115     }
116 
117     /**
118      * This removes all the items. It clears the map and the double linked list.
119      * <p>
120      * @see java.util.Map#clear()
121      */
122     public void clear()
123     {
124         map.clear();
125         list.removeAll();
126     }
127 
128     /**
129      * Returns true if the map is empty.
130      * <p>
131      * @see java.util.Map#isEmpty()
132      */
133     public boolean isEmpty()
134     {
135         return map.size() == 0;
136     }
137 
138     /**
139      * Returns true if the map contains an element for the supplied key.
140      * <p>
141      * @see java.util.Map#containsKey(java.lang.Object)
142      */
143     public boolean containsKey( Object key )
144     {
145         return map.containsKey( key );
146     }
147 
148     /**
149      * This is an expensive operation that determines if the object supplied is mapped to any key.
150      * <p>
151      * @see java.util.Map#containsValue(java.lang.Object)
152      */
153     public boolean containsValue( Object value )
154     {
155         return map.containsValue( value );
156     }
157 
158     /**
159      * @return map.values();
160      */
161     public Collection<V> values()
162     {
163         List<V> valueList = new ArrayList<V>(map.size());
164 
165         for (LRUElementDescriptor<K, V> value : map.values())
166         {
167             valueList.add(value.getPayload());
168         }
169 
170         return valueList;
171     }
172 
173     /**
174      * @param source
175      */
176     public void putAll( Map<? extends K, ? extends V> source )
177     {
178         if ( source != null )
179         {
180             for (Map.Entry<? extends K, ? extends V> entry : source.entrySet())
181             {
182                 this.put( entry.getKey(), entry.getValue() );
183             }
184         }
185     }
186 
187     /**
188      * @param key
189      * @return Object
190      */
191     public V get( Object key )
192     {
193         V retVal = null;
194 
195         if ( log.isDebugEnabled() )
196         {
197             log.debug( "getting item  for key " + key );
198         }
199 
200         LRUElementDescriptor<K, V> me = map.get( key );
201 
202         if ( me != null )
203         {
204             hitCnt++;
205             if ( log.isDebugEnabled() )
206             {
207                 log.debug( "LRUMap hit for " + key );
208             }
209 
210             retVal = me.getPayload();
211 
212             list.makeFirst( me );
213         }
214         else
215         {
216             missCnt++;
217             log.debug( "LRUMap miss for " + key );
218         }
219 
220         // verifyCache();
221         return retVal;
222     }
223 
224     /**
225      * This gets an element out of the map without adjusting it's posisiton in the LRU. In other
226      * words, this does not count as being used. If the element is the last item in the list, it
227      * will still be the last itme in the list.
228      * <p>
229      * @param key
230      * @return Object
231      */
232     public V getQuiet( Object key )
233     {
234         V ce = null;
235 
236         LRUElementDescriptor<K, V> me = map.get( key );
237         if ( me != null )
238         {
239             if ( log.isDebugEnabled() )
240             {
241                 log.debug( "LRUMap quiet hit for " + key );
242             }
243 
244             ce = me.getPayload();
245         }
246         else if ( log.isDebugEnabled() )
247         {
248             log.debug( "LRUMap quiet miss for " + key );
249         }
250 
251         return ce;
252     }
253 
254     /**
255      * @param key
256      * @return Object removed
257      */
258     public V remove( Object key )
259     {
260         if ( log.isDebugEnabled() )
261         {
262             log.debug( "removing item for key: " + key );
263         }
264 
265         // remove single item.
266         LRUElementDescriptor<K, V> me = map.remove( key );
267 
268         if ( me != null )
269         {
270             list.remove( me );
271             return me.getPayload();
272         }
273 
274         return null;
275     }
276 
277     /**
278      * @param key
279      * @param value
280      * @return Object
281      */
282     public V put(K key, V value)
283     {
284         putCnt++;
285 
286         LRUElementDescriptor<K, V> old = null;
287         synchronized ( this )
288         {
289             // TODO address double synchronization of addFirst, use write lock
290             addFirst( key, value );
291             // this must be synchronized
292             old = map.put(list.getFirst().getKey(), list.getFirst());
293 
294             // If the node was the same as an existing node, remove it.
295             if ( old != null && list.getFirst().getKey().equals(old.getKey()))
296             {
297                 list.remove( old );
298             }
299         }
300 
301         int size = map.size();
302         // If the element limit is reached, we need to spool
303 
304         if ( this.maxObjects >= 0 && size > this.maxObjects )
305         {
306             if ( log.isDebugEnabled() )
307             {
308                 log.debug( "In memory limit reached, removing least recently used." );
309             }
310 
311             // Write the last 'chunkSize' items to disk.
312             int chunkSizeCorrected = Math.min( size, getChunkSize() );
313 
314             if ( log.isDebugEnabled() )
315             {
316                 log.debug( "About to remove the least recently used. map size: " + size + ", max objects: "
317                     + this.maxObjects + ", items to spool: " + chunkSizeCorrected );
318             }
319 
320             // The spool will put them in a disk event queue, so there is no
321             // need to pre-queue the queuing. This would be a bit wasteful
322             // and wouldn't save much time in this synchronous call.
323 
324             for ( int i = 0; i < chunkSizeCorrected; i++ )
325             {
326                 synchronized ( this )
327                 {
328                     if ( list.getLast() != null )
329                     {
330                         if (list.getLast() != null )
331                         {
332                             processRemovedLRU(list.getLast().getKey(), list.getLast().getPayload());
333                             if ( !map.containsKey(list.getLast().getKey()))
334                             {
335                                 log.error( "update: map does not contain key: "
336                                     + list.getLast().getKey() );
337                                 verifyCache();
338                             }
339                             if ( map.remove( list.getLast().getKey() ) == null )
340                             {
341                                 log.warn( "update: remove failed for key: "
342                                     + list.getLast().getKey() );
343                                 verifyCache();
344                             }
345                         }
346                         else
347                         {
348                             throw new Error( "update: last.ce is null!" );
349                         }
350                         list.removeLast();
351                     }
352                     else
353                     {
354                         verifyCache();
355                         throw new Error( "update: last is null!" );
356                     }
357                 }
358             }
359 
360             if ( log.isDebugEnabled() )
361             {
362                 log.debug( "update: After spool map size: " + map.size() );
363             }
364             if ( map.size() != dumpCacheSize() )
365             {
366                 log.error( "update: After spool, size mismatch: map.size() = " + map.size() + ", linked list size = "
367                     + dumpCacheSize() );
368             }
369         }
370 
371         if ( old != null )
372         {
373             return old.getPayload();
374         }
375         return null;
376     }
377 
378     /**
379      * Adds a new node to the start of the link list.
380      * <p>
381      * @param key
382      * @param val The feature to be added to the First
383      */
384     private synchronized void addFirst(K key, V val)
385     {
386         LRUElementDescriptor<K, V> me = new LRUElementDescriptor<K, V>(key, val);
387         list.addFirst( me );
388     }
389 
390     /**
391      * Returns the size of the list.
392      * <p>
393      * @return int
394      */
395     private int dumpCacheSize()
396     {
397         return list.size();
398     }
399 
400     /**
401      * Dump the cache entries from first to list for debugging.
402      */
403     @SuppressWarnings("unchecked") // No generics for public fields
404     public void dumpCacheEntries()
405     {
406         log.debug( "dumpingCacheEntries" );
407         for ( LRUElementDescriptor<K, V> me = list.getFirst(); me != null; me = (LRUElementDescriptor<K, V>) me.next )
408         {
409             if ( log.isDebugEnabled() )
410             {
411                 log.debug( "dumpCacheEntries> key=" + me.getKey() + ", val=" + me.getPayload() );
412             }
413         }
414     }
415 
416     /**
417      * Dump the cache map for debugging.
418      */
419     public void dumpMap()
420     {
421         log.debug( "dumpingMap" );
422         for (Map.Entry<K, LRUElementDescriptor<K, V>> e : map.entrySet())
423         {
424             LRUElementDescriptor<K, V> me = e.getValue();
425             if ( log.isDebugEnabled() )
426             {
427                 log.debug( "dumpMap> key=" + e.getKey() + ", val=" + me.getPayload() );
428             }
429         }
430     }
431 
432     /**
433      * Checks to see if all the items that should be in the cache are. Checks consistency between
434      * List and map.
435      */
436     @SuppressWarnings("unchecked") // No generics for public fields
437     protected void verifyCache()
438     {
439         if ( !log.isDebugEnabled() )
440         {
441             return;
442         }
443 
444         boolean found = false;
445         log.debug( "verifycache: mapContains " + map.size() + " elements, linked list contains " + dumpCacheSize()
446             + " elements" );
447         log.debug( "verifycache: checking linked list by key " );
448         for (LRUElementDescriptor<K, V> li = list.getFirst(); li != null; li = (LRUElementDescriptor<K, V>) li.next )
449         {
450             K key = li.getKey();
451             if ( !map.containsKey( key ) )
452             {
453                 log.error( "verifycache: map does not contain key : " + li.getKey() );
454                 log.error( "li.hashcode=" + li.getKey().hashCode() );
455                 log.error( "key class=" + key.getClass() );
456                 log.error( "key hashcode=" + key.hashCode() );
457                 log.error( "key toString=" + key.toString() );
458                 if ( key instanceof GroupAttrName )
459                 {
460                     GroupAttrName<?> name = (GroupAttrName<?>) key;
461                     log.error( "GroupID hashcode=" + name.groupId.hashCode() );
462                     log.error( "GroupID.class=" + name.groupId.getClass() );
463                     log.error( "AttrName hashcode=" + name.attrName.hashCode() );
464                     log.error( "AttrName.class=" + name.attrName.getClass() );
465                 }
466                 dumpMap();
467             }
468             else if ( map.get( li.getKey() ) == null )
469             {
470                 log.error( "verifycache: linked list retrieval returned null for key: " + li.getKey() );
471             }
472         }
473 
474         log.debug( "verifycache: checking linked list by value " );
475         for (LRUElementDescriptor<K, V> li3 = list.getFirst(); li3 != null; li3 = (LRUElementDescriptor<K, V>) li3.next )
476         {
477             if ( map.containsValue( li3 ) == false )
478             {
479                 log.error( "verifycache: map does not contain value : " + li3 );
480                 dumpMap();
481             }
482         }
483 
484         log.debug( "verifycache: checking via keysets!" );
485         for (Iterator<K> itr2 = map.keySet().iterator(); itr2.hasNext(); )
486         {
487             found = false;
488             Serializable val = null;
489             try
490             {
491                 val = (Serializable) itr2.next();
492             }
493             catch ( NoSuchElementException nse )
494             {
495                 log.error( "verifycache: no such element exception" );
496                 continue;
497             }
498 
499             for (LRUElementDescriptor<K, V> li2 = list.getFirst(); li2 != null; li2 = (LRUElementDescriptor<K, V>) li2.next )
500             {
501                 if ( val.equals( li2.getKey() ) )
502                 {
503                     found = true;
504                     break;
505                 }
506             }
507             if ( !found )
508             {
509                 log.error( "verifycache: key not found in list : " + val );
510                 dumpCacheEntries();
511                 if ( map.containsKey( val ) )
512                 {
513                     log.error( "verifycache: map contains key" );
514                 }
515                 else
516                 {
517                     log.error( "verifycache: map does NOT contain key, what the HECK!" );
518                 }
519             }
520         }
521     }
522 
523     /**
524      * Logs an error is an element that should be in the cache is not.
525      * <p>
526      * @param key
527      */
528     @SuppressWarnings("unchecked") // No generics for public fields
529     protected void verifyCache( Object key )
530     {
531         if ( !log.isDebugEnabled() )
532         {
533             return;
534         }
535 
536         boolean found = false;
537 
538         // go through the linked list looking for the key
539         for (LRUElementDescriptor<K, V> li = list.getFirst(); li != null; li = (LRUElementDescriptor<K, V>) li.next )
540         {
541             if ( li.getKey() == key )
542             {
543                 found = true;
544                 log.debug( "verifycache(key) key match: " + key );
545                 break;
546             }
547         }
548         if ( !found )
549         {
550             log.error( "verifycache(key), couldn't find key! : " + key );
551         }
552     }
553 
554     /**
555      * This is called when an item is removed from the LRU. We just log some information.
556      * <p>
557      * Children can implement this method for special behavior.
558      * @param key
559      * @param value
560      */
561     protected void processRemovedLRU(K key, V value )
562     {
563         if ( log.isDebugEnabled() )
564         {
565             log.debug( "Removing key: [" + key + "] from LRUMap store, value = [" + value + "]" );
566             log.debug( "LRUMap store size: '" + this.size() + "'." );
567         }
568     }
569 
570     /**
571      * The chunk size is the number of items to remove when the max is reached. By default it is 1.
572      * <p>
573      * @param chunkSize The chunkSize to set.
574      */
575     public void setChunkSize( int chunkSize )
576     {
577         this.chunkSize = chunkSize;
578     }
579 
580     /**
581      * @return Returns the chunkSize.
582      */
583     public int getChunkSize()
584     {
585         return chunkSize;
586     }
587 
588     /**
589      * @return IStats
590      */
591     public IStats getStatistics()
592     {
593         IStats stats = new Stats();
594         stats.setTypeName( "LRUMap" );
595 
596         ArrayList<IStatElement> elems = new ArrayList<IStatElement>();
597 
598         IStatElement se = null;
599 
600         se = new StatElement();
601         se.setName( "List Size" );
602         se.setData( "" + list.size() );
603         elems.add( se );
604 
605         se = new StatElement();
606         se.setName( "Map Size" );
607         se.setData( "" + map.size() );
608         elems.add( se );
609 
610         se = new StatElement();
611         se.setName( "Put Count" );
612         se.setData( "" + putCnt );
613         elems.add( se );
614 
615         se = new StatElement();
616         se.setName( "Hit Count" );
617         se.setData( "" + hitCnt );
618         elems.add( se );
619 
620         se = new StatElement();
621         se.setName( "Miss Count" );
622         se.setData( "" + missCnt );
623         elems.add( se );
624 
625         // get an array and put them in the Stats object
626         IStatElement[] ses = elems.toArray( new StatElement[0] );
627         stats.setStatElements( ses );
628 
629         return stats;
630     }
631 
632     /**
633      * This returns a set of entries. Our LRUMapEntry is used since the value stored in the
634      * underlying map is a node in the double linked list. We wouldn't want to return this to the
635      * client, so we construct a new entry with the payload of the node.
636      * <p>
637      * TODO we should return out own set wrapper, so we can avoid the extra object creation if it
638      * isn't necessary.
639      * <p>
640      * @see java.util.Map#entrySet()
641      */
642     public synchronized Set<Map.Entry<K, V>> entrySet()
643     {
644         // todo, we should return a defensive copy
645         Set<Map.Entry<K, LRUElementDescriptor<K, V>>> entries = map.entrySet();
646 
647         Set<Map.Entry<K, V>> unWrapped = new HashSet<Map.Entry<K, V>>();
648 
649         for (Map.Entry<K, LRUElementDescriptor<K, V>> pre : entries )
650         {
651             Map.Entry<K, V> post = new LRUMapEntry<K, V>(pre.getKey(), pre.getValue().getPayload());
652             unWrapped.add(post);
653         }
654 
655         return unWrapped;
656     }
657 
658     /**
659      * @return map.keySet();
660      */
661     public Set<K> keySet()
662     {
663         // TODO fix this, it needs to return the keys inside the wrappers.
664         return map.keySet();
665     }
666 
667     /**
668      * @return the max objects size.
669      */
670     public int getMaxObjects()
671     {
672         return maxObjects;
673     }
674 }