diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/CacheLIRS.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/CacheLIRS.java index 26e44e7..7e21088 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/CacheLIRS.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/cache/CacheLIRS.java @@ -28,6 +28,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.slf4j.Logger; @@ -75,6 +76,31 @@ 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. + *

+ * Note: It is not safe to call any of {@code CacheLIRS}'s + * method from withing this callback. Any such call might result in + * undefined behaviour and Java level deadlocks. + *

+ * The method may be called twice for the same key (first if the entry + * is resident, and later if the entry is non-resident). + * + * @param key the evicted item's key + * @param value the evicted item's value or {@code null} if non-resident + */ + void evicted(@Nonnull K key, @Nullable V value); + } + + /** * The maximum memory this cache should use. */ private long maxMemory; @@ -94,7 +120,12 @@ public class CacheLIRS implements LoadingCache { 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 @@ -112,7 +143,7 @@ public class CacheLIRS implements LoadingCache { * @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 +154,12 @@ public class CacheLIRS implements LoadingCache { * @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, - 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 +170,7 @@ public class CacheLIRS implements LoadingCache { 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; @@ -160,7 +194,25 @@ public class CacheLIRS implements LoadingCache { s.totalLoadTime = old.totalLoadTime; s.evictionCount = old.evictionCount; } - segments[i] = s; + setSegment(i, s); + } + } + + private void setSegment(int index, Segment s) { + Segment old = segments[index]; + segments[index] = s; + if (evicted != null && old != null && old != s) { + old.evictedAll(); + } + } + + void evicted(Entry entry) { + if (evicted == null) { + return; + } + K key = entry.key; + if (key != null) { + evicted.evicted(key, entry.value); } } @@ -564,7 +616,12 @@ public class CacheLIRS implements LoadingCache { void clear() { for (Segment s : segments) { - s.clear(); + synchronized (s) { + if (evicted != null) { + s.evictedAll(); + } + s.clear(); + } } } @@ -718,6 +775,22 @@ public class CacheLIRS implements LoadingCache { clear(); } + public void evictedAll() { + for (Entry e = stack.stackNext; e != stack; e = e.stackNext) { + if (e.value != null) { + cache.evicted(e); + } + } + for (Entry e = queue.queueNext; e != queue; e = e.queueNext) { + if (e.stackNext == null) { + cache.evicted(e); + } + } + for (Entry e = queue2.queueNext; e != queue2; e = e.queueNext) { + cache.evicted(e); + } + } + synchronized void clear() { // calculate the size of the map array @@ -1097,17 +1170,18 @@ public class CacheLIRS implements LoadingCache { if (e.isHot()) { // when removing a hot entry, the newest cold entry gets hot, // so the number of hot entries does not change - e = queue.queueNext; - if (e != queue) { - removeFromQueue(e); - if (e.stackNext == null) { - addToStackBottom(e); + Entry nc = queue.queueNext; + if (nc != queue) { + removeFromQueue(nc); + if (nc.stackNext == null) { + addToStackBottom(nc); } } } else { removeFromQueue(e); } pruneStack(); + cache.evicted(e); } /** @@ -1135,6 +1209,7 @@ public class CacheLIRS implements LoadingCache { usedMemory -= e.memory; evictionCount++; removeFromQueue(e); + cache.evicted(e); e.value = null; e.memory = 0; addToQueue(queue2, e); @@ -1405,6 +1480,7 @@ public class CacheLIRS implements LoadingCache { private int averageWeight = 100; private int segmentCount = 16; private int stackMoveDistance = 16; + private EvictionCallback evicted; public Builder recordStats() { return this; @@ -1449,16 +1525,22 @@ public class CacheLIRS implements LoadingCache { return this; } + public Builder evictionCallback(EvictionCallback evicted) { + this.evicted = evicted; + return this; + } + public CacheLIRS build() { return build(null); } - + + @SuppressWarnings("unchecked") public CacheLIRS build( CacheLoader cacheLoader) { - @SuppressWarnings("unchecked") Weigher w = (Weigher) weigher; + EvictionCallback ec = (EvictionCallback) evicted; return new CacheLIRS(w, maxWeight, averageWeight, - segmentCount, stackMoveDistance, cacheLoader); + segmentCount, stackMoveDistance, cacheLoader, ec); } } diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/cache/CacheTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/cache/CacheTest.java index a4cc151..ba0c90a 100644 --- a/oak-core/src/test/java/org/apache/jackrabbit/oak/cache/CacheTest.java +++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/cache/CacheTest.java @@ -18,6 +18,8 @@ */ package org.apache.jackrabbit.oak.cache; +import static com.google.common.collect.Sets.newHashSet; +import static java.lang.String.valueOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -29,10 +31,12 @@ import java.util.HashSet; 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 +577,7 @@ public class CacheTest { } 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 +684,95 @@ public class CacheTest { // expected to log a warning, but not fail cache.refresh(-1); } + + @Test + public void evictionCallback() throws ExecutionException { + final Set evictedKeys = newHashSet(); + final Set evictedValues = newHashSet(); + CacheLIRS cache = CacheLIRS.newBuilder() + .maximumSize(100) + .evictionCallback(new EvictionCallback() { + @Override + public void evicted(String key, Integer value) { + evictedKeys.add(key); + if (value != null) { + assertEquals(key, valueOf(value)); + evictedValues.add(value); + } + } + }) + .build(); + + for (int k = 0; k < 200; k++) { + cache.put(valueOf(k), k); + } + + assertTrue(evictedKeys.size() <= evictedValues.size()); + for (String key : evictedKeys) { + assertFalse(cache.containsKey(key)); + } + + cache.invalidateAll(); + assertEquals(200, evictedKeys.size()); + assertEquals(200, evictedValues.size()); + } + @Test + public void evictionCallbackRandomized() throws ExecutionException { + final HashMap evictedMap = new HashMap(); + final HashSet evictedNonResidentSet = new HashSet(); + CacheLIRS cache = CacheLIRS.newBuilder() + .maximumSize(10) + .evictionCallback(new EvictionCallback() { + @Override + public void evicted(Integer key, Integer value) { + if (value == null) { + assertTrue(evictedNonResidentSet.add(key)); + } else { + assertEquals(null, evictedMap.put(key, value)); + } + } + }) + .build(); + Random r = new Random(1); + for (int k = 0; k < 10000; k++) { + if (r.nextInt(20) == 0) { + evictedMap.clear(); + evictedNonResidentSet.clear(); + long size = cache.size(); + long sizeNonResident = cache.sizeNonResident(); + cache.invalidateAll(); + assertEquals(evictedMap.size(), size); + assertEquals(evictedNonResidentSet.size(), sizeNonResident); + } + evictedMap.clear(); + evictedNonResidentSet.clear(); + int key = r.nextInt(20); + if (r.nextBoolean()) { + cache.put(key, k); + } else { + cache.get(key); + } + for (Entry ev : evictedMap.entrySet()) { + int ek = ev.getKey(); + if (ek == key) { + // the same key was inserted just now + } else { + assertFalse(cache.containsKey(ek)); + } + } + for (Entry ev : evictedMap.entrySet()) { + int ek = ev.getKey(); + Integer v = ev.getValue(); + // an old value + assertTrue(v < k); + if (ek == key) { + // the same key was inserted just now + } else { + assertFalse(cache.containsKey(ek)); + } + } + } + } + }