diff --git a/src/main/java/com/avaje/ebean/annotation/CacheQueryTuning.java b/src/main/java/com/avaje/ebean/annotation/CacheQueryTuning.java new file mode 100644 index 000000000..2fd1464e5 --- /dev/null +++ b/src/main/java/com/avaje/ebean/annotation/CacheQueryTuning.java @@ -0,0 +1,55 @@ +package com.avaje.ebean.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specify cache tuning for query caching on a specific entity type. + *

+ * If this is not specified then the system default settings are used. + *

+ */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface CacheQueryTuning { + + /** + * The maximum size for the cache. + *

+ * This defaults to 0 which means unlimited. + *

+ */ + int maxSize() default 0; + + /** + * The maximum time (in seconds) that a cache entry is allowed to stay in the + * cache when it has not been accessed. + *

+ * This defaults to 0 which means unlimited. + *

+ */ + int maxIdleSecs() default 0; + + /** + * The maximum time (in seconds) a cache entry is allowed to stay in the + * cache. + *

+ * This is not generally required as the cache entries are automatically + * evicted when related data changes are committed. + *

+ *

+ * This defaults to 0 which means unlimited. + *

+ */ + 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/ServerCacheOptions.java b/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java index 21b0d1c65..493ae8c21 100644 --- a/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java +++ b/src/main/java/com/avaje/ebean/cache/ServerCacheOptions.java @@ -1,5 +1,6 @@ package com.avaje.ebean.cache; +import com.avaje.ebean.annotation.CacheQueryTuning; import com.avaje.ebean.annotation.CacheTuning; /** @@ -30,13 +31,13 @@ public class ServerCacheOptions { } /** - * Create merging default options with the deployment specified ones. + * Create from the cacheTuning deployment annotation. */ - public ServerCacheOptions(ServerCacheOptions defaults) { - this.maxSize = defaults.getMaxSize(); - this.maxIdleSecs = defaults.getMaxIdleSecs(); - this.maxSecsToLive = defaults.getMaxIdleSecs(); - this.trimFrequency = defaults.getTrimFrequency(); + public ServerCacheOptions(CacheQueryTuning cacheTuning) { + this.maxSize = cacheTuning.maxSize(); + this.maxIdleSecs = cacheTuning.maxIdleSecs(); + this.maxSecsToLive = cacheTuning.maxSecsToLive(); + this.trimFrequency = cacheTuning.trimFrequency(); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultCacheHolder.java b/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultCacheHolder.java index 55e7e4a28..ae0accd8b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultCacheHolder.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultCacheHolder.java @@ -1,5 +1,6 @@ package com.avaje.ebeaninternal.server.cache; +import com.avaje.ebean.annotation.CacheQueryTuning; import com.avaje.ebean.annotation.CacheTuning; import com.avaje.ebean.cache.ServerCache; import com.avaje.ebean.cache.ServerCacheFactory; @@ -24,29 +25,15 @@ public class DefaultCacheHolder { private final ServerCacheOptions defaultOptions; - private final boolean useBeanTuning; - /** * Create with a cache factory and default cache options. * * @param cacheFactory the factory for creating the cache * @param defaultOptions the default options for tuning the cache - * @param useBeanTuning if true then use the bean class specific tuning. This is - * generally false for the query cache. */ - public DefaultCacheHolder(ServerCacheFactory cacheFactory, - ServerCacheOptions defaultOptions, boolean useBeanTuning) { - + public DefaultCacheHolder(ServerCacheFactory cacheFactory, ServerCacheOptions defaultOptions) { this.cacheFactory = cacheFactory; this.defaultOptions = defaultOptions; - this.useBeanTuning = useBeanTuning; - } - - /** - * Return the default cache options. - */ - public ServerCacheOptions getDefaultOptions() { - return defaultOptions; } /** @@ -61,7 +48,7 @@ public class DefaultCacheHolder { synchronized (monitor) { cache = synchMap.get(cacheKey); if (cache == null) { - ServerCacheOptions options = getCacheOptions(cacheKey); + ServerCacheOptions options = getCacheOptions(cacheKey, type); cache = cacheFactory.createCache(type, cacheKey, options); synchMap.put(cacheKey, cache); concMap.put(cacheKey, cache); @@ -94,25 +81,41 @@ public class DefaultCacheHolder { /** * Return the cache options for a given bean type. */ - private ServerCacheOptions getCacheOptions(String beanType) { + ServerCacheOptions getCacheOptions(String beanType, ServerCacheType type) { - if (useBeanTuning) { - // read the deployment annotation - try { - Class cls = Class.forName(beanType); - CacheTuning cacheTuning = cls.getAnnotation(CacheTuning.class); - if (cacheTuning != null) { - ServerCacheOptions o = new ServerCacheOptions(cacheTuning); - o.applyDefaults(defaultOptions); - return o; - } - } catch (ClassNotFoundException e) { - // ignore + try { + Class cls = Class.forName(beanType); + switch (type) { + case QUERY: + return getQueryOptions(cls); + default: + return getBeanOptions(cls); } + } catch (ClassNotFoundException e) { + // ignore } return defaultOptions.copy(); + } + private ServerCacheOptions getQueryOptions(Class cls) { + CacheQueryTuning tuning = cls.getAnnotation(CacheQueryTuning.class); + if (tuning != null) { + ServerCacheOptions o = new ServerCacheOptions(tuning); + o.applyDefaults(defaultOptions); + return o; + } + return defaultOptions.copy(); + } + + private ServerCacheOptions getBeanOptions(Class cls) { + CacheTuning cacheTuning = cls.getAnnotation(CacheTuning.class); + if (cacheTuning != null) { + ServerCacheOptions o = new ServerCacheOptions(cacheTuning); + o.applyDefaults(defaultOptions); + return o; + } + return defaultOptions.copy(); } } 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 31143aecd..9269616f0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache.java @@ -34,7 +34,6 @@ public class DefaultServerCache implements ServerCache { */ public static final CompareByLastAccess BY_LAST_ACCESS = new CompareByLastAccess(); - /** * The underlying map (ConcurrentHashMap or similar) */ @@ -89,7 +88,23 @@ public class DefaultServerCache implements ServerCache { this.maxSize = maxSize; this.maxIdleSecs = maxIdleSecs; this.maxSecsToLive = maxSecsToLive; - this.trimFrequency = trimFrequency; + this.trimFrequency = determineTrim(maxIdleSecs, maxSecsToLive, trimFrequency); + } + + /** + * Determine a good trimFrequency as half of maxIdleSecs (or maxSecsToLive). + */ + int determineTrim(int maxIdleSecs, int maxSecsToLive, int trimFrequency) { + if (trimFrequency > 0) { + return trimFrequency; + } + if (maxIdleSecs > 0) { + return maxIdleSecs / 2 - 1; + } + if (maxSecsToLive > 0) { + return maxSecsToLive / 2 - 1; + } + return 0; } @Override diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheManager.java b/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheManager.java index 052f1ccd6..93865ac9e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheManager.java @@ -31,10 +31,10 @@ public class DefaultServerCacheManager implements ServerCacheManager { */ public DefaultServerCacheManager(ServerCacheFactory cacheFactory, ServerCacheOptions defaultBeanOptions, ServerCacheOptions defaultQueryOptions) { this.cacheFactory = cacheFactory; - this.beanCache = new DefaultCacheHolder(cacheFactory, defaultBeanOptions, true); - this.queryCache = new DefaultCacheHolder(cacheFactory, defaultQueryOptions, false); - this.naturalKeyCache = new DefaultCacheHolder(cacheFactory, defaultQueryOptions, false); - this.collectionIdsCache = new DefaultCacheHolder(cacheFactory, defaultQueryOptions, false); + this.beanCache = new DefaultCacheHolder(cacheFactory, defaultBeanOptions); + this.queryCache = new DefaultCacheHolder(cacheFactory, defaultQueryOptions); + this.naturalKeyCache = new DefaultCacheHolder(cacheFactory, defaultBeanOptions); + this.collectionIdsCache = new DefaultCacheHolder(cacheFactory, defaultBeanOptions); } /** diff --git a/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java new file mode 100644 index 000000000..d0cbd1052 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java @@ -0,0 +1,52 @@ +package com.avaje.ebeaninternal.server.cache; + +import com.avaje.ebean.cache.ServerCacheOptions; +import com.avaje.ebean.cache.ServerCacheType; +import com.avaje.tests.model.basic.Article; +import com.avaje.tests.model.basic.Order; +import com.avaje.tests.model.basic.Product; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class DefaultCacheHolder_getCacheOptions_Test { + + private DefaultCacheHolder cacheHolder; + + public DefaultCacheHolder_getCacheOptions_Test() { + + ServerCacheOptions defaultOptions = new ServerCacheOptions(); + defaultOptions.setMaxSize(10000); + defaultOptions.setMaxSecsToLive(120); + + this.cacheHolder = new DefaultCacheHolder(null, defaultOptions); + } + + @Test + public void beanOptions_when_set() { + + ServerCacheOptions options = cacheHolder.getCacheOptions(Article.class.getName(), ServerCacheType.BEAN); + assertEquals(options.getMaxSecsToLive(), 45); + } + + @Test + public void beanOptions_when_notSet_expect_default() { + + ServerCacheOptions options = cacheHolder.getCacheOptions(Order.class.getName(), ServerCacheType.BEAN); + assertEquals(options.getMaxSecsToLive(), 120); + } + + @Test + public void queryOptions_when_set() { + + ServerCacheOptions options = cacheHolder.getCacheOptions(Product.class.getName(), ServerCacheType.QUERY); + assertEquals(options.getMaxSecsToLive(), 15); + } + + @Test + public void queryOptions_when_notSet_expect_default() { + + ServerCacheOptions options = cacheHolder.getCacheOptions(Order.class.getName(), ServerCacheType.QUERY); + assertEquals(options.getMaxSecsToLive(), 120); + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheTest.java b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheTest.java index 15167a919..5d84632dc 100644 --- a/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheTest.java +++ b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCacheTest.java @@ -51,9 +51,38 @@ public class DefaultServerCacheTest { } @Test - public void testGetTrimSize() throws Exception { + public void trimFreq_halfIdle() throws Exception { - DefaultServerCache cache = createCache(); - assertEquals(90, cache.getTrimSize()); + DefaultServerCache cache = new DefaultServerCache("", null, 10000, 10, 20, 0); + assertEquals(cache.trimFrequency, 4); } + + @Test + public void trimFreq_halfIdle_withRounding() throws Exception { + + DefaultServerCache cache = new DefaultServerCache("", null, 10000, 11, 20, 0); + assertEquals(cache.trimFrequency, 4); + } + + @Test + public void trimFreq_halfTTL() throws Exception { + + DefaultServerCache cache = new DefaultServerCache("", null, 10000, 0, 20, 0); + assertEquals(cache.trimFrequency, 9); + } + + @Test + public void trimFreq_halfTTL_withRounding() throws Exception { + + DefaultServerCache cache = new DefaultServerCache("", null, 10000, 0, 21, 0); + assertEquals(cache.trimFrequency, 9); + } + + @Test + public void trimFreq_explicit() throws Exception { + + DefaultServerCache cache = new DefaultServerCache("", null, 10000, 10, 20, 42); + assertEquals(cache.trimFrequency, 42); + } + } \ No newline at end of file diff --git a/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java new file mode 100644 index 000000000..9706c95a0 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java @@ -0,0 +1,59 @@ +package com.avaje.ebeaninternal.server.cache; + +import com.avaje.ebean.cache.ServerCacheOptions; +import com.avaje.ebean.cache.ServerCacheStatistics; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.Random; + + +public class DefaultServerCache_RunEvictionTest { + + + private DefaultServerCache createCache() { + + ServerCacheOptions cacheOptions = new ServerCacheOptions(); + cacheOptions.setMaxSize(10000); + cacheOptions.setMaxIdleSecs(1); + cacheOptions.setMaxSecsToLive(2); + cacheOptions.setTrimFrequency(1); + + return new DefaultServerCache("foo", cacheOptions); + } + + private final DefaultServerCache cache; + + private final Random random = new Random(); + + public DefaultServerCache_RunEvictionTest(){ + this.cache = createCache(); + } + + @Ignore + @Test + public void runEvict() throws InterruptedException { + + for (int i = 0; i < 15; i++) { + doStuff(); + cache.runEviction(); + ServerCacheStatistics statistics = cache.getStatistics(true); + System.out.println(statistics); + Thread.sleep(500); + } + } + + private void doStuff() { + + for (int i = 0; i < 500; i++) { + String key = ""+random.nextInt(20000); + + int mode = random.nextInt(10); + if (mode < 8) { + cache.get(key); + } else { + cache.put(key, key+"-"+System.currentTimeMillis()); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/com/avaje/tests/model/basic/Article.java b/src/test/java/com/avaje/tests/model/basic/Article.java index 27888a897..bd507166d 100644 --- a/src/test/java/com/avaje/tests/model/basic/Article.java +++ b/src/test/java/com/avaje/tests/model/basic/Article.java @@ -1,66 +1,64 @@ package com.avaje.tests.model.basic; -import java.util.ArrayList; -import java.util.List; +import com.avaje.ebean.annotation.CacheStrategy; +import com.avaje.ebean.annotation.CacheTuning; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.OneToMany; - -import com.avaje.ebean.annotation.CacheStrategy; +import java.util.ArrayList; +import java.util.List; @CacheStrategy +@CacheTuning(maxSecsToLive = 45) @Entity public class Article extends BasicDomain { - private static final long serialVersionUID = 1L; + String name; - String name; - - String author; - - @OneToMany(cascade=CascadeType.ALL) - List
sections; + String author; - public Article() { - - } - - public Article(String name, String author) { - this.name = name; - this.author = author; - } - - public String getName() { - return name; - } + @OneToMany(cascade = CascadeType.ALL) + List
sections; - public void setName(String name) { - this.name = name; - } + public Article() { + } - public String getAuthor() { - return author; - } + public Article(String name, String author) { + this.name = name; + this.author = author; + } - public void setAuthor(String author) { - this.author = author; - } + public String getName() { + return name; + } - public List
getSections() { - return sections; - } + public void setName(String name) { + this.name = name; + } - public void setSections(List
sections) { - this.sections = sections; + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public List
getSections() { + return sections; + } + + public void setSections(List
sections) { + this.sections = sections; + } + + public void addSection(Section s) { + if (sections == null) { + sections = new ArrayList
(); } - - public void addSection(Section s){ - if (sections == null){ - sections = new ArrayList
(); - } - sections.add(s); - } - + sections.add(s); + } + } diff --git a/src/test/java/com/avaje/tests/model/basic/Product.java b/src/test/java/com/avaje/tests/model/basic/Product.java index b471420de..320abd02c 100644 --- a/src/test/java/com/avaje/tests/model/basic/Product.java +++ b/src/test/java/com/avaje/tests/model/basic/Product.java @@ -9,6 +9,7 @@ import javax.persistence.Table; import javax.persistence.Version; import javax.validation.constraints.Size; +import com.avaje.ebean.annotation.CacheQueryTuning; import com.avaje.ebean.annotation.CacheStrategy; import com.avaje.ebean.annotation.CreatedTimestamp; import com.avaje.ebean.annotation.DocStore; @@ -18,6 +19,7 @@ import com.avaje.ebean.annotation.DocStore; */ @DocStore @CacheStrategy +@CacheQueryTuning(maxSecsToLive = 15) @Entity @Table(name = "o_product") public class Product implements Serializable {