View Javadoc

1   /***
2    *  Copyright 2003-2010 Terracotta, Inc.
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.store;
18  
19  import net.sf.ehcache.CacheEntry;
20  import net.sf.ehcache.CacheException;
21  import net.sf.ehcache.Ehcache;
22  import net.sf.ehcache.Element;
23  import net.sf.ehcache.Status;
24  import net.sf.ehcache.config.CacheConfiguration;
25  import net.sf.ehcache.config.PinningConfiguration;
26  import net.sf.ehcache.writer.CacheWriterManager;
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  import java.util.ArrayList;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Map;
34  
35  
36  /***
37   * An implementation of a LruMemoryStore.
38   * <p/>
39   * This uses {@link java.util.LinkedHashMap} as its backing map. It uses the {@link java.util.LinkedHashMap} LRU
40   * feature. LRU for this implementation means least recently accessed.
41   *
42   * @author <a href="mailto:gluck@thoughtworks.com">Greg Luck</a>
43   * @version $Id: LruMemoryStore.html 13146 2011-08-01 17:12:39Z oletizi $
44   */
45  public class LruMemoryStore extends AbstractStore {
46  
47      private static final Logger LOG = LoggerFactory.getLogger(LruMemoryStore.class.getName());
48  
49      /***
50       * The cache this store is associated with.
51       */
52      protected Ehcache cache;
53  
54      /***
55       * Map where items are stored by key.
56       */
57      protected Map map;
58  
59      /***
60       * The DiskStore associated with this MemoryStore.
61       */
62      protected final Store diskStore;
63  
64      /***
65       * status.
66       */
67      protected Status status;
68  
69      /***
70       * The maximum size of the store (0 == no limit)
71       */
72      protected int maximumSize;
73  
74      private volatile boolean pinningEnabled;
75  
76  
77      /***
78       * Constructor for the LruMemoryStore object
79       * The backing {@link java.util.LinkedHashMap} is created with LRU by access order.
80       */
81      public LruMemoryStore(Ehcache cache, Store diskStore) {
82          status = Status.STATUS_UNINITIALISED;
83          this.maximumSize = cache.getCacheConfiguration().getMaxElementsInMemory();
84          this.pinningEnabled = determineCachePinned(cache.getCacheConfiguration());
85          this.cache = cache;
86          this.diskStore = diskStore;
87          map = new SpoolingLinkedHashMap();
88          status = Status.STATUS_ALIVE;
89      }
90  
91      private boolean determineCachePinned(CacheConfiguration cacheConfiguration) {
92          PinningConfiguration pinningConfiguration = cacheConfiguration.getPinningConfiguration();
93          if (pinningConfiguration == null) {
94              return false;
95          }
96  
97          switch (pinningConfiguration.getStore()) {
98              case LOCALHEAP:
99                  return true;
100 
101             case LOCALMEMORY:
102                 return !cacheConfiguration.isOverflowToOffHeap();
103 
104             case INCACHE:
105                 return !(cacheConfiguration.isOverflowToOffHeap() || cacheConfiguration.isOverflowToDisk() || cacheConfiguration.isDiskPersistent());
106 
107             default:
108                 throw new IllegalArgumentException();
109         }
110     }
111 
112     /***
113      * Puts an item in the cache. Note that this automatically results in
114      * {@link net.sf.ehcache.store.LruMemoryStore.SpoolingLinkedHashMap#removeEldestEntry} being called.
115      *
116      * @param element the element to add
117      */
118     public final boolean put(Element element) throws CacheException {
119         return putInternal(element, null);
120     }
121 
122     /***
123      * {@inheritDoc}
124      */
125     public final boolean putWithWriter(Element element, CacheWriterManager writerManager) throws CacheException {
126         return putInternal(element, writerManager);
127     }
128 
129     private synchronized boolean putInternal(Element element, CacheWriterManager writerManager) throws CacheException {
130         boolean newPut = true;
131         if (element != null) {
132             newPut = map.put(element.getObjectKey(), element) == null;
133             if (writerManager != null) {
134                 writerManager.put(element);
135             }
136             doPut(element);
137         }
138         return newPut;
139     }
140 
141     /***
142      * Allow specialised actions over adding the element to the map.
143      *
144      * @param element
145      */
146     protected void doPut(Element element) throws CacheException {
147         //empty
148     }
149 
150     /***
151      * Gets an item from the cache.
152      * <p/>
153      * The last access time in {@link net.sf.ehcache.Element} is updated.
154      *
155      * @param key the cache key
156      * @return the element, or null if there was no match for the key
157      */
158     public final synchronized Element get(Object key) {
159         return (Element) map.get(key);
160     }
161 
162     /***
163      * Gets an item from the cache, without updating statistics.
164      *
165      * @param key the cache key
166      * @return the element, or null if there was no match for the key
167      */
168     public final synchronized Element getQuiet(Object key) {
169         return get(key);
170     }
171 
172     /***
173      * Removes an Element from the store.
174      *
175      * @param key the key of the Element, usually a String
176      * @return the Element if one was found, else null
177      */
178     public final Element remove(Object key) {
179         return removeInternal(key, null);
180     }
181 
182     /***
183      * {@inheritDoc}
184      */
185     public final Element removeWithWriter(Object key, CacheWriterManager writerManager) throws CacheException {
186         return removeInternal(key, writerManager);
187     }
188 
189     private synchronized Element removeInternal(Object key, CacheWriterManager writerManager) throws CacheException {
190         // remove single item.
191         Element element = (Element) map.remove(key);
192         if (writerManager != null) {
193             writerManager.remove(new CacheEntry(key, element));
194         }
195         if (element != null) {
196             return element;
197         } else {
198             return null;
199         }
200     }
201 
202     /***
203      * Remove all of the elements from the store.
204      */
205     public final synchronized void removeAll() throws CacheException {
206         clear();
207     }
208 
209     /***
210      * Clears any data structures and places it back to its state when it was first created.
211      */
212     protected final void clear() {
213         map.clear();
214     }
215 
216     /***
217      * Prepares for shutdown.
218      */
219     public final synchronized void dispose() {
220         if (status.equals(Status.STATUS_SHUTDOWN)) {
221             return;
222         }
223         status = Status.STATUS_SHUTDOWN;
224         flush();
225 
226         //release reference to cache
227         cache = null;
228     }
229 
230 
231     /***
232      * Flush to disk only if the cache is diskPersistent.
233      */
234     public final void flush() {
235         if (cache.getCacheConfiguration().isDiskPersistent()) {
236             if (LOG.isDebugEnabled()) {
237                 LOG.debug(cache.getName() + " is persistent. Spooling " + map.size() + " elements to the disk store.");
238             }
239             spoolAllToDisk();
240         }
241 
242         //should be emptied if clearOnFlush is true
243         if (cache.getCacheConfiguration().isClearOnFlush()) {
244             clear();
245         }
246     }
247 
248 
249     /***
250      * Spools all elements to disk, in preparation for shutdown.
251      * <p/>
252      * This revised implementation is a little slower but avoids using increased memory during the method.
253      */
254     protected final void spoolAllToDisk() {
255         boolean clearOnFlush = cache.getCacheConfiguration().isClearOnFlush();
256         for (Object key : getKeys()) {
257             Element element = (Element) map.get(key);
258             if (element != null) {
259                 if (!element.isSerializable()) {
260                     if (LOG.isWarnEnabled()) {
261                         LOG.warn("Object with key " + element.getObjectKey()
262                                  + " is not Serializable and is not being overflowed to disk.");
263                     }
264                 } else {
265                     spoolToDisk(element);
266                     //Don't notify listeners. They are not being removed from the cache, only a store
267                     //Leave it in the memory store for performance if do not want to clear on flush
268                     if (clearOnFlush) {
269                         remove(key);
270                     }
271                 }
272             }
273         }
274     }
275 
276     /***
277      * Puts the element in the DiskStore.
278      * Should only be called if isOverflowToDisk is true
279      * <p/>
280      * Relies on being called from a synchronized method
281      *
282      * @param element The Element
283      */
284     protected void spoolToDisk(Element element) {
285         diskStore.put(element);
286         if (LOG.isDebugEnabled()) {
287             LOG.debug(cache.getName() + "Cache: spool to disk done for: " + element.getObjectKey());
288         }
289     }
290 
291     /***
292      * Gets the status of the MemoryStore.
293      */
294     public final Status getStatus() {
295         return status;
296     }
297 
298     /***
299      * Gets an Array of the keys for all elements in the memory cache.
300      * <p/>
301      * Does not check for expired entries
302      *
303      * @return An Object[]
304      */
305     public final synchronized List getKeys() {
306         return new ArrayList(map.keySet());
307     }
308 
309     /***
310      * Returns the current cache size.
311      *
312      * @return The size value
313      */
314     public final int getSize() {
315         return map.size();
316     }
317 
318     /***
319      * Returns nothing since a disk store isn't clustered
320      *
321      * @return returns 0
322      */
323     public final int getTerracottaClusteredSize() {
324         return 0;
325     }
326 
327 
328     /***
329      * An unsynchronized check to see if a key is in the Store. No check is made to see if the Element is expired.
330      *
331      * @param key The Element key
332      * @return true if found. If this method return false, it means that an Element with the given key is definitely not in the MemoryStore.
333      *         If it returns true, there is an Element there. An attempt to get it may return null if the Element has expired.
334      */
335     public final boolean containsKey(Object key) {
336         return map.containsKey(key);
337     }
338 
339 
340     /***
341      * Measures the size of the memory store by measuring the serialized size of all elements.
342      * If the objects are not Serializable they count as 0.
343      * <p/>
344      * Warning: This method can be very expensive to run. Allow approximately 1 second
345      * per 1MB of entries. Running this method could create liveness problems
346      * because the object lock is held for a long period
347      *
348      * @return the size, in bytes
349      */
350     public final synchronized long getSizeInBytes() throws CacheException {
351         long sizeInBytes = 0;
352         for (Iterator iterator = map.values().iterator(); iterator.hasNext();) {
353             Element element = (Element) iterator.next();
354             if (element != null) {
355                 sizeInBytes += element.getSerializedSize();
356             }
357         }
358         return sizeInBytes;
359     }
360 
361 
362     /***
363      * Evict the <code>Element</code>.
364      * <p/>
365      * Evict means that the <code>Element</code> is:
366      * <ul>
367      * <li>if, the store is diskPersistent, the <code>Element</code> is spooled to the DiskStore
368      * <li>if not, the <code>Element</code> is removed.
369      * </ul>
370      *
371      * @param element the <code>Element</code> to be evicted.
372      */
373     protected final void evict(Element element) throws CacheException {
374         boolean spooled = false;
375         if (cache.getCacheConfiguration().isOverflowToDisk()) {
376             if (!element.isSerializable()) {
377                 if (LOG.isWarnEnabled()) {
378                     LOG.warn(new StringBuilder("Object with key ").append(element.getObjectKey())
379                             .append(" is not Serializable and cannot be overflowed to disk").toString());
380                 }
381             } else {
382                 spoolToDisk(element);
383                 spooled = true;
384             }
385         }
386 
387         if (!spooled) {
388             cache.getCacheEventNotificationService().notifyElementEvicted(element, false);
389         }
390     }
391 
392     /***
393      * Before eviction elements are checked.
394      *
395      * @param element
396      */
397     protected final void notifyExpiry(Element element) {
398         cache.getCacheEventNotificationService().notifyElementExpiry(element, false);
399     }
400 
401 
402     /***
403      * An algorithm to tell if the MemoryStore is at or beyond its carrying capacity.
404      */
405     protected final boolean isFull() {
406         return maximumSize > 0 && map.size() > maximumSize;
407     }
408 
409     /***
410      * Expire all elsments.
411      * <p/>
412      * This is a default implementation which does nothing. Expiry on demand is only
413      * implemented for disk stores.
414      */
415     public void expireElements() {
416         //empty implementation
417     }
418 
419     /***
420      * Memory stores are never backed up and always return false
421      */
422     public boolean bufferFull() {
423         return false;
424     }
425 
426 
427     /***
428      * Package local access to the map for testing
429      */
430     Map getBackingMap() {
431         return map;
432     }
433 
434     /***
435      * {@inheritDoc}
436      */
437     public Object getMBean() {
438         return null;
439     }
440 
441     /***
442      * An extension of LinkedHashMap which overrides {@link #removeEldestEntry}
443      * to persist cache entries to the auxiliary cache before they are removed.
444      * <p/>
445      * This implementation also provides LRU by access order.
446      */
447     public final class SpoolingLinkedHashMap extends java.util.LinkedHashMap {
448         private static final int INITIAL_CAPACITY = 100;
449         private static final float GROWTH_FACTOR = .75F;
450 
451         /***
452          * Default constructor.
453          * Will create an initial capacity of 100, a loading of .75 and
454          * LRU by access order.
455          */
456         public SpoolingLinkedHashMap() {
457             super(INITIAL_CAPACITY, GROWTH_FACTOR, true);
458         }
459 
460         /***
461          * Returns <tt>true</tt> if this map should remove its eldest entry.
462          * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
463          * inserting a new entry into the map.  It provides the implementer
464          * with the opportunity to remove the eldest entry each time a new one
465          * is added.  This is useful if the map represents a cache: it allows
466          * the map to reduce memory consumption by deleting stale entries.
467          * <p/>
468          * Will return true if:
469          * <ol>
470          * <li> the element has expired
471          * <li> the cache size is greater than the in-memory actual.
472          * In this case we spool to disk before returning.
473          * </ol>
474          *
475          * @param eldest The least recently inserted entry in the map, or if
476          *               this is an access-ordered map, the least recently accessed
477          *               entry.  This is the entry that will be removed it this
478          *               method returns <tt>true</tt>.  If the map was empty prior
479          *               to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
480          *               in this invocation, this will be the entry that was just
481          *               inserted; in other words, if the map contains a single
482          *               entry, the eldest entry is also the newest.
483          * @return true if the eldest entry should be removed
484          *         from the map; <tt>false</t> if it should be retained.
485          */
486         @Override
487         protected final boolean removeEldestEntry(Map.Entry eldest) {
488             Element element = (Element) eldest.getValue();
489             return element != null && removeLeastRecentlyUsedElement(element);
490         }
491 
492         /***
493          * {@inheritDoc}
494          */
495         @Override
496         public Object put(Object key, Object value) {
497             Object put = super.put(key, value);
498 
499             Iterator it = entrySet().iterator();
500             while (isFull() && it.hasNext()) {
501                 Map.Entry entry = (Map.Entry) it.next();
502                 if (removeEldestEntry(entry)) {
503                     it.remove();
504                 }
505             }
506 
507             return put;
508         }
509 
510         /***
511          * Relies on being called from a synchronized method
512          *
513          * @param element
514          * @return true if the LRU element should be removed
515          */
516         private boolean removeLeastRecentlyUsedElement(Element element) throws CacheException {
517             //check for expiry and remove before going to the trouble of spooling it
518             if (element.isExpired()) {
519                 notifyExpiry(element);
520                 return true;
521             }
522 
523             if (isFull() && !element.isPinned() && !pinningEnabled) {
524                 evict(element);
525                 return true;
526             } else {
527                 return false;
528             }
529 
530         }
531     }
532 
533 
534     /***
535      * @return the current eviction policy. This may not be the configured policy, if it has been
536      *         dynamically set.
537      * @see #setEvictionPolicy(Policy)
538      */
539     public Policy getEvictionPolicy() {
540         return new LruPolicy();
541     }
542 
543     /***
544      * Sets the eviction policy strategy. The Store will use a policy at startup. The store may allow changing
545      * the eviction policy strategy dynamically. Otherwise implementations will throw an exception if this method
546      * is called.
547      *
548      * @param policy the new policy
549      */
550     public void setEvictionPolicy(Policy policy) {
551         throw new UnsupportedOperationException("This store is LRU only. It does not support changing the eviction" +
552                 " strategy.");
553     }
554 
555     /***
556      * {@inheritDoc}
557      */
558     public Object getInternalContext() {
559         return null;
560     }
561 
562     /***
563      * {@inheritDoc}
564      */
565     public boolean containsKeyInMemory(Object key) {
566         return containsKey(key);
567     }
568 
569     /***
570      * {@inheritDoc}
571      */
572     public boolean containsKeyOffHeap(Object key) {
573         return false;
574     }
575 
576     /***
577      * {@inheritDoc}
578      */
579     public boolean containsKeyOnDisk(Object key) {
580         return false;
581     }
582 
583     /***
584      * {@inheritDoc}
585      */
586     public Policy getInMemoryEvictionPolicy() {
587         return getEvictionPolicy();
588     }
589 
590     /***
591      * {@inheritDoc}
592      */
593     public int getInMemorySize() {
594         return getSize();
595     }
596 
597     /***
598      * {@inheritDoc}
599      */
600     public long getInMemorySizeInBytes() {
601         return getSizeInBytes();
602     }
603 
604     /***
605      * {@inheritDoc}
606      */
607     public int getOffHeapSize() {
608         return 0;
609     }
610 
611     /***
612      * {@inheritDoc}
613      */
614     public long getOffHeapSizeInBytes() {
615         return 0;
616     }
617 
618     /***
619      * {@inheritDoc}
620      */
621     public int getOnDiskSize() {
622         return 0;
623     }
624 
625     /***
626      * {@inheritDoc}
627      */
628     public long getOnDiskSizeInBytes() {
629         return 0;
630     }
631 
632     /***
633      * {@inheritDoc}
634      */
635     public void setInMemoryEvictionPolicy(Policy policy) {
636         setEvictionPolicy(policy);
637     }
638 
639     /***
640      * Unsupported in LruMemoryStore
641      */
642     public Element putIfAbsent(Element element) throws NullPointerException {
643         throw new UnsupportedOperationException();
644     }
645 
646     /***
647      * Unsupported in LruMemoryStore
648      */
649     public Element removeElement(Element element, ElementValueComparator comparator) throws NullPointerException {
650         throw new UnsupportedOperationException();
651     }
652 
653     /***
654      * Unsupported in LruMemoryStore
655      */
656     public boolean replace(Element old, Element element, ElementValueComparator comparator)
657             throws NullPointerException, IllegalArgumentException {
658         throw new UnsupportedOperationException();
659     }
660 
661     /***
662      * Unsupported in LruMemoryStore
663      */
664     public Element replace(Element element) throws NullPointerException {
665         throw new UnsupportedOperationException();
666     }
667 }
668