#652 - ENH: L2 cache - add @CacheQueryTuning annotation

This commit is contained in:
Robin Bygrave
2016-04-18 09:46:42 +12:00
parent 1b8b05790e
commit 2de78cd20f
10 changed files with 303 additions and 89 deletions
@@ -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.
* <p>
* If this is not specified then the system default settings are used.
* </p>
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheQueryTuning {
/**
* The maximum size for the cache.
* <p>
* This defaults to 0 which means unlimited.
* </p>
*/
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.
* <p>
* This defaults to 0 which means unlimited.
* </p>
*/
int maxIdleSecs() default 0;
/**
* The maximum time (in seconds) a cache entry is allowed to stay in the
* cache.
* <p>
* This is not generally required as the cache entries are automatically
* evicted when related data changes are committed.
* </p>
* <p>
* This defaults to 0 which means unlimited.
* </p>
*/
int maxSecsToLive() default 0;
/**
* The frequency (in seconds) that cache trimming should occur.
* <p>
* This is a hint for cache implementations that use background cache trimming.
* </p>
*/
int trimFrequency() default 0;
}
@@ -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();
}
/**
@@ -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();
}
}
@@ -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
@@ -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);
}
/**
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
}
}
@@ -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<Section> 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<Section> 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<Section> getSections() {
return sections;
}
public void setName(String name) {
this.name = name;
}
public void setSections(List<Section> sections) {
this.sections = sections;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public List<Section> getSections() {
return sections;
}
public void setSections(List<Section> sections) {
this.sections = sections;
}
public void addSection(Section s) {
if (sections == null) {
sections = new ArrayList<Section>();
}
public void addSection(Section s){
if (sections == null){
sections = new ArrayList<Section>();
}
sections.add(s);
}
sections.add(s);
}
}
@@ -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 {