Index: oak-core/src/main/java/org/apache/jackrabbit/oak/cache/CacheLIRS.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/cache/CacheLIRS.java (date 1436795853000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/cache/CacheLIRS.java (date 1436783762000) @@ -30,9 +30,6 @@ import javax.annotation.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheStats; import com.google.common.cache.LoadingCache; @@ -40,6 +37,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.UncheckedExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A scan resistant cache. It is meant to cache objects that are relatively @@ -71,10 +70,27 @@ * @param the value type */ public class CacheLIRS implements LoadingCache { - + private static final Logger LOG = LoggerFactory.getLogger(CacheLIRS.class); /** + * Listener for items that are evicted from the cache. The listener + * is called for both, resident and non-resident items. In the + * latter case the passed value is {@code null}. + * @param type of the key + * @param type of the value + */ + public interface EvictionCallback { + + /** + * Indicates eviction of an item. + * @param key the evicted item's key + * @param value the evicted item's value or {@code null} if non-resident + */ + void evicted(K key, V value); + } + + /** * The maximum memory this cache should use. */ private long maxMemory; @@ -90,20 +106,25 @@ private final int segmentShift; private final int segmentMask; private final int stackMoveDistance; - + private final Weigher weigher; - + private final CacheLoader loader; - + /** + * The eviction listener of this cache or {@code null} if none. + */ + private final EvictionCallback evicted; + + /** * A concurrent hash map of keys where loading is in progress. Key: the * cache key. Value: a synchronization object. The threads that wait for the * value to be loaded need to wait on the synchronization object. The * loading thread will notify all waiting threads once loading is done. */ - final ConcurrentHashMap loadingInProgress = + final ConcurrentHashMap loadingInProgress = new ConcurrentHashMap(); - + /** * Create a new cache with the given number of entries, and the default * settings (an average size of 1 per entry, 16 segments, and stack move @@ -112,7 +133,7 @@ * @param maxEntries the maximum number of entries */ public CacheLIRS(int maxEntries) { - this(null, maxEntries, 1, 16, maxEntries / 100, null); + this(null, maxEntries, 1, 16, maxEntries / 100, null, null); } /** @@ -123,10 +144,12 @@ * @param segmentCount the number of cache segments (must be a power of 2) * @param stackMoveDistance how many other item are to be moved to the top * of the stack before the current item is moved + * @param evicted the eviction listener of this segment or {@code null} if none. */ @SuppressWarnings("unchecked") - CacheLIRS(Weigher weigher, long maxMemory, int averageMemory, + CacheLIRS(Weigher weigher, long maxMemory, int averageMemory, - int segmentCount, int stackMoveDistance, final CacheLoader loader) { + int segmentCount, int stackMoveDistance, final CacheLoader loader, + EvictionCallback evicted) { this.weigher = weigher; setMaxMemory(maxMemory); setAverageMemory(averageMemory); @@ -137,6 +160,7 @@ this.segmentMask = segmentCount - 1; this.stackMoveDistance = stackMoveDistance; segments = new Segment[segmentCount]; + this.evicted = evicted; invalidateAll(); this.segmentShift = Integer.numberOfTrailingZeros(segments[0].entries.length); this.loader = loader; @@ -151,7 +175,7 @@ for (int i = 0; i < segmentCount; i++) { Segment old = segments[i]; Segment s = new Segment(this, - max, averageMemory, stackMoveDistance); + max, averageMemory, stackMoveDistance, evicted); if (old != null) { s.hitCount = old.hitCount; s.missCount = old.missCount; @@ -159,7 +183,16 @@ s.loadExceptionCount = old.loadExceptionCount; s.totalLoadTime = old.totalLoadTime; s.evictionCount = old.evictionCount; + + if (evicted != null) { + for (Entry entry : old.entries) { + while (entry != null) { + evicted.evicted(entry.key, entry.value); + entry = entry.mapNext; - } + } + } + } + } segments[i] = s; } } @@ -214,20 +247,20 @@ public void put(K key, V value) { put(key, value, sizeOf(key, value)); } - + @Override public V get(K key, Callable valueLoader) throws ExecutionException { int hash = getHash(key); return getSegment(hash).get(key, hash, valueLoader); } - + /** * Get the value, loading it if needed. *

* If there is an exception loading, an UncheckedExecutionException is * thrown. - * + * * @param key the key * @return the value * @throws UncheckedExecutionException @@ -240,10 +273,10 @@ throw new UncheckedExecutionException(e); } } - + /** * Get the value, loading it if needed. - * + * * @param key the key * @return the value * @throws ExecutionException @@ -253,14 +286,14 @@ int hash = getHash(key); return getSegment(hash).get(key, hash, loader); } - + /** * Re-load the value for the given key. *

* If there is an exception while loading, it is logged and ignored. This * method calls CacheLoader.reload, but synchronously replaces the old * value. - * + * * @param key the key */ @Override @@ -272,7 +305,7 @@ LOG.warn("Could not refresh value for key " + key, e); } } - + V replace(K key, V value) { int hash = getHash(key); return getSegment(hash).replace(key, hash, value, sizeOf(key, value)); @@ -292,7 +325,7 @@ int hash = getHash(key); return getSegment(hash).putIfAbsent(key, hash, value, sizeOf(key, value)); } - + /** * Get the value for the given key if the entry is cached. This method * adjusts the internal state of the cache sometimes, to ensure commonly @@ -322,7 +355,7 @@ } return weigher.weigh(key, value); } - + /** * Remove an entry. Both resident and non-resident entries can be * removed. @@ -334,7 +367,7 @@ int hash = getHash(key); getSegment(hash).invalidate(key, hash); } - + /** * Remove an entry. Both resident and non-resident entries can be * removed. @@ -346,7 +379,7 @@ int hash = getHash(key); return getSegment(hash).remove(key, hash); } - + @SuppressWarnings("unchecked") @Override public void invalidateAll(Iterable keys) { @@ -472,7 +505,7 @@ } return map.entrySet(); } - + protected Collection values() { ArrayList list = new ArrayList(); for (K k : keySet()) { @@ -483,7 +516,7 @@ } return list; } - + boolean containsValue(Object value) { for (Segment s : segments) { for (K k : s.keySet()) { @@ -561,7 +594,7 @@ } return x; } - + void clear() { for (Segment s : segments) { s.clear(); @@ -583,11 +616,11 @@ } return keys; } - + @Override public CacheStats stats() { long hitCount = 0; - long missCount = 0; + long missCount = 0; long loadSuccessCount = 0; long loadExceptionCount = 0; long totalLoadTime = 0; @@ -600,7 +633,7 @@ totalLoadTime += s.totalLoadTime; evictionCount += s.evictionCount; } - CacheStats stats = new CacheStats(hitCount, missCount, loadSuccessCount, + CacheStats stats = new CacheStats(hitCount, missCount, loadSuccessCount, loadExceptionCount, totalLoadTime, evictionCount); return stats; } @@ -639,7 +672,7 @@ * The currently used memory. */ long usedMemory; - + long hitCount; long missCount; long loadSuccessCount; @@ -703,18 +736,25 @@ private int stackMoveCounter; /** + * The eviction listener of this segment or {@code null} if none. + */ + private final EvictionCallback evicted; + + /** * Create a new cache. - * - * @param maxMemory the maximum memory to use + * @param maxMemory the maximum memory to use * @param averageMemory the average memory usage of an object * @param stackMoveDistance the number of other entries to be moved to * the top of the stack before moving an entry to the top + * @param evicted the eviction listener of this segment or {@code null} if none. */ - Segment(CacheLIRS cache, long maxMemory, int averageMemory, int stackMoveDistance) { + Segment(CacheLIRS cache, long maxMemory, int averageMemory, int stackMoveDistance, + EvictionCallback evicted) { this.cache = cache; setMaxMemory(maxMemory); setAverageMemory(averageMemory); this.stackMoveDistance = stackMoveDistance; + this.evicted = evicted; clear(); } @@ -844,7 +884,7 @@ addToStack(e); } } - + V get(K key, int hash, Callable valueLoader) throws ExecutionException { // we can not synchronize on a per-segment object while loading, // because we don't want to block cache access while loading, and @@ -881,7 +921,7 @@ loadNow.notifyAll(); } } - } + } // another thread is (or was) already loading synchronized (alreadyLoading) { // loading might have been finished, so check again @@ -903,7 +943,7 @@ } } } - + V load(K key, int hash, Callable valueLoader) throws ExecutionException { V value; long start = System.nanoTime(); @@ -920,7 +960,7 @@ put(key, hash, value, cache.sizeOf(key, value)); return value; } - + V get(K key, int hash, CacheLoader loader) throws ExecutionException { // avoid synchronization if it's in the cache V value = get(key, hash); @@ -950,9 +990,9 @@ return value; } } - + synchronized V replace(K key, int hash, V value, int memory) { - if (containsKey(key, hash)) { + if (containsKey(key, hash)) { return put(key, hash, value, memory); } return null; @@ -975,7 +1015,7 @@ } return false; } - + synchronized V remove(Object key, int hash) { V old = get(key, hash); // even if old is null, there might still be a cold entry @@ -996,7 +1036,7 @@ if (loader == null) { // no loader - no refresh return; - } + } V value; V old = get(key, hash); long start = System.nanoTime(); @@ -1070,7 +1110,7 @@ */ synchronized void invalidate(Object key, int hash) { Entry[] array = entries; - int mask = array.length - 1; + int mask = array.length - 1; int index = hash & mask; Entry e = array[index]; if (e == null) { @@ -1108,7 +1148,10 @@ removeFromQueue(e); } pruneStack(); + if (evicted != null) { + evicted.evicted(e.key, e.value); - } + } + } /** * Evict cold entries (resident and non-resident) until the memory limit is @@ -1189,7 +1232,7 @@ */ Entry find(Object key, int hash) { Entry[] array = entries; - int mask = array.length - 1; + int mask = array.length - 1; int index = hash & mask; Entry e = array[index]; while (e != null && !e.key.equals(key)) { @@ -1394,17 +1437,18 @@ } } - + /** * A builder for the cache. */ public static class Builder { - + private Weigher weigher; private long maxWeight; private int averageWeight = 100; private int segmentCount = 16; private int stackMoveDistance = 16; + private EvictionCallback evicted; public Builder recordStats() { return this; @@ -1419,12 +1463,12 @@ this.maxWeight = maxWeight; return this; } - + public Builder averageWeight(int averageWeight) { this.averageWeight = averageWeight; return this; } - + public Builder maximumSize(int maxSize) { this.maxWeight = maxSize; this.averageWeight = 1; @@ -1444,25 +1488,29 @@ if (stackMoveDistance < 0) { LOG.warn("Illegal stack move distance: " + stackMoveDistance + ", using 16"); stackMoveDistance = 16; - } + } this.stackMoveDistance = stackMoveDistance; return this; } + public Builder evictionCallback(EvictionCallback evicted) { + this.evicted = evicted; + return this; + } + public CacheLIRS build() { return build(null); } - + public CacheLIRS build(CacheLoader cacheLoader) { return new CacheLIRS(weigher, maxWeight, averageWeight, - segmentCount, stackMoveDistance, cacheLoader); + segmentCount, stackMoveDistance, cacheLoader, evicted); } - } /** * Create a builder. - * + * * @return the builder */ public static Builder newBuilder() { Index: oak-core/src/test/java/org/apache/jackrabbit/oak/cache/CacheTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/cache/CacheTest.java (date 1436795853000) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/cache/CacheTest.java (date 1436783762000) @@ -18,6 +18,7 @@ */ package org.apache.jackrabbit.oak.cache; +import static com.google.common.collect.Sets.newHashSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -29,10 +30,12 @@ import java.util.List; import java.util.Map.Entry; import java.util.Random; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; +import org.apache.jackrabbit.oak.cache.CacheLIRS.EvictionCallback; import org.junit.Test; import com.google.common.cache.CacheLoader; @@ -573,7 +576,7 @@ } private static CacheLIRS createCache(int maxSize, int averageSize) { - return new CacheLIRS(null, maxSize, averageSize, 1, 0, null); + return new CacheLIRS(null, maxSize, averageSize, 1, 0, null, null); } @Test @@ -680,5 +683,31 @@ // expected to log a warning, but not fail cache.refresh(-1); } + + @Test + public void evictionCallback() throws ExecutionException { + final Set evicted = newHashSet(); + CacheLIRS cache = CacheLIRS.newBuilder() + .maximumSize(100) + .evictionCallback(new EvictionCallback() { + @Override + public void evicted(String key, Integer value) { + evicted.add(key); + } + }) + .build(); + + for (int k = 0; k < 200; k++) { + cache.put(String.valueOf(k), k); + } + + for (String key : evicted) { + assertFalse(cache.containsKey(key)); + } + + cache.invalidateAll(); + assertEquals(200, evicted.size()); + } + - + }