diff --git a/src/main/java/com/avaje/ebean/annotation/CacheTuning.java b/src/main/java/com/avaje/ebean/annotation/CacheTuning.java index 4b5924e30..85b400936 100644 --- a/src/main/java/com/avaje/ebean/annotation/CacheTuning.java +++ b/src/main/java/com/avaje/ebean/annotation/CacheTuning.java @@ -44,4 +44,12 @@ public @interface CacheTuning { *

*/ int maxSecsToLive() default 0; + + /** + * The frequency (in seconds) that cache trimming should occur. + *

+ * This is a hint for cache implementations that use background cache trimming. + *

+ */ + int trimFrequency() default 0; } diff --git a/src/main/java/com/avaje/ebean/cache/ServerCache.java b/src/main/java/com/avaje/ebean/cache/ServerCache.java index 00a105166..195446c20 100644 --- a/src/main/java/com/avaje/ebean/cache/ServerCache.java +++ b/src/main/java/com/avaje/ebean/cache/ServerCache.java @@ -48,12 +48,6 @@ public interface ServerCache { */ Object put(Object id, Object value); - /** - * Put the value in the cache but only if a matching value is not already in - * the cache. - */ - Object putIfAbsent(Object id, Object value); - /** * Remove a entry from the cache given its id. */ diff --git a/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java b/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java index a26be30b7..21b0d1c65 100644 --- a/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java +++ b/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java @@ -10,6 +10,7 @@ public class ServerCacheOptions { private int maxSize; private int maxIdleSecs; private int maxSecsToLive; + private int trimFrequency; /** * Construct with no set options. @@ -25,15 +26,17 @@ public class ServerCacheOptions { this.maxSize = cacheTuning.maxSize(); this.maxIdleSecs = cacheTuning.maxIdleSecs(); this.maxSecsToLive = cacheTuning.maxSecsToLive(); + this.trimFrequency = cacheTuning.trimFrequency(); } /** * Create merging default options with the deployment specified ones. */ - public ServerCacheOptions(ServerCacheOptions d) { - this.maxSize = d.getMaxSize(); - this.maxIdleSecs = d.getMaxIdleSecs(); - this.maxSecsToLive = d.getMaxIdleSecs(); + public ServerCacheOptions(ServerCacheOptions defaults) { + this.maxSize = defaults.getMaxSize(); + this.maxIdleSecs = defaults.getMaxIdleSecs(); + this.maxSecsToLive = defaults.getMaxIdleSecs(); + this.trimFrequency = defaults.getTrimFrequency(); } /** @@ -50,6 +53,9 @@ public class ServerCacheOptions { if (maxSecsToLive == 0) { maxSecsToLive = defaults.getMaxSecsToLive(); } + if (trimFrequency == 0) { + trimFrequency = defaults.getTrimFrequency(); + } } /** @@ -61,7 +67,7 @@ public class ServerCacheOptions { copy.maxSize = maxSize; copy.maxIdleSecs = maxIdleSecs; copy.maxSecsToLive = maxSecsToLive; - + copy.trimFrequency = trimFrequency; return copy; } @@ -107,4 +113,17 @@ public class ServerCacheOptions { this.maxSecsToLive = maxSecsToLive; } + /** + * Return the trim frequency in seconds. + */ + public int getTrimFrequency() { + return trimFrequency; + } + + /** + * Set the trim frequency in seconds. + */ + public void setTrimFrequency(int trimFrequency) { + this.trimFrequency = trimFrequency; + } } diff --git a/src/main/java/com/avaje/ebean/cache/ServerCacheStatistics.java b/src/main/java/com/avaje/ebean/cache/ServerCacheStatistics.java index 969418917..ed048cd5e 100644 --- a/src/main/java/com/avaje/ebean/cache/ServerCacheStatistics.java +++ b/src/main/java/com/avaje/ebean/cache/ServerCacheStatistics.java @@ -5,9 +5,9 @@ package com.avaje.ebean.cache; *

* These can be monitored to review the effectiveness of a particular cache. *

- * - * @author rbygrave - * + *

+ * Depending on the cache implementation not all the statistics may be collected. + *

*/ public class ServerCacheStatistics { @@ -17,21 +17,63 @@ public class ServerCacheStatistics { protected int size; - protected int hitCount; + protected long hitCount; - protected int missCount; + protected long missCount; + + protected long insertCount; + + protected long updateCount; + + protected long removeCount; + + protected long clearCount; + + protected long evictionRunCount; + + protected long evictionRunMicros; + + protected long evictByIdle; + + protected long evictByTTL; + + protected long evictByLRU; public String toString() { StringBuilder sb = new StringBuilder(); sb.append(cacheName); + sb.append(" maxSize:").append(maxSize); sb.append(" size:").append(size); sb.append(" hitRatio:").append(getHitRatio()); - sb.append(" hitCount:").append(hitCount); - sb.append(" missCount:").append(missCount); - sb.append(" maxSize:").append(maxSize); + sb.append(" hit:").append(hitCount); + sb.append(" miss:").append(missCount); + sb.append(" insert:").append(insertCount); + sb.append(" update:").append(updateCount); + sb.append(" remove:").append(removeCount); + sb.append(" clear:").append(clearCount); + sb.append(" evictByIdle:").append(evictByIdle); + sb.append(" evictByTTL:").append(evictByTTL); + sb.append(" evictByLRU:").append(evictByLRU); + sb.append(" evictionRunCount:").append(evictionRunCount); + sb.append(" evictionRunMicros:").append(evictionRunMicros); return sb.toString(); } + /** + * Returns an int from 0 to 100 (percentage) for the hit ratio. + *

+ * A hit ratio of 100 means every get request against the cache hits an entry. + *

+ */ + public int getHitRatio() { + long totalCount = hitCount + missCount; + if (totalCount == 0) { + return 0; + } else { + return (int)(hitCount * 100 / totalCount); + } + } + /** * Return the name of the cache. */ @@ -49,28 +91,28 @@ public class ServerCacheStatistics { /** * Return the hit count. The number of successful gets. */ - public int getHitCount() { + public long getHitCount() { return hitCount; } /** * Set the hit count. */ - public void setHitCount(int hitCount) { + public void setHitCount(long hitCount) { this.hitCount = hitCount; } /** * Return the miss count. The number of gets that returned null. */ - public int getMissCount() { + public long getMissCount() { return missCount; } /** * Set the miss count. */ - public void setMissCount(int missCount) { + public void setMissCount(long missCount) { this.missCount = missCount; } @@ -107,18 +149,128 @@ public class ServerCacheStatistics { } /** - * Returns an int from 0 to 100 (percentage) for the hit ratio. - *

- * A hit ratio of 100 means every get request against the cache hits an entry. - *

+ * Set the put insert count. */ - public int getHitRatio() { - int totalCount = hitCount + missCount; - if (totalCount == 0) { - return 0; - } else { - return hitCount * 100 / totalCount; - } + public void setInsertCount(long insertCount) { + this.insertCount = insertCount; } + /** + * Return the put insert count. + */ + public long getInsertCount() { + return insertCount; + } + + /** + * Set the put update count. + */ + public void setUpdateCount(long updateCount) { + this.updateCount = updateCount; + } + + /** + * Return the put update count. + */ + public long getUpdateCount() { + return updateCount; + } + + /** + * Set the remove count. + */ + public void setRemoveCount(long removeCount) { + this.removeCount = removeCount; + } + + /** + * Return the remove count. + */ + public long getRemoveCount() { + return removeCount; + } + + /** + * Set the clear count. + */ + public void setClearCount(long clearCount) { + this.clearCount = clearCount; + } + + /** + * Return the clear count. + */ + public long getClearCount() { + return clearCount; + } + + /** + * Set the eviction run count. + */ + public void setEvictionRunCount(long evictCount) { + this.evictionRunCount = evictCount; + } + + /** + * Return the eviction run count. + */ + public long getEvictionRunCount() { + return evictionRunCount; + } + + /** + * Set the eviction run time in micros. + */ + public void setEvictionRunMicros(long evictionRunMicros) { + this.evictionRunMicros = evictionRunMicros; + } + + /** + * Return the eviction run time in micros. + */ + public long getEvictionRunMicros() { + return evictionRunMicros; + } + + /** + * Set the count of entries evicted due to idle time. + */ + public void setEvictByIdle(long evictByIdle) { + this.evictByIdle = evictByIdle; + } + + /** + * Return the count of entries evicted due to idle time. + */ + public long getEvictByIdle() { + return evictByIdle; + } + + /** + * Set the count of entries evicted due to time to live. + */ + public void setEvictByTTL(long evictByTTL) { + this.evictByTTL = evictByTTL; + } + + /** + * Return the count of entries evicted due to time to live. + */ + public long getEvictByTTL() { + return evictByTTL; + } + + /** + * Set the count of entries evicted due to time least recently used. + */ + public void setEvictByLRU(long evictByLRU) { + this.evictByLRU = evictByLRU; + } + + /** + * Return the count of entries evicted due to time least recently used. + */ + public long getEvictByLRU() { + return evictByLRU; + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache.java b/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache.java index f14dcec37..851933071 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache.java @@ -1,22 +1,18 @@ package com.avaje.ebeaninternal.server.cache; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - import com.avaje.ebean.BackgroundExecutor; import com.avaje.ebean.EbeanServer; import com.avaje.ebean.cache.ServerCache; import com.avaje.ebean.cache.ServerCacheOptions; import com.avaje.ebean.cache.ServerCacheStatistics; +import com.avaje.ebeaninternal.server.util.LongAdder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** * The default cache implementation. @@ -27,375 +23,392 @@ import org.slf4j.LoggerFactory; */ public class DefaultServerCache implements ServerCache { - private static final Logger logger = LoggerFactory.getLogger(DefaultServerCache.class); + protected static final Logger logger = LoggerFactory.getLogger(DefaultServerCache.class); - private static final CacheEntryComparator comparator = new CacheEntryComparator(); - - private final ConcurrentHashMap map = new ConcurrentHashMap(); - - private final AtomicInteger missCount = new AtomicInteger(); - - private final AtomicInteger removedHitCount = new AtomicInteger(); - - private final Object monitor = new Object(); - - private final String name; - - private int maxSize; - - private long trimFrequency; - - private int maxIdleSecs; - - private int maxSecsToLive; - - public DefaultServerCache(String name, ServerCacheOptions options) { - this(name, options.getMaxSize(), options.getMaxIdleSecs(), options.getMaxSecsToLive()); - } - - public DefaultServerCache(String name, int maxSize, int maxIdleSecs, int maxSecsToLive) { - this.name = name; - this.maxSize = maxSize; - this.maxIdleSecs = maxIdleSecs; - this.maxSecsToLive = maxSecsToLive; - this.trimFrequency = 60; - - } - - public void init(EbeanServer server) { - - TrimTask trim = new TrimTask(); - - BackgroundExecutor executor = server.getBackgroundExecutor(); - executor.executePeriodically(trim, trimFrequency, TimeUnit.SECONDS); - } - - - - - public ServerCacheStatistics getStatistics(boolean reset) { - - ServerCacheStatistics s = new ServerCacheStatistics(); - s.setCacheName(name); - s.setMaxSize(maxSize); - - // these counters won't necessarily be consistent with - // respect to each other as activity can occur while - // they are being calculated - int mc = reset ? missCount.getAndSet(0) : missCount.get(); - int hc = getHitCount(reset); - int size = size(); - - s.setSize(size); - s.setHitCount(hc); - s.setMissCount(mc); - - return s; - } - - public int getHitRatio() { - - int mc = missCount.get(); - int hc = getHitCount(false); - - int totalCount = hc + mc; - if (totalCount == 0){ - return 0; - } else { - return hc * 100 / totalCount; - } - - } - - private int getHitCount(boolean reset) { - - int hc = reset ? removedHitCount.getAndSet(0) : removedHitCount.get(); - - for (CacheEntry cacheEntry : map.values()) { - hc += cacheEntry.getHitCount(reset); - } - - return hc; - } + /** + * Compare by last access time (for LRU eviction). + */ + public static final CompareByLastAccess BY_LAST_ACCESS = new CompareByLastAccess(); - public ServerCacheOptions getOptions() { - synchronized (monitor) { - ServerCacheOptions o = new ServerCacheOptions(); - o.setMaxIdleSecs(maxIdleSecs); - o.setMaxSize(maxSize); - o.setMaxSecsToLive(maxSecsToLive); - return o; - } - } - - public void setOptions(ServerCacheOptions o) { - synchronized (monitor) { - maxIdleSecs = o.getMaxIdleSecs(); - maxSize = o.getMaxSize(); - maxSecsToLive = o.getMaxSecsToLive(); - } - } - - - /** - * Return the max cache size. - */ - public int getMaxSize() { - return maxSize; - } + /** + * The underlying map (ConcurrentHashMap or similar) + */ + protected final Map map; - /** - * Set the max cache size. - */ - public void setMaxSize(int maxSize) { - synchronized (monitor) { - this.maxSize = maxSize; - } - } + // LongAdder is a highly concurrent low latency counter (back ported from Java8) + protected final LongAdder missCount = new LongAdder(); + protected final LongAdder hitCount = new LongAdder(); + protected final LongAdder insertCount = new LongAdder(); + protected final LongAdder updateCount = new LongAdder(); + protected final LongAdder removeCount = new LongAdder(); + protected final LongAdder clearCount = new LongAdder(); - /** - * Return the max idle time. - */ - public long getMaxIdleSecs() { - return maxIdleSecs; - } + protected final LongAdder evictByIdle = new LongAdder(); + protected final LongAdder evictByTTL = new LongAdder(); + protected final LongAdder evictByLRU = new LongAdder(); + protected final LongAdder evictCount = new LongAdder(); + protected final LongAdder evictMicros = new LongAdder(); - /** - * Set the max idle time. - */ - public void setMaxIdleSecs(int maxIdleSecs) { - synchronized (monitor) { - this.maxIdleSecs = maxIdleSecs; - } - } + protected final Object monitor = new Object(); - /** - * Return the maximum time to live. - */ - public long getMaxSecsToLive() { - return maxSecsToLive; - } + protected final String name; - /** - * Set the maximum time to live. - */ - public void setMaxSecsToLive(int maxSecsToLive) { - synchronized (monitor) { - this.maxSecsToLive = maxSecsToLive; - } - } - - /** - * Return the name of the cache. - */ - public String getName() { - return name; - } + protected int maxSize; - /** - * Clear the cache. - */ - public void clear() { - map.clear(); - } + protected int trimFrequency; - /** - * Return a value from the cache. - */ - public Object get(Object key) { - - CacheEntry entry = map.get(key); - - if (entry == null){ - missCount.incrementAndGet(); - return null; - - } else { - // get value incrementing last - // access time and hitCount - return entry.getValue(); - } - } + protected int maxIdleSecs; - /** - * Put a value into the cache. - */ - public Object put(Object key, Object value) { - // put new entry with create time - CacheEntry entry = map.put(key, new CacheEntry(key, value)); - if (entry == null){ - return null; - } else { - int removedHits = entry.getHitCount(true); - removedHitCount.addAndGet(removedHits); - return entry.getValue(); - } - } + protected int maxSecsToLive; - /** - * Put a value into the cache but only if absent. - */ - public Object putIfAbsent(Object key, Object value) { - CacheEntry entry = map.putIfAbsent(key, new CacheEntry(key, value)); - if (entry == null){ - return null; - } else { - return entry.getValue(); - } - } + /** + * Construct using a ConcurrentHashMap and cache options. + */ + public DefaultServerCache(String name, ServerCacheOptions options) { + this(name, new ConcurrentHashMap(), options); + } - /** - * Remove an entry from the cache. - */ - public Object remove(Object key) { - CacheEntry entry = map.remove(key); - if (entry == null){ - return null; - } else { - int removedHits = entry.getHitCount(true); - removedHitCount.addAndGet(removedHits); - return entry.getValue(); - } - } + /** + * Construct passing in name, map and base eviction controls as ServerCacheOptions. + */ + public DefaultServerCache(String name, Map map, ServerCacheOptions options) { + this(name, map, options.getMaxSize(), options.getMaxIdleSecs(), options.getMaxSecsToLive(), options.getTrimFrequency()); + } - /** - * Return the number of elements in the cache. - */ - public int size() { - return map.size(); - } + /** + * Construct passing in name, map and base eviction controls. + */ + public DefaultServerCache(String name, Map map, int maxSize, int maxIdleSecs, int maxSecsToLive, int trimFrequency) { + this.name = name; + this.map = map; + this.maxSize = maxSize; + this.maxIdleSecs = maxIdleSecs; + this.maxSecsToLive = maxSecsToLive; + this.trimFrequency = trimFrequency; + } - /** - * The task used to periodically trim the cache. - */ - private class TrimTask implements Runnable { + @Override + public void init(EbeanServer server) { - public void run() { + EvictionRunnable trim = new EvictionRunnable(); - long startTime = System.currentTimeMillis(); - - if (logger.isTraceEnabled()){ - logger.trace("trimming cache " + name); - } - - int trimmedByIdle = 0; - int trimmedByTTL = 0; - int trimmedByLRU = 0; + // default to trimming the cache every 60 seconds + long trimFreqSecs = (trimFrequency == 0) ? 60 : trimFrequency; - boolean trimMaxSize = maxSize > 0 && maxSize < size(); + BackgroundExecutor executor = server.getBackgroundExecutor(); + executor.executePeriodically(trim, trimFreqSecs, TimeUnit.SECONDS); + } - ArrayList activeList = new ArrayList(); + @Override + public ServerCacheStatistics getStatistics(boolean reset) { - long idleExpire = System.currentTimeMillis() - (maxIdleSecs*1000); - long ttlExpire = System.currentTimeMillis() - (maxSecsToLive*1000); + ServerCacheStatistics cacheStats = new ServerCacheStatistics(); + cacheStats.setCacheName(name); + cacheStats.setMaxSize(maxSize); - Iterator it = map.values().iterator(); - while (it.hasNext()) { - CacheEntry cacheEntry = it.next(); - if (maxIdleSecs > 0 && idleExpire > cacheEntry.getLastAccessTime()) { - it.remove(); - trimmedByIdle++; + // these counters won't necessarily be consistent with + // respect to each other as activity can occur while + // they are being calculated here but they should be good enough + // and we don't want to reduce concurrent use to make them consistent + long clear = reset ? clearCount.sumThenReset() : clearCount.sum(); + long remove = reset ? removeCount.sumThenReset() : removeCount.sum(); + long update = reset ? updateCount.sumThenReset() : updateCount.sum(); + long insert = reset ? insertCount.sumThenReset() : insertCount.sum(); + long miss = reset ? missCount.sumThenReset() : missCount.sum(); + long hit = reset ? hitCount.sumThenReset() : hitCount.sum(); - } else if (maxSecsToLive > 0 && ttlExpire > cacheEntry.getCreateTime()) { - it.remove(); - trimmedByTTL++; + long evict = reset ? evictCount.sumThenReset() : evictCount.sum(); + long evictTime = reset ? evictMicros.sumThenReset() : evictMicros.sum(); + long evictIdle = reset ? evictByIdle.sumThenReset() : evictByIdle.sum(); + long evictTTL = reset ? evictByTTL.sumThenReset() : evictByTTL.sum(); + long evictLRU = reset ? evictByLRU.sumThenReset() : evictByLRU.sum(); - } else if (trimMaxSize) { - activeList.add(cacheEntry); - } - } + int size = size(); - if (trimMaxSize) { - trimmedByLRU = activeList.size() - maxSize; + cacheStats.setSize(size); + cacheStats.setHitCount(hit); + cacheStats.setMissCount(miss); + cacheStats.setInsertCount(insert); + cacheStats.setUpdateCount(update); + cacheStats.setRemoveCount(remove); + cacheStats.setClearCount(clear); - if (trimmedByLRU > 0) { - // sort into last access time ascending - Collections.sort(activeList, comparator); - for (int i = maxSize; i < activeList.size(); i++) { - // remove if still in the cache - map.remove(activeList.get(i).getKey()); - } - } - } - - long exeTime = System.currentTimeMillis() - startTime; - - if (logger.isDebugEnabled()){ - logger.debug("Executed trim of cache " + name + " in ["+exeTime - +"]millis idle[" + trimmedByIdle + "] timeToLive[" - + trimmedByTTL + "] accessTime[" - + trimmedByLRU + "]"); - } + cacheStats.setEvictionRunCount(evict); + cacheStats.setEvictionRunMicros(evictTime); + cacheStats.setEvictByIdle(evictIdle); + cacheStats.setEvictByTTL(evictTTL); + cacheStats.setEvictByLRU(evictLRU); - } + return cacheStats; + } - } + @Override + public int getHitRatio() { - /** - * Comparator for sorting by last access time. - */ - private static class CacheEntryComparator implements Comparator, Serializable { + long mc = missCount.sum(); + long hc = hitCount.sum(); - private static final long serialVersionUID = 1L; + long totalCount = hc + mc; + if (totalCount == 0) { + return 0; + } else { + return (int) (hc * 100 / totalCount); + } + } - public int compare(CacheEntry o1, CacheEntry o2) { - - return o1.getLastAccessLong().compareTo(o2.getLastAccessLong()); - } - } - - /** - * Wraps the values to additionally hold createTime and lastAccessTime. - */ - public static class CacheEntry { + /** + * Return the options controlling the cache. + */ + @Override + public ServerCacheOptions getOptions() { + synchronized (monitor) { + ServerCacheOptions options = new ServerCacheOptions(); + options.setMaxIdleSecs(maxIdleSecs); + options.setMaxSize(maxSize); + options.setMaxSecsToLive(maxSecsToLive); + options.setTrimFrequency(trimFrequency); + return options; + } + } - private final Object key; - private final Object value; - private final long createTime; - private final AtomicInteger hitCount = new AtomicInteger(); - private Long lastAccessTime; + /** + * Set the options controlling the cache + */ + @Override + public void setOptions(ServerCacheOptions options) { + synchronized (monitor) { + maxIdleSecs = options.getMaxIdleSecs(); + maxSize = options.getMaxSize(); + maxSecsToLive = options.getMaxSecsToLive(); + } + } - public CacheEntry(Object key, Object value) { - this.key = key; - this.value = value; - this.createTime = System.currentTimeMillis(); - this.lastAccessTime = Long.valueOf(createTime); - } + /** + * Return the name of the cache. + */ + public String getName() { + return name; + } - public Object getKey() { - return key; - } + /** + * Clear the cache. + */ + @Override + public void clear() { + clearCount.increment(); + map.clear(); + } - public Object getValue() { - // object assignment is atomic - hitCount.incrementAndGet(); - this.lastAccessTime = Long.valueOf(System.currentTimeMillis()); - return value; - } + /** + * Return a value from the cache. + */ + @Override + public Object get(Object key) { - public long getCreateTime() { - return createTime; - } + CacheEntry entry = map.get(key); + if (entry == null) { + missCount.increment(); + return null; - public long getLastAccessTime() { - return lastAccessTime.longValue(); - } + } else { + // Important that hitCount.increment() MUST be low latency under concurrent + // use hence must use LongAdder or better here + hitCount.increment(); + return entry.getValue(); + } + } - public Long getLastAccessLong() { - return lastAccessTime; - } + /** + * Put a value into the cache. + */ + @Override + public Object put(Object key, Object value) { + CacheEntry entry = map.put(key, new CacheEntry(key, value)); + if (entry == null) { + insertCount.increment(); + return null; + } else { + updateCount.increment(); + return entry.getValue(); + } + } - public int getHitCount(boolean reset) { - if (reset){ - return hitCount.getAndSet(0); + /** + * Remove an entry from the cache. + */ + @Override + public Object remove(Object key) { + CacheEntry entry = map.remove(key); + if (entry == null) { + return null; + } else { + removeCount.increment(); + return entry.getValue(); + } + } + + /** + * Return the number of elements in the cache. + */ + @Override + public int size() { + return map.size(); + } + + /** + * Return the size to trim to based on the max size. + *

+ * This returns 90% of the max size. + *

+ */ + protected int getTrimSize() { + return (maxSize * 90 / 100); + } + + /** + * Run the eviction based on Idle time, Time to live and LRU last access. + */ + public void runEviction() { + + long trimForMaxSize; + if (maxSize == 0) { + trimForMaxSize = 0; + } else { + trimForMaxSize = size() - maxSize; + } + + if (maxIdleSecs == 0 && maxSecsToLive == 0 && trimForMaxSize < 0) { + // nothing to trim on this cache + return; + } + + long startNanos = System.nanoTime(); + + long trimmedByIdle = 0; + long trimmedByTTL = 0; + long trimmedByLRU = 0; + + ArrayList activeList = new ArrayList(); + + long idleExpire = System.currentTimeMillis() - (maxIdleSecs * 1000); + long ttlExpire = System.currentTimeMillis() - (maxSecsToLive * 1000); + + Iterator it = map.values().iterator(); + while (it.hasNext()) { + CacheEntry cacheEntry = it.next(); + if (maxIdleSecs > 0 && idleExpire > cacheEntry.getLastAccessTime()) { + it.remove(); + trimmedByIdle++; + + } else if (maxSecsToLive > 0 && ttlExpire > cacheEntry.getCreateTime()) { + it.remove(); + trimmedByTTL++; + + } else if (trimForMaxSize > 0) { + activeList.add(cacheEntry); + } + } + + if (trimForMaxSize > 0) { + trimmedByLRU = activeList.size() - maxSize; + if (trimmedByLRU > 0) { + // sort into last access time ascending + Collections.sort(activeList, BY_LAST_ACCESS); + int trimSize = getTrimSize(); + for (int i = trimSize; i < activeList.size(); i++) { + // remove if still in the cache + map.remove(activeList.get(i).getKey()); + } + } + } + + long exeNanos = System.nanoTime() - startNanos; + long exeMicros = TimeUnit.MICROSECONDS.convert(exeNanos, TimeUnit.NANOSECONDS); + + // increment the eviction statistics + evictMicros.add(exeMicros); + evictCount.increment(); + evictByIdle.add(trimmedByIdle); + evictByTTL.add(trimmedByTTL); + evictByLRU.add(trimmedByLRU); + + if (logger.isDebugEnabled()) { + logger.debug("Executed trim of cache {} in [{}]millis idle[{}] timeToLive[{}] accessTime[{}]" + , name, exeMicros, trimmedByIdle, trimmedByTTL, trimmedByLRU); + } + } + + /** + * Runnable that calls the eviction routine. + */ + public class EvictionRunnable implements Runnable { + + @Override + public void run() { + runEviction(); + } + } + + /** + * Comparator for sorting by last access time. + */ + public static class CompareByLastAccess implements Comparator, Serializable { + + private static final long serialVersionUID = 1L; + + public int compare(CacheEntry entry1, CacheEntry entry2) { + return Long.compare(entry1.getLastAccessTime(), entry2.getLastAccessTime()); + } + } + + /** + * Wraps the value to additionally hold createTime and lastAccessTime and hit counter. + */ + public static class CacheEntry { + + private final Object key; + private final Object value; + private final long createTime; + private long lastAccessTime; + + public CacheEntry(Object key, Object value) { + this.key = key; + this.value = value; + this.createTime = System.currentTimeMillis(); + this.lastAccessTime = createTime; + } + + /** + * Return the entry key. + */ + public Object getKey() { + return key; + } + + /** + * Return the entry value. + */ + public Object getValue() { + // long assignment should be atomic these days (Ref Cliff Click) + lastAccessTime = System.currentTimeMillis(); + return value; + } + + /** + * Return the time the entry was created. + */ + public long getCreateTime() { + return createTime; + } + + /** + * Return the time the entry was last accessed. + */ + public long getLastAccessTime() { + return lastAccessTime; + } + + } - } else { - return hitCount.get(); - } - } - public int getHitCount() { - return hitCount.get(); - } - } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java index 41449ab7d..621baf802 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultContainer.java @@ -219,7 +219,16 @@ public class DefaultContainer implements SpiContainer { ServerCacheFactory cacheFactory = serverConfig.getServerCacheFactory(); if (cacheFactory == null) { - cacheFactory = new DefaultServerCacheFactory(); + ServiceLoader cacheFactories = ServiceLoader.load(ServerCacheFactory.class); + Iterator iterator = cacheFactories.iterator(); + if (iterator.hasNext()) { + // use the cacheFactory (via classpath service loader) + cacheFactory = iterator.next(); + logger.debug("using ServerCacheFactory {}", cacheFactory.getClass()); + } else { + // use the built in default + cacheFactory = new DefaultServerCacheFactory(); + } } return new DefaultServerCacheManager(cacheFactory, beanOptions, queryOptions); diff --git a/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheTest.java b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheTest.java new file mode 100644 index 000000000..15167a919 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheTest.java @@ -0,0 +1,59 @@ +package com.avaje.ebeaninternal.server.cache; + +import com.avaje.ebean.cache.ServerCacheOptions; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class DefaultServerCacheTest { + + private DefaultServerCache createCache() { + + ServerCacheOptions cacheOptions = new ServerCacheOptions(); + cacheOptions.setMaxSize(100); + cacheOptions.setMaxIdleSecs(60); + cacheOptions.setMaxSecsToLive(600); + cacheOptions.setTrimFrequency(60); + + return new DefaultServerCache("foo", cacheOptions); + } + + @Test + public void testGetHitRatio() throws Exception { + + DefaultServerCache cache = createCache(); + assertEquals(0, cache.getHitRatio()); + cache.put("A", "A"); + cache.get("A"); + assertEquals(100, cache.getHitRatio()); + cache.get("B"); + assertEquals(50, cache.getHitRatio()); + cache.get("B"); + cache.get("B"); + assertEquals(25, cache.getHitRatio()); + } + + @Test + public void testSize() throws Exception { + + DefaultServerCache cache = createCache(); + assertEquals(0, cache.size()); + cache.put("A", "A"); + assertEquals(1, cache.size()); + cache.put("A", "B"); + assertEquals(1, cache.size()); + cache.put("B", "B"); + assertEquals(2, cache.size()); + + cache.remove("B"); + cache.remove("A"); + assertEquals(0, cache.size()); + } + + @Test + public void testGetTrimSize() throws Exception { + + DefaultServerCache cache = createCache(); + assertEquals(90, cache.getTrimSize()); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/tests/cache/TestCacheBasic.java b/src/test/java/com/avaje/tests/cache/TestCacheBasic.java index 08a2af940..c97810490 100644 --- a/src/test/java/com/avaje/tests/cache/TestCacheBasic.java +++ b/src/test/java/com/avaje/tests/cache/TestCacheBasic.java @@ -28,7 +28,7 @@ public class TestCacheBasic extends BaseTestCase { Country c0 = Ebean.getReference(Country.class, "NZ"); ServerCacheStatistics statistics = countryCache.getStatistics(false); - int hc = statistics.getHitCount(); + long hc = statistics.getHitCount(); Assert.assertEquals(1, hc); Assert.assertNotNull(c0);