/** Returns if the cache should bypass the read buffer. */ boolean skipReadBuffer() { return fastpath() && frequencySketch().isNotInitialized(); }
/** * Determines if the candidate should be accepted into the main space, as determined by its * frequency relative to the victim. A small amount of randomness is used to protect against hash * collision attacks, where the victim's frequency is artificially raised so that no new entries * are admitted. * * @param candidateKey the key for the entry being proposed for long term retention * @param victimKey the key for the entry chosen by the eviction policy for replacement * @return if the candidate should be admitted and the victim ejected */ @GuardedBy("evictionLock") boolean admit(K candidateKey, K victimKey) { int victimFreq = frequencySketch().frequency(victimKey); int candidateFreq = frequencySketch().frequency(candidateKey); if (candidateFreq > victimFreq) { return true; } else if (candidateFreq <= 5) { // The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack // exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm // candidate reduces the number of random acceptances to minimize the impact on the hit rate. return false; } int random = ThreadLocalRandom.current().nextInt(); return ((random & 127) == 0); }
/** * Sets the maximum weighted size of the cache. The caller may need to perform a maintenance cycle * to eagerly evicts entries until the cache shrinks to the appropriate size. */ @GuardedBy("evictionLock") void setMaximum(long maximum) { requireArgument(maximum >= 0); long max = Math.min(maximum, MAXIMUM_CAPACITY); long eden = max - (long) (max * PERCENT_MAIN); long mainProtected = (long) ((max - eden) * PERCENT_MAIN_PROTECTED); lazySetMaximum(max); lazySetEdenMaximum(eden); lazySetMainProtectedMaximum(mainProtected); if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) { // Lazily initialize when close to the maximum size frequencySketch().ensureCapacity(max); } }
@Test(dataProvider = "caches") @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, population = Population.EMPTY, maximumSize = Maximum.FULL) public void exceedsMaximumBufferSize_onRead(Cache<Integer, Integer> cache, CacheContext context) { BoundedLocalCache<Integer, Integer> localCache = asBoundedLocalCache(cache); Node<Integer, Integer> dummy = localCache.nodeFactory.newNode( new WeakKeyReference<>(null, null), null, null, 1, 0); localCache.frequencySketch().ensureCapacity(1); Buffer<Node<Integer, Integer>> buffer = localCache.readBuffer; for (int i = 0; i < BoundedBuffer.BUFFER_SIZE; i++) { buffer.offer(dummy); } assertThat(buffer.offer(dummy), is(Buffer.FULL)); localCache.afterRead(dummy, 0, /* recordHit */ true); assertThat(buffer.offer(dummy), is(not(Buffer.FULL))); }
/** Force the random seed to a predictable value. */ public static void ensureRandomSeed(Cache<?, ?> cache) { BoundedLocalCache<?, ?> map = unwrap(cache); if (map == null) { return; } resetThreadLocalRandom(); if (map.evicts()) { ensureRandomSeed(map.frequencySketch()); } }
@Test(dataProvider = "caches") @CacheSpec(compute = Compute.SYNC, implementation = Implementation.Caffeine, population = Population.EMPTY, initialCapacity = InitialCapacity.EXCESSIVE, maximumSize = Maximum.TEN, weigher = CacheWeigher.DEFAULT) public void evict_wtinylfu(Cache<Integer, Integer> cache, CacheContext context) throws Exception { // Enforce full initialization of internal structures; clear sketch BoundedLocalCache<Integer, Integer> localCache = asBoundedLocalCache(cache); localCache.frequencySketch().ensureCapacity(10); for (int i = 0; i < 10; i++) { cache.put(i, -i); } checkContainsInOrder(cache, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8); // re-order checkReorder(cache, asList(0, 1, 2), 9, 3, 4, 5, 6, 7, 8, 0, 1, 2); // evict 9, 10, 11 checkEvict(cache, asList(10, 11, 12), 12, 3, 4, 5, 6, 7, 8, 0, 1, 2); // re-order checkReorder(cache, asList(6, 7, 8), 12, 3, 4, 5, 0, 1, 2, 6, 7, 8); // evict 12, 13, 14 checkEvict(cache, asList(13, 14, 15), 15, 3, 4, 5, 0, 1, 2, 6, 7, 8); assertThat(context, hasEvictionCount(6)); }
/** * Returns an unmodifiable snapshot map ordered in eviction order, either ascending or descending. * Beware that obtaining the mappings is <em>NOT</em> a constant-time operation. * * @param limit the maximum number of entries * @param transformer a function that unwraps the value * @param hottest the iteration order * @return an unmodifiable snapshot in a specified order */ @SuppressWarnings("GuardedByChecker") Map<K, V> evictionOrder(int limit, Function<V, V> transformer, boolean hottest) { Supplier<Iterator<Node<K, V>>> iteratorSupplier = () -> { Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> { K key = node.getKey(); return (key == null) ? 0 : frequencySketch().frequency(key); }); if (hottest) { PeekingIterator<Node<K, V>> secondary = PeekingIterator.comparing( accessOrderProbationDeque().descendingIterator(), accessOrderEdenDeque().descendingIterator(), comparator); return PeekingIterator.concat(accessOrderProtectedDeque().descendingIterator(), secondary); } else { PeekingIterator<Node<K, V>> primary = PeekingIterator.comparing( accessOrderEdenDeque().iterator(), accessOrderProbationDeque().iterator(), comparator.reversed()); return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator()); } }; return fixedSnapshot(iteratorSupplier, limit, transformer); }
/** Updates the node's location in the page replacement policy. */ @GuardedBy("evictionLock") void onAccess(Node<K, V> node) { if (evicts()) { K key = node.getKey(); if (key == null) { return; } frequencySketch().increment(key); if (node.inEden()) { reorder(accessOrderEdenDeque(), node); } else if (node.inMainProbation()) { reorderProbation(node); } else { reorder(accessOrderProtectedDeque(), node); } } else if (expiresAfterAccess()) { reorder(accessOrderEdenDeque(), node); } if (expiresVariable()) { timerWheel().reschedule(node); } }
/** * Determines if the candidate should be accepted into the main space, as determined by its * frequency relative to the victim. A small amount of randomness is used to protect against hash * collision attacks, where the victim's frequency is artificially raised so that no new entries * are admitted. * * @param candidateKey the key for the entry being proposed for long term retention * @param victimKey the key for the entry chosen by the eviction policy for replacement * @return if the candidate should be admitted and the victim ejected */ @GuardedBy("evictionLock") boolean admit(K candidateKey, K victimKey) { int victimFreq = frequencySketch().frequency(victimKey); int candidateFreq = frequencySketch().frequency(candidateKey); if (candidateFreq > victimFreq) { return true; } else if (candidateFreq <= 5) { // The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack // exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm // candidate reduces the number of random acceptances to minimize the impact on the hit rate. return false; } int random = ThreadLocalRandom.current().nextInt(); return ((random & 127) == 0); }
/** Returns if the cache should bypass the read buffer. */ boolean skipReadBuffer() { return fastpath() && frequencySketch().isNotInitialized(); }
/** * Sets the maximum weighted size of the cache. The caller may need to perform a maintenance cycle * to eagerly evicts entries until the cache shrinks to the appropriate size. */ @GuardedBy("evictionLock") void setMaximum(long maximum) { requireArgument(maximum >= 0); long max = Math.min(maximum, MAXIMUM_CAPACITY); long eden = max - (long) (max * PERCENT_MAIN); long mainProtected = (long) ((max - eden) * PERCENT_MAIN_PROTECTED); lazySetMaximum(max); lazySetEdenMaximum(eden); lazySetMainProtectedMaximum(mainProtected); if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) { // Lazily initialize when close to the maximum size frequencySketch().ensureCapacity(max); } }
/** Updates the node's location in the page replacement policy. */ @GuardedBy("evictionLock") void onAccess(Node<K, V> node) { if (evicts()) { K key = node.getKey(); if (key == null) { return; } frequencySketch().increment(key); if (node.inEden()) { reorder(accessOrderEdenDeque(), node); } else if (node.inMainProbation()) { reorderProbation(node); } else { reorder(accessOrderProtectedDeque(), node); } } else if (expiresAfterAccess()) { reorder(accessOrderEdenDeque(), node); } if (expiresVariable()) { timerWheel().reschedule(node); } }
/** * Returns an unmodifiable snapshot map ordered in eviction order, either ascending or descending. * Beware that obtaining the mappings is <em>NOT</em> a constant-time operation. * * @param limit the maximum number of entries * @param transformer a function that unwraps the value * @param hottest the iteration order * @return an unmodifiable snapshot in a specified order */ @SuppressWarnings("GuardedByChecker") Map<K, V> evictionOrder(int limit, Function<V, V> transformer, boolean hottest) { Supplier<Iterator<Node<K, V>>> iteratorSupplier = () -> { Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> { K key = node.getKey(); return (key == null) ? 0 : frequencySketch().frequency(key); }); if (hottest) { PeekingIterator<Node<K, V>> secondary = PeekingIterator.comparing( accessOrderProbationDeque().descendingIterator(), accessOrderEdenDeque().descendingIterator(), comparator); return PeekingIterator.concat(accessOrderProtectedDeque().descendingIterator(), secondary); } else { PeekingIterator<Node<K, V>> primary = PeekingIterator.comparing( accessOrderEdenDeque().iterator(), accessOrderProbationDeque().iterator(), comparator.reversed()); return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator()); } }; return fixedSnapshot(iteratorSupplier, limit, transformer); }