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     public void dumpCacheEntries()
404     {
405         log.debug( "dumpingCacheEntries" );
406         for ( LRUElementDescriptor<K, V> me = list.getFirst(); me != null; me = (LRUElementDescriptor<K, V>) me.next )
407         {
408             if ( log.isDebugEnabled() )
409             {
410                 log.debug( "dumpCacheEntries> key=" + me.getKey() + ", val=" + me.getPayload() );
411             }
412         }
413     }
414 
415     /**
416      * Dump the cache map for debugging.
417      */
418     public void dumpMap()
419     {
420         log.debug( "dumpingMap" );
421         for (Map.Entry<K, LRUElementDescriptor<K, V>> e : map.entrySet())
422         {
423             LRUElementDescriptor<K, V> me = e.getValue();
424             if ( log.isDebugEnabled() )
425             {
426                 log.debug( "dumpMap> key=" + e.getKey() + ", val=" + me.getPayload() );
427             }
428         }
429     }
430 
431     /**
432      * Checks to see if all the items that should be in the cache are. Checks consistency between
433      * List and map.
434      */
435     protected void verifyCache()
436     {
437         if ( !log.isDebugEnabled() )
438         {
439             return;
440         }
441 
442         boolean found = false;
443         log.debug( "verifycache: mapContains " + map.size() + " elements, linked list contains " + dumpCacheSize()
444             + " elements" );
445         log.debug( "verifycache: checking linked list by key " );
446         for (LRUElementDescriptor<K, V> li = list.getFirst(); li != null; li = (LRUElementDescriptor<K, V>) li.next )
447         {
448             K key = li.getKey();
449             if ( !map.containsKey( key ) )
450             {
451                 log.error( "verifycache: map does not contain key : " + li.getKey() );
452                 log.error( "li.hashcode=" + li.getKey().hashCode() );
453                 log.error( "key class=" + key.getClass() );
454                 log.error( "key hashcode=" + key.hashCode() );
455                 log.error( "key toString=" + key.toString() );
456                 if ( key instanceof GroupAttrName )
457                 {
458                     GroupAttrName name = (GroupAttrName) key;
459                     log.error( "GroupID hashcode=" + name.groupId.hashCode() );
460                     log.error( "GroupID.class=" + name.groupId.getClass() );
461                     log.error( "AttrName hashcode=" + name.attrName.hashCode() );
462                     log.error( "AttrName.class=" + name.attrName.getClass() );
463                 }
464                 dumpMap();
465             }
466             else if ( map.get( li.getKey() ) == null )
467             {
468                 log.error( "verifycache: linked list retrieval returned null for key: " + li.getKey() );
469             }
470         }
471 
472         log.debug( "verifycache: checking linked list by value " );
473         for (LRUElementDescriptor<K, V> li3 = list.getFirst(); li3 != null; li3 = (LRUElementDescriptor<K, V>) li3.next )
474         {
475             if ( map.containsValue( li3 ) == false )
476             {
477                 log.error( "verifycache: map does not contain value : " + li3 );
478                 dumpMap();
479             }
480         }
481 
482         log.debug( "verifycache: checking via keysets!" );
483         for (Iterator<K> itr2 = map.keySet().iterator(); itr2.hasNext(); )
484         {
485             found = false;
486             Serializable val = null;
487             try
488             {
489                 val = (Serializable) itr2.next();
490             }
491             catch ( NoSuchElementException nse )
492             {
493                 log.error( "verifycache: no such element exception" );
494                 continue;
495             }
496 
497             for (LRUElementDescriptor<K, V> li2 = list.getFirst(); li2 != null; li2 = (LRUElementDescriptor<K, V>) li2.next )
498             {
499                 if ( val.equals( li2.getKey() ) )
500                 {
501                     found = true;
502                     break;
503                 }
504             }
505             if ( !found )
506             {
507                 log.error( "verifycache: key not found in list : " + val );
508                 dumpCacheEntries();
509                 if ( map.containsKey( val ) )
510                 {
511                     log.error( "verifycache: map contains key" );
512                 }
513                 else
514                 {
515                     log.error( "verifycache: map does NOT contain key, what the HECK!" );
516                 }
517             }
518         }
519     }
520 
521     /**
522      * Logs an error is an element that should be in the cache is not.
523      * <p>
524      * @param key
525      */
526     protected void verifyCache( Object key )
527     {
528         if ( !log.isDebugEnabled() )
529         {
530             return;
531         }
532 
533         boolean found = false;
534 
535         // go through the linked list looking for the key
536         for (LRUElementDescriptor<K, V> li = list.getFirst(); li != null; li = (LRUElementDescriptor<K, V>) li.next )
537         {
538             if ( li.getKey() == key )
539             {
540                 found = true;
541                 log.debug( "verifycache(key) key match: " + key );
542                 break;
543             }
544         }
545         if ( !found )
546         {
547             log.error( "verifycache(key), couldn't find key! : " + key );
548         }
549     }
550 
551     /**
552      * This is called when an item is removed from the LRU. We just log some information.
553      * <p>
554      * Children can implement this method for special behavior.
555      * @param key
556      * @param value
557      */
558     protected void processRemovedLRU(K key, V value )
559     {
560         if ( log.isDebugEnabled() )
561         {
562             log.debug( "Removing key: [" + key + "] from LRUMap store, value = [" + value + "]" );
563             log.debug( "LRUMap store size: '" + this.size() + "'." );
564         }
565     }
566 
567     /**
568      * The chunk size is the number of items to remove when the max is reached. By default it is 1.
569      * <p>
570      * @param chunkSize The chunkSize to set.
571      */
572     public void setChunkSize( int chunkSize )
573     {
574         this.chunkSize = chunkSize;
575     }
576 
577     /**
578      * @return Returns the chunkSize.
579      */
580     public int getChunkSize()
581     {
582         return chunkSize;
583     }
584 
585     /**
586      * @return IStats
587      */
588     public IStats getStatistics()
589     {
590         IStats stats = new Stats();
591         stats.setTypeName( "LRUMap" );
592 
593         ArrayList<IStatElement> elems = new ArrayList<IStatElement>();
594 
595         IStatElement se = null;
596 
597         se = new StatElement();
598         se.setName( "List Size" );
599         se.setData( "" + list.size() );
600         elems.add( se );
601 
602         se = new StatElement();
603         se.setName( "Map Size" );
604         se.setData( "" + map.size() );
605         elems.add( se );
606 
607         se = new StatElement();
608         se.setName( "Put Count" );
609         se.setData( "" + putCnt );
610         elems.add( se );
611 
612         se = new StatElement();
613         se.setName( "Hit Count" );
614         se.setData( "" + hitCnt );
615         elems.add( se );
616 
617         se = new StatElement();
618         se.setName( "Miss Count" );
619         se.setData( "" + missCnt );
620         elems.add( se );
621 
622         // get an array and put them in the Stats object
623         IStatElement[] ses = elems.toArray( new StatElement[0] );
624         stats.setStatElements( ses );
625 
626         return stats;
627     }
628 
629     /**
630      * This returns a set of entries. Our LRUMapEntry is used since the value stored in the
631      * underlying map is a node in the double linked list. We wouldn't want to return this to the
632      * client, so we construct a new entry with the payload of the node.
633      * <p>
634      * TODO we should return out own set wrapper, so we can avoid the extra object creation if it
635      * isn't necessary.
636      * <p>
637      * @see java.util.Map#entrySet()
638      */
639     public synchronized Set<Map.Entry<K, V>> entrySet()
640     {
641         // todo, we should return a defensive copy
642         Set<Map.Entry<K, LRUElementDescriptor<K, V>>> entries = map.entrySet();
643 
644         Set<Map.Entry<K, V>> unWrapped = new HashSet<Map.Entry<K, V>>();
645 
646         for (Map.Entry<K, LRUElementDescriptor<K, V>> pre : entries )
647         {
648             Map.Entry<K, V> post = new LRUMapEntry<K, V>(pre.getKey(), pre.getValue().getPayload());
649             unWrapped.add(post);
650         }
651 
652         return unWrapped;
653     }
654 
655     /**
656      * @return map.keySet();
657      */
658     public Set<K> keySet()
659     {
660         // TODO fix this, it needs to return the keys inside the wrappers.
661         return map.keySet();
662     }
663 
664     /**
665      * @return the max objects size.
666      */
667     public int getMaxObjects()
668     {
669         return maxObjects;
670     }
671 }