diff --git a/pom.xml b/pom.xml
index a75900802..4cfee2676 100644
--- a/pom.xml
+++ b/pom.xml
@@ -117,7 +117,7 @@
io.ebeanebean-annotation
- 3.11
+ 3.12
diff --git a/src/main/java/io/ebean/cache/QueryCacheEntry.java b/src/main/java/io/ebean/cache/QueryCacheEntry.java
new file mode 100644
index 000000000..31f1134d9
--- /dev/null
+++ b/src/main/java/io/ebean/cache/QueryCacheEntry.java
@@ -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.
+ *
+ * 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.
+ *
+ */
+public class QueryCacheEntry {
+
+ private final Object value;
+
+ private final Set 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 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 getDependentTables() {
+ return dependentTables;
+ }
+
+ /**
+ * Return the timestamp used to check for modifications on the dependent tables.
+ */
+ public long getTimestamp() {
+ return timestamp;
+ }
+}
diff --git a/src/main/java/io/ebean/cache/QueryCacheEntryValidate.java b/src/main/java/io/ebean/cache/QueryCacheEntryValidate.java
new file mode 100644
index 000000000..f94c07a7d
--- /dev/null
+++ b/src/main/java/io/ebean/cache/QueryCacheEntryValidate.java
@@ -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);
+}
diff --git a/src/main/java/io/ebean/cache/ServerCacheConfig.java b/src/main/java/io/ebean/cache/ServerCacheConfig.java
new file mode 100644
index 000000000..a2a6d5c52
--- /dev/null
+++ b/src/main/java/io/ebean/cache/ServerCacheConfig.java
@@ -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;
+ }
+}
diff --git a/src/main/java/io/ebean/cache/ServerCacheFactory.java b/src/main/java/io/ebean/cache/ServerCacheFactory.java
index 269cb9ae1..54c71e7d1 100644
--- a/src/main/java/io/ebean/cache/ServerCacheFactory.java
+++ b/src/main/java/io/ebean/cache/ServerCacheFactory.java
@@ -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,15 @@ 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);
+ /**
+ * Return a ServerCacheNotify that we will send ServerCacheNotification events to.
+ *
+ * This is used if a ServerCacheNotifyPlugin is not supplied.
+ *
+ *
+ * @param listener The listener that should be used to process the notification events.
+ */
+ ServerCacheNotify createCacheNotify(ServerCacheNotify listener);
}
diff --git a/src/main/java/io/ebean/cache/ServerCacheNotification.java b/src/main/java/io/ebean/cache/ServerCacheNotification.java
new file mode 100644
index 000000000..4b5b08a6f
--- /dev/null
+++ b/src/main/java/io/ebean/cache/ServerCacheNotification.java
@@ -0,0 +1,29 @@
+package io.ebean.cache;
+
+import java.util.Set;
+
+/**
+ * Notification event that dependent tables have been modified.
+ *
+ * This is sent to other interested servers (in the cluster).
+ *
+ */
+public class ServerCacheNotification {
+
+ private final long modifyTimestamp;
+
+ private final Set dependentTables;
+
+ public ServerCacheNotification(long modifyTimestamp, Set dependentTables) {
+ this.modifyTimestamp = modifyTimestamp;
+ this.dependentTables = dependentTables;
+ }
+
+ public long getModifyTimestamp() {
+ return modifyTimestamp;
+ }
+
+ public Set getDependentTables() {
+ return dependentTables;
+ }
+}
diff --git a/src/main/java/io/ebean/cache/ServerCacheNotify.java b/src/main/java/io/ebean/cache/ServerCacheNotify.java
new file mode 100644
index 000000000..3e9f30a56
--- /dev/null
+++ b/src/main/java/io/ebean/cache/ServerCacheNotify.java
@@ -0,0 +1,12 @@
+package io.ebean.cache;
+
+/**
+ * Interface for both listening to notification changes and sending them to other members of the cluster.
+ */
+public interface ServerCacheNotify {
+
+ /**
+ * Notify other server cache members of the table modifications or process the notifications.
+ */
+ void notify(ServerCacheNotification notification);
+}
diff --git a/src/main/java/io/ebean/cache/ServerCacheNotifyPlugin.java b/src/main/java/io/ebean/cache/ServerCacheNotifyPlugin.java
new file mode 100644
index 000000000..2e87a067c
--- /dev/null
+++ b/src/main/java/io/ebean/cache/ServerCacheNotifyPlugin.java
@@ -0,0 +1,17 @@
+package io.ebean.cache;
+
+import io.ebean.config.ServerConfig;
+
+/**
+ * Plugin that provides a ServerCacheNotify implementation.
+ *
+ * Is supplied this will be used to send the ServerCacheNotification event to other cluster members.
+ *
+ */
+public interface ServerCacheNotifyPlugin {
+
+ /**
+ * Create a ServerCacheNotify implementation given the server configuration.
+ */
+ ServerCacheNotify create(ServerConfig serverConfig);
+}
diff --git a/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/src/main/java/io/ebeaninternal/api/SpiTransaction.java
index e134986c5..0054757da 100644
--- a/src/main/java/io/ebeaninternal/api/SpiTransaction.java
+++ b/src/main/java/io/ebeaninternal/api/SpiTransaction.java
@@ -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).
diff --git a/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
index 02057236c..ffbb3c727 100644
--- a/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
+++ b/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java
@@ -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);
diff --git a/src/main/java/io/ebeaninternal/api/TransactionEvent.java b/src/main/java/io/ebeaninternal/api/TransactionEvent.java
index 44ba7cf4d..711fa49dd 100644
--- a/src/main/java/io/ebeaninternal/api/TransactionEvent.java
+++ b/src/main/java/io/ebeaninternal/api/TransactionEvent.java
@@ -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);
}
diff --git a/src/main/java/io/ebeaninternal/api/TransactionEventTable.java b/src/main/java/io/ebeaninternal/api/TransactionEventTable.java
index cde507c1a..fe5338cf4 100644
--- a/src/main/java/io/ebeaninternal/api/TransactionEventTable.java
+++ b/src/main/java/io/ebeaninternal/api/TransactionEventTable.java
@@ -1,8 +1,8 @@
package io.ebeaninternal.api;
import io.ebean.event.BulkTableEvent;
-import io.ebeaninternal.server.cluster.BinaryMessage;
-import io.ebeaninternal.server.cluster.BinaryMessageList;
+import io.ebeaninternal.server.cluster.binarymessage.BinaryMessage;
+import io.ebeaninternal.server.cluster.binarymessage.BinaryMessageList;
import java.io.DataInput;
import java.io.DataOutputStream;
@@ -102,7 +102,7 @@ public final class TransactionEventTable implements Serializable {
os.writeBoolean(insert);
os.writeBoolean(update);
os.writeBoolean(delete);
-
+ os.close();
msgList.add(msg);
}
diff --git a/src/main/java/io/ebeaninternal/server/cache/CacheChangeSet.java b/src/main/java/io/ebeaninternal/server/cache/CacheChangeSet.java
index f0c09f6cb..45956d181 100644
--- a/src/main/java/io/ebeaninternal/server/cache/CacheChangeSet.java
+++ b/src/main/java/io/ebeaninternal/server/cache/CacheChangeSet.java
@@ -17,22 +17,26 @@ public class CacheChangeSet {
private final List entries = new ArrayList<>();
+ private final Set touchedTables = new HashSet<>();
+
private final Set> queryCaches = new HashSet<>();
private final Map manyChangeMap = new HashMap<>();
- /**
- * Set of "base tables" modified used to invalidate entities based on views.
- */
- private final Set 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 touchedTables() {
+ return touchedTables;
}
/**
@@ -40,7 +44,7 @@ public class CacheChangeSet {
*
* Return the set of table changes to process invalidation for entities based on views.
*/
- public Set 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 void addBeanRemove(BeanDescriptor desc, Object id) {
+ touchedTables.add(desc.getBaseTable());
entries.add(new CacheChangeBeanRemove(desc, id));
- if (viewEntityInvalidation) {
- viewInvalidation.add(desc.getBaseTable());
- }
}
/**
* Update a bean entry.
*/
public void addBeanUpdate(BeanDescriptor desc, Object id, Map 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.
*/
diff --git a/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java b/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
index b61b48080..36a7ed571 100644
--- a/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
+++ b/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
@@ -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;
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java b/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
index 092fe7b9a..20e966f6e 100644
--- a/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
+++ b/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
@@ -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() {
diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
index b8bee5cc4..33e85f34b 100644
--- a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
+++ b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
@@ -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
*/
-public final class PostCommitProcessing {
+final class PostCommitProcessing {
private static final Logger logger = LoggerFactory.getLogger(PostCommitProcessing.class);
@@ -50,7 +51,7 @@ public final class PostCommitProcessing {
/**
* Create for an external modification.
*/
- public PostCommitProcessing(ClusterManager clusterManager, TransactionManager manager, TransactionEvent event) {
+ PostCommitProcessing(ClusterManager clusterManager, TransactionManager manager, TransactionEvent event) {
this.clusterManager = clusterManager;
this.manager = manager;
@@ -67,7 +68,7 @@ public final class PostCommitProcessing {
/**
* Create for a transaction.
*/
- public PostCommitProcessing(ClusterManager clusterManager, TransactionManager manager, SpiTransaction transaction) {
+ PostCommitProcessing(ClusterManager clusterManager, TransactionManager manager, SpiTransaction transaction) {
this.clusterManager = clusterManager;
this.manager = manager;
@@ -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 touched = cacheChanges.touchedTables();
+ if (touched != null && !touched.isEmpty()) {
+ manager.processTouchedTables(touched, cacheChanges.modificationTimestamp());
+ // TODO: Propagate touched tables to other cluster members
+ }
+ cacheChanges.apply();
}
}
@@ -205,6 +211,10 @@ public final class PostCommitProcessing {
RemoteTransactionEvent remoteTransactionEvent = new RemoteTransactionEvent(serverName);
+ Set touched = cacheChanges.touchedTables();
+ if (touched != null && !touched.isEmpty()) {
+ remoteTransactionEvent.addRemoteTableMod(new RemoteTableMod(cacheChanges.modificationTimestamp(), touched));
+ }
if (beanPersistIdMap != null) {
for (BeanPersistIds beanPersist : beanPersistIdMap.values()) {
remoteTransactionEvent.addBeanPersistIds(beanPersist);
diff --git a/src/main/java/io/ebeaninternal/server/transaction/RemoteTableMod.java b/src/main/java/io/ebeaninternal/server/transaction/RemoteTableMod.java
new file mode 100644
index 000000000..1c6eda418
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/transaction/RemoteTableMod.java
@@ -0,0 +1,55 @@
+package io.ebeaninternal.server.transaction;
+
+import io.ebeaninternal.server.cluster.binarymessage.BinaryMessage;
+import io.ebeaninternal.server.cluster.binarymessage.BinaryMessageList;
+
+import java.io.DataInput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public class RemoteTableMod {
+
+ private final long timestamp;
+
+ private final Set tables;
+
+ public RemoteTableMod(long timestamp, Set tables) {
+ this.timestamp = timestamp;
+ this.tables = tables;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public Set getTables() {
+ return tables;
+ }
+
+ public static RemoteTableMod readBinaryMessage(DataInput dataInput) throws IOException {
+
+ long timestamp = dataInput.readLong();
+ int count = dataInput.readInt();
+
+ Set tables = new LinkedHashSet<>();
+ for (int i = 0; i < count; i++) {
+ tables.add(dataInput.readUTF());
+ }
+ return new RemoteTableMod(timestamp, tables);
+ }
+
+ public void writeBinary(BinaryMessageList msgList) throws IOException {
+ BinaryMessage msg = new BinaryMessage(tables.size() * 20 + 30);
+ DataOutputStream os = msg.getOs();
+ os.writeInt(BinaryMessage.TYPE_TABLEMOD);
+ os.writeLong(timestamp);
+ os.writeInt(tables.size());
+ for (String table : tables) {
+ os.writeUTF(table);
+ }
+ os.close();
+ msgList.add(msg);
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/RemoteTransactionEvent.java b/src/main/java/io/ebeaninternal/server/transaction/RemoteTransactionEvent.java
index f6047d209..54112ca66 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/RemoteTransactionEvent.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/RemoteTransactionEvent.java
@@ -3,7 +3,7 @@ package io.ebeaninternal.server.transaction;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.TransactionEventTable.TableIUD;
import io.ebeaninternal.server.cache.RemoteCacheEvent;
-import io.ebeaninternal.server.cluster.BinaryMessageList;
+import io.ebeaninternal.server.cluster.binarymessage.BinaryMessageList;
import java.io.IOException;
import java.util.ArrayList;
@@ -19,6 +19,8 @@ public class RemoteTransactionEvent implements Runnable {
private RemoteCacheEvent remoteCacheEvent;
+ private RemoteTableMod remoteTableMod;
+
private String serverName;
private transient SpiEbeanServer server;
@@ -53,6 +55,10 @@ public class RemoteTransactionEvent implements Runnable {
public void writeBinaryMessage(BinaryMessageList msgList) throws IOException {
+ if (remoteTableMod != null) {
+ remoteTableMod.writeBinary(msgList);
+ }
+
if (tableList != null) {
for (TableIUD aTableList : tableList) {
aTableList.writeBinaryMessage(msgList);
@@ -113,6 +119,10 @@ public class RemoteTransactionEvent implements Runnable {
tableList.add(tableIud);
}
+ public void addRemoteTableMod(RemoteTableMod remoteTableMod) {
+ this.remoteTableMod = remoteTableMod;
+ }
+
public String getServerName() {
return serverName;
}
@@ -141,4 +151,8 @@ public class RemoteTransactionEvent implements Runnable {
return remoteCacheEvent;
}
+ public RemoteTableMod getRemoteTableMod() {
+ return remoteTableMod;
+ }
+
}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TableModState.java b/src/main/java/io/ebeaninternal/server/transaction/TableModState.java
new file mode 100644
index 000000000..b876a4ed0
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/transaction/TableModState.java
@@ -0,0 +1,70 @@
+package io.ebeaninternal.server.transaction;
+
+import io.ebean.cache.QueryCacheEntry;
+import io.ebean.cache.QueryCacheEntryValidate;
+import io.ebean.cache.ServerCacheNotification;
+import io.ebean.cache.ServerCacheNotify;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Holds timestamp of last modification per table.
+ *
+ * This information is used to validate entries in the L2 query caches.
+ *
+ */
+public class TableModState implements QueryCacheEntryValidate, ServerCacheNotify {
+
+ private static final Logger log = LoggerFactory.getLogger("io.ebean.cache.TABLEMOD");
+
+ private Map tableModStamp = new ConcurrentHashMap<>();
+
+ /**
+ * Set the modified timestamp on the tables that have been touched.
+ */
+ void touch(Set touchedTables, long modTimestamp) {
+ for (String tableName : touchedTables) {
+ tableModStamp.put(tableName, modTimestamp);
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("TableModState updated - " + tableModStamp);
+ }
+ }
+
+ /**
+ * Return true if all the tables are valid based on timestamp comparison.
+ */
+ boolean isValid(Set 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 dependentTables = entry.getDependentTables();
+ if (dependentTables != null && !dependentTables.isEmpty()) {
+ return isValid(dependentTables, entry.getTimestamp());
+ }
+ return true;
+ }
+
+ /**
+ * Update the table modification timestamps based on remote table modification events.
+ */
+ @Override
+ public void notify(ServerCacheNotification notification) {
+
+ // TODO: Change to use ClockService ...
+ long modifyTimestamp = notification.getModifyTimestamp();
+ touch(notification.getDependentTables(), modifyTimestamp);
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
index 0e9c91d62..4a007a9b3 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
@@ -5,6 +5,8 @@ import io.ebean.ProfileLocation;
import io.ebean.TxScope;
import io.ebean.annotation.PersistBatch;
import io.ebean.annotation.TxType;
+import io.ebean.cache.ServerCacheNotification;
+import io.ebean.cache.ServerCacheNotify;
import io.ebean.config.CurrentTenantProvider;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
@@ -27,6 +29,7 @@ import io.ebeaninternal.metric.MetricFactory;
import io.ebeaninternal.metric.TimedMetric;
import io.ebeaninternal.metric.TimedMetricMap;
import io.ebeaninternal.server.cluster.ClusterManager;
+import io.ebeaninternal.server.core.ClockService;
import io.ebeaninternal.server.deploy.BeanDescriptorManager;
import io.ebeaninternal.server.profile.TimedProfileLocation;
import io.ebeaninternal.server.profile.TimedProfileLocationRegistry;
@@ -46,7 +49,7 @@ import java.util.Set;
/**
* Manages transactions.
*
- * Keeps the Cache and Cluster in synch when transactions are committed.
+ * Keeps the Cache and Cluster in sync when transactions are committed.
*
*/
public class TransactionManager implements SpiTransactionManager {
@@ -136,6 +139,10 @@ public class TransactionManager implements SpiTransactionManager {
private final TimedMetricMap txnNamed;
private final TransactionScopeManager scopeManager;
+ private final TableModState tableModState;
+ private final ServerCacheNotify cacheNotify;
+ private final ClockService clockService;
+
/**
* Create the TransactionManager
*/
@@ -157,6 +164,9 @@ public class TransactionManager implements SpiTransactionManager {
this.clusterManager = options.clusterManager;
this.serverName = options.config.getName();
this.scopeManager = options.scopeManager;
+ this.tableModState = options.tableModState;
+ this.cacheNotify = options.cacheNotify;
+ this.clockService = options.clockService;
this.backgroundExecutor = options.backgroundExecutor;
this.dataSourceSupplier = options.dataSourceSupplier;
this.docStoreActive = options.config.getDocStoreConfig().isActive();
@@ -178,6 +188,13 @@ public class TransactionManager implements SpiTransactionManager {
scopeManager.register(this);
}
+ /**
+ * Return the NOW timestamp in epoch millis.
+ */
+ public long clockNowMillis() {
+ return clockService.nowMillis();
+ }
+
/**
* Create a new scoped transaction.
*/
@@ -434,7 +451,7 @@ public class TransactionManager implements SpiTransactionManager {
private void externalModificationEvent(TransactionEventTable tableEvents) {
- TransactionEvent event = new TransactionEvent();
+ TransactionEvent event = new TransactionEvent(clockNowMillis());
event.add(tableEvents);
PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, event);
@@ -495,10 +512,12 @@ public class TransactionManager implements SpiTransactionManager {
/**
* Invalidate the query caches for entities based on views.
*/
- public void processViewInvalidation(Set viewInvalidation) {
- if (!viewInvalidation.isEmpty()) {
- beanDescriptorManager.processViewInvalidation(viewInvalidation);
+ public void processTouchedTables(Set touchedTables, long modTimestamp) {
+ tableModState.touch(touchedTables, modTimestamp);
+ if (viewInvalidation) {
+ beanDescriptorManager.processViewInvalidation(touchedTables);
}
+ cacheNotify.notify(new ServerCacheNotification(modTimestamp, touchedTables));
}
/**
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
index fe8b8d217..f55389a19 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
@@ -1,10 +1,12 @@
package io.ebeaninternal.server.transaction;
import io.ebean.BackgroundExecutor;
+import io.ebean.cache.ServerCacheNotify;
import io.ebean.config.ServerConfig;
import io.ebeaninternal.api.SpiLogManager;
import io.ebeaninternal.api.SpiProfileHandler;
import io.ebeaninternal.server.cluster.ClusterManager;
+import io.ebeaninternal.server.core.ClockService;
import io.ebeaninternal.server.deploy.BeanDescriptorManager;
import io.ebeanservice.docstore.api.DocStoreUpdateProcessor;
@@ -24,10 +26,15 @@ public class TransactionManagerOptions {
final SpiProfileHandler profileHandler;
final TransactionScopeManager scopeManager;
final SpiLogManager logManager;
+ final TableModState tableModState;
+ final ServerCacheNotify cacheNotify;
+ final ClockService clockService;
+
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, ServerCacheNotify cacheNotify, ClockService clockService) {
this.notifyL2CacheInForeground = notifyL2CacheInForeground;
this.config = config;
@@ -39,6 +46,9 @@ public class TransactionManagerOptions {
this.dataSourceSupplier = dataSourceSupplier;
this.profileHandler = profileHandler;
this.logManager = logManager;
+ this.tableModState = tableModState;
+ this.cacheNotify = cacheNotify;
+ this.clockService = clockService;
}
}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java
index 3094c9ad5..046ae15d6 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java
@@ -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);
}
-
+
}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java
index beefe1f47..6cbc82c8f 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java
@@ -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
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheConfigTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheConfigTest.java
new file mode 100644
index 000000000..bb03cce18
--- /dev/null
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheConfigTest.java
@@ -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);
+ }
+}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java
index 00ff68631..ead5158ff 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java
@@ -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);
- }
}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java
index 4ba1d82a4..a2d630c98 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java
@@ -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;
diff --git a/src/test/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWriteTest.java b/src/test/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWriteTest.java
new file mode 100644
index 000000000..cf5eeee87
--- /dev/null
+++ b/src/test/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWriteTest.java
@@ -0,0 +1,118 @@
+package io.ebeaninternal.server.cluster.binarymessage;
+
+import io.ebean.BaseTestCase;
+import io.ebean.EbeanServer;
+import io.ebeaninternal.api.TDSpiEbeanServer;
+import io.ebeaninternal.api.TransactionEventTable;
+import io.ebeaninternal.server.cache.RemoteCacheEvent;
+import io.ebeaninternal.server.cluster.MessageServerProvider;
+import io.ebeaninternal.server.core.PersistRequest;
+import io.ebeaninternal.server.deploy.BeanDescriptor;
+import io.ebeaninternal.server.transaction.BeanPersistIds;
+import io.ebeaninternal.server.transaction.RemoteTableMod;
+import io.ebeaninternal.server.transaction.RemoteTransactionEvent;
+import org.junit.Test;
+import org.tests.model.basic.Customer;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class MessageReadWriteTest extends BaseTestCase {
+
+ private BeanDescriptor customerBeanDescriptor = getBeanDescriptor(Customer.class);
+
+ private TDEbeanServer mockEbeanServer = new TDEbeanServer();
+
+ private MessageReadWrite readWrite = new MessageReadWrite(new TDMessageServerProvider());
+
+ @Override
+ protected BeanDescriptor getBeanDescriptor(Class cls) {
+ return super.getBeanDescriptor(cls);
+ }
+
+ @Test
+ public void readWrite() throws IOException {
+
+ RemoteTransactionEvent event = new RemoteTransactionEvent("db");
+
+ long timestamp = System.currentTimeMillis();
+ Set tables = new HashSet<>();
+ Collections.addAll(tables, "one", "two", "three");
+
+ event.addRemoteTableMod(new RemoteTableMod(timestamp, tables));
+ event.addRemoteCacheEvent(new RemoteCacheEvent(Customer.class));
+
+ event.addTableIUD(new TransactionEventTable.TableIUD("foo", true, false, true));
+ event.addTableIUD(new TransactionEventTable.TableIUD("bar", false, true, false));
+
+
+ BeanPersistIds beanPersistIds = new BeanPersistIds(customerBeanDescriptor);
+ beanPersistIds.addId(PersistRequest.Type.INSERT, 42);
+ beanPersistIds.addId(PersistRequest.Type.INSERT, 43);
+ beanPersistIds.addId(PersistRequest.Type.UPDATE, 55);
+ beanPersistIds.addId(PersistRequest.Type.DELETE, 66);
+
+ event.addBeanPersistIds(beanPersistIds);
+
+ byte[] binaryMessage = readWrite.write(event);
+
+ RemoteTransactionEvent read = readWrite.read(binaryMessage);
+
+ // cache event
+ RemoteCacheEvent remoteCacheEvent = read.getRemoteCacheEvent();
+ assertThat(remoteCacheEvent.getClearCaches()).containsOnly(Customer.class.getName());
+
+ // table mod
+ RemoteTableMod remoteTableMod = read.getRemoteTableMod();
+ assertThat(remoteTableMod.getTimestamp()).isEqualTo(timestamp);
+ assertThat(remoteTableMod.getTables()).isEqualTo(tables);
+
+ // Bean persist ids
+ List beanPersistList = read.getBeanPersistList();
+ assertThat(beanPersistList).hasSize(3);
+ assertThat(beanPersistList.get(0).getInsertIds()).containsOnly(42, 43);
+ assertThat(beanPersistList.get(1).getUpdateIds()).containsOnly(55);
+ assertThat(beanPersistList.get(2).getDeleteIds()).containsOnly(66);
+ }
+
+ @Test
+ public void readWrite_RemoteTableMod() throws IOException {
+
+ RemoteTransactionEvent event = new RemoteTransactionEvent("db");
+
+ long timestamp = System.currentTimeMillis();
+ Set tables = new HashSet<>();
+ Collections.addAll(tables, "one", "two", "three");
+
+ event.addRemoteTableMod(new RemoteTableMod(timestamp, tables));
+
+ byte[] binaryMessage = readWrite.write(event);
+
+ RemoteTransactionEvent read = readWrite.read(binaryMessage);
+
+ RemoteTableMod remoteTableMod = read.getRemoteTableMod();
+
+ assertThat(remoteTableMod.getTimestamp()).isEqualTo(timestamp);
+ assertThat(remoteTableMod.getTables()).isEqualTo(tables);
+ }
+
+ class TDMessageServerProvider implements MessageServerProvider {
+
+ @Override
+ public EbeanServer getServer(String name) {
+ return mockEbeanServer;
+ }
+ }
+
+ class TDEbeanServer extends TDSpiEbeanServer {
+ @Override
+ public BeanDescriptor> getBeanDescriptorById(String descriptorId) {
+ return customerBeanDescriptor;
+ }
+ }
+}
diff --git a/src/test/java/io/ebeaninternal/server/transaction/TableModStateTest.java b/src/test/java/io/ebeaninternal/server/transaction/TableModStateTest.java
new file mode 100644
index 000000000..5abcb1820
--- /dev/null
+++ b/src/test/java/io/ebeaninternal/server/transaction/TableModStateTest.java
@@ -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 setOf(String... tables) {
+ Set touched = new HashSet<>();
+ Collections.addAll(touched, tables);
+ return touched;
+ }
+}
diff --git a/src/test/java/org/tests/cache/TestQueryCacheTableDependency.java b/src/test/java/org/tests/cache/TestQueryCacheTableDependency.java
new file mode 100644
index 000000000..f578fdfdf
--- /dev/null
+++ b/src/test/java/org/tests/cache/TestQueryCacheTableDependency.java
@@ -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 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);
+
+ }
+}
diff --git a/src/test/java/org/tests/model/basic/Address.java b/src/test/java/org/tests/model/basic/Address.java
index 83d1b5c18..a11d38a0d 100644
--- a/src/test/java/org/tests/model/basic/Address.java
+++ b/src/test/java/org/tests/model/basic/Address.java
@@ -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 {