+ * 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..d59bfd3c4 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,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);
}
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/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);
@@ -51,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;
@@ -68,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;
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TableModState.java b/src/main/java/io/ebeaninternal/server/transaction/TableModState.java
index d2aa4143a..b876a4ed0 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TableModState.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TableModState.java
@@ -2,6 +2,10 @@ 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;
@@ -13,24 +17,28 @@ import java.util.concurrent.ConcurrentHashMap;
* This information is used to validate entries in the L2 query caches.
*
*/
-public class TableModState implements QueryCacheEntryValidate {
+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.
*/
- public void touch(Set touchedTables, long modTimestamp) {
-
+ 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.
*/
- public boolean isValid(Set tables, long sinceTimestamp) {
+ boolean isValid(Set tables, long sinceTimestamp) {
for (String tableName : tables) {
Long modTime = tableModStamp.get(tableName);
if (modTime != null && modTime > sinceTimestamp ) {
@@ -48,4 +56,15 @@ public class TableModState implements QueryCacheEntryValidate {
}
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 0ff2894d6..b462b970b 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;
@@ -137,6 +139,7 @@ public class TransactionManager implements SpiTransactionManager {
private final TransactionScopeManager scopeManager;
private final TableModState tableModState;
+ private final ServerCacheNotify cacheNotify;
/**
* Create the TransactionManager
@@ -160,6 +163,7 @@ public class TransactionManager implements SpiTransactionManager {
this.serverName = options.config.getName();
this.scopeManager = options.scopeManager;
this.tableModState = options.tableModState;
+ this.cacheNotify = options.cacheNotify;
this.backgroundExecutor = options.backgroundExecutor;
this.dataSourceSupplier = options.dataSourceSupplier;
this.docStoreActive = options.config.getDocStoreConfig().isActive();
@@ -510,6 +514,7 @@ public class TransactionManager implements SpiTransactionManager {
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 16f5dafe9..1b8ce39a8 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
@@ -1,6 +1,7 @@
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;
@@ -25,12 +26,13 @@ public class TransactionManagerOptions {
final TransactionScopeManager scopeManager;
final SpiLogManager logManager;
final TableModState tableModState;
+ final ServerCacheNotify cacheNotify;
public TransactionManagerOptions(boolean notifyL2CacheInForeground, ServerConfig config, TransactionScopeManager scopeManager, ClusterManager clusterManager,
BackgroundExecutor backgroundExecutor, DocStoreUpdateProcessor docStoreUpdateProcessor,
BeanDescriptorManager descMgr, DataSourceSupplier dataSourceSupplier, SpiProfileHandler profileHandler,
- SpiLogManager logManager, TableModState tableModState) {
+ SpiLogManager logManager, TableModState tableModState, ServerCacheNotify cacheNotify) {
this.notifyL2CacheInForeground = notifyL2CacheInForeground;
this.config = config;
@@ -43,6 +45,7 @@ public class TransactionManagerOptions {
this.profileHandler = profileHandler;
this.logManager = logManager;
this.tableModState = tableModState;
+ this.cacheNotify = cacheNotify;
}
}
From bd3c99e67f79ef588fa0b8e58f7790aa97143046 Mon Sep 17 00:00:00 2001
From: rob bygrave
Date: Sat, 16 Jun 2018 00:22:44 +1200
Subject: [PATCH 3/4] #1427 - QueryCache should be cleared, if one of a
dependent bean is updated
Wrap Clock as ClockService to use in TransactionManager and DefaultServer (and allow for changing the Clock implementation for testing).
---
.../server/core/ClockService.java | 29 +++++++++++++++++++
.../server/core/DefaultServer.java | 8 ++---
.../server/core/InternalConfiguration.java | 9 +++++-
.../ImplicitReadOnlyTransaction.java | 2 +-
.../server/transaction/JdbcTransaction.java | 2 +-
.../transaction/TransactionManager.java | 9 ++++--
.../TransactionManagerOptions.java | 5 +++-
7 files changed, 53 insertions(+), 11 deletions(-)
create mode 100644 src/main/java/io/ebeaninternal/server/core/ClockService.java
diff --git a/src/main/java/io/ebeaninternal/server/core/ClockService.java b/src/main/java/io/ebeaninternal/server/core/ClockService.java
new file mode 100644
index 000000000..227a7b1cf
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/core/ClockService.java
@@ -0,0 +1,29 @@
+package io.ebeaninternal.server.core;
+
+import java.time.Clock;
+
+/**
+ * Wraps the Clock such that we can change the Clock for testing purposes.
+ */
+public class ClockService {
+
+ private Clock clock;
+
+ public ClockService(Clock clock) {
+ this.clock = clock;
+ }
+
+ /**
+ * Change the clock for testing purposes.
+ */
+ public void setClock(Clock clock) {
+ this.clock = clock;
+ }
+
+ /**
+ * Return the Clock current timestamp in millis.
+ */
+ public long nowMillis() {
+ return clock.millis();
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
index 89fe28b6b..a540ecb1b 100644
--- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
+++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
@@ -154,7 +154,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
/**
* Clock to use for WhenModified and WhenCreated.
*/
- private Clock clock;
+ private ClockService clockService;
private final CallStackFactory callStackFactory;
@@ -283,7 +283,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
this.beanLoader = new DefaultBeanLoader(this);
this.jsonContext = config.createJsonContext(this);
this.dataTimeZone = config.getDataTimeZone();
- this.clock = config.getServerConfig().getClock();
+ this.clockService = config.getClockService();
DocStoreIntegration docStoreComponents = config.createDocStoreIntegration(this);
this.transactionManager = config.createTransactionManager(docStoreComponents.updateProcessor());
@@ -515,12 +515,12 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
@Override
public long clockNow() {
- return clock.millis();
+ return clockService.nowMillis();
}
@Override
public void setClock(Clock clock) {
- this.clock = clock;
+ this.clockService.setClock(clock);
}
@Override
diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
index 4beb65c58..f3b537217 100644
--- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
+++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
@@ -122,6 +122,8 @@ public class InternalConfiguration {
private final DtoBeanManager dtoBeanManager;
+ private final ClockService clockService;
+
private final DataTimeZone dataTimeZone;
private final Binder binder;
@@ -166,6 +168,7 @@ public class InternalConfiguration {
this.online = online;
this.serverConfig = serverConfig;
+ this.clockService = new ClockService(serverConfig.getClock());
this.logManager = initLogManager();
this.docStoreFactory = initDocStoreFactory(serverConfig.service(DocStoreFactory.class));
this.jsonFactory = serverConfig.getJsonFactory();
@@ -233,6 +236,10 @@ public class InternalConfiguration {
return docStoreFactory;
}
+ public ClockService getClockService() {
+ return clockService;
+ }
+
/**
* Check if this is a SpiServerPlugin and if so 'collect' it to give the complete list
* later on the DefaultServer for late call to configure().
@@ -423,7 +430,7 @@ public class InternalConfiguration {
TransactionManagerOptions options =
new TransactionManagerOptions(notifyL2CacheInForeground, serverConfig, scopeManager, clusterManager, backgroundExecutor,
indexUpdateProcessor, beanDescriptorManager, dataSource(), profileHandler(), logManager,
- tableModState, cacheNotify);
+ tableModState, cacheNotify, clockService);
if (serverConfig.isExplicitTransactionBeginMode()) {
return new ExplicitTransactionManager(options);
diff --git a/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
index 1ccc44dbd..44e5b4bd1 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
@@ -80,7 +80,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
this.connection = connection;
this.persistenceContext = new DefaultPersistenceContext();
this.startNanos = System.nanoTime();
- this.startMillis = manager.clockNowEpoch();
+ this.startMillis = manager.clockNowMillis();
}
/**
diff --git a/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
index da8e53781..2fbaab79a 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
@@ -212,7 +212,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
this.batchOnCascadeMode = PersistBatch.NONE;
this.onQueryOnly = OnQueryOnly.ROLLBACK;
} else {
- this.startMillis = manager.clockNowEpoch();
+ this.startMillis = manager.clockNowMillis();
this.logSql = manager.isLogSql();
this.logSummary = manager.isLogSummary();
this.skipCacheAfterWrite = manager.isSkipCacheAfterWrite();
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
index b462b970b..4a007a9b3 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
@@ -29,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;
@@ -140,6 +141,7 @@ public class TransactionManager implements SpiTransactionManager {
private final TableModState tableModState;
private final ServerCacheNotify cacheNotify;
+ private final ClockService clockService;
/**
* Create the TransactionManager
@@ -164,6 +166,7 @@ public class TransactionManager implements SpiTransactionManager {
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();
@@ -188,8 +191,8 @@ public class TransactionManager implements SpiTransactionManager {
/**
* Return the NOW timestamp in epoch millis.
*/
- public long clockNowEpoch() {
- return System.currentTimeMillis(); // TODO: Review when we supply a Clock via ServerConfig.
+ public long clockNowMillis() {
+ return clockService.nowMillis();
}
/**
@@ -448,7 +451,7 @@ public class TransactionManager implements SpiTransactionManager {
private void externalModificationEvent(TransactionEventTable tableEvents) {
- TransactionEvent event = new TransactionEvent(clockNowEpoch());
+ TransactionEvent event = new TransactionEvent(clockNowMillis());
event.add(tableEvents);
PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, event);
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
index 1b8ce39a8..f55389a19 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
@@ -6,6 +6,7 @@ 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;
@@ -27,12 +28,13 @@ public class TransactionManagerOptions {
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, TableModState tableModState, ServerCacheNotify cacheNotify) {
+ SpiLogManager logManager, TableModState tableModState, ServerCacheNotify cacheNotify, ClockService clockService) {
this.notifyL2CacheInForeground = notifyL2CacheInForeground;
this.config = config;
@@ -46,6 +48,7 @@ public class TransactionManagerOptions {
this.logManager = logManager;
this.tableModState = tableModState;
this.cacheNotify = cacheNotify;
+ this.clockService = clockService;
}
}
From 9c73309dfd0bfe935202183e228a9b0282262fb9 Mon Sep 17 00:00:00 2001
From: rob bygrave
Date: Sat, 16 Jun 2018 15:15:54 +1200
Subject: [PATCH 4/4] #1427 - QueryCache should be cleared, if one of a
dependent bean is updated
Update RemoteTransactionEvent, add (back) common binary message handling (Common to TCP, K8s and upcoming binary pubsub)
---
.../api/TransactionEventTable.java | 6 +-
.../server/cache/RemoteCacheEvent.java | 4 +-
.../server/cluster/ClusterManager.java | 2 +-
.../server/cluster/MessageServerProvider.java | 14 ++
.../binarymessage/BinaryDataReader.java | 75 +++++++++
.../binarymessage/BinaryDataWriter.java | 49 ++++++
.../{ => binarymessage}/BinaryMessage.java | 3 +-
.../BinaryMessageList.java | 2 +-
.../cluster/binarymessage/ClusterMessage.java | 147 ++++++++++++++++++
.../InvalidMessageException.java | 8 +
.../binarymessage/MessageReadWrite.java | 37 +++++
.../server/cluster/binarymessage/MsgKeys.java | 19 +++
.../server/transaction/BeanPersistIds.java | 18 ++-
.../transaction/PostCommitProcessing.java | 4 +
.../server/transaction/RemoteTableMod.java | 55 +++++++
.../transaction/RemoteTransactionEvent.java | 16 +-
.../binarymessage/MessageReadWriteTest.java | 118 ++++++++++++++
17 files changed, 563 insertions(+), 14 deletions(-)
create mode 100644 src/main/java/io/ebeaninternal/server/cluster/MessageServerProvider.java
create mode 100644 src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataReader.java
create mode 100644 src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataWriter.java
rename src/main/java/io/ebeaninternal/server/cluster/{ => binarymessage}/BinaryMessage.java (94%)
rename src/main/java/io/ebeaninternal/server/cluster/{ => binarymessage}/BinaryMessageList.java (85%)
create mode 100644 src/main/java/io/ebeaninternal/server/cluster/binarymessage/ClusterMessage.java
create mode 100644 src/main/java/io/ebeaninternal/server/cluster/binarymessage/InvalidMessageException.java
create mode 100644 src/main/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWrite.java
create mode 100644 src/main/java/io/ebeaninternal/server/cluster/binarymessage/MsgKeys.java
create mode 100644 src/main/java/io/ebeaninternal/server/transaction/RemoteTableMod.java
create mode 100644 src/test/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWriteTest.java
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/RemoteCacheEvent.java b/src/main/java/io/ebeaninternal/server/cache/RemoteCacheEvent.java
index e0def309f..2d04df0f5 100644
--- a/src/main/java/io/ebeaninternal/server/cache/RemoteCacheEvent.java
+++ b/src/main/java/io/ebeaninternal/server/cache/RemoteCacheEvent.java
@@ -1,7 +1,7 @@
package io.ebeaninternal.server.cache;
-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;
diff --git a/src/main/java/io/ebeaninternal/server/cluster/ClusterManager.java b/src/main/java/io/ebeaninternal/server/cluster/ClusterManager.java
index 6fd56c713..6f02f5b50 100644
--- a/src/main/java/io/ebeaninternal/server/cluster/ClusterManager.java
+++ b/src/main/java/io/ebeaninternal/server/cluster/ClusterManager.java
@@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentHashMap;
/**
* Manages the cluster service.
*/
-public class ClusterManager {
+public class ClusterManager implements MessageServerProvider {
private static final Logger clusterLogger = LoggerFactory.getLogger("io.ebean.Cluster");
diff --git a/src/main/java/io/ebeaninternal/server/cluster/MessageServerProvider.java b/src/main/java/io/ebeaninternal/server/cluster/MessageServerProvider.java
new file mode 100644
index 000000000..f3c388d2b
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cluster/MessageServerProvider.java
@@ -0,0 +1,14 @@
+package io.ebeaninternal.server.cluster;
+
+import io.ebean.EbeanServer;
+
+/**
+ * Returns EbeanServer instances for remote message reading.
+ */
+public interface MessageServerProvider {
+
+ /**
+ * Return the EbeanServer instance by name.
+ */
+ EbeanServer getServer(String name);
+}
diff --git a/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataReader.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataReader.java
new file mode 100644
index 000000000..f097172f4
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataReader.java
@@ -0,0 +1,75 @@
+package io.ebeaninternal.server.cluster.binarymessage;
+
+import io.ebeaninternal.api.SpiEbeanServer;
+import io.ebeaninternal.api.TransactionEventTable;
+import io.ebeaninternal.server.cache.RemoteCacheEvent;
+import io.ebeaninternal.server.cluster.MessageServerProvider;
+import io.ebeaninternal.server.transaction.BeanPersistIds;
+import io.ebeaninternal.server.transaction.RemoteTableMod;
+import io.ebeaninternal.server.transaction.RemoteTransactionEvent;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+
+/**
+ * Reads the binary message returning RemoteTransactionEvent.
+ */
+class BinaryDataReader {
+
+ private final MessageServerProvider serverProvider;
+ private final DataInputStream dataInput;
+
+ private SpiEbeanServer server;
+ private RemoteTransactionEvent event;
+
+ BinaryDataReader(MessageServerProvider serverProvider, byte[] data) {
+ this.serverProvider = serverProvider;
+ this.dataInput = new DataInputStream(new ByteArrayInputStream(data));
+ }
+
+ /**
+ * Read the binary message returning a RemoteTransactionEvent.
+ */
+ RemoteTransactionEvent read() throws IOException {
+
+ String serverName = dataInput.readUTF();
+
+ this.server = (SpiEbeanServer) serverProvider.getServer(serverName);
+ if (server == null) {
+ throw new IllegalStateException("EbeanServer not found for name [" + serverName + "]");
+ }
+ this.event = new RemoteTransactionEvent(server);
+ boolean more = dataInput.readBoolean();
+ while (more) {
+ readMessage();
+ more = dataInput.readBoolean();
+ }
+ return event;
+ }
+
+ private void readMessage() throws IOException {
+
+ int msgType = dataInput.readInt();
+ switch (msgType) {
+ case BinaryMessage.TYPE_BEANIUD:
+ event.addBeanPersistIds(BeanPersistIds.readBinaryMessage(server, dataInput));
+ break;
+
+ case BinaryMessage.TYPE_TABLEIUD:
+ event.addTableIUD(TransactionEventTable.TableIUD.readBinaryMessage(dataInput));
+ break;
+
+ case BinaryMessage.TYPE_CACHE:
+ event.addRemoteCacheEvent(RemoteCacheEvent.readBinaryMessage(dataInput));
+ break;
+
+ case BinaryMessage.TYPE_TABLEMOD:
+ event.addRemoteTableMod(RemoteTableMod.readBinaryMessage(dataInput));
+ break;
+
+ default:
+ throw new RuntimeException("Invalid Transaction msgType " + msgType);
+ }
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataWriter.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataWriter.java
new file mode 100644
index 000000000..8f08bba56
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryDataWriter.java
@@ -0,0 +1,49 @@
+package io.ebeaninternal.server.cluster.binarymessage;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Writes BinaryMessageList to DataHolder raw byte[].
+ */
+class BinaryDataWriter {
+
+ private final BinaryMessageList messageList;
+
+ private final ByteArrayOutputStream buffer;
+
+ private final DataOutputStream dataOut;
+
+ private final String serverName;
+
+ BinaryDataWriter(String serverName, BinaryMessageList messageList) {
+ this.serverName = serverName;
+ this.messageList = messageList;
+ this.buffer = new ByteArrayOutputStream(256);
+ this.dataOut = new DataOutputStream(buffer);
+ }
+
+ /**
+ * Write the message as raw byte[].
+ */
+ byte[] write() throws IOException {
+
+ //TODO: Add Protocol version and "podId" to support pubsub cluster
+ //dataOut.writeInt(MsgKeys.MESSAGE_KEY);
+ dataOut.writeUTF(serverName);
+ for (BinaryMessage msg : messageList.getList()) {
+ write(msg);
+ }
+ dataOut.writeBoolean(false);
+ dataOut.flush();
+ return buffer.toByteArray();
+ }
+
+ private void write(BinaryMessage msg) throws IOException {
+
+ dataOut.writeBoolean(true);
+ dataOut.write(msg.getByteArray());
+ }
+
+}
diff --git a/src/main/java/io/ebeaninternal/server/cluster/BinaryMessage.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryMessage.java
similarity index 94%
rename from src/main/java/io/ebeaninternal/server/cluster/BinaryMessage.java
rename to src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryMessage.java
index b78b3f7eb..158df57da 100644
--- a/src/main/java/io/ebeaninternal/server/cluster/BinaryMessage.java
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryMessage.java
@@ -1,4 +1,4 @@
-package io.ebeaninternal.server.cluster;
+package io.ebeaninternal.server.cluster.binarymessage;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
@@ -24,6 +24,7 @@ public class BinaryMessage {
public static final int TYPE_BEANIUD = 1;
public static final int TYPE_TABLEIUD = 2;
public static final int TYPE_CACHE = 3;
+ public static final int TYPE_TABLEMOD = 4;
public static final int TYPE_MSGACK = 8;
public static final int TYPE_MSGRESEND = 9;
diff --git a/src/main/java/io/ebeaninternal/server/cluster/BinaryMessageList.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryMessageList.java
similarity index 85%
rename from src/main/java/io/ebeaninternal/server/cluster/BinaryMessageList.java
rename to src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryMessageList.java
index f84b7995c..fd057fd03 100644
--- a/src/main/java/io/ebeaninternal/server/cluster/BinaryMessageList.java
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/BinaryMessageList.java
@@ -1,4 +1,4 @@
-package io.ebeaninternal.server.cluster;
+package io.ebeaninternal.server.cluster.binarymessage;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/main/java/io/ebeaninternal/server/cluster/binarymessage/ClusterMessage.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/ClusterMessage.java
new file mode 100644
index 000000000..2e6b189ea
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/ClusterMessage.java
@@ -0,0 +1,147 @@
+package io.ebeaninternal.server.cluster.binarymessage;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * The message broadcast around the cluster.
+ */
+public class ClusterMessage {
+
+ private static final int MAX_LENGTH = 10 * 1024 * 1024;
+
+ private final String registerIp;
+
+ private final String podName;
+
+ private final boolean register;
+
+ private final byte[] data;
+
+ /**
+ * Create a register message.
+ */
+ public static ClusterMessage register(String registerIp, boolean register, String podName) {
+ return new ClusterMessage(registerIp, register, podName);
+ }
+
+ /**
+ * Create a transaction message.
+ */
+ public static ClusterMessage transEvent(byte[] data) {
+ return new ClusterMessage(data);
+ }
+
+ /**
+ * Create for register online/offline message.
+ */
+ private ClusterMessage(String registerIp, boolean register, String podName) {
+ this.registerIp = registerIp;
+ this.register = register;
+ this.podName = podName;
+ this.data = null;
+ }
+
+ /**
+ * Create for a transaction message.
+ */
+ private ClusterMessage(byte[] data) {
+ this.data = data;
+ this.registerIp = null;
+ this.podName = null;
+ this.register = false;
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (registerIp != null) {
+ sb.append("register ");
+ sb.append(register);
+ sb.append(" ");
+ sb.append(registerIp);
+ } else {
+ sb.append("[data]");
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Return true if this is a register event as opposed to a transaction message.
+ */
+ public boolean isRegisterEvent() {
+ return registerIp != null;
+ }
+
+ /**
+ * Return the register host for online/offline message.
+ */
+ public String getRegisterIp() {
+ return registerIp;
+ }
+
+ public String getPodName() {
+ return podName;
+ }
+
+ /**
+ * Return true if register is true for a online/offline message.
+ */
+ public boolean isRegister() {
+ return register;
+ }
+
+ /**
+ * Return the raw message data.
+ */
+ public byte[] getData() {
+ return data;
+ }
+
+ /**
+ * Write the message in binary form.
+ */
+ public void write(DataOutputStream dataOutput) throws IOException {
+
+ if (data != null) {
+ // write data message
+ dataOutput.writeInt(MsgKeys.DATA);
+ dataOutput.writeInt(data.length);
+ dataOutput.write(data);
+ } else {
+ // write header message
+ dataOutput.writeInt(MsgKeys.HEADER);
+ dataOutput.writeUTF(getRegisterIp());
+ dataOutput.writeBoolean(register);
+ dataOutput.writeUTF(getPodName());
+ }
+ dataOutput.flush();
+ }
+
+ /**
+ * Read the message from binary form.
+ */
+ public static ClusterMessage read(DataInputStream dataInput) throws IOException, InvalidMessageException {
+
+ int key = dataInput.readInt();
+ if (key == MsgKeys.DATA) {
+ int length = dataInput.readInt();
+ if (length > MAX_LENGTH) {
+ throw new IOException("Message data too large length:"+length);
+ }
+ byte[] data = new byte[length];
+ dataInput.readFully(data);
+ return new ClusterMessage(data);
+
+ } else if (key == MsgKeys.HEADER) {
+ String host = dataInput.readUTF();
+ boolean registered = dataInput.readBoolean();
+ String podName = dataInput.readUTF();
+ return new ClusterMessage(host, registered, podName);
+
+ } else {
+ throw new InvalidMessageException("Invalid message key:" + key);
+ }
+ }
+
+}
diff --git a/src/main/java/io/ebeaninternal/server/cluster/binarymessage/InvalidMessageException.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/InvalidMessageException.java
new file mode 100644
index 000000000..d05d0b3c2
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/InvalidMessageException.java
@@ -0,0 +1,8 @@
+package io.ebeaninternal.server.cluster.binarymessage;
+
+public class InvalidMessageException extends Exception {
+
+ public InvalidMessageException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWrite.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWrite.java
new file mode 100644
index 000000000..fe7cab1c2
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/MessageReadWrite.java
@@ -0,0 +1,37 @@
+package io.ebeaninternal.server.cluster.binarymessage;
+
+import io.ebeaninternal.server.cluster.MessageServerProvider;
+import io.ebeaninternal.server.transaction.RemoteTransactionEvent;
+
+import java.io.IOException;
+
+/**
+ * Mechanism to convert RemoteTransactionEvent to/from byte[] content.
+ */
+public class MessageReadWrite {
+
+ private final MessageServerProvider serverProvider;
+
+ public MessageReadWrite(MessageServerProvider serverProvider) {
+ this.serverProvider = serverProvider;
+ }
+
+ /**
+ * Convert the RemoteTransactionEvent to raw byte[] content.
+ */
+ public byte[] write(RemoteTransactionEvent transEvent) throws IOException {
+
+ BinaryMessageList messageList = new BinaryMessageList();
+ transEvent.writeBinaryMessage(messageList);
+
+ return new BinaryDataWriter(transEvent.getServerName(), messageList).write();
+ }
+
+ /**
+ * Convert the byte[] content to RemoteTransactionEvent.
+ */
+ public RemoteTransactionEvent read(byte[] data) throws IOException {
+
+ return new BinaryDataReader(serverProvider, data).read();
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/cluster/binarymessage/MsgKeys.java b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/MsgKeys.java
new file mode 100644
index 000000000..9daeaa9bd
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cluster/binarymessage/MsgKeys.java
@@ -0,0 +1,19 @@
+package io.ebeaninternal.server.cluster.binarymessage;
+
+public interface MsgKeys {
+
+ /**
+ * Used to identify client on connection initiation.
+ */
+ int HELLO = 182;
+
+ /**
+ * Used to confirm protocol on reading messages.
+ */
+ int HEADER = 11;
+
+ /**
+ * Used to confirm protocol on reading messages.
+ */
+ int DATA = 12;
+}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java b/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java
index cf77829ee..934877759 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/BeanPersistIds.java
@@ -1,8 +1,8 @@
package io.ebeaninternal.server.transaction;
import io.ebeaninternal.api.SpiEbeanServer;
-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 io.ebeaninternal.server.core.PersistRequest;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.id.IdBinder;
@@ -140,7 +140,7 @@ public class BeanPersistIds {
idBinder.writeData(os, idList.get(i));
}
- os.flush();
+ os.close();
msgList.add(m);
} while (i < eof);
@@ -167,7 +167,7 @@ public class BeanPersistIds {
return sb.toString();
}
- void addId(PersistRequest.Type type, Serializable id) {
+ public void addId(PersistRequest.Type type, Serializable id) {
switch (type) {
case INSERT:
addInsertId(id);
@@ -210,10 +210,18 @@ public class BeanPersistIds {
return beanDescriptor;
}
- List getDeleteIds() {
+ public List getDeleteIds() {
return deleteIds;
}
+ public List getInsertIds() {
+ return insertIds;
+ }
+
+ public List getUpdateIds() {
+ return updateIds;
+ }
+
/**
* Notify the cache of this event that came from another server in the cluster.
*/
diff --git a/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java b/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java
index 08a85fd7d..c84593646 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java
@@ -211,6 +211,10 @@ 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/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;
+ }
+ }
+}