All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.jackrabbit.oak.cache.CacheLIRS Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.oak.cache;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import org.apache.jackrabbit.guava.common.cache.CacheLoader;
import org.apache.jackrabbit.guava.common.cache.CacheStats;
import org.apache.jackrabbit.guava.common.cache.LoadingCache;
import org.apache.jackrabbit.guava.common.cache.RemovalCause;
import org.apache.jackrabbit.guava.common.cache.Weigher;
import org.apache.jackrabbit.guava.common.collect.ImmutableMap;
import org.apache.jackrabbit.guava.common.util.concurrent.ListenableFuture;
import org.apache.jackrabbit.guava.common.util.concurrent.UncheckedExecutionException;
import org.apache.jackrabbit.oak.commons.annotations.Internal;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *  For Oak internal use only. Do not use outside Oak components.
 *  

* A scan resistant cache. It is meant to cache objects that are relatively * costly to acquire, for example file content. *

* This implementation is multi-threading safe and supports concurrent access. * Null keys or null values are not allowed. The map fill factor is at most 75%. *

* Each entry is assigned a distinct memory size, and the cache will try to use * at most the specified amount of memory. The memory unit is not relevant, * however it is suggested to use bytes as the unit. *

* This class implements an approximation of the the LIRS replacement algorithm * invented by Xiaodong Zhang and Song Jiang as described in * http://www.cse.ohio-state.edu/~zhang/lirs-sigmetrics-02.html with a few * smaller changes: An additional queue for non-resident entries is used, to * prevent unbound memory usage. The maximum size of this queue is at most the * size of the rest of the stack. About 6.25% of the mapped entries are cold. *

* Internally, the cache is split into a number of segments, and each segment is * an individual LIRS cache. *

* Accessed entries are only moved to the top of the stack if at least a number * of other entries have been moved to the front (1% by default). Write access * and moving entries to the top of the stack is synchronized per segment. * * @author Thomas Mueller * @param the key type * @param the value type * * @deprecated The Jackrabbit Oak Cache library is designed for Oak-internal use only and thus deprecated. It will not be part of the AEM SDK after April 2023. */ @Internal(since = "1.1.1") @Deprecated(since = "2022-12-01") public class CacheLIRS implements LoadingCache { static final Logger LOG = LoggerFactory.getLogger(CacheLIRS.class); static final ThreadLocal CURRENTLY_LOADING = new ThreadLocal(); private static final AtomicInteger NEXT_CACHE_ID = new AtomicInteger(); private static final boolean PUT_HOT = Boolean.parseBoolean(System.getProperty("oak.cacheLIRS.putHot", "true")); /** * 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 * * @deprecated The Jackrabbit Oak Cache library is designed for Oak-internal use only and thus deprecated. It will not be part of the AEM SDK after April 2023. */ @Deprecated(since = "2022-12-01") 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 * @param cause the cause of the eviction */ void evicted(@NotNull K key, @Nullable V value, @NotNull RemovalCause cause); } final int cacheId = NEXT_CACHE_ID.getAndIncrement(); /** * The maximum memory this cache should use. */ private long maxMemory; /** * The average memory used by one entry. */ private int averageMemory; private final Segment[] segments; private final int segmentCount; 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 = 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 * distance equals to the maximum number of entries divided by 100). * * @param maxEntries the maximum number of entries */ public CacheLIRS(int maxEntries) { this(null, maxEntries, 1, 16, maxEntries / 100, null, null, null); } /** * Create a new cache with the given memory size. * * @param maxMemory the maximum memory to use (1 or larger) * @param averageMemory the average memory (1 or larger) * @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. */ @Deprecated(since = "1.20.0", forRemoval = true) @SuppressWarnings("unchecked") CacheLIRS(Weigher weigher, long maxMemory, int averageMemory, int segmentCount, int stackMoveDistance, final CacheLoader loader, EvictionCallback evicted, String module) { LOG.debug("Init #{}, module={}, maxMemory={}, segmentCount={}, stackMoveDistance={}", cacheId, module, maxMemory, segmentCount, segmentCount); this.weigher = weigher; setMaxMemory(maxMemory); setAverageMemory(averageMemory); if (Integer.bitCount(segmentCount) != 1) { throw new IllegalArgumentException("The segment count must be a power of 2, is " + segmentCount); } this.segmentCount = segmentCount; 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; } /** * Remove all entries. */ @Override public void invalidateAll() { long max = Math.max(1, maxMemory / segmentCount); for (int i = 0; i < segmentCount; i++) { Segment old = segments[i]; Segment s = new Segment(this, max, averageMemory, stackMoveDistance); if (old != null) { s.hitCount = old.hitCount; s.missCount = old.missCount; s.loadSuccessCount = old.loadSuccessCount; s.loadExceptionCount = old.loadExceptionCount; s.totalLoadTime = old.totalLoadTime; s.evictionCount = old.evictionCount; } 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(RemovalCause.EXPLICIT); } } void evicted(Entry entry, RemovalCause cause) { if (evicted == null) { return; } K key = entry.key; if (key != null) { evicted.evicted(key, entry.value, cause); } } /** * Check whether there is a resident entry for the given key. This * method does not adjust the internal state of the cache. * * @param key the key (may not be null) * @return true if there is a resident entry */ public boolean containsKey(Object key) { int hash = getHash(key); return getSegment(hash).containsKey(key, hash); } /** * Get the value for the given key if the entry is cached. This method does * not modify the internal state. * * @param key the key (may not be null) * @return the value, or null if there is no resident entry */ public V peek(K key) { int hash = getHash(key); Entry e = getSegment(hash).find(key, hash); return e == null ? null : e.value; } /** * Add an entry to the cache. This method is an explicit memory size * (weight), and not using the weigher even if configured. The entry may or * may not exist in the cache yet. This method will usually mark unknown * entries as cold and known entries as hot. * * @param key the key (may not be null) * @param value the value (may not be null) * @param memory the memory used for the given entry * @return the old value, or null if there was no resident entry */ public V put(K key, V value, int memory) { int hash = getHash(key); return getSegment(hash).put(key, hash, value, memory); } /** * Add an entry to the cache. If a weigher is specified, it is used, * otherwise the average memory size is used. * * @param key the key (may not be null) * @param value the value (may not be null) */ @Override 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 */ @Override public V getUnchecked(K key) { try { return get(key); } catch (ExecutionException e) { throw new UncheckedExecutionException(e); } } /** * Get the value, loading it if needed. * * @param key the key * @return the value * @throws ExecutionException */ @Override public V get(K key) throws ExecutionException { 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 public void refresh(K key) { int hash = getHash(key); try { getSegment(hash).refresh(key, hash, loader); } catch (ExecutionException e) { 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)); } boolean replace(K key, V oldValue, V newValue) { int hash = getHash(key); return getSegment(hash).replace(key, hash, oldValue, newValue, sizeOf(key, newValue)); } boolean remove(Object key, Object value) { int hash = getHash(key); return getSegment(hash).remove(key, hash, value); } protected V putIfAbsent(K key, V value) { 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 * used entries stay in the cache. * * @param key the key (may not be null) * @return the value, or null if there is no resident entry */ @Override @Nullable public V getIfPresent(Object key) { int hash = getHash(key); return getSegment(hash).get(key, hash); } /** * Get the size of the given value. The default implementation returns the * average memory as configured for this cache. * * @param key the key * @param value the value * @return the size */ protected int sizeOf(K key, V value) { if (weigher == null) { return averageMemory; } return weigher.weigh(key, value); } /** * Remove an entry. Both resident and non-resident entries can be * removed. * * @param key the key (may not be null) */ @Override public void invalidate(Object key) { int hash = getHash(key); getSegment(hash).invalidate(key, hash, RemovalCause.EXPLICIT); } /** * Remove an entry. Both resident and non-resident entries can be * removed. * * @param key the key (may not be null) * @return the old value or null */ public V remove(Object key) { int hash = getHash(key); return getSegment(hash).remove(key, hash); } @SuppressWarnings("unchecked") @Override public void invalidateAll(Iterable keys) { for (K k : (Iterable) keys) { invalidate(k); } } /** * Get the memory used for the given key. * * @param key the key (may not be null) * @return the memory, or 0 if there is no resident entry */ public int getMemory(K key) { int hash = getHash(key); return getSegment(hash).getMemory(key, hash); } private Segment getSegment(int hash) { int segmentIndex = (hash >>> segmentShift) & segmentMask; return segments[segmentIndex]; } /** * Get the hash code for the given key. The hash code is * further enhanced to spread the values more evenly. * * @param key the key * @return the hash code */ static int getHash(Object key) { int hash = key.hashCode(); // a supplemental secondary hash function // to protect against hash codes that don't differ much hash = ((hash >>> 16) ^ hash) * 0x45d9f3b; hash = ((hash >>> 16) ^ hash) * 0x45d9f3b; hash = (hash >>> 16) ^ hash; return hash; } /** * Get the currently used memory. * * @return the used memory */ public long getUsedMemory() { long x = 0; for (Segment s : segments) { x += s.usedMemory; } return x; } /** * Set the maximum memory this cache should use. This will not * immediately cause entries to get removed however; it will only change * the limit. To resize the internal array, call the clear method. * * @param maxMemory the maximum size (1 or larger) */ public void setMaxMemory(long maxMemory) { if (maxMemory < 0) { throw new IllegalArgumentException("Max memory must not be negative"); } this.maxMemory = maxMemory; if (segments != null) { long max = 1 + maxMemory / segments.length; for (Segment s : segments) { s.setMaxMemory(max); } } } /** * Set the average memory used per entry. It is used to calculate the * length of the internal array. * * @param averageMemory the average memory used (1 or larger) */ public void setAverageMemory(int averageMemory) { if (averageMemory <= 0) { throw new IllegalArgumentException("Average memory must be larger than 0"); } this.averageMemory = averageMemory; if (segments != null) { for (Segment s : segments) { s.setAverageMemory(averageMemory); } } } /** * Get the average memory used per entry. * * @return the average memory */ public int getAverageMemory() { return averageMemory; } /** * Get the maximum memory to use. * * @return the maximum memory */ public long getMaxMemory() { return maxMemory; } /** * Get the entry set for all resident entries. * * @return the entry set */ public synchronized Set> entrySet() { HashMap map = new HashMap(); for (K k : keySet()) { V v = peek(k); if (v != null) { map.put(k, v); } } return map.entrySet(); } protected Collection values() { ArrayList list = new ArrayList(); for (K k : keySet()) { V v = peek(k); if (v != null) { list.add(v); } } return list; } boolean containsValue(Object value) { for (Segment s : segments) { for (K k : s.keySet()) { V v = peek(k); if (v != null && v.equals(value)) { return true; } } } return false; } /** * Get the set of keys for resident entries. * * @return the set of keys */ public synchronized Set keySet() { HashSet set = new HashSet(); for (Segment s : segments) { set.addAll(s.keySet()); } return set; } /** * Get the number of non-resident entries in the cache. * * @return the number of non-resident entries */ public int sizeNonResident() { int x = 0; for (Segment s : segments) { x += s.queue2Size; } return x; } /** * Get the length of the internal map array. * * @return the size of the array */ public int sizeMapArray() { int x = 0; for (Segment s : segments) { x += s.entries.length; } return x; } /** * Get the number of hot entries in the cache. * * @return the number of hot entries */ public int sizeHot() { int x = 0; for (Segment s : segments) { x += s.mapSize - s.queueSize - s.queue2Size; } return x; } /** * Get the number of resident entries. * * @return the number of entries */ @Override public long size() { int x = 0; for (Segment s : segments) { x += s.mapSize - s.queue2Size; } return x; } void clear() { for (Segment s : segments) { synchronized (s) { if (evicted != null) { s.evictedAll(RemovalCause.EXPLICIT); } s.clear(); } } } /** * Get the list of keys. This method allows to read the internal state of * the cache. * * @param cold if true, only keys for the cold entries are returned * @param nonResident true for non-resident entries * @return the key list */ public synchronized List keys(boolean cold, boolean nonResident) { ArrayList keys = new ArrayList(); for (Segment s : segments) { keys.addAll(s.keys(cold, nonResident)); } return keys; } @Override public CacheStats stats() { long hitCount = 0; long missCount = 0; long loadSuccessCount = 0; long loadExceptionCount = 0; long totalLoadTime = 0; long evictionCount = 0; for (Segment s : segments) { hitCount += s.hitCount.longValue(); missCount += s.missCount.longValue(); loadSuccessCount += s.loadSuccessCount.longValue(); loadExceptionCount += s.loadExceptionCount.longValue(); totalLoadTime += s.totalLoadTime.longValue(); evictionCount += s.evictionCount.longValue(); } CacheStats stats = new CacheStats(hitCount, missCount, loadSuccessCount, loadExceptionCount, totalLoadTime, evictionCount); return stats; } /** * A cache segment * * @param the key type * @param the value type * * @deprecated The Jackrabbit Oak Cache library is designed for Oak-internal use only and thus deprecated. It will not be part of the AEM SDK after April 2023. */ @Deprecated(since = "2022-12-01") static class Segment { /** * The number of (hot, cold, and non-resident) entries in the map. */ int mapSize; /** * The size of the LIRS queue for resident cold entries. */ int queueSize; /** * The size of the LIRS queue for non-resident cold entries. */ int queue2Size; /** * The map array. The size is always a power of 2. The bit mask that is * applied to the key hash code to get the index in the map array. The * mask is the length of the array minus one. */ Entry[] entries; /** * The currently used memory. */ long usedMemory; LongAdder hitCount = new LongAdder(); LongAdder missCount = new LongAdder(); LongAdder loadSuccessCount = new LongAdder(); LongAdder loadExceptionCount = new LongAdder(); LongAdder totalLoadTime = new LongAdder(); LongAdder evictionCount = new LongAdder(); /** * The cache. */ private final CacheLIRS cache; /** * How many other item are to be moved to the top of the stack before * the current item is moved. */ private final int stackMoveDistance; /** * The maximum memory this cache should use. */ private long maxMemory; /** * The average memory used by one entry. */ private int averageMemory; /** * The LIRS stack size. */ private int stackSize; /** * The stack of recently referenced elements. This includes all hot * entries, and the recently referenced cold entries. Resident cold * entries that were not recently referenced, as well as non-resident * cold entries, are not in the stack. *

* There is always at least one entry: the head entry. */ private Entry stack; /** * The queue of resident cold entries. *

* There is always at least one entry: the head entry. */ private Entry queue; /** * The queue of non-resident cold entries. *

* There is always at least one entry: the head entry. */ private Entry queue2; /** * The number of times any item was moved to the top of the stack. */ private int stackMoveCounter; /** * Create a new cache. * @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) { this.cache = cache; setMaxMemory(maxMemory); setAverageMemory(averageMemory); this.stackMoveDistance = stackMoveDistance; clear(); } public void evictedAll(RemovalCause cause) { for (Entry e = stack.stackNext; e != stack; e = e.stackNext) { if (e.value != null) { cache.evicted(e, cause); } } for (Entry e = queue.queueNext; e != queue; e = e.queueNext) { if (e.stackNext == null) { cache.evicted(e, cause); } } for (Entry e = queue2.queueNext; e != queue2; e = e.queueNext) { cache.evicted(e, cause); } } synchronized void clear() { // calculate the size of the map array // assume a fill factor of at most 80% long maxLen = (long) (maxMemory / averageMemory / 0.75); // the size needs to be a power of 2 long l = 8; while (l < maxLen) { l += l; } // the array size is at most 2^31 elements int len = (int) Math.min(1L << 31, l); // initialize the stack and queue heads stack = new Entry(); stack.stackPrev = stack.stackNext = stack; queue = new Entry(); queue.queuePrev = queue.queueNext = queue; queue2 = new Entry(); queue2.queuePrev = queue2.queueNext = queue2; // first set to a small array, to avoiding out of memory @SuppressWarnings("unchecked") Entry[] small = new Entry[1]; entries = small; @SuppressWarnings("unchecked") Entry[] e = new Entry[len]; entries = e; mapSize = 0; usedMemory = 0; stackSize = queueSize = queue2Size = 0; } /** * Get the memory used for the given key. * * @param key the key (may not be null) * @param hash the hash * @return the memory, or 0 if there is no resident entry */ int getMemory(K key, int hash) { Entry e = find(key, hash); return e == null ? 0 : e.memory; } /** * 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 * used entries stay in the cache. * * @param key the key (may not be null) * @param hash the hash * @return the value, or null if there is no resident entry */ V get(Object key, int hash) { if (LOG.isTraceEnabled()) { LOG.trace("#{} get hash {} key {}", cache.cacheId, hash, key); } Entry e = find(key, hash); if (e == null) { // the entry was not found missCount.increment(); return null; } V value = e.value; if (value == null) { // it was a non-resident entry missCount.increment(); return null; } if (e.isHot()) { if (e != stack.stackNext) { if (stackMoveDistance == 0 || stackMoveCounter - e.topMove > stackMoveDistance) { access(key, hash); } } } else { access(key, hash); } hitCount.increment(); return value; } /** * Access an item, moving the entry to the top of the stack or front of the * queue if found. * * @param key the key */ private synchronized void access(Object key, int hash) { Entry e = find(key, hash); if (e == null || e.value == null) { return; } if (e.isHot()) { if (e != stack.stackNext) { if (stackMoveDistance == 0 || stackMoveCounter - e.topMove > stackMoveDistance) { // move a hot entry to the top of the stack // unless it is already there boolean wasEnd = e == stack.stackPrev; removeFromStack(e); if (wasEnd) { // if moving the last entry, the last entry // could now be cold, which is not allowed pruneStack(); } addToStack(e); } } } else { removeFromQueue(e); if (e.stackNext != null) { // resident cold entries become hot // if they are on the stack removeFromStack(e); // which means a hot entry needs to become cold // (this entry is cold, that means there is at least one // more entry in the stack, which must be hot) convertOldestHotToCold(); } else { // cold entries that are not on the stack // move to the front of the queue addToQueue(queue, e); } // in any case, the cold entry is moved to the top of the stack 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 // because the value loader could access the cache (for example, // using put, or another get with a loader), which might result in a // deadlock // we loop here because another thread might load the value, // but loading might fail there, so we might need to repeat this while (true) { V value = get(key, hash); // the (hopefully) normal case if (value != null) { return value; } // if we are within a loader, and are currently loading // an entry, then we need to avoid a possible deadlock // (we ensure that while loading an entry, we only load // entries with a higher hash code, so there is a clear order) Integer outer = CURRENTLY_LOADING.get(); if (outer != null && hash <= outer) { // to prevent a deadlock, we also load the value ourselves return load(key, hash, valueLoader); } ConcurrentHashMap loading = cache.loadingInProgress; // the object we have to wait for in case another thread loads // this value AtomicBoolean alreadyLoading; // synchronized on this object, even before we put it in the // cache, so that all other threads that get this object can // synchronized and wait for it AtomicBoolean loadNow = new AtomicBoolean(); // we synchronize a bit early here, but that's fine (we don't // optimize for the case where loading is extremely quick) synchronized (loadNow) { alreadyLoading = loading.putIfAbsent(key, loadNow); if (alreadyLoading == null) { // we are loading ourselves try { CURRENTLY_LOADING.set(hash); return load(key, hash, valueLoader); } finally { loading.remove(key); if (loadNow.get()) { // notify other threads, but only if // they wait for this to be loaded loadNow.notifyAll(); } CURRENTLY_LOADING.remove(); } } } // another thread is (or was) already loading synchronized (alreadyLoading) { alreadyLoading.set(true); // loading might have been finished, so check again AtomicBoolean alreadyLoading2 = loading.get(key); if (alreadyLoading2 != alreadyLoading) { // loading has completed before we could synchronize, // so we repeat continue; } // still loading: wait try { // we could wait longer than 10 ms, but we are // in case notify is not called for some weird reason // (for example out of memory) alreadyLoading.wait(10); } catch (InterruptedException e) { // ignore } } } } V load(K key, int hash, Callable valueLoader) throws ExecutionException { V value; long start = System.nanoTime(); try { value = valueLoader.call(); loadSuccessCount.increment(); } catch (Exception e) { loadExceptionCount.increment(); throw new ExecutionException(e); } finally { long time = System.nanoTime() - start; totalLoadTime.add(time); } 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); if (value != null) { return value; } if (loader == null) { return null; } synchronized (this) { value = get(key, hash); if (value != null) { return value; } long start = System.nanoTime(); try { value = loader.load(key); loadSuccessCount.increment(); } catch (Exception e) { loadExceptionCount.increment(); throw new ExecutionException(e); } finally { long time = System.nanoTime() - start; totalLoadTime.add(time); } put(key, hash, value, cache.sizeOf(key, value)); return value; } } synchronized V replace(K key, int hash, V value, int memory) { if (containsKey(key, hash)) { return put(key, hash, value, memory); } return null; } synchronized boolean replace(K key, int hash, V oldValue, V newValue, int memory) { V old = get(key, hash); if (old != null && old.equals(oldValue)) { put(key, hash, newValue, memory); return true; } return false; } synchronized boolean remove(Object key, int hash, Object value) { V old = get(key, hash); if (old != null && old.equals(value)) { invalidate(key, hash, RemovalCause.EXPLICIT); return true; } 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 invalidate(key, hash, RemovalCause.EXPLICIT); return old; } synchronized V putIfAbsent(K key, int hash, V value, int memory) { V old = get(key, hash); if (old == null) { put(key, hash, value, memory); return null; } return old; } synchronized void refresh(K key, int hash, CacheLoader loader) throws ExecutionException { if (loader == null) { // no loader - no refresh return; } V value; V old = get(key, hash); long start = System.nanoTime(); try { if (old == null) { value = loader.load(key); } else { ListenableFuture future = loader.reload(key, old); value = future.get(); } loadSuccessCount.increment(); } catch (Exception e) { loadExceptionCount.increment(); throw new ExecutionException(e); } finally { long time = System.nanoTime() - start; totalLoadTime.add(time); } put(key, hash, value, cache.sizeOf(key, value)); } /** * Add an entry to the cache. The entry may or may not exist in the * cache yet. This method will usually mark unknown entries as cold and * known entries as hot. * * @param key the key (may not be null) * @param hash the hash * @param value the value (may not be null) * @param memory the memory used for the given entry * @return the old value, or null if there was no resident entry */ synchronized V put(K key, int hash, V value, int memory) { if (value == null) { throw new NullPointerException("The value may not be null"); } V old; Entry e = find(key, hash); boolean existed; if (e == null) { existed = false; old = null; } else { existed = true; old = e.value; invalidate(key, hash, RemovalCause.REPLACED); } e = new Entry(key, value, memory); Entry[] array = entries; int mask = array.length - 1; int index = hash & mask; e.mapNext = array[index]; array[index] = e; usedMemory += memory; if (usedMemory > maxMemory && mapSize > 0) { // an old entry needs to be removed evict(e); } mapSize++; // added entries are always added to the stack addToStack(e); if (existed) { // if it was there before (even non-resident), it becomes hot if (PUT_HOT) { access(key, hash); } } return old; } /** * Remove an entry. Both resident and non-resident entries can be * removed. * * @param key the key (may not be null) * @param hash the hash */ synchronized void invalidate(Object key, int hash, RemovalCause cause) { Entry[] array = entries; int mask = array.length - 1; int index = hash & mask; Entry e = array[index]; if (e == null) { return; } if (e.key.equals(key)) { array[index] = e.mapNext; } else { Entry last; do { last = e; e = e.mapNext; if (e == null) { return; } } while (!e.key.equals(key)); last.mapNext = e.mapNext; } mapSize--; usedMemory -= e.memory; if (e.stackNext != null) { removeFromStack(e); } if (e.isHot()) { // when removing a hot entry, the newest cold entry gets hot, // so the number of hot entries does not change Entry nc = queue.queueNext; if (nc != queue) { removeFromQueue(nc); if (nc.stackNext == null) { addToStackBottom(nc); } } } else { removeFromQueue(e); } pruneStack(); cache.evicted(e, cause); } /** * Evict cold entries (resident and non-resident) until the memory limit is * reached. The new entry is added as a cold entry, except if it is the only * entry. * * @param newCold a new cold entry */ private void evict(Entry newCold) { // ensure there are not too many hot entries: right shift of 5 is // division by 32, that means if there are only 1/32 (3.125%) or // less cold entries, a hot entry needs to become cold while (queueSize <= (mapSize >>> 5) && stackSize > 0) { convertOldestHotToCold(); } if (stackSize > 0) { // the new cold entry is at the top of the queue addToQueue(queue, newCold); } // the oldest resident cold entries become non-resident // but at least one cold entry (the new one) must stay while (usedMemory > maxMemory && queueSize > 1) { Entry e = queue.queuePrev; usedMemory -= e.memory; evictionCount.increment(); removeFromQueue(e); cache.evicted(e, RemovalCause.SIZE); e.value = null; e.memory = 0; addToQueue(queue2, e); // the size of the non-resident-cold entries needs to be limited while (queue2Size + queue2Size > stackSize) { e = queue2.queuePrev; int hash = getHash(e.key); invalidate(e.key, hash, RemovalCause.SIZE); } } } private void convertOldestHotToCold() { // the last entry of the stack is known to be hot Entry last = stack.stackPrev; if (last == stack) { // never remove the stack head itself (this would mean the // internal structure of the cache is corrupt) throw new IllegalStateException(); } // remove from stack - which is done anyway in the stack pruning, but we // can do it here as well removeFromStack(last); // adding an entry to the queue will make it cold addToQueue(queue, last); pruneStack(); } /** * Ensure the last entry of the stack is cold. */ private void pruneStack() { while (true) { Entry last = stack.stackPrev; // must stop at a hot entry or the stack head, // but the stack head itself is also hot, so we // don't have to test it if (last.isHot()) { break; } // the cold entry is still in the queue removeFromStack(last); } } /** * Try to find an entry in the map. * * @param key the key * @param hash the hash * @return the entry (might be a non-resident) */ Entry find(Object key, int hash) { Entry[] array = entries; int mask = array.length - 1; int index = hash & mask; Entry e = array[index]; while (e != null && !e.key.equals(key)) { e = e.mapNext; } return e; } private void addToStack(Entry e) { e.stackPrev = stack; e.stackNext = stack.stackNext; e.stackNext.stackPrev = e; stack.stackNext = e; stackSize++; e.topMove = stackMoveCounter++; } private void addToStackBottom(Entry e) { e.stackNext = stack; e.stackPrev = stack.stackPrev; e.stackPrev.stackNext = e; stack.stackPrev = e; stackSize++; } private void removeFromStack(Entry e) { e.stackPrev.stackNext = e.stackNext; e.stackNext.stackPrev = e.stackPrev; e.stackPrev = e.stackNext = null; stackSize--; } private void addToQueue(Entry q, Entry e) { e.queuePrev = q; e.queueNext = q.queueNext; e.queueNext.queuePrev = e; q.queueNext = e; if (e.value != null) { queueSize++; } else { queue2Size++; } } private void removeFromQueue(Entry e) { e.queuePrev.queueNext = e.queueNext; e.queueNext.queuePrev = e.queuePrev; e.queuePrev = e.queueNext = null; if (e.value != null) { queueSize--; } else { queue2Size--; } } /** * Get the list of keys. This method allows to read the internal state of * the cache. * * @param cold if true, only keys for the cold entries are returned * @param nonResident true for non-resident entries * @return the key list */ synchronized List keys(boolean cold, boolean nonResident) { ArrayList keys = new ArrayList(); if (cold) { Entry start = nonResident ? queue2 : queue; for (Entry e = start.queueNext; e != start; e = e.queueNext) { keys.add(e.key); } } else { for (Entry e = stack.stackNext; e != stack; e = e.stackNext) { keys.add(e.key); } } return keys; } /** * Check whether there is a resident entry for the given key. This * method does not adjust the internal state of the cache. * * @param key the key (may not be null) * @param hash the hash * @return true if there is a resident entry */ boolean containsKey(Object key, int hash) { Entry e = find(key, hash); return e != null && e.value != null; } /** * Get the set of keys for resident entries. * * @return the set of keys */ synchronized Set keySet() { HashSet set = new HashSet(); for (Entry e = stack.stackNext; e != stack; e = e.stackNext) { set.add(e.key); } for (Entry e = queue.queueNext; e != queue; e = e.queueNext) { set.add(e.key); } return set; } /** * Set the maximum memory this cache should use. This will not * immediately cause entries to get removed however; it will only change * the limit. To resize the internal array, call the clear method. * * @param maxMemory the maximum size (1 or larger) */ void setMaxMemory(long maxMemory) { if (maxMemory <= 0) { throw new IllegalArgumentException("Max memory must be larger than 0"); } this.maxMemory = maxMemory; } /** * Set the average memory used per entry. It is used to calculate the * length of the internal array. * * @param averageMemory the average memory used (1 or larger) */ void setAverageMemory(int averageMemory) { if (averageMemory <= 0) { throw new IllegalArgumentException("Average memory must be larger than 0"); } this.averageMemory = averageMemory; } } /** * A cache entry. Each entry is either hot (low inter-reference recency; * LIR), cold (high inter-reference recency; HIR), or non-resident-cold. Hot * entries are in the stack only. Cold entries are in the queue, and may be * in the stack. Non-resident-cold entries have their value set to null and * are in the stack and in the non-resident queue. * * @param the key type * @param the value type * * @deprecated The Jackrabbit Oak Cache library is designed for Oak-internal use only and thus deprecated. It will not be part of the AEM SDK after April 2023. */ @Deprecated(since = "2022-12-01") static class Entry { /** * The key. */ final K key; /** * The value. Set to null for non-resident-cold entries. */ V value; /** * The estimated memory used. */ int memory; /** * When the item was last moved to the top of the stack. */ int topMove; /** * The next entry in the stack. */ Entry stackNext; /** * The previous entry in the stack. */ Entry stackPrev; /** * The next entry in the queue (either the resident queue or the * non-resident queue). */ Entry queueNext; /** * The previous entry in the queue. */ Entry queuePrev; /** * The next entry in the map */ Entry mapNext; Entry(K key, V value, int memory) { this.key = key; this.value = value; this.memory = memory; } Entry() { this(null, null, 0); } /** * Whether this entry is hot. Cold entries are in one of the two queues. * * @return whether the entry is hot */ boolean isHot() { return queueNext == null; } } /** * A builder for the cache. * * @deprecated The Jackrabbit Oak Cache library is designed for Oak-internal use only and thus deprecated. It will not be part of the AEM SDK after April 2023. */ @Deprecated(since = "2022-12-01") public static class Builder { private String module; 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; } public Builder module(String module) { this.module = module; return this; } /** * Set the weigher which is used if memory usage of an entry is not * explicitly set (when adding entries). * * @param weigher the weigher * @return this */ public Builder weigher(Weigher weigher) { this.weigher = weigher; return this; } /** * Set the total maximum weight. If the cache is heavier, then entries * are evicted. * * @param maxWeight the maximum weight * @return this */ public Builder maximumWeight(long maxWeight) { this.maxWeight = maxWeight; return this; } /** * Set the average weight of an entry. This is used, together with the * maximum weight, to calculate the length of the internal array of the * cache. * * For higher performance, the weight should be set relatively low, at * the cost of some space. To save space, the average weight should be * set high, at the cost of some performance. * * @param averageWeight the average weight * @return this */ public Builder averageWeight(int averageWeight) { this.averageWeight = averageWeight; return this; } /** * Set the maximum size (in number of entries). This is the same as * setting the average weight of an entry to 1, and the maximum weight * to the maximum size. * * @param maxSize the maximum size * @return this */ public Builder maximumSize(long maxSize) { this.maxWeight = maxSize; this.averageWeight = 1; return this; } public Builder segmentCount(int segmentCount) { if (Integer.bitCount(segmentCount) != 1 || segmentCount < 0 || segmentCount > 65536) { LOG.warn("Illegal segment count: " + segmentCount + ", using 16"); segmentCount = 16; } this.segmentCount = segmentCount; return this; } /** * How many other item are to be moved to the top of the stack before * the current item is moved. The default is 16. Using higher values * will avoid re-ordering in many cases, so less time is spent * reordering. But this somewhat reduces cache hit rate, and eviction * will become more random. Typically, cache hit rate can be improved by * using smaller values, and access performance can be improved using * larger values. Using values larger than 128 is not recommended. */ public Builder stackMoveDistance(int stackMoveDistance) { 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, evicted, module); } } /** * Create a builder. * * @return the builder */ public static Builder newBuilder() { return new Builder(); } @Override public ImmutableMap getAllPresent(Iterable keys) { throw new UnsupportedOperationException(); } @Override public ConcurrentMap asMap() { return new ConcurrentMap() { @Override public int size() { long size = CacheLIRS.this.size(); return (int) Math.min(size, Integer.MAX_VALUE); } @Override public boolean isEmpty() { return CacheLIRS.this.size() == 0; } @Override public boolean containsKey(Object key) { return CacheLIRS.this.containsKey(key); } @Override public boolean containsValue(Object value) { return CacheLIRS.this.containsValue(value); } @SuppressWarnings("unchecked") @Override public V get(Object key) { return CacheLIRS.this.peek((K) key); } @Override public V put(K key, V value) { return CacheLIRS.this.put(key, value, sizeOf(key, value)); } @Override public V remove(Object key) { @SuppressWarnings("unchecked") V old = CacheLIRS.this.getUnchecked((K) key); CacheLIRS.this.invalidate(key); return old; } @Override public void putAll(Map m) { for (Map.Entry e : m.entrySet()) { put(e.getKey(), e.getValue()); } } @Override public void clear() { CacheLIRS.this.clear(); } @Override public Set keySet() { return CacheLIRS.this.keySet(); } @Override public Collection values() { return CacheLIRS.this.values(); } @Override public Set> entrySet() { return CacheLIRS.this.entrySet(); } @Override public V putIfAbsent(K key, V value) { return CacheLIRS.this.putIfAbsent(key, value); } @Override public boolean remove(Object key, Object value) { return CacheLIRS.this.remove(key, value); } @Override public boolean replace(K key, V oldValue, V newValue) { return CacheLIRS.this.replace(key, oldValue, newValue); } @Override public V replace(K key, V value) { return CacheLIRS.this.replace(key, value); } }; } @Override public void cleanUp() { // nothing to do } @Override public void putAll(Map m) { for (Map.Entry e : m.entrySet()) { put(e.getKey(), e.getValue()); } } @Override public ImmutableMap getAll(Iterable keys) throws ExecutionException { throw new UnsupportedOperationException(); } @Override public V apply(K key) { throw new UnsupportedOperationException(); } public boolean isEmpty() { return size() == 0; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy