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
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
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
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
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
267
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
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
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