#1427 - QueryCache should be cleared, if one of a dependent bean is updated

Initial work, does not include propagation across cluster.
This commit is contained in:
rob bygrave
2018-06-15 17:36:23 +12:00
parent 59537227b6
commit 96cc8d7605
51 changed files with 952 additions and 283 deletions
+1 -1
View File
@@ -117,7 +117,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-annotation</artifactId>
<version>3.11</version>
<version>3.12</version>
</dependency>
<dependency>
+54
View File
@@ -0,0 +1,54 @@
package io.ebean.cache;
import java.util.Set;
/**
* For query cache entries we additionally hold the dependent tables and timestamp for the query result.
* <p>
* We use the dependent tables and timestamp to validate that tables the query joins to have not been
* modified since the query cache entry was cached. If any dependent tables have since been modified
* the query cache entry is treated as invalid.
* </p>
*/
public class QueryCacheEntry {
private final Object value;
private final Set<String> dependentTables;
private final long timestamp;
/**
* Create with dependent tables and timestamp.
*
* @param value The query result being cached
* @param dependentTables The extra tables the query is dependent on (joins to)
* @param timestamp The timestamp that the query uses to check for modifications
*/
public QueryCacheEntry(Object value, Set<String> dependentTables, long timestamp) {
this.value = value;
this.dependentTables = dependentTables;
this.timestamp = timestamp;
}
/**
* Return the actual query result.
*/
public Object getValue() {
return value;
}
/**
* Return the tables the query result is dependent on.
*/
public Set<String> getDependentTables() {
return dependentTables;
}
/**
* Return the timestamp used to check for modifications on the dependent tables.
*/
public long getTimestamp() {
return timestamp;
}
}
@@ -0,0 +1,12 @@
package io.ebean.cache;
/**
* Used to validate that a query cache entry is still valid based on dependent tables.
*/
public interface QueryCacheEntryValidate {
/**
* Return true if the entry is still valid based on dependent tables.
*/
boolean isValid(QueryCacheEntry queryCacheEntry);
}
+65
View File
@@ -0,0 +1,65 @@
package io.ebean.cache;
import io.ebean.config.CurrentTenantProvider;
/**
* Configuration used to create ServerCache instances.
*/
public class ServerCacheConfig {
private final ServerCacheType type;
private final String cacheKey;
private final ServerCacheOptions cacheOptions;
private final CurrentTenantProvider tenantProvider;
private final QueryCacheEntryValidate queryCacheEntryValidate;
public ServerCacheConfig(ServerCacheType type, String cacheKey, ServerCacheOptions cacheOptions, CurrentTenantProvider tenantProvider, QueryCacheEntryValidate queryCacheEntryValidate) {
this.type = type;
this.cacheKey = cacheKey;
this.cacheOptions = cacheOptions;
this.tenantProvider = tenantProvider;
this.queryCacheEntryValidate = queryCacheEntryValidate;
}
/**
* Return the cache type.
*/
public ServerCacheType getType() {
return type;
}
/**
* Return the name of the cache.
*/
public String getCacheKey() {
return cacheKey;
}
/**
* Return the tuning options.
*/
public ServerCacheOptions getCacheOptions() {
return cacheOptions;
}
/**
* Return the current tenant provider.
*/
public CurrentTenantProvider getTenantProvider() {
return tenantProvider;
}
/**
* Return the service that provides validation for query cache entries.
*/
public QueryCacheEntryValidate getQueryCacheEntryValidate() {
return queryCacheEntryValidate;
}
/**
* Return true if the cache is a query cache.
*/
public boolean isQueryCache() {
return type == ServerCacheType.QUERY;
}
}
+1 -3
View File
@@ -1,7 +1,5 @@
package io.ebean.cache;
import io.ebean.config.CurrentTenantProvider;
/**
* Defines method for constructing caches for beans and queries.
*/
@@ -10,6 +8,6 @@ public interface ServerCacheFactory {
/**
* Create the cache for the given type with options.
*/
ServerCache createCache(ServerCacheType type, String cacheKey, CurrentTenantProvider tenantProvider, ServerCacheOptions cacheOptions);
ServerCache createCache(ServerCacheConfig config);
}
@@ -101,6 +101,11 @@ public interface SpiTransaction extends Transaction {
*/
String getId();
/**
* Return the start timestamp for the transaction (JVM side).
*/
long getStartMillis();
/**
* Return true if this transaction has updateAllLoadedProperties set.
* If null is returned the server default is used (set on ServerConfig).
@@ -30,6 +30,11 @@ public abstract class SpiTransactionProxy implements SpiTransaction {
return transaction.translate(message, cause);
}
@Override
public long getStartMillis() {
return transaction.getStartMillis();
}
@Override
public void setLabel(String label) {
transaction.setLabel(label);
@@ -27,6 +27,8 @@ public class TransactionEvent implements Serializable {
*/
private final transient boolean local;
private final long modificationTimestamp;
private TransactionEventTable eventTables;
private transient TransactionEventBeans eventBeans;
@@ -36,8 +38,9 @@ public class TransactionEvent implements Serializable {
/**
* Create the TransactionEvent, one per Transaction.
*/
public TransactionEvent() {
public TransactionEvent(long modificationTimestamp) {
this.local = true;
this.modificationTimestamp = modificationTimestamp;
}
public void addDeleteById(BeanDescriptor<?> desc, Object id) {
@@ -108,8 +111,8 @@ public class TransactionEvent implements Serializable {
/**
* Build and return the cache changeSet.
*/
public CacheChangeSet buildCacheChanges(boolean viewInvalidation) {
CacheChangeSet changeSet = new CacheChangeSet(viewInvalidation);
public CacheChangeSet buildCacheChanges() {
CacheChangeSet changeSet = new CacheChangeSet(modificationTimestamp);
if (eventBeans != null) {
eventBeans.notifyCache(changeSet);
}
@@ -17,22 +17,26 @@ public class CacheChangeSet {
private final List<CacheChange> entries = new ArrayList<>();
private final Set<String> touchedTables = new HashSet<>();
private final Set<BeanDescriptor<?>> queryCaches = new HashSet<>();
private final Map<ManyKey, ManyChange> manyChangeMap = new HashMap<>();
/**
* Set of "base tables" modified used to invalidate entities based on views.
*/
private final Set<String> viewInvalidation = new HashSet<>();
private final boolean viewEntityInvalidation;
private final long modificationTimestamp;
/**
* Construct specifying if we also need to process invalidation for entities based on views.
*/
public CacheChangeSet(boolean viewEntityInvalidation) {
this.viewEntityInvalidation = viewEntityInvalidation;
public CacheChangeSet(long modificationTimestamp) {
this.modificationTimestamp = modificationTimestamp;
}
/**
* Return the touched tables.
*/
public Set<String> touchedTables() {
return touchedTables;
}
/**
@@ -40,7 +44,7 @@ public class CacheChangeSet {
* <p>
* Return the set of table changes to process invalidation for entities based on views.
*/
public Set<String> apply() {
public void apply() {
for (BeanDescriptor<?> entry : queryCaches) {
entry.clearQueryCache();
}
@@ -50,7 +54,13 @@ public class CacheChangeSet {
for (CacheChange entry : manyChangeMap.values()) {
entry.apply();
}
return viewInvalidation;
}
/**
* Add an entry to clear a query cache.
*/
public void addInvalidate(BeanDescriptor<?> descriptor) {
touchedTables.add(descriptor.getBaseTable());
}
/**
@@ -85,29 +95,23 @@ public class CacheChangeSet {
* On bean insert register table for view based entity invalidation.
*/
public void addBeanInsert(String baseTable) {
if (viewEntityInvalidation) {
viewInvalidation.add(baseTable);
}
touchedTables.add(baseTable);
}
/**
* Remove a bean from the cache.
*/
public <T> void addBeanRemove(BeanDescriptor<T> desc, Object id) {
touchedTables.add(desc.getBaseTable());
entries.add(new CacheChangeBeanRemove(desc, id));
if (viewEntityInvalidation) {
viewInvalidation.add(desc.getBaseTable());
}
}
/**
* Update a bean entry.
*/
public <T> void addBeanUpdate(BeanDescriptor<T> desc, Object id, Map<String, Object> changes, boolean updateNaturalKey, long version) {
touchedTables.add(desc.getBaseTable());
entries.add(new CacheChangeBeanUpdate(desc, id, changes, updateNaturalKey, version));
if (viewEntityInvalidation) {
viewInvalidation.add(desc.getBaseTable());
}
}
/**
@@ -125,6 +129,13 @@ public class CacheChangeSet {
return manyChangeMap.computeIfAbsent(key, ManyChange::new);
}
/**
* Return the modification timestamp for these changes.
*/
public long modificationTimestamp() {
return modificationTimestamp;
}
/**
* Changes for a specific many property.
*/
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.QueryCacheEntryValidate;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.config.CurrentTenantProvider;
@@ -19,6 +20,8 @@ public class CacheManagerOptions {
private CurrentTenantProvider currentTenantProvider;
private QueryCacheEntryValidate queryCacheEntryValidate;
private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
private ServerCacheOptions beanDefault = new ServerCacheOptions();
private ServerCacheOptions queryDefault = new ServerCacheOptions();
@@ -45,8 +48,9 @@ public class CacheManagerOptions {
return this;
}
public CacheManagerOptions with(ServerCacheFactory cacheFactory) {
public CacheManagerOptions with(ServerCacheFactory cacheFactory, QueryCacheEntryValidate queryCacheEntryValidate) {
this.cacheFactory = cacheFactory;
this.queryCacheEntryValidate = queryCacheEntryValidate;
return this;
}
@@ -82,4 +86,8 @@ public class CacheManagerOptions {
public ClusterManager getClusterManager() {
return clusterManager;
}
public QueryCacheEntryValidate getQueryCacheEntryValidate() {
return queryCacheEntryValidate;
}
}
@@ -2,7 +2,9 @@ package io.ebeaninternal.server.cache;
import io.ebean.annotation.CacheBeanTuning;
import io.ebean.annotation.CacheQueryTuning;
import io.ebean.cache.QueryCacheEntryValidate;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
@@ -33,22 +35,14 @@ class DefaultCacheHolder {
private final CurrentTenantProvider tenantProvider;
DefaultCacheHolder(CacheManagerOptions builder) {
this(builder.getCacheFactory(), builder.getBeanDefault(), builder.getQueryDefault(), builder.getCurrentTenantProvider());
}
private final QueryCacheEntryValidate queryCacheEntryValidate;
/**
* Create with a cache factory and default cache options.
*
* @param cacheFactory the factory for creating the cache
* @param beanDefault the default options for tuning bean caches
* @param queryDefault the default options for tuning query caches
*/
DefaultCacheHolder(ServerCacheFactory cacheFactory, ServerCacheOptions beanDefault, ServerCacheOptions queryDefault, CurrentTenantProvider tenantProvider) {
this.cacheFactory = cacheFactory;
this.beanDefault = beanDefault;
this.queryDefault = queryDefault;
this.tenantProvider = tenantProvider;
DefaultCacheHolder(CacheManagerOptions builder) {
this.cacheFactory = builder.getCacheFactory();
this.beanDefault = builder.getBeanDefault();
this.queryDefault = builder.getQueryDefault();
this.tenantProvider = builder.getCurrentTenantProvider();
this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate();
}
ServerCache getCache(Class<?> beanType, String cacheKey, ServerCacheType type) {
@@ -76,7 +70,7 @@ class DefaultCacheHolder {
collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key);
}
}
return cacheFactory.createCache(type, key, tenantProvider, options);
return cacheFactory.createCache(new ServerCacheConfig(type, key, options, tenantProvider, queryCacheEntryValidate));
}
void clearAll() {
@@ -2,10 +2,8 @@ package io.ebeaninternal.server.cache;
import io.ebean.BackgroundExecutor;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.cache.TenantAwareKey;
import io.ebean.config.CurrentTenantProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,7 +13,6 @@ import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
@@ -65,47 +62,14 @@ public class DefaultServerCache implements ServerCache {
protected TenantAwareKey tenantAwareKey;
/**
* Construct using a ConcurrentHashMap and cache options.
*/
public DefaultServerCache(String name, CurrentTenantProvider tenantProvider, ServerCacheOptions options) {
this(name, new ConcurrentHashMap<>(), tenantProvider, options);
}
/**
* Construct passing in name, map and base eviction controls as ServerCacheOptions.
*/
public DefaultServerCache(String name, Map<Object, CacheEntry> map, CurrentTenantProvider tenantProvider, ServerCacheOptions options) {
this(name, map, tenantProvider, options.getMaxSize(), options.getMaxIdleSecs(), options.getMaxSecsToLive(), options.getTrimFrequency());
}
/**
* Construct passing in name, map and base eviction controls.
*/
public DefaultServerCache(String name, Map<Object, CacheEntry> map, CurrentTenantProvider tenantProvider, int maxSize, int maxIdleSecs, int maxSecsToLive, int trimFrequency) {
this.name = name;
this.map = map;
this.maxSize = maxSize;
this.tenantAwareKey = new TenantAwareKey(tenantProvider);
this.maxIdleSecs = maxIdleSecs;
this.maxSecsToLive = maxSecsToLive;
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;
public DefaultServerCache(DefaultServerCacheConfig config) {
this.name = config.getName();
this.map = config.getMap();
this.maxSize = config.getMaxSize();
this.tenantAwareKey = new TenantAwareKey(config.getTenantProvider());
this.maxIdleSecs = config.getMaxIdleSecs();
this.maxSecsToLive = config.getMaxSecsToLive();
this.trimFrequency = config.determineTrimFrequency();
}
public void periodicTrim(BackgroundExecutor executor) {
@@ -193,7 +157,7 @@ public class DefaultServerCache implements ServerCache {
/**
* Return the tenant aware key.
*/
private Object key(Object id) {
protected Object key(Object id) {
return tenantAwareKey.key(id);
}
@@ -203,7 +167,7 @@ public class DefaultServerCache implements ServerCache {
@Override
public Object get(Object id) {
CacheEntry entry = map.get(key(id));
CacheEntry entry = getCacheEntry(id);
if (entry == null) {
missCount.increment();
return null;
@@ -212,10 +176,24 @@ public class DefaultServerCache implements ServerCache {
// Important that hitCount.increment() MUST be low latency under concurrent
// use hence must use LongAdder or better here
hitCount.increment();
return entry.getValue();
return unwrapEntry(entry);
}
}
/**
* Unwrap the cache entry - override for query cache to unwrap to the query result.
*/
protected Object unwrapEntry(CacheEntry entry) {
return entry.getValue();
}
/**
* Get the cache entry - override for query cache to validate dependent tables.
*/
protected CacheEntry getCacheEntry(Object id) {
return map.get(key(id));
}
@Override
public void putAll(Map<Object, Object> keyValues) {
keyValues.forEach(this::put);
@@ -0,0 +1,80 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.QueryCacheEntryValidate;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.config.CurrentTenantProvider;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DefaultServerCacheConfig {
private final ServerCacheConfig config;
private int maxSize;
private int maxIdleSecs;
private int maxSecsToLive;
private int trimFrequency;
private Map<Object, DefaultServerCache.CacheEntry> map;
public DefaultServerCacheConfig(ServerCacheConfig config) {
this(config, new ConcurrentHashMap<>());
}
public DefaultServerCacheConfig(ServerCacheConfig config, Map<Object, DefaultServerCache.CacheEntry> map) {
this.config = config;
this.map = map;
ServerCacheOptions options = config.getCacheOptions();
this.maxIdleSecs = options.getMaxIdleSecs();
this.maxSecsToLive = options.getMaxSecsToLive();
this.trimFrequency = options.getTrimFrequency();
this.maxSize = options.getMaxSize();
}
public CurrentTenantProvider getTenantProvider() {
return config.getTenantProvider();
}
public QueryCacheEntryValidate getQueryCacheEntryValidate() {
return config.getQueryCacheEntryValidate();
}
public String getName() {
return config.getCacheKey();
}
public Map<Object, DefaultServerCache.CacheEntry> getMap() {
return map;
}
public int getMaxSize() {
return maxSize;
}
public int getMaxIdleSecs() {
return maxIdleSecs;
}
public int getMaxSecsToLive() {
return maxSecsToLive;
}
/**
* Determine a good trimFrequency as half of maxIdleSecs (or maxSecsToLive).
*/
public int determineTrimFrequency() {
if (trimFrequency > 0) {
return trimFrequency;
}
if (maxIdleSecs > 0) {
return maxIdleSecs / 2 - 1;
}
if (maxSecsToLive > 0) {
return maxSecsToLive / 2 - 1;
}
return 0;
}
}
@@ -2,10 +2,8 @@ package io.ebeaninternal.server.cache;
import io.ebean.BackgroundExecutor;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import io.ebean.config.CurrentTenantProvider;
/**
@@ -30,9 +28,15 @@ class DefaultServerCacheFactory implements ServerCacheFactory {
}
@Override
public ServerCache createCache(ServerCacheType type, String cacheKey, CurrentTenantProvider tenantProvider, ServerCacheOptions cacheOptions) {
public ServerCache createCache(ServerCacheConfig config) {
DefaultServerCache cache = new DefaultServerCache(cacheKey, tenantProvider, cacheOptions);
DefaultServerCache cache;
if (config.isQueryCache()) {
// use a server cache aware of extra validation and QueryCacheEntry
cache = new DefaultServerQueryCache(new DefaultServerCacheConfig(config));
} else {
cache = new DefaultServerCache(new DefaultServerCacheConfig(config));
}
if (executor != null) {
cache.periodicTrim(executor);
}
@@ -0,0 +1,41 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.QueryCacheEntry;
import io.ebean.cache.QueryCacheEntryValidate;
/**
* Server cache for query caching.
* <p>
* Entries in this cache contain QueryCacheEntry and we need to additionally
* validate the entries when hit for changes to dependent tables.
* </p>
*/
public class DefaultServerQueryCache extends DefaultServerCache {
private final QueryCacheEntryValidate queryCacheEntryValidate;
public DefaultServerQueryCache(DefaultServerCacheConfig config) {
super(config);
this.queryCacheEntryValidate = config.getQueryCacheEntryValidate();
}
protected Object unwrapEntry(CacheEntry entry) {
return ((QueryCacheEntry) entry.getValue()).getValue();
}
@Override
protected CacheEntry getCacheEntry(Object id) {
Object key = key(id);
CacheEntry entry = map.get(key);
if (entry == null) {
return null;
}
QueryCacheEntry value = (QueryCacheEntry) entry.getValue();
if (!queryCacheEntryValidate.isValid(value)) {
map.remove(key);
removeCount.increment();
return null;
}
return entry;
}
}
@@ -12,6 +12,9 @@ public class CacheOptions {
*/
public static final CacheOptions NO_CACHING = new CacheOptions();
public static final CacheOptions INVALIDATE_QUERY_CACHE = new CacheOptions(true);
private final boolean invalidateQueryCache;
private final boolean enableBeanCache;
private final boolean enableQueryCache;
private final boolean readOnly;
@@ -21,6 +24,18 @@ public class CacheOptions {
* Construct for no caching.
*/
private CacheOptions() {
invalidateQueryCache = false;
enableBeanCache = false;
enableQueryCache = false;
readOnly = false;
naturalKey = null;
}
/**
* Construct for invalidateQueryCache.
*/
private CacheOptions(boolean invalidateQueryCache) {
this.invalidateQueryCache = invalidateQueryCache;
enableBeanCache = false;
enableQueryCache = false;
readOnly = false;
@@ -31,12 +46,21 @@ public class CacheOptions {
* Construct with cache annotation.
*/
public CacheOptions(Cache cache, String[] naturalKey) {
invalidateQueryCache = false;
enableBeanCache = cache.enableBeanCache();
enableQueryCache = cache.enableQueryCache();
readOnly = cache.readOnly();
this.naturalKey = naturalKey;
}
/**
* Return true if this is InvalidateQueryCache. A Bean that itself isn't L2
* cached but invalidates query cache entries that join to it.
*/
public boolean isInvalidateQueryCache() {
return invalidateQueryCache;
}
/**
* Return true if bean caching is enabled.
*/
@@ -138,11 +138,10 @@ public class DefaultContainer implements SpiContainer {
// executor and l2 caching service setup early (used during server construction)
SpiBackgroundExecutor executor = createBackgroundExecutor(serverConfig);
SpiCacheManager cacheManager = getCacheManager(online, serverConfig, executor);
InternalConfiguration c = new InternalConfiguration(clusterManager, cacheManager, executor, serverConfig, bootupClasses);
InternalConfiguration c = new InternalConfiguration(online, clusterManager, executor, serverConfig, bootupClasses);
DefaultServer server = new DefaultServer(c, c.cache());
DefaultServer server = new DefaultServer(c, c.cacheManager());
// generate and run DDL if required
// if there are any other tasks requiring action in their plugins, do them as well
@@ -165,52 +164,6 @@ public class DefaultContainer implements SpiContainer {
}
}
/**
* Create and return the CacheManager.
*/
private SpiCacheManager getCacheManager(boolean online, ServerConfig serverConfig, BackgroundExecutor executor) {
if (!online || serverConfig.isDisableL2Cache()) {
// use local only L2 cache implementation as placeholder
return new DefaultServerCacheManager();
}
// reasonable default settings are for a cache per bean type
ServerCacheOptions beanOptions = new ServerCacheOptions();
beanOptions.setMaxSize(serverConfig.getCacheMaxSize());
beanOptions.setMaxIdleSecs(serverConfig.getCacheMaxIdleTime());
beanOptions.setMaxSecsToLive(serverConfig.getCacheMaxTimeToLive());
// reasonable default settings for the query cache per bean type
ServerCacheOptions queryOptions = new ServerCacheOptions();
queryOptions.setMaxSize(serverConfig.getQueryCacheMaxSize());
queryOptions.setMaxIdleSecs(serverConfig.getQueryCacheMaxIdleTime());
queryOptions.setMaxSecsToLive(serverConfig.getQueryCacheMaxTimeToLive());
boolean localL2Caching = false;
ServerCachePlugin plugin = serverConfig.getServerCachePlugin();
if (plugin == null) {
ServiceLoader<ServerCachePlugin> cacheFactories = ServiceLoader.load(ServerCachePlugin.class);
Iterator<ServerCachePlugin> iterator = cacheFactories.iterator();
if (iterator.hasNext()) {
// use the cacheFactory (via classpath service loader)
plugin = iterator.next();
logger.debug("using ServerCacheFactory {}", plugin.getClass());
} else {
// use the built in default l2 caching which is local cache based
localL2Caching = true;
plugin = new DefaultServerCachePlugin();
}
}
ServerCacheFactory factory = plugin.create(serverConfig, executor);
CacheManagerOptions builder = new CacheManagerOptions(clusterManager, serverConfig, localL2Caching)
.with(beanOptions, queryOptions)
.with(factory);
return new DefaultServerCacheManager(builder);
}
/**
* Get the entities, scalarTypes, Listeners etc combining the class registered
* ones with the already created instances.
@@ -3,7 +3,10 @@ package io.ebeaninternal.server.core;
import com.fasterxml.jackson.core.JsonFactory;
import io.ebean.ExpressionFactory;
import io.ebean.annotation.Platform;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheManager;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCachePlugin;
import io.ebean.config.ExternalTransactionManager;
import io.ebean.config.ProfilingConfig;
import io.ebean.config.ServerConfig;
@@ -27,7 +30,10 @@ import io.ebeaninternal.api.SpiProfileHandler;
import io.ebeaninternal.dbmigration.DbOffline;
import io.ebeaninternal.server.autotune.AutoTuneService;
import io.ebeaninternal.server.autotune.service.AutoTuneServiceFactory;
import io.ebeaninternal.server.cache.CacheManagerOptions;
import io.ebeaninternal.server.cache.DefaultCacheAdapter;
import io.ebeaninternal.server.cache.DefaultServerCacheManager;
import io.ebeaninternal.server.cache.DefaultServerCachePlugin;
import io.ebeaninternal.server.cache.SpiCacheManager;
import io.ebeaninternal.server.changelog.DefaultChangeLogListener;
import io.ebeaninternal.server.changelog.DefaultChangeLogPrepare;
@@ -69,6 +75,7 @@ import io.ebeaninternal.server.transaction.ExplicitTransactionManager;
import io.ebeaninternal.server.transaction.ExternalTransactionScopeManager;
import io.ebeaninternal.server.transaction.JtaTransactionManager;
import io.ebeaninternal.server.transaction.NoopProfileHandler;
import io.ebeaninternal.server.transaction.TableModState;
import io.ebeaninternal.server.transaction.TransactionManager;
import io.ebeaninternal.server.transaction.TransactionManagerOptions;
import io.ebeaninternal.server.transaction.TransactionScopeManager;
@@ -84,6 +91,7 @@ import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
@@ -96,6 +104,10 @@ public class InternalConfiguration {
private static final Logger logger = LoggerFactory.getLogger(InternalConfiguration.class);
private final TableModState tableModState = new TableModState();
private final boolean online;
private final ServerConfig serverConfig;
private final BootupClasses bootupClasses;
@@ -124,6 +136,10 @@ public class InternalConfiguration {
private final SpiCacheManager cacheManager;
private final ServerCachePlugin serverCachePlugin;
private boolean localL2Caching;
private final ExpressionFactory expressionFactory;
private final SpiBackgroundExecutor backgroundExecutor;
@@ -141,17 +157,16 @@ public class InternalConfiguration {
private final SpiLogManager logManager;
public InternalConfiguration(ClusterManager clusterManager,
SpiCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor,
public InternalConfiguration(boolean online, ClusterManager clusterManager, SpiBackgroundExecutor backgroundExecutor,
ServerConfig serverConfig, BootupClasses bootupClasses) {
this.online = online;
this.serverConfig = serverConfig;
this.logManager = initLogManager();
this.docStoreFactory = initDocStoreFactory(serverConfig.service(DocStoreFactory.class));
this.jsonFactory = serverConfig.getJsonFactory();
this.clusterManager = clusterManager;
this.backgroundExecutor = backgroundExecutor;
this.cacheManager = cacheManager;
this.bootupClasses = bootupClasses;
this.databasePlatform = serverConfig.getDatabasePlatform();
@@ -164,6 +179,9 @@ public class InternalConfiguration {
this.deployCreateProperties = new DeployCreateProperties(typeManager);
this.deployUtil = new DeployUtil(typeManager, serverConfig);
this.serverCachePlugin = initServerCachePlugin();
this.cacheManager = initCacheManager();
InternalConfigXmlRead xmlRead = new InternalConfigXmlRead(serverConfig);
this.dtoBeanManager = new DtoBeanManager(typeManager, xmlRead.readDtoMapping());
@@ -400,7 +418,7 @@ public class InternalConfiguration {
TransactionManagerOptions options =
new TransactionManagerOptions(notifyL2CacheInForeground, serverConfig, scopeManager, clusterManager, backgroundExecutor,
indexUpdateProcessor, beanDescriptorManager, dataSource(), profileHandler(), logManager);
indexUpdateProcessor, beanDescriptorManager, dataSource(), profileHandler(), logManager, tableModState);
if (serverConfig.isExplicitTransactionBeginMode()) {
return new ExplicitTransactionManager(options);
@@ -493,7 +511,7 @@ public class InternalConfiguration {
return dataTimeZone;
}
public ServerCacheManager cache() {
public ServerCacheManager cacheManager() {
return new DefaultCacheAdapter(cacheManager);
}
@@ -537,4 +555,54 @@ public class InternalConfiguration {
public SpiLogManager getLogManager() {
return logManager;
}
private ServerCachePlugin initServerCachePlugin() {
ServerCachePlugin plugin = serverConfig.getServerCachePlugin();
if (plugin == null) {
ServiceLoader<ServerCachePlugin> cacheFactories = ServiceLoader.load(ServerCachePlugin.class);
Iterator<ServerCachePlugin> iterator = cacheFactories.iterator();
if (iterator.hasNext()) {
// use the cacheFactory (via classpath service loader)
plugin = iterator.next();
logger.debug("using ServerCacheFactory {}", serverCachePlugin.getClass());
} else {
// use the built in default l2 caching which is local cache based
localL2Caching = true;
plugin = new DefaultServerCachePlugin();
}
}
return plugin;
}
/**
* Create and return the CacheManager.
*/
private SpiCacheManager initCacheManager() {
if (!online || serverConfig.isDisableL2Cache()) {
// use local only L2 cache implementation as placeholder
return new DefaultServerCacheManager();
}
ServerCacheFactory factory = serverCachePlugin.create(serverConfig, backgroundExecutor);
// reasonable default settings are for a cache per bean type
ServerCacheOptions beanOptions = new ServerCacheOptions();
beanOptions.setMaxSize(serverConfig.getCacheMaxSize());
beanOptions.setMaxIdleSecs(serverConfig.getCacheMaxIdleTime());
beanOptions.setMaxSecsToLive(serverConfig.getCacheMaxTimeToLive());
// reasonable default settings for the query cache per bean type
ServerCacheOptions queryOptions = new ServerCacheOptions();
queryOptions.setMaxSize(serverConfig.getQueryCacheMaxSize());
queryOptions.setMaxIdleSecs(serverConfig.getQueryCacheMaxIdleTime());
queryOptions.setMaxSecsToLive(serverConfig.getQueryCacheMaxTimeToLive());
CacheManagerOptions builder = new CacheManagerOptions(clusterManager, serverConfig, localL2Caching)
.with(beanOptions, queryOptions)
.with(factory, tableModState);
return new DefaultServerCacheManager(builder);
}
}
@@ -8,6 +8,7 @@ import io.ebean.Version;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
import io.ebean.cache.QueryCacheEntry;
import io.ebean.common.BeanList;
import io.ebean.common.CopyOnFirstWriteList;
import io.ebean.event.BeanFindController;
@@ -83,6 +84,8 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
private boolean inlineCountDistinct;
private Set<String> dependentTables;
/**
* Create the InternalQueryRequest.
*/
@@ -207,7 +210,7 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
/**
* Prepare the query and calculate the query plan key.
*/
public void prepareQuery() {
void prepareQuery() {
beanDescriptor.prepareQuery(query);
adapterPreQuery();
this.secondaryQueries = query.convertJoins();
@@ -680,8 +683,10 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
}
}
public void putToQueryCache(Object queryResult) {
beanDescriptor.queryCachePut(cacheKey, queryResult);
public void putToQueryCache(Object result) {
// use transaction start where as query statement start would be better at READ_COMMITTED
long asOfTimestamp = transaction.getStartMillis();
beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, asOfTimestamp));
}
/**
@@ -752,4 +757,13 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
public boolean isInlineCountDistinct() {
return inlineCountDistinct;
}
public void addDependentTables(Set<String> tables) {
if (tables != null && !tables.isEmpty()) {
if (dependentTables == null) {
dependentTables = new LinkedHashSet<>();
}
dependentTables.addAll(tables);
}
}
}
@@ -11,6 +11,7 @@ import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.PersistenceContext;
import io.ebean.cache.QueryCacheEntry;
import io.ebean.config.EncryptKey;
import io.ebean.config.ServerConfig;
import io.ebean.config.dbplatform.IdType;
@@ -1361,8 +1362,8 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
/**
* Put a query result into the query cache.
*/
public void queryCachePut(Object id, Object queryResult) {
cacheHelp.queryCachePut(id, queryResult);
public void queryCachePut(Object id, QueryCacheEntry entry) {
cacheHelp.queryCachePut(id, entry);
}
/**
@@ -4,6 +4,7 @@ import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.PersistenceContext;
import io.ebean.cache.QueryCacheEntry;
import io.ebean.cache.ServerCache;
import io.ebeaninternal.api.BeanCacheResult;
import io.ebeaninternal.api.TransactionEventTable.TableIUD;
@@ -54,6 +55,7 @@ final class BeanDescriptorCacheHelp<T> {
* Flag indicating this bean has no relationships.
*/
private final boolean cacheSharableBeans;
private final boolean invalidateQueryCache;
private final Class<?> beanType;
@@ -84,6 +86,7 @@ final class BeanDescriptorCacheHelp<T> {
this.cacheName = beanType.getSimpleName();
this.cacheManager = cacheManager;
this.cacheOptions = cacheOptions;
this.invalidateQueryCache = cacheOptions.isInvalidateQueryCache();
this.cacheSharableBeans = cacheSharableBeans;
this.propertiesOneImported = propertiesOneImported;
this.naturalKey = cacheOptions.getNaturalKey();
@@ -111,7 +114,7 @@ final class BeanDescriptorCacheHelp<T> {
* Derive the cache notify flags.
*/
void deriveNotifyFlags() {
cacheNotifyOnAll = (beanCache != null || queryCache != null);
cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null);
cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes();
if (logger.isDebugEnabled()) {
@@ -225,14 +228,14 @@ final class BeanDescriptorCacheHelp<T> {
/**
* Put a query result into the query cache.
*/
void queryCachePut(Object id, Object queryResult) {
void queryCachePut(Object id, QueryCacheEntry entry) {
if (queryCache == null) {
throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)");
}
if (queryLog.isDebugEnabled()) {
queryLog.debug(" PUT {}({})", cacheName, id);
}
queryCache.put(id, queryResult);
queryCache.put(id, entry);
}
@@ -723,30 +726,42 @@ final class BeanDescriptorCacheHelp<T> {
* Add appropriate cache changes to support delete by id.
*/
void handleDelete(Object id, CacheChangeSet changeSet) {
if (beanCache != null) {
changeSet.addBeanRemove(desc, id);
if (invalidateQueryCache) {
changeSet.addInvalidate(desc);
} else {
if (beanCache != null) {
changeSet.addBeanRemove(desc, id);
}
cacheDeleteImported(true, null, changeSet);
}
cacheDeleteImported(true, null, changeSet);
}
/**
* Add appropriate cache changes to support delete bean.
*/
void handleDelete(Object id, PersistRequestBean<T> deleteRequest, CacheChangeSet changeSet) {
queryCacheClear(changeSet);
if (beanCache != null) {
changeSet.addBeanRemove(desc, id);
if (invalidateQueryCache) {
changeSet.addInvalidate(desc);
} else {
queryCacheClear(changeSet);
if (beanCache != null) {
changeSet.addBeanRemove(desc, id);
}
cacheDeleteImported(true, deleteRequest.getEntityBean(), changeSet);
}
cacheDeleteImported(true, deleteRequest.getEntityBean(), changeSet);
}
/**
* Add appropriate cache changes to support insert.
*/
void handleInsert(PersistRequestBean<T> insertRequest, CacheChangeSet changeSet) {
queryCacheClear(changeSet);
cacheDeleteImported(false, insertRequest.getEntityBean(), changeSet);
changeSet.addBeanInsert(desc.getBaseTable());
if (invalidateQueryCache) {
changeSet.addInvalidate(desc);
} else {
queryCacheClear(changeSet);
cacheDeleteImported(false, insertRequest.getEntityBean(), changeSet);
changeSet.addBeanInsert(desc.getBaseTable());
}
}
private void cacheDeleteImported(boolean clear, EntityBean entityBean, CacheChangeSet changeSet) {
@@ -759,28 +774,30 @@ final class BeanDescriptorCacheHelp<T> {
* Add appropriate changes to support update.
*/
void handleUpdate(Object id, PersistRequestBean<T> updateRequest, CacheChangeSet changeSet) {
if (invalidateQueryCache) {
changeSet.addInvalidate(desc);
queryCacheClear(changeSet);
} else {
queryCacheClear(changeSet);
if (beanCache == null) {
// query caching only
return;
}
if (beanCache == null) {
// query caching only
return;
}
List<BeanPropertyAssocMany<?>> manyCollections = updateRequest.getUpdatedManyCollections();
if (manyCollections != null) {
for (BeanPropertyAssocMany<?> many : manyCollections) {
if (!many.isElementCollection()) {
Object details = many.getValue(updateRequest.getEntityBean());
CachedManyIds entry = createManyIds(many, details);
if (entry != null) {
changeSet.addManyPut(desc, many.getName(), id, entry);
List<BeanPropertyAssocMany<?>> manyCollections = updateRequest.getUpdatedManyCollections();
if (manyCollections != null) {
for (BeanPropertyAssocMany<?> many : manyCollections) {
if (!many.isElementCollection()) {
Object details = many.getValue(updateRequest.getEntityBean());
CachedManyIds entry = createManyIds(many, details);
if (entry != null) {
changeSet.addManyPut(desc, many.getName(), id, entry);
}
}
}
}
updateRequest.addBeanUpdate(changeSet);
}
updateRequest.addBeanUpdate(changeSet);
}
/**
@@ -440,6 +440,13 @@ public class DeployBeanDescriptor<T> {
this.inheritInfo = inheritInfo;
}
/**
* Set that this type invalidates query caches.
*/
public void setInvalidateQueryCache() {
this.cacheOptions = CacheOptions.INVALIDATE_QUERY_CACHE;
}
/**
* Enable L2 bean and query caching based on Cache annotation.
*/
@@ -7,6 +7,7 @@ import io.ebean.annotation.Draftable;
import io.ebean.annotation.DraftableElement;
import io.ebean.annotation.History;
import io.ebean.annotation.Index;
import io.ebean.annotation.InvalidateQueryCache;
import io.ebean.annotation.ReadAudit;
import io.ebean.annotation.UpdateMode;
import io.ebean.annotation.View;
@@ -181,9 +182,16 @@ public class AnnotationClass extends AnnotationParser {
descriptor.setUpdateChangesOnly(updateMode.updateChangesOnly());
}
Cache cache = AnnotationUtil.findAnnotationRecursive(cls, Cache.class);
if (cache != null && !disableL2Cache) {
descriptor.setCache(cache);
if (!disableL2Cache) {
Cache cache = AnnotationUtil.findAnnotationRecursive(cls, Cache.class);
if (cache != null) {
descriptor.setCache(cache);
} else {
InvalidateQueryCache invalidateQueryCache = AnnotationUtil.findAnnotationRecursive(cls, InvalidateQueryCache.class);
if (invalidateQueryCache != null) {
descriptor.setInvalidateQueryCache();
}
}
}
Set<NamedQuery> namedQueries = AnnotationUtil.findAnnotationsRecursive(cls, NamedQuery.class);
@@ -38,6 +38,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
/**
* An object that represents a SqlSelect statement.
@@ -811,4 +812,8 @@ public class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfileTran
PreparedStatement getPstmt() {
return pstmt;
}
public Set<String> getDependentTables() {
return queryPlan.getDependentTables();
}
}
@@ -25,6 +25,8 @@ import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -111,6 +113,15 @@ public class CQueryEngine {
if (request.isLogSummary()) {
request.getTransaction().logSummary(rcQuery.getSummary());
}
if (request.isQueryCachePut() && !list.isEmpty()) {
request.addDependentTables(rcQuery.getDependentTables());
list = Collections.unmodifiableList(list);
request.putToQueryCache(list);
if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) {
list = new ArrayList<>(list);
}
}
return list;
} catch (SQLException e) {
@@ -173,6 +184,11 @@ public class CQueryEngine {
request.getTransaction().end();
}
if (request.isQueryCachePut()) {
request.addDependentTables(rcQuery.getDependentTables());
request.putToQueryCache(count);
}
return count;
} catch (SQLException e) {
@@ -385,6 +401,9 @@ public class CQueryEngine {
}
request.executeSecondaryQueries(false);
if (request.isQueryCachePut()) {
request.addDependentTables(cquery.getDependentTables());
}
return beanCollection;
@@ -17,6 +17,7 @@ import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Base compiled query request for single attribute queries.
@@ -188,4 +189,8 @@ class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent {
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.getProfileId(), rowCount, query.getProfileId());
}
Set<String> getDependentTables() {
return queryPlan.getDependentTables();
}
}
@@ -24,6 +24,8 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Set;
/**
* Represents a query for a given SQL statement.
@@ -88,6 +90,8 @@ public class CQueryPlan {
*/
private volatile String auditQueryHash;
private final Set<String> dependentTables;
/**
* Create a query plan based on a OrmQueryRequest.
*/
@@ -110,6 +114,7 @@ public class CQueryPlan {
this.logWhereSql = logWhereSql;
this.encryptedProps = sqlTree.getEncryptedProps();
this.stats = new CQueryPlanStats(this, server.isCollectQueryOrigins());
this.dependentTables = sqlTree.dependentTables();
}
/**
@@ -134,6 +139,7 @@ public class CQueryPlan {
this.logWhereSql = logWhereSql;
this.encryptedProps = sqlTree.getEncryptedProps();
this.stats = new CQueryPlanStats(this, server.isCollectQueryOrigins());
this.dependentTables = (rawSql) ? Collections.emptySet() : sqlTree.dependentTables();
}
private String location() {
@@ -154,6 +160,10 @@ public class CQueryPlan {
return beanType;
}
public Set<String> getDependentTables() {
return dependentTables;
}
public ProfileLocation getProfileLocation() {
return profileLocation;
}
@@ -12,6 +12,7 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Set;
/**
* Executes the select row count query.
@@ -154,4 +155,8 @@ class CQueryRowCount implements SpiProfileTransactionEvent {
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.getProfileId(), rowCount, query.getProfileId());
}
Set<String> getDependentTables() {
return queryPlan.getDependentTables();
}
}
@@ -88,41 +88,20 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine {
public <T> int findCount(OrmQueryRequest<T> request) {
flushJdbcBatchOnQuery(request);
int result = queryEngine.findCount(request);
if (request.getQuery().getUseQueryCache().isPut()) {
request.putToQueryCache(result);
}
return result;
return queryEngine.findCount(request);
}
@Override
public <A> List<A> findIds(OrmQueryRequest<?> request) {
flushJdbcBatchOnQuery(request);
List<A> result = queryEngine.findIds(request);
if (request.getQuery().getUseQueryCache().isPut()) {
result = Collections.unmodifiableList(result);
request.putToQueryCache(result);
if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) {
result = new ArrayList<>(result);
}
}
return result;
return queryEngine.findIds(request);
}
@Override
public <A> List<A> findSingleAttributeList(OrmQueryRequest<?> request) {
flushJdbcBatchOnQuery(request);
List<A> result = queryEngine.findSingleAttributeList(request);
if (!result.isEmpty() && request.getQuery().getUseQueryCache().isPut()) {
// load the query result into the query cache
result = Collections.unmodifiableList(result);
request.putToQueryCache(result);
if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) {
result = new ArrayList<>(result);
}
}
return result;
return queryEngine.findSingleAttributeList(request);
}
@Override
@@ -3,6 +3,7 @@ package io.ebeaninternal.server.query;
import io.ebeaninternal.api.SpiQuery;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -163,4 +164,13 @@ class SqlTree {
boolean isSingleProperty() {
return rootNode.isSingleProperty();
}
/**
* Return the tables that are joined in this query.
*/
Set<String> dependentTables() {
Set<String> tables = new LinkedHashSet<>();
rootNode.dependentTables(tables);
return tables;
}
}
@@ -9,6 +9,7 @@ import io.ebeaninternal.server.type.ScalarType;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
interface SqlTreeNode {
@@ -87,4 +88,9 @@ interface SqlTreeNode {
* Return true if the query is known to only have a single property selected.
*/
boolean isSingleProperty();
/**
* Add dependent tables to the given set.
*/
void dependentTables(Set<String> tables);
}
@@ -21,6 +21,7 @@ import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Normal bean included in the query.
@@ -578,6 +579,14 @@ class SqlTreeNodeBean implements SqlTreeNode {
}
}
@Override
public void dependentTables(Set<String> tables) {
tables.add(nodeBeanProp.target().getBaseTable(temporalMode));
for (SqlTreeNode child : children) {
child.dependentTables(tables);
}
}
/**
* Join to base table for this node. This includes a join to the intersection
* table if this is a ManyToMany node.
@@ -11,6 +11,7 @@ import io.ebeaninternal.server.type.ScalarType;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* The purpose is to add an extra join to the query.
@@ -100,6 +101,16 @@ class SqlTreeNodeExtraJoin implements SqlTreeNode {
children.add(child);
}
@Override
public void dependentTables(Set<String> tables) {
tables.add(assocBeanProperty.target().getBaseTable(SpiQuery.TemporalMode.CURRENT));
if (children != null) {
for (SqlTreeNode child : children) {
child.dependentTables(tables);
}
}
}
@Override
public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) {
@@ -7,6 +7,7 @@ import io.ebeaninternal.server.deploy.DbSqlContext;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
final class SqlTreeNodeManyRoot extends SqlTreeNodeBean {
@@ -10,6 +10,7 @@ import io.ebeaninternal.server.deploy.TableJoin;
import io.ebeaninternal.server.type.ScalarType;
import java.util.List;
import java.util.Set;
/**
* Join to Many (or child of a many) to support where clause predicates on many properties.
@@ -109,6 +110,11 @@ class SqlTreeNodeManyWhereJoin implements SqlTreeNode {
}
}
@Override
public void dependentTables(Set<String> tables) {
tables.add(nodeBeanProp.target().getBaseTable(SpiQuery.TemporalMode.CURRENT));
}
@Override
public void buildRawSqlSelectChain(List<String> selectChain) {
// nothing to add
@@ -5,6 +5,7 @@ import io.ebeaninternal.server.deploy.DbSqlContext;
import io.ebeaninternal.server.deploy.TableJoin;
import java.util.List;
import java.util.Set;
/**
* Represents the root node of the Sql Tree.
@@ -77,4 +78,10 @@ final class SqlTreeNodeRoot extends SqlTreeNodeBean {
return joinType;
}
@Override
public void dependentTables(Set<String> tables) {
for (SqlTreeNode child : children) {
child.dependentTables(tables);
}
}
}
@@ -67,6 +67,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
private Map<String, Object> userObjects;
private long startNanos;
private long startMillis;
/**
* Create without a tenantId.
@@ -79,6 +80,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
this.connection = connection;
this.persistenceContext = new DefaultPersistenceContext();
this.startNanos = System.nanoTime();
this.startMillis = manager.clockNowEpoch();
}
/**
@@ -89,6 +91,12 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
this.tenantId = tenantId;
}
@Override
public long getStartMillis() {
// not used on read only transaction
return startMillis;
}
@Override
public void setLabel(String label) {
// do nothing
@@ -187,6 +187,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
protected ProfileLocation profileLocation;
protected final long startNanos;
private final long startMillis;
/**
* Create a new JdbcTransaction.
@@ -203,6 +204,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
this.startNanos = System.nanoTime();
if (manager == null) {
this.startMillis = System.currentTimeMillis();
this.logSql = false;
this.logSummary = false;
this.skipCacheAfterWrite = true;
@@ -210,6 +212,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
this.batchOnCascadeMode = PersistBatch.NONE;
this.onQueryOnly = OnQueryOnly.ROLLBACK;
} else {
this.startMillis = manager.clockNowEpoch();
this.logSql = manager.isLogSql();
this.logSummary = manager.isLogSummary();
this.skipCacheAfterWrite = manager.isSkipCacheAfterWrite();
@@ -235,6 +238,11 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
return label;
}
@Override
public long getStartMillis() {
return startMillis;
}
@Override
public long profileOffset() {
return (profileStream == null) ? 0 : profileStream.offset();
@@ -853,7 +861,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
public TransactionEvent getEvent() {
queryOnly = false;
if (event == null) {
event = new TransactionEvent();
event = new TransactionEvent(startMillis);
}
return event;
}
@@ -1050,7 +1058,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
// the event has been sent to the transaction manager
// for postCommit processing (l2 cache updates etc)
// start a new transaction event
event = new TransactionEvent();
event = new TransactionEvent(startMillis);
} catch (Exception e) {
doRollback(e);
@@ -47,6 +47,12 @@ class NoTransaction implements SpiTransaction {
}
@Override
public long getStartMillis() {
// not used
return System.currentTimeMillis();
}
@Override
public boolean isActive() {
// always false
@@ -14,6 +14,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Set;
/**
* Performs post commit processing using a background thread.
@@ -88,10 +89,10 @@ public final class PostCommitProcessing {
processTableEvents(event.getEventTables());
if (manager.notifyL2CacheInForeground) {
// process l2 cache changes in foreground
processCacheChanges(event.buildCacheChanges(manager.viewInvalidation));
processCacheChanges(event.buildCacheChanges());
} else {
// collect l2 cache changes for delayed background processing
cacheChanges = event.buildCacheChanges(manager.viewInvalidation);
cacheChanges = event.buildCacheChanges();
}
}
@@ -165,7 +166,12 @@ public final class PostCommitProcessing {
*/
private void processCacheChanges(CacheChangeSet cacheChanges) {
if (cacheChanges != null) {
manager.processViewInvalidation(cacheChanges.apply());
Set<String> touched = cacheChanges.touchedTables();
if (touched != null && !touched.isEmpty()) {
manager.processTouchedTables(touched, cacheChanges.modificationTimestamp());
// TODO: Propagate touched tables to other cluster members
}
cacheChanges.apply();
}
}
@@ -0,0 +1,51 @@
package io.ebeaninternal.server.transaction;
import io.ebean.cache.QueryCacheEntry;
import io.ebean.cache.QueryCacheEntryValidate;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Holds timestamp of last modification per table.
* <p>
* This information is used to validate entries in the L2 query caches.
* </p>
*/
public class TableModState implements QueryCacheEntryValidate {
private Map<String,Long> tableModStamp = new ConcurrentHashMap<>();
/**
* Set the modified timestamp on the tables that have been touched.
*/
public void touch(Set<String> touchedTables, long modTimestamp) {
for (String tableName : touchedTables) {
tableModStamp.put(tableName, modTimestamp);
}
}
/**
* Return true if all the tables are valid based on timestamp comparison.
*/
public boolean isValid(Set<String> tables, long sinceTimestamp) {
for (String tableName : tables) {
Long modTime = tableModStamp.get(tableName);
if (modTime != null && modTime > sinceTimestamp ) {
return false;
}
}
return true;
}
@Override
public boolean isValid(QueryCacheEntry entry) {
Set<String> dependentTables = entry.getDependentTables();
if (dependentTables != null && !dependentTables.isEmpty()) {
return isValid(dependentTables, entry.getTimestamp());
}
return true;
}
}
@@ -46,7 +46,7 @@ import java.util.Set;
/**
* Manages transactions.
* <p>
* Keeps the Cache and Cluster in synch when transactions are committed.
* Keeps the Cache and Cluster in sync when transactions are committed.
* </p>
*/
public class TransactionManager implements SpiTransactionManager {
@@ -136,6 +136,8 @@ public class TransactionManager implements SpiTransactionManager {
private final TimedMetricMap txnNamed;
private final TransactionScopeManager scopeManager;
private final TableModState tableModState;
/**
* Create the TransactionManager
*/
@@ -157,6 +159,7 @@ public class TransactionManager implements SpiTransactionManager {
this.clusterManager = options.clusterManager;
this.serverName = options.config.getName();
this.scopeManager = options.scopeManager;
this.tableModState = options.tableModState;
this.backgroundExecutor = options.backgroundExecutor;
this.dataSourceSupplier = options.dataSourceSupplier;
this.docStoreActive = options.config.getDocStoreConfig().isActive();
@@ -178,6 +181,13 @@ public class TransactionManager implements SpiTransactionManager {
scopeManager.register(this);
}
/**
* Return the NOW timestamp in epoch millis.
*/
public long clockNowEpoch() {
return System.currentTimeMillis(); // TODO: Review when we supply a Clock via ServerConfig.
}
/**
* Create a new scoped transaction.
*/
@@ -434,7 +444,7 @@ public class TransactionManager implements SpiTransactionManager {
private void externalModificationEvent(TransactionEventTable tableEvents) {
TransactionEvent event = new TransactionEvent();
TransactionEvent event = new TransactionEvent(clockNowEpoch());
event.add(tableEvents);
PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, event);
@@ -495,9 +505,10 @@ public class TransactionManager implements SpiTransactionManager {
/**
* Invalidate the query caches for entities based on views.
*/
public void processViewInvalidation(Set<String> viewInvalidation) {
if (!viewInvalidation.isEmpty()) {
beanDescriptorManager.processViewInvalidation(viewInvalidation);
public void processTouchedTables(Set<String> touchedTables, long modTimestamp) {
tableModState.touch(touchedTables, modTimestamp);
if (viewInvalidation) {
beanDescriptorManager.processViewInvalidation(touchedTables);
}
}
@@ -24,10 +24,13 @@ public class TransactionManagerOptions {
final SpiProfileHandler profileHandler;
final TransactionScopeManager scopeManager;
final SpiLogManager logManager;
final TableModState tableModState;
public TransactionManagerOptions(boolean notifyL2CacheInForeground, ServerConfig config, TransactionScopeManager scopeManager, ClusterManager clusterManager,
BackgroundExecutor backgroundExecutor, DocStoreUpdateProcessor docStoreUpdateProcessor,
BeanDescriptorManager descMgr, DataSourceSupplier dataSourceSupplier, SpiProfileHandler profileHandler, SpiLogManager logManager) {
BeanDescriptorManager descMgr, DataSourceSupplier dataSourceSupplier, SpiProfileHandler profileHandler,
SpiLogManager logManager, TableModState tableModState) {
this.notifyL2CacheInForeground = notifyL2CacheInForeground;
this.config = config;
@@ -39,6 +42,7 @@ public class TransactionManagerOptions {
this.dataSourceSupplier = dataSourceSupplier;
this.profileHandler = profileHandler;
this.logManager = logManager;
this.tableModState = tableModState;
}
}
@@ -3,6 +3,8 @@ package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import io.ebean.config.ServerConfig;
import io.ebeaninternal.server.transaction.TableModState;
import org.tests.model.basic.Contact;
import org.tests.model.basic.Customer;
import org.junit.Test;
@@ -17,10 +19,17 @@ public class DefaultCacheHolderTest {
private final ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
private final ServerCacheOptions defaultOptions = new ServerCacheOptions();
@Test
public void getCache_normal() throws Exception {
private CacheManagerOptions options() {
return new CacheManagerOptions(null, new ServerConfig(), true)
.with(defaultOptions, defaultOptions)
.with(cacheFactory, new TableModState());
}
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, null);
@Test
public void getCache_normal() {
DefaultCacheHolder holder = new DefaultCacheHolder(options());
DefaultServerCache cache = cache(holder, Customer.class, "customer");
assertThat(cache.getName()).isEqualTo("customer_B");
@@ -40,7 +49,9 @@ public class DefaultCacheHolderTest {
@Test
public void getCache_multiTenant() throws Exception {
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, tenantId::get);
CacheManagerOptions builder = options().with(tenantId::get);
DefaultCacheHolder holder = new DefaultCacheHolder(builder);
tenantId.set("ten_1");
DefaultServerCache cache = cache(holder, Customer.class, "customer");
@@ -48,26 +59,26 @@ public class DefaultCacheHolderTest {
cache.put("1", "value-for-tenant1");
cache.put("2", "an other value-for-tenant1");
assertThat(cache.size()).isEqualTo(2);
tenantId.set("ten_2");
cache.put("1", "value-for-tenant2");
cache.put("2", "an other value-for-tenant2");
assertThat(cache.size()).isEqualTo(4);
assertThat(cache.get("1")).isEqualTo("value-for-tenant2");
assertThat(cache.get("2")).isEqualTo("an other value-for-tenant2");
tenantId.set("ten_1");
assertThat(cache.get("1")).isEqualTo("value-for-tenant1");
assertThat(cache.get("2")).isEqualTo("an other value-for-tenant1");
Exception exInThread[] = new Exception[1];
Exception exInThread[] = new Exception[1];
Thread t = new Thread() {
@Override
public void run() {
@@ -77,29 +88,29 @@ public class DefaultCacheHolderTest {
cache.put("1", "value-for-tenant2");
cache.put("2", "an other value-for-tenant2");
tenantId.set(null);
cache.clear();
} catch (Exception e) {
exInThread[0] = e;
}
};
};
// do some async work
t.start();
t.join();
if (exInThread[0] != null) {
if (exInThread[0] != null) {
throw exInThread[0];
}
assertThat(cache.size()).isEqualTo(0);
}
@Test
public void clearAll() throws Exception {
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, null);
public void clearAll() {
DefaultCacheHolder holder = new DefaultCacheHolder(options());
DefaultServerCache cache = cache(holder, Customer.class, "customer");
cache.put("foo", "foo");
assertThat(cache.size()).isEqualTo(1);
@@ -109,8 +120,11 @@ public class DefaultCacheHolderTest {
}
@Test
public void clearAll_multiTenant() throws Exception {
DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, tenantId::get);
public void clearAll_multiTenant() {
CacheManagerOptions options = options().with(tenantId::get);
DefaultCacheHolder holder = new DefaultCacheHolder(options);
DefaultServerCache cache = cache(holder, Customer.class, "customer");
cache.put("foo", "foo");
assertThat(cache.size()).isEqualTo(1);
@@ -118,5 +132,5 @@ public class DefaultCacheHolderTest {
holder.clearAll();
assertThat(cache.size()).isEqualTo(0);
}
}
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import io.ebean.config.ServerConfig;
import org.tests.model.basic.Article;
import org.tests.model.basic.Order;
import org.tests.model.basic.Product;
@@ -19,7 +20,10 @@ public class DefaultCacheHolder_getCacheOptions_Test {
defaultOptions.setMaxSize(10000);
defaultOptions.setMaxSecsToLive(120);
this.cacheHolder = new DefaultCacheHolder(null, defaultOptions, defaultOptions, null);
CacheManagerOptions builder = new CacheManagerOptions(null, new ServerConfig(), true)
.with(defaultOptions, defaultOptions);
this.cacheHolder = new DefaultCacheHolder(builder);
}
@Test
@@ -0,0 +1,52 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import org.junit.Test;
import static org.junit.Assert.*;
public class DefaultServerCacheConfigTest {
private DefaultServerCacheConfig create(int maxSize, int maxIdleSecs, int maxSecsToLive, int trimFreq) {
ServerCacheOptions options = new ServerCacheOptions();
options.setMaxSize(maxSize);
options.setMaxIdleSecs(maxIdleSecs);
options.setMaxSecsToLive(maxSecsToLive);
options.setTrimFrequency(trimFreq);
return new DefaultServerCacheConfig(new ServerCacheConfig(null, null, options, null, null));
}
@Test
public void trimFreq_halfIdle() {
assertEquals(create(10000,10,20, 0).determineTrimFrequency(), 4);
}
@Test
public void trimFreq_halfIdle_withRounding() {
assertEquals(create(10000,11,20, 0).determineTrimFrequency(), 4);
}
@Test
public void trimFreq_halfTTL() {
assertEquals(create(10000,0,20, 0).determineTrimFrequency(), 9);
}
@Test
public void trimFreq_halfTTL_withRounding() {
assertEquals(create(10000,0,21, 0).determineTrimFrequency(), 9);
}
@Test
public void trimFreq_explicit() {
assertEquals(create(10000,10,20, 42).determineTrimFrequency(), 42);
}
}
@@ -1,6 +1,8 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -15,11 +17,13 @@ public class DefaultServerCacheTest {
cacheOptions.setMaxSecsToLive(600);
cacheOptions.setTrimFrequency(60);
return new DefaultServerCache("foo", null, cacheOptions);
ServerCacheConfig con = new ServerCacheConfig(ServerCacheType.BEAN, "foo", cacheOptions, null, null);
DefaultServerCacheConfig config = new DefaultServerCacheConfig(con);
return new DefaultServerCache(config);
}
@Test
public void testGetHitRatio() throws Exception {
public void testGetHitRatio() {
DefaultServerCache cache = createCache();
assertEquals(0, cache.getHitRatio());
@@ -34,7 +38,7 @@ public class DefaultServerCacheTest {
}
@Test
public void testSize() throws Exception {
public void testSize() {
DefaultServerCache cache = createCache();
assertEquals(0, cache.size());
@@ -50,39 +54,5 @@ public class DefaultServerCacheTest {
assertEquals(0, cache.size());
}
@Test
public void trimFreq_halfIdle() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 10, 20, 0);
assertEquals(cache.trimFrequency, 4);
}
@Test
public void trimFreq_halfIdle_withRounding() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 11, 20, 0);
assertEquals(cache.trimFrequency, 4);
}
@Test
public void trimFreq_halfTTL() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 0, 20, 0);
assertEquals(cache.trimFrequency, 9);
}
@Test
public void trimFreq_halfTTL_withRounding() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 0, 21, 0);
assertEquals(cache.trimFrequency, 9);
}
@Test
public void trimFreq_explicit() throws Exception {
DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 10, 20, 42);
assertEquals(cache.trimFrequency, 42);
}
}
@@ -1,7 +1,9 @@
package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.cache.ServerCacheType;
import org.junit.Ignore;
import org.junit.Test;
@@ -19,7 +21,8 @@ public class DefaultServerCache_RunEvictionTest {
cacheOptions.setMaxSecsToLive(2);
cacheOptions.setTrimFrequency(1);
return new DefaultServerCache("foo", null, cacheOptions);
ServerCacheConfig con = new ServerCacheConfig(ServerCacheType.BEAN, "foo", cacheOptions, null, null);
return new DefaultServerCache(new DefaultServerCacheConfig(con));
}
private final DefaultServerCache cache;
@@ -0,0 +1,44 @@
package io.ebeaninternal.server.transaction;
import org.junit.Test;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.junit.Assert.*;
public class TableModStateTest {
private TableModState tableModState = new TableModState();
@Test
public void isValid() {
long now = System.currentTimeMillis();
tableModState.touch(setOf("one", "two", "three"), now);
// empty
assertTrue(tableModState.isValid(Collections.emptySet(), 12L));
// no entry
assertTrue(tableModState.isValid(setOf("noEntry"), 12L));
// later timestamp
assertTrue(tableModState.isValid(setOf("one"), now + 1));
assertTrue(tableModState.isValid(setOf("one", "two", "noEntry"), now + 1));
// invalid
assertFalse(tableModState.isValid(setOf("one"), now - 1));
assertFalse(tableModState.isValid(setOf("one", "two"), now - 1));
assertFalse(tableModState.isValid(setOf("three", "two"), now - 1));
}
private Set<String> setOf(String... tables) {
Set<String> touched = new HashSet<>();
Collections.addAll(touched, tables);
return touched;
}
}
@@ -0,0 +1,59 @@
package org.tests.cache;
import io.ebean.BaseTestCase;
import io.ebean.Ebean;
import io.ebean.cache.ServerCache;
import org.junit.Test;
import org.tests.model.basic.Address;
import org.tests.model.basic.Customer;
import org.tests.model.basic.ResetBasicData;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestQueryCacheTableDependency extends BaseTestCase {
@Test
public void testFindCountOnDependent() {
ResetBasicData.reset();
ServerCache customerCache = Ebean.getServerCacheManager().getQueryCache(Customer.class);
customerCache.clear();
List<Address> addrs = Ebean.find(Address.class)
.where().eq("line2", "St Lukes")
.findList();
int custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lukes")
.findCount();
assertThat(custs).isEqualTo(3);
custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lukes")
.findCount();
assertThat(custs).isEqualTo(3);
Address a1 = addrs.get(0);
a1.setLine2("St Lucky");
Ebean.save(a1);
custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lukes")
.findCount();
assertThat(custs).isEqualTo(2); // cache says 3
custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
.where().eq("billingAddress.line2", "St Lucky")
.findCount();
assertThat(custs).isEqualTo(1);
}
}
@@ -1,5 +1,6 @@
package org.tests.model.basic;
import io.ebean.annotation.InvalidateQueryCache;
import org.tests.model.basic.metaannotation.SizeMedium;
import javax.persistence.Column;
@@ -14,6 +15,11 @@ import java.sql.Timestamp;
/**
* Address entity bean.
*/
// Address is not L2 cached directly but it is joined to queries that are cached
// What InvalidateQueryCache means is that we propagate a table modification event
// when address is changed ... and cached queries that join to address will be
// invalidated accordingly.
@InvalidateQueryCache
@Entity
@Table(name = "o_address")
public class Address {