diff --git a/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/CacheState.java b/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/CacheState.java index e33019a84..1bd9429d7 100644 --- a/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/CacheState.java +++ b/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/CacheState.java @@ -18,172 +18,172 @@ */ package org.exoplatform.services.cache.concurrent; -import org.exoplatform.services.log.Log; - import java.io.Serializable; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; +import org.exoplatform.services.log.Log; + /** * Really the cache state (we need it because of the clear cache consistency). */ -class CacheState -{ - - private final Log log; - - private final ConcurrentFIFOExoCache config; - - final ConcurrentHashMap> map; - - final Queue> queue; - - CacheState(ConcurrentFIFOExoCache config, Log log) - { - this.log = log; - this.config = config; - this.map = new ConcurrentHashMap>(); - this.queue = new SynchronizedQueue>(log); - } - - public void assertConsistency() - { - if (queue instanceof SynchronizedQueue) - { - ((SynchronizedQueue)queue).assertConsistency(); +public class CacheState { + + private static final long MAX_ITEMS_REACHED_TRACE_PERIODICITY = 60000l; // 60s + + private static final long MIN_MISS_COUNT = 1000l; + + private final Log log; + + private final ConcurrentFIFOExoCache config; + + final ConcurrentHashMap> map; + + final Queue> queue; + + long lastMaxItemsReachedLogTime; + + public CacheState(ConcurrentFIFOExoCache config, Log log) { + this.log = log; + this.config = config; + this.map = new ConcurrentHashMap<>(); + this.queue = new SynchronizedQueue<>(log); + } + + public void assertConsistency() { + if (queue instanceof SynchronizedQueue synchronizedQueue) { // NOSONAR + synchronizedQueue.assertConsistency(); + } + int mapSize = map.size(); + int effectiveQueueSize = queue.size(); + if (effectiveQueueSize != mapSize) { + throw new AssertionError("The map size is " + mapSize + " is different from the queue size " + effectiveQueueSize); + } + } + + public V get(Serializable name) { + ObjectRef entry = map.get(name); + if (entry != null) { + V o = entry.getObject(); + if (entry.isValid()) { + config.hits.incrementAndGet(); + config.onGet(entry.name, o); + return o; + } else { + config.misses.incrementAndGet(); + if (map.remove(name, entry)) { + queue.remove(entry); + } + config.onExpire(entry.name, o); + traceExcessiveMissCountRatio(); } - int mapSize = map.size(); - int effectiveQueueSize = queue.size(); - if (effectiveQueueSize != mapSize) - { - throw new AssertionError("The map size is " + mapSize + " is different from the queue size " - + effectiveQueueSize); + } else { + config.misses.incrementAndGet(); + traceExcessiveMissCountRatio(); + } + return null; + } + + private boolean isTraceEnabled() { + return log != null && log.isTraceEnabled(); + } + + private void trace(String message) { + log.trace(message + " [" + Thread.currentThread().getName() + "]"); + } + + /** + * Do a put with the provided expiration time. + * + * @param expirationTime the expiration time + * @param name the cache key + * @param obj the cached value + */ + void put(long expirationTime, K name, V obj, boolean local) { + boolean trace = isTraceEnabled(); + ObjectRef nextRef = new SimpleObjectRef<>(expirationTime, name, obj); + ObjectRef previousRef = map.put(name, nextRef); + + // Remove previous (promoted as first element) + if (previousRef != null) { + queue.remove(previousRef); + if (trace) { + trace("Replaced item=" + previousRef.serial + " with item=" + nextRef.serial + " in the map"); } - } - - public V get(Serializable name) - { - ObjectRef entry = map.get(name); - if (entry != null) - { - V o = entry.getObject(); - if (entry.isValid()) - { - config.hits.incrementAndGet(); - config.onGet(entry.name, o); - return o; - } - else - { - config.misses.incrementAndGet(); - if (map.remove(name, entry)) - { - queue.remove(entry); - } - config.onExpire(entry.name, o); - } + } else if (trace) { + trace("Added item=" + nextRef.serial + " to map"); + } + + // Add to the queue + queue.add(nextRef); + + // Perform eviction from queue + ArrayList> evictedRefs = queue.trim(config.maxSize); + if (evictedRefs != null && !evictedRefs.isEmpty()) { + for (ObjectRef evictedRef : evictedRefs) { + // We remove it from the map only if it was the same entry + // it could have been removed concurrently by an explicit remove + // or by a promotion + map.remove(evictedRef.name, evictedRef); + + // Expiration callback + config.onExpire(evictedRef.name, evictedRef.getObject()); } - else - { - config.misses.incrementAndGet(); + traceMaxItemsReached(); + } + + // Put callback + if (local) { + config.onPutLocal(name, obj); + } else { + config.onPut(name, obj); + } + } + + public V remove(Serializable name) { + boolean trace = isTraceEnabled(); + ObjectRef item = map.remove(name); + if (item != null) { + if (trace) { + trace("Removed item=" + item.serial + " from the map going to remove it"); } - return null; - } - - private boolean isTraceEnabled() - { - return log != null && log.isTraceEnabled(); - } - - private void trace(String message) - { - log.trace(message + " [" + Thread.currentThread().getName() + "]"); - } - - /** - * Do a put with the provided expiration time. - * - * @param expirationTime the expiration time - * @param name the cache key - * @param obj the cached value - */ - void put(long expirationTime, K name, V obj, boolean local) - { - boolean trace = isTraceEnabled(); - ObjectRef nextRef = new SimpleObjectRef(expirationTime, name, obj); - ObjectRef previousRef = map.put(name, nextRef); - - // Remove previous (promoted as first element) - if (previousRef != null) - { - queue.remove(previousRef); - if (trace) - { - trace("Replaced item=" + previousRef.serial + " with item=" + nextRef.serial + " in the map"); - } - } - else if (trace) - { - trace("Added item=" + nextRef.serial + " to map"); - } - - // Add to the queue - queue.add(nextRef); - - // Perform eviction from queue - ArrayList> evictedRefs = queue.trim(config.maxSize); - if (evictedRefs != null) - { - for (ObjectRef evictedRef : evictedRefs) - { - // We remove it from the map only if it was the same entry - // it could have been removed concurrently by an explicit remove - // or by a promotion - map.remove(evictedRef.name, evictedRef); - - // Expiration callback - config.onExpire(evictedRef.name, evictedRef.getObject()); - } + boolean removed = queue.remove(item); + boolean valid = removed && item.isValid(); + V object = item.getObject(); + if (valid) { + config.onRemove(item.name, object); + return object; + } else { + config.onExpire(item.name, object); + return null; } + } else { + return null; + } + } + + private void traceExcessiveMissCountRatio() { + int missCount = config.misses.get(); + int maxSize = config.getMaxSize(); + int hitCount = config.hits.get(); + if (missCount > MIN_MISS_COUNT + && missCount % Math.max(MIN_MISS_COUNT, Math.min(maxSize, hitCount)) == 0) { // NOSONAR + log.warn("Cache '{}' seems to have an excessive miss count '{}' (hits count: '{}'), please consider reviewing Max Size '{}' and TTL '{}s' configurations.", + config.getName(), + String.format("%,d", missCount), + String.format("%,d", hitCount), + String.format("%,d", config.getMaxSize()), + String.format("%,d", config.getLiveTimeMillis() / 1000l)); + } + } + + private void traceMaxItemsReached() { + if (lastMaxItemsReachedLogTime < (System.currentTimeMillis() - MAX_ITEMS_REACHED_TRACE_PERIODICITY)) { + lastMaxItemsReachedLogTime = System.currentTimeMillis(); + log.warn("Cache '{}' Max items '{}' reached, please consider reviewing Max Size and TTL configurations.", + config.getName(), + config.getMaxSize()); + } + } - // Put callback - if (local) - { - config.onPutLocal(name, obj); - } - else - { - config.onPut(name, obj); - } - } - - public V remove(Serializable name) - { - boolean trace = isTraceEnabled(); - ObjectRef item = map.remove(name); - if (item != null) - { - if (trace) - { - trace("Removed item=" + item.serial + " from the map going to remove it"); - } - boolean removed = queue.remove(item); - boolean valid = removed && item.isValid(); - V object = item.getObject(); - if (valid) - { - config.onRemove(item.name, object); - return object; - } - else - { - config.onExpire(item.name, object); - return null; - } - } - else - { - return null; - } - } } diff --git a/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/ConcurrentFIFOExoCache.java b/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/ConcurrentFIFOExoCache.java index 0f58c74d0..de7213759 100644 --- a/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/ConcurrentFIFOExoCache.java +++ b/exo.kernel.component.cache/src/main/java/org/exoplatform/services/cache/concurrent/ConcurrentFIFOExoCache.java @@ -21,10 +21,10 @@ import org.exoplatform.services.cache.CacheListener; import org.exoplatform.services.cache.CachedObjectSelector; import org.exoplatform.services.cache.ExoCache; +import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import java.io.Serializable; -import java.io.UnsupportedEncodingException; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -32,327 +32,306 @@ import java.util.concurrent.atomic.AtomicInteger; /** - * An {@link org.exoplatform.services.cache.ExoCache} implementation based on {@link java.util.concurrent.ConcurrentHashMap} - * that minimize locking. Cache entries are maintained in a fifo list that is used for the fifo eviction policy. - * + * An {@link org.exoplatform.services.cache.ExoCache} implementation based on + * {@link java.util.concurrent.ConcurrentHashMap} that minimize locking. Cache + * entries are maintained in a fifo list that is used for the fifo eviction + * policy. */ -public class ConcurrentFIFOExoCache implements ExoCache -{ +public class ConcurrentFIFOExoCache implements ExoCache { - private static int DEFAULT_MAX_SIZE = 50; + private static final Log LOGGER = + ExoLogger.getExoLogger(ConcurrentFIFOExoCache.class); - private final Log log; + private static final int DEFAULT_MAX_SIZE = 50; - private volatile long liveTimeMillis; + private static final String NULL_CACHE_KEY_MESSAGE = "No null cache key accepted"; - volatile int maxSize; + private final Log log; - private CopyOnWriteArrayList> listeners; + private volatile long liveTimeMillis; - private CacheState state; + volatile int maxSize; - AtomicInteger hits = new AtomicInteger(); + private CopyOnWriteArrayList> listeners; - AtomicInteger misses = new AtomicInteger(); + private CacheState state; - private String label; + AtomicInteger hits = new AtomicInteger(); - private String name; + AtomicInteger misses = new AtomicInteger(); - private boolean logEnabled = false; + private String label; - public ConcurrentFIFOExoCache() - { - this(DEFAULT_MAX_SIZE); - } + private String name; - public ConcurrentFIFOExoCache(Log log) - { - this(DEFAULT_MAX_SIZE, log); - } + private boolean logEnabled = false; - public ConcurrentFIFOExoCache(int maxSize) - { - this(null, maxSize); - } + public ConcurrentFIFOExoCache() { + this(DEFAULT_MAX_SIZE); + } - public ConcurrentFIFOExoCache(int maxSize, Log log) - { - this(null, maxSize, log); - } + public ConcurrentFIFOExoCache(Log log) { + this(DEFAULT_MAX_SIZE, log); + } - public ConcurrentFIFOExoCache(String name, int maxSize) - { - this(name, maxSize, null); - } + public ConcurrentFIFOExoCache(int maxSize) { + this(null, maxSize); + } - public ConcurrentFIFOExoCache(String name, int maxSize, Log log) - { - this.maxSize = maxSize; - this.name = name; - this.state = new CacheState(this, log); - this.liveTimeMillis = -1; - this.log = log; - this.listeners = new CopyOnWriteArrayList>(); - } + public ConcurrentFIFOExoCache(int maxSize, Log log) { + this(null, maxSize, log); + } - public void assertConsistent() - { - state.assertConsistency(); - } + public ConcurrentFIFOExoCache(String name, int maxSize) { + this(name, maxSize, null); + } - public String getName() - { - return name; - } - - public void setName(String s) - { - name = s; - } - - public String getLabel() - { - if (label == null) - { - if (name.length() > 30) - { - String shortLabel = name.substring(name.lastIndexOf(".") + 1); - setLabel(shortLabel); - return shortLabel; - } - return name; - } - return label; - } - - public void setLabel(String name) - { - label = name; - } - - public long getLiveTime() - { - long tmp = getLiveTimeMillis(); - return tmp == -1 ? -1 : tmp / 1000; - } - - public void setLiveTime(long period) - { - setLiveTimeMillis(period * 1000); - } - - public long getLiveTimeMillis() - { - return liveTimeMillis; - } - - public void setLiveTimeMillis(long liveTimeMillis) - { - if (liveTimeMillis < 0) - { - liveTimeMillis = -1; - } - this.liveTimeMillis = liveTimeMillis; - } - - public int getMaxSize() - { - return maxSize; - } - - public void setMaxSize(int max) - { - this.maxSize = max; - } - - public V get(Serializable name) - { - if (name == null) - { - return null; - } - return state.get(name); - } - - public void put(K name, V obj) - { - if (name == null) - { - throw new IllegalArgumentException("No null cache key accepted"); - } - if (liveTimeMillis != 0) - { - long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE; - state.put(expirationTime, name, obj, false); - } - } + public ConcurrentFIFOExoCache(String name, int maxSize, Log log) { + this.maxSize = maxSize; + this.name = name; + if (log == null) { + log = LOGGER; + } + this.state = new CacheState<>(this, log); + this.liveTimeMillis = -1; + this.log = log; + this.listeners = new CopyOnWriteArrayList<>(); + } - public void putLocal(K name, V obj) - { - if (name == null) - { - throw new IllegalArgumentException("No null cache key accepted"); - } - if (liveTimeMillis != 0) - { - long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE; - state.put(expirationTime, name, obj, true); - } - } + public void assertConsistent() { + state.assertConsistency(); + } - public void putMap(Map objs) - { - if (objs == null) - { - throw new IllegalArgumentException("No null map accepted"); - } - long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE; - for (Serializable name : objs.keySet()) - { - if (name == null) - { - throw new IllegalArgumentException("No null cache key accepted"); - } - } - for (Map.Entry entry : objs.entrySet()) - { - state.put(expirationTime, entry.getKey(), entry.getValue(), false); - } - } + @Override + public String getName() { + return name; + } - public V remove(Serializable name) - { - if (name == null) - { - throw new IllegalArgumentException("No null cache key accepted"); - } - return state.remove(name); - } - - public List getCachedObjects() - { - LinkedList list = new LinkedList(); - for (ObjectRef objectRef : state.map.values()) - { - V object = objectRef.getObject(); - if (objectRef.isValid()) - { - list.add(object); - } + @Override + public void setName(String s) { + name = s; + } + + @Override + public String getLabel() { + if (label == null) { + if (name.length() > 30) { + String shortLabel = name.substring(name.lastIndexOf(".") + 1); + setLabel(shortLabel); + return shortLabel; } - return list; - } - - public List removeCachedObjects() - { - List list = getCachedObjects(); - clearCache(); - return list; - } - - public void clearCache() - { - state = new CacheState(this, log); - } - - public void select(CachedObjectSelector selector) throws Exception - { - if (selector == null) - { - throw new IllegalArgumentException("No null selector"); + return name; + } + return label; + } + + @Override + public void setLabel(String name) { + label = name; + } + + @Override + public long getLiveTime() { + long tmp = getLiveTimeMillis(); + return tmp == -1 ? -1 : tmp / 1000; + } + + @Override + public void setLiveTime(long period) { + setLiveTimeMillis(period * 1000); + } + + public long getLiveTimeMillis() { + return liveTimeMillis; + } + + public void setLiveTimeMillis(long liveTimeMillis) { + if (liveTimeMillis < 0) { + liveTimeMillis = -1; + } + this.liveTimeMillis = liveTimeMillis; + } + + @Override + public int getMaxSize() { + return maxSize; + } + + @Override + public void setMaxSize(int max) { + this.maxSize = max; + } + + @Override + public V get(Serializable name) { + if (name == null) { + return null; + } + return state.get(name); + } + + @Override + public void put(K name, V obj) { + if (name == null) { + throw new IllegalArgumentException(NULL_CACHE_KEY_MESSAGE); + } + if (liveTimeMillis != 0) { + long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE; + state.put(expirationTime, name, obj, false); + } + } + + @Override + public void putLocal(K name, V obj) { + if (name == null) { + throw new IllegalArgumentException(NULL_CACHE_KEY_MESSAGE); + } + if (liveTimeMillis != 0) { + long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE; + state.put(expirationTime, name, obj, true); + } + } + + @Override + public void putMap(Map objs) { + if (objs == null) { + throw new IllegalArgumentException("No null map accepted"); + } + long expirationTime = liveTimeMillis > 0 ? System.currentTimeMillis() + liveTimeMillis : Long.MAX_VALUE; + for (Serializable keyName : objs.keySet()) { + if (keyName == null) { + throw new IllegalArgumentException(NULL_CACHE_KEY_MESSAGE); } - for (Map.Entry> entry : state.map.entrySet()) - { - K key = entry.getKey(); - ObjectRef info = entry.getValue(); - if (selector.select(key, info)) - { - selector.onSelect(this, key, info); - } + } + for (Map.Entry entry : objs.entrySet()) { + state.put(expirationTime, entry.getKey(), entry.getValue(), false); + } + } + + @Override + public V remove(Serializable name) { + if (name == null) { + throw new IllegalArgumentException(NULL_CACHE_KEY_MESSAGE); + } + return state.remove(name); + } + + @Override + public List getCachedObjects() { + LinkedList list = new LinkedList<>(); + for (ObjectRef objectRef : state.map.values()) { + V object = objectRef.getObject(); + if (objectRef.isValid()) { + list.add(object); } - } - - public int getCacheSize() - { - return state.queue.size(); - } - - public int getCacheHit() - { - return hits.get(); - } - - public int getCacheMiss() - { - return misses.get(); - } - - public synchronized void addCacheListener(CacheListener listener) - { - if (listener == null) - { - throw new IllegalArgumentException("The listener cannot be null"); + } + return list; + } + + @Override + public List removeCachedObjects() { + List list = getCachedObjects(); + clearCache(); + return list; + } + + public void clearCache() { + state = new CacheState<>(this, log); + } + + @Override + public void select(CachedObjectSelector selector) throws Exception { + if (selector == null) { + throw new IllegalArgumentException("No null selector"); + } + for (Map.Entry> entry : state.map.entrySet()) { + K key = entry.getKey(); + ObjectRef info = entry.getValue(); + if (selector.select(key, info)) { + selector.onSelect(this, key, info); } - listeners.add(new ListenerContext(listener, this)); - } - - public boolean isLogEnabled() - { - return logEnabled; - } - - public void setLogEnabled(boolean logEnabled) - { - this.logEnabled = logEnabled; - } - - // - - public void onExpire(K key, V obj) - { - if (!listeners.isEmpty()) - for (ListenerContext context : listeners) - context.onExpire(key, obj); - } - - public void onRemove(K key, V obj) - { - if (!listeners.isEmpty()) - for (ListenerContext context : listeners) - context.onRemove(key, obj); - } - - public void onPut(K key, V obj) - { - if (!listeners.isEmpty()) - for (ListenerContext context : listeners) - context.onPut(key, obj); - } - - public void onPutLocal(K key, V obj) - { - if (!listeners.isEmpty()) - for (ListenerContext context : listeners) - context.onPutLocal(key, obj); - } - - public void onGet(K key, V obj) - { - if (!listeners.isEmpty()) - for (ListenerContext context : listeners) - context.onGet(key, obj); - } - - public void onClearCache() - { - if (!listeners.isEmpty()) - for (ListenerContext context : listeners) - context.onClearCache(); - } - - public List> getListeners() { - return listeners; - } - -} \ No newline at end of file + } + } + + @Override + public int getCacheSize() { + return state.queue.size(); + } + + @Override + public int getCacheHit() { + return hits.get(); + } + + @Override + public int getCacheMiss() { + return misses.get(); + } + + @Override + public synchronized void addCacheListener(CacheListener listener) { + if (listener == null) { + throw new IllegalArgumentException("The listener cannot be null"); + } + listeners.add(new ListenerContext<>(listener, this)); + } + + @Override + public boolean isLogEnabled() { + return logEnabled; + } + + @Override + public void setLogEnabled(boolean logEnabled) { + this.logEnabled = logEnabled; + } + + // + + @Override + public void onExpire(K key, V obj) { + if (!listeners.isEmpty()) + for (ListenerContext context : listeners) + context.onExpire(key, obj); + } + + @Override + public void onRemove(K key, V obj) { + if (!listeners.isEmpty()) + for (ListenerContext context : listeners) + context.onRemove(key, obj); + } + + @Override + public void onPut(K key, V obj) { + if (!listeners.isEmpty()) + for (ListenerContext context : listeners) + context.onPut(key, obj); + } + + @Override + public void onPutLocal(K key, V obj) { + if (!listeners.isEmpty()) + for (ListenerContext context : listeners) + context.onPutLocal(key, obj); + } + + @Override + public void onGet(K key, V obj) { + if (!listeners.isEmpty()) + for (ListenerContext context : listeners) + context.onGet(key, obj); + } + + @Override + public void onClearCache() { + if (!listeners.isEmpty()) + for (ListenerContext context : listeners) + context.onClearCache(); + } + + public List> getListeners() { + return listeners; + } + +}