org.apache.jackrabbit.oak.cache.CacheLIRS Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*
* 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 extends V> 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 extends V> 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 extends V> 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 extends K, ? extends V> m) {
for (Map.Entry extends K, ? extends V> 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 extends K, ? extends V> m) {
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
put(e.getKey(), e.getValue());
}
}
@Override
public ImmutableMap getAll(Iterable extends K> keys) throws ExecutionException {
throw new UnsupportedOperationException();
}
@Override
public V apply(K key) {
throw new UnsupportedOperationException();
}
public boolean isEmpty() {
return size() == 0;
}
}