View Javadoc
1   package org.apache.commons.jcs3.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.util.AbstractMap;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.concurrent.locks.Lock;
29  import java.util.concurrent.locks.ReentrantLock;
30  import java.util.stream.Collectors;
31  
32  import org.apache.commons.jcs3.engine.control.group.GroupAttrName;
33  import org.apache.commons.jcs3.engine.stats.StatElement;
34  import org.apache.commons.jcs3.engine.stats.Stats;
35  import org.apache.commons.jcs3.engine.stats.behavior.IStatElement;
36  import org.apache.commons.jcs3.engine.stats.behavior.IStats;
37  import org.apache.commons.jcs3.log.Log;
38  import org.apache.commons.jcs3.log.LogManager;
39  
40  /**
41   * This is a simple LRUMap. It implements most of the map methods. It is not recommended that you
42   * use any but put, get, remove, and clear.
43   * <p>
44   * Children can implement the processRemovedLRU method if they want to handle the removal of the
45   * least recently used item.
46   * </p>
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   * <p>
52   * Locking is done on the instance.
53   * </p>
54   */
55  public abstract class AbstractLRUMap<K, V>
56      implements Map<K, V>
57  {
58      /** The logger */
59      private static final Log log = LogManager.getLog( AbstractLRUMap.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      private final Map<K, LRUElementDescriptor<K, V>> map;
66  
67      /** lock to keep map and list synchronous */
68      private final Lock lock = new ReentrantLock();
69  
70      /** stats */
71      private long hitCnt;
72  
73      /** stats */
74      private long missCnt;
75  
76      /** stats */
77      private long putCnt;
78  
79      /**
80       * This creates an unbounded version. Setting the max objects will result in spooling on
81       * subsequent puts.
82       */
83      public AbstractLRUMap()
84      {
85          list = new DoubleLinkedList<>();
86  
87          // normal hashtable is faster for
88          // sequential keys.
89          map = new ConcurrentHashMap<>();
90      }
91  
92  
93      /**
94       * This simply returns the number of elements in the map.
95       * <p>
96       * @see java.util.Map#size()
97       */
98      @Override
99      public int size()
100     {
101         return map.size();
102     }
103 
104     /**
105      * This removes all the items. It clears the map and the double linked list.
106      * <p>
107      * @see java.util.Map#clear()
108      */
109     @Override
110     public void clear()
111     {
112         lock.lock();
113         try
114         {
115             map.clear();
116             list.removeAll();
117         }
118         finally
119         {
120             lock.unlock();
121         }
122     }
123 
124     /**
125      * Returns true if the map is empty.
126      * <p>
127      * @see java.util.Map#isEmpty()
128      */
129     @Override
130     public boolean isEmpty()
131     {
132         return map.isEmpty();
133     }
134 
135     /**
136      * Returns true if the map contains an element for the supplied key.
137      * <p>
138      * @see java.util.Map#containsKey(Object)
139      */
140     @Override
141     public boolean containsKey( final Object key )
142     {
143         return map.containsKey( key );
144     }
145 
146     /**
147      * This is an expensive operation that determines if the object supplied is mapped to any key.
148      * <p>
149      * @see java.util.Map#containsValue(Object)
150      */
151     @Override
152     public boolean containsValue( final Object value )
153     {
154         return map.containsValue( value );
155     }
156 
157     /**
158      * @return map.values();
159      */
160     @Override
161     public Collection<V> values()
162     {
163         return map.values().stream()
164                 .map(LRUElementDescriptor::getPayload)
165                 .collect(Collectors.toList());
166     }
167 
168     /**
169      * @param source
170      */
171     @Override
172     public void putAll( final Map<? extends K, ? extends V> source )
173     {
174         if ( source != null )
175         {
176             source.forEach(this::put);
177         }
178     }
179 
180     /**
181      * @param key
182      * @return Object
183      */
184     @Override
185     public V get( final Object key )
186     {
187         final V retVal;
188 
189         log.debug( "getting item  for key {0}", key );
190 
191         final LRUElementDescriptor<K, V> me = map.get( key );
192 
193         if ( me == null )
194         {
195             missCnt++;
196             retVal = null;
197         }
198         else
199         {
200             hitCnt++;
201             retVal = me.getPayload();
202             list.makeFirst( me );
203         }
204 
205         if ( me == null )
206         {
207             log.debug( "LRUMap miss for {0}", key );
208         }
209         else
210         {
211             log.debug( "LRUMap hit for {0}", key );
212         }
213 
214         // verifyCache();
215         return retVal;
216     }
217 
218     /**
219      * This gets an element out of the map without adjusting it's position in the LRU. In other
220      * words, this does not count as being used. If the element is the last item in the list, it
221      * will still be the last time in the list.
222      * <p>
223      * @param key
224      * @return Object
225      */
226     public V getQuiet( final Object key )
227     {
228         V ce = null;
229         final LRUElementDescriptor<K, V> me = map.get( key );
230 
231         if ( me != null )
232         {
233             ce = me.getPayload();
234         }
235 
236         if ( me == null )
237         {
238             log.debug( "LRUMap quiet miss for {0}", key );
239         }
240         else
241         {
242             log.debug( "LRUMap quiet hit for {0}", key );
243         }
244 
245         return ce;
246     }
247 
248     /**
249      * @param key
250      * @return Object removed
251      */
252     @Override
253     public V remove( final Object key )
254     {
255         log.debug( "removing item for key: {0}", key );
256 
257         // remove single item.
258         lock.lock();
259         try
260         {
261             final LRUElementDescriptor<K, V> me = map.remove(key);
262 
263             if (me != null)
264             {
265                 list.remove(me);
266                 return me.getPayload();
267             }
268         }
269         finally
270         {
271             lock.unlock();
272         }
273 
274         return null;
275     }
276 
277     /**
278      * @param key
279      * @param value
280      * @return Object
281      */
282     @Override
283     public V put(final K key, final V value)
284     {
285         putCnt++;
286 
287         LRUElementDescriptor<K, V> old = null;
288         final LRUElementDescriptor<K, V> me = new LRUElementDescriptor<>(key, value);
289 
290         lock.lock();
291         try
292         {
293             list.addFirst( me );
294             old = map.put(key, me);
295 
296             // If the node was the same as an existing node, remove it.
297             if ( old != null && key.equals(old.getKey()))
298             {
299                 list.remove( old );
300             }
301         }
302         finally
303         {
304             lock.unlock();
305         }
306 
307         // If the element limit is reached, we need to spool
308         if (shouldRemove())
309         {
310             log.debug( "In memory limit reached, removing least recently used." );
311 
312             // The spool will put them in a disk event queue, so there is no
313             // need to pre-queue the queuing. This would be a bit wasteful
314             // and wouldn't save much time in this synchronous call.
315             while (shouldRemove())
316             {
317                 lock.lock();
318                 try
319                 {
320                     final LRUElementDescriptor<K, V> last = list.getLast();
321                     if (last == null) {
322                         verifyCache();
323                         throw new Error("update: last is null!");
324                     }
325                     processRemovedLRU(last.getKey(), last.getPayload());
326                     if (map.remove(last.getKey()) == null)
327                     {
328                         log.warn("update: remove failed for key: {0}",
329                                 last::getKey);
330                         verifyCache();
331                     }
332                     list.removeLast();
333                 }
334                 finally
335                 {
336                     lock.unlock();
337                 }
338             }
339 
340             log.debug( "update: After spool map size: {0}", map::size);
341             if ( map.size() != list.size() )
342             {
343                 log.error("update: After spool, size mismatch: map.size() = {0}, "
344                         + "linked list size = {1}",
345                         map::size, list::size);
346             }
347         }
348 
349         if ( old != null )
350         {
351             return old.getPayload();
352         }
353         return null;
354     }
355 
356     protected abstract boolean shouldRemove();
357 
358     /**
359      * Dump the cache entries from first to list for debugging.
360      */
361     @SuppressWarnings("unchecked") // No generics for public fields
362     public void dumpCacheEntries()
363     {
364         if (log.isTraceEnabled())
365         {
366             log.trace("dumpingCacheEntries");
367             for (LRUElementDescriptor<K, V> me = list.getFirst(); me != null; me = (LRUElementDescriptor<K, V>) me.next)
368             {
369                 log.trace("dumpCacheEntries> key={0}, val={1}", me.getKey(), me.getPayload());
370             }
371         }
372     }
373 
374     /**
375      * Dump the cache map for debugging.
376      */
377     public void dumpMap()
378     {
379         if (log.isTraceEnabled())
380         {
381             log.trace("dumpingMap");
382             map.forEach((key, value) -> log.trace("dumpMap> key={0}, val={1}", key, value.getPayload()));
383         }
384     }
385 
386     /**
387      * Checks to see if all the items that should be in the cache are. Checks consistency between
388      * List and map.
389      */
390     @SuppressWarnings("unchecked") // No generics for public fields
391     protected void verifyCache()
392     {
393         if ( !log.isTraceEnabled() )
394         {
395             return;
396         }
397 
398         log.trace( "verifycache: mapContains {0} elements, linked list "
399                 + "contains {1} elements", map.size(), list.size() );
400         log.trace( "verifycache: checking linked list by key" );
401         for (LRUElementDescriptor<K, V> li = list.getFirst(); li != null; li = (LRUElementDescriptor<K, V>) li.next )
402         {
403             final K key = li.getKey();
404             if ( !map.containsKey( key ) )
405             {
406                 log.error( "verifycache: map does not contain key : {0}", li.getKey() );
407                 log.error( "li.hashCode={0}", li.getKey().hashCode() );
408                 log.error( "key class={0}", key.getClass() );
409                 log.error( "key hashCode={0}", key.hashCode() );
410                 log.error( "key toString={0}", key.toString() );
411                 if ( key instanceof GroupAttrName )
412                 {
413                     final GroupAttrName<?> name = (GroupAttrName<?>) key;
414                     log.error( "GroupID hashCode={0}", name.groupId.hashCode() );
415                     log.error( "GroupID.class={0}", name.groupId.getClass() );
416                     log.error( "AttrName hashCode={0}", name.attrName.hashCode() );
417                     log.error( "AttrName.class={0}", name.attrName.getClass() );
418                 }
419                 dumpMap();
420             }
421             else if ( map.get( li.getKey() ) == null )
422             {
423                 log.error( "verifycache: linked list retrieval returned null for key: {0}",
424                         li.getKey() );
425             }
426         }
427 
428         log.trace( "verifycache: checking linked list by value " );
429         for (LRUElementDescriptor<K, V> li3 = list.getFirst(); li3 != null; li3 = (LRUElementDescriptor<K, V>) li3.next )
430         {
431             if (!map.containsValue(li3))
432             {
433                 log.error( "verifycache: map does not contain value : {0}", li3 );
434                 dumpMap();
435             }
436         }
437 
438         log.trace( "verifycache: checking via keysets!" );
439         map.keySet().stream()
440             .filter(key -> {
441                 for (LRUElementDescriptor<K, V> li2 = list.getFirst(); li2 != null; li2 = (LRUElementDescriptor<K, V>) li2.next )
442                 {
443                     if ( key.equals( li2.getKey() ) )
444                     {
445                         return true;
446                     }
447                 }
448 
449                 log.error( "verifycache: key not found in list : {0}", key );
450                 dumpCacheEntries();
451                 if ( map.containsKey( key ) )
452                 {
453                     log.error( "verifycache: map contains key" );
454                 }
455                 else
456                 {
457                     log.error( "verifycache: map does NOT contain key, what the HECK!" );
458                 }
459 
460                 return false;
461             })
462             .findFirst();
463     }
464 
465     /**
466      * This is called when an item is removed from the LRU. We just log some information.
467      * <p>
468      * Children can implement this method for special behavior.
469      * @param key
470      * @param value
471      */
472     protected void processRemovedLRU(final K key, final V value )
473     {
474         log.debug( "Removing key: [{0}] from LRUMap store, value = [{1}]", key, value );
475         log.debug( "LRUMap store size: \"{0}\".", this.size() );
476     }
477 
478     /**
479      * @return IStats
480      */
481     public IStats getStatistics()
482     {
483         final IStats stats = new Stats();
484         stats.setTypeName( "LRUMap" );
485 
486         final ArrayList<IStatElement<?>> elems = new ArrayList<>();
487 
488         elems.add(new StatElement<>( "List Size", Integer.valueOf(list.size()) ) );
489         elems.add(new StatElement<>( "Map Size", Integer.valueOf(map.size()) ) );
490         elems.add(new StatElement<>( "Put Count", Long.valueOf(putCnt) ) );
491         elems.add(new StatElement<>( "Hit Count", Long.valueOf(hitCnt) ) );
492         elems.add(new StatElement<>( "Miss Count", Long.valueOf(missCnt) ) );
493 
494         stats.setStatElements( elems );
495 
496         return stats;
497     }
498 
499     /**
500      * This returns a set of entries. Our LRUMapEntry is used since the value stored in the
501      * underlying map is a node in the double linked list. We wouldn't want to return this to the
502      * client, so we construct a new entry with the payload of the node.
503      * <p>
504      * TODO we should return out own set wrapper, so we can avoid the extra object creation if it
505      * isn't necessary.
506      * <p>
507      * @see java.util.Map#entrySet()
508      */
509     @Override
510     public Set<Map.Entry<K, V>> entrySet()
511     {
512         lock.lock();
513         try
514         {
515             return map.entrySet().stream()
516                     .map(entry -> new AbstractMap.SimpleEntry<>(
517                             entry.getKey(), entry.getValue().getPayload()))
518                     .collect(Collectors.toSet());
519         }
520         finally
521         {
522             lock.unlock();
523         }
524     }
525 
526     /**
527      * @return map.keySet();
528      */
529     @Override
530     public Set<K> keySet()
531     {
532         return map.values().stream()
533                 .map(LRUElementDescriptor::getKey)
534                 .collect(Collectors.toSet());
535     }
536 }