diff --git a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java index c1b2d9f25..bf9dce216 100644 --- a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java +++ b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java @@ -887,6 +887,13 @@ public interface DatabaseBuilder { @Deprecated DatabaseBuilder setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExecutorWrapper); + /** + * Enable tenant-partitioned caches. When enabled each tenant gets its own cache namespace, + * improving cache-hit ratio by preventing cross-tenant key collisions. + * Use {@link SpiCacheManager#clearTenant(Object)} when a tenant is deactivated. + */ + DatabaseBuilder tenantPartitionedCache(boolean tenantPartitionedCache); + /** * Set the L2 cache default max size. */ @@ -2567,6 +2574,11 @@ public interface DatabaseBuilder { */ boolean isAutoPersistUpdates(); + /** + * Return true if caches are partitioned by tenant. + */ + boolean isTenantPartitionedCache(); + /** * Return the L2 cache default max size. */ diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index 33dbf399f..839f80f96 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -432,6 +432,8 @@ public class DatabaseConfig implements DatabaseBuilder.Settings { private int backgroundExecutorShutdownSecs = 30; private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper(); + private boolean tenantPartitionedCache; + // defaults for the L2 bean caching private int cacheMaxSize = 10000; @@ -1175,6 +1177,17 @@ public class DatabaseConfig implements DatabaseBuilder.Settings { return cacheMaxSize; } + @Override + public boolean isTenantPartitionedCache() { + return tenantPartitionedCache; + } + + @Override + public DatabaseConfig tenantPartitionedCache(boolean tenantPartitionedCache) { + this.tenantPartitionedCache = tenantPartitionedCache; + return this; + } + @Override public DatabaseConfig setCacheMaxSize(int cacheMaxSize) { this.cacheMaxSize = cacheMaxSize; @@ -2228,6 +2241,15 @@ public class DatabaseConfig implements DatabaseBuilder.Settings { ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders); ddlHeader = p.get("ddl.header", ddlHeader); + tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache); + + cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize); + cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime); + cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive); + queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize); + queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime); + queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive); + // read tenant-configuration from config: // tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION String mode = p.get("tenant.mode"); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java index deda681e2..4808b8673 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java @@ -13,8 +13,9 @@ import io.ebeaninternal.server.cluster.ClusterManager; public final class CacheManagerOptions { private final ClusterManager clusterManager; - private final DatabaseBuilder.Settings databaseBuilder; + private final String serverName; private final boolean localL2Caching; + private final boolean tenantPartitionedCache; private CurrentTenantProvider currentTenantProvider; private QueryCacheEntryValidate queryCacheEntryValidate; private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory(); @@ -24,7 +25,8 @@ public final class CacheManagerOptions { CacheManagerOptions() { this.localL2Caching = true; this.clusterManager = null; - this.databaseBuilder = null; + this.serverName = "db"; + this.tenantPartitionedCache = false; this.cacheFactory = new DefaultServerCacheFactory(); this.beanDefault = new ServerCacheOptions(); this.queryDefault = new ServerCacheOptions(); @@ -32,9 +34,10 @@ public final class CacheManagerOptions { public CacheManagerOptions(ClusterManager clusterManager, DatabaseBuilder.Settings config, boolean localL2Caching) { this.clusterManager = clusterManager; - this.databaseBuilder = config; + this.serverName = config.getName(); this.localL2Caching = localL2Caching; this.currentTenantProvider = config.getCurrentTenantProvider(); + this.tenantPartitionedCache = config.isTenantPartitionedCache(); } public CacheManagerOptions with(ServerCacheOptions beanDefault, ServerCacheOptions queryDefault) { @@ -55,7 +58,7 @@ public final class CacheManagerOptions { } public String getServerName() { - return (databaseBuilder == null) ? "db" : databaseBuilder.getName(); + return serverName; } public boolean isLocalL2Caching() { @@ -85,4 +88,8 @@ public final class CacheManagerOptions { public QueryCacheEntryValidate getQueryCacheEntryValidate() { return queryCacheEntryValidate; } + + public boolean isTenantPartitionedCache() { + return tenantPartitionedCache; + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java index aba5ab363..fc511f6ef 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java @@ -9,6 +9,7 @@ import io.ebean.config.CurrentTenantProvider; import io.ebean.meta.MetricVisitor; import io.ebean.util.AnnotationUtil; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; @@ -32,6 +33,7 @@ final class DefaultCacheHolder { private final ServerCacheOptions queryDefault; private final CurrentTenantProvider tenantProvider; private final QueryCacheEntryValidate queryCacheEntryValidate; + private final boolean tenantPartitionedCache; DefaultCacheHolder(CacheManagerOptions builder) { this.cacheFactory = builder.getCacheFactory(); @@ -39,6 +41,7 @@ final class DefaultCacheHolder { this.queryDefault = builder.getQueryDefault(); this.tenantProvider = builder.getCurrentTenantProvider(); this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate(); + this.tenantPartitionedCache = builder.isTenantPartitionedCache(); } void visitMetrics(MetricVisitor visitor) { @@ -56,16 +59,31 @@ final class DefaultCacheHolder { return getCacheInternal(beanType, ServerCacheType.COLLECTION_IDS, collectionProperty); } + private String tenantKey(String beanName) { + if (tenantPartitionedCache) { + return beanName + '.' + tenantProvider.currentId(); + } + return beanName; + } + private String key(String beanName, ServerCacheType type) { + if (tenantPartitionedCache) { + return beanName + '.' + tenantProvider.currentId() + type.code(); + } return beanName + type.code(); } private String key(String beanName, String collectionProperty, ServerCacheType type) { - if (collectionProperty != null) { - return beanName + "." + collectionProperty + type.code(); - } else { - return beanName + type.code(); + StringBuilder sb = new StringBuilder(beanName.length() + 64); + sb.append(beanName); + if (tenantPartitionedCache) { + sb.append('.').append(tenantProvider.currentId()); } + if (collectionProperty != null) { + sb.append('.').append(collectionProperty); + } + sb.append(type.code()); + return sb.toString(); } /** @@ -82,12 +100,15 @@ final class DefaultCacheHolder { if (type == ServerCacheType.COLLECTION_IDS) { lock.lock(); try { - collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key); + collectIdCaches.computeIfAbsent(tenantKey(beanType.getName()), s -> new ConcurrentSkipListSet<>()).add(key); } finally { lock.unlock(); } } - return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate)); + // in partitioned mode each ServerCache instance is already tenant-scoped via its key, + // so the tenantProvider is not needed inside the cache itself + CurrentTenantProvider cacheProvider = tenantPartitionedCache ? null : tenantProvider; + return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, cacheProvider, queryCacheEntryValidate)); } void clearAll() { @@ -100,17 +121,75 @@ final class DefaultCacheHolder { public void clear(String name) { log.log(DEBUG, "clear {0}", name); - clearIfExists(key(name, ServerCacheType.QUERY)); - clearIfExists(key(name, ServerCacheType.BEAN)); - clearIfExists(key(name, ServerCacheType.NATURAL_KEY)); - Set keys = collectIdCaches.get(name); - if (keys != null) { - for (String collectionIdKey : keys) { - clearIfExists(collectionIdKey); + if (tenantPartitionedCache) { + // In partitioned mode, tenantProvider.currentId() may be null/wrong on a background + // invalidation thread. Scan all cache entries for this entity type across all tenants. + clearAllTenantsFor(name); + } else { + clearIfExists(key(name, ServerCacheType.QUERY)); + clearIfExists(key(name, ServerCacheType.BEAN)); + clearIfExists(key(name, ServerCacheType.NATURAL_KEY)); + Set keys = collectIdCaches.get(name); + if (keys != null) { + for (String collectionIdKey : keys) { + clearIfExists(collectionIdKey); + } } } } + private void clearAllTenantsFor(String name) { + // Keys in partitioned mode: beanName.tenantId_X or beanName.tenantId.prop_X + // collectIdCaches keys: beanName.tenantId + // Both start with beanName + '.' so we can use prefix scan. + String prefix = name + '.'; + for (Map.Entry entry : allCaches.entrySet()) { + if (entry.getKey().startsWith(prefix)) { + log.log(TRACE, "clear cache {0}", entry.getKey()); + entry.getValue().clear(); + } + } + lock.lock(); + try { + for (Map.Entry> entry : collectIdCaches.entrySet()) { + if (entry.getKey().startsWith(prefix)) { + for (String collectionIdKey : entry.getValue()) { + clearIfExists(collectionIdKey); + } + } + } + } finally { + lock.unlock(); + } + } + + /** + * Remove all cache entries belonging to the given tenant. + * Call this when a tenant is deactivated to prevent unbounded memory growth. + */ + void clearTenant(Object tenantId) { + String tenantIdStr = String.valueOf(tenantId); + // Keys look like: beanName.tenantId_X or beanName.tenantId.prop_X + String segmentBeforeTypeCode = '.' + tenantIdStr + '_'; + String segmentBeforeProperty = '.' + tenantIdStr + '.'; + allCaches.entrySet().removeIf(e -> { + String k = e.getKey(); + return k.contains(segmentBeforeTypeCode) || k.contains(segmentBeforeProperty); + }); + // collectIdCaches keys look like: beanName.tenantId + String collectKeySuffix = '.' + tenantIdStr; + lock.lock(); + try { + collectIdCaches.entrySet().removeIf(e -> e.getKey().endsWith(collectKeySuffix)); + } finally { + lock.unlock(); + } + } + + boolean isTenantPartitionedCache() { + return tenantPartitionedCache; + } + private void clearIfExists(String fullKey) { ServerCache cache = allCaches.get(fullKey); if (cache != null) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java index 0b7264ff9..dd23b91ac 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java @@ -154,4 +154,14 @@ public final class DefaultServerCacheManager implements SpiCacheManager { return cacheHolder.getCache(beanType, ServerCacheType.BEAN); } + @Override + public boolean isTenantPartitionedCache() { + return cacheHolder.isTenantPartitionedCache(); + } + + @Override + public void clearTenant(Object tenantId) { + cacheHolder.clearTenant(tenantId); + } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java index bf6d4d5a2..f589a0ff3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java @@ -88,4 +88,17 @@ public interface SpiCacheManager { */ void clearLocal(Class beanType); + /** + * Returns true if this cache manager runs in tenant-partitioned mode. + * In this mode caches are namespaced per tenant to improve cache-hit ratio. + */ + boolean isTenantPartitionedCache(); + + /** + * Remove all cache entries belonging to the given tenant. + * Call this when a tenant is deactivated to prevent unbounded memory growth + * in tenant-partitioned mode. + */ + void clearTenant(Object tenantId); + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 199cd037a..9a2cf58ea 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -320,7 +320,7 @@ public class BeanDescriptor implements BeanType, STreeType, SpiBeanType { this.idOnlyReference = isIdOnlyReference(propertiesBaseScalar); boolean noRelationships = propertiesOne.length + propertiesMany.length == 0; this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly(); - this.cacheHelp = new BeanDescriptorCacheHelp<>(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported); + this.cacheHelp = BeanDescriptorCacheHelp.create(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported); this.jsonHelp = initJsonHelp(); this.draftHelp = new BeanDescriptorDraftHelp<>(this); this.docStoreAdapter = owner.createDocStoreBeanAdapter(this, deploy); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index 94ec84bf4..b47e3977e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -27,7 +27,7 @@ import static java.lang.System.Logger.Level.*; * * @param The entity bean type */ -final class BeanDescriptorCacheHelp { +abstract class BeanDescriptorCacheHelp { private static final System.Logger log = CoreLog.internal; @@ -36,7 +36,7 @@ final class BeanDescriptorCacheHelp { private static final System.Logger manyLog = AppLog.getLogger("io.ebean.cache.COLL"); private static final System.Logger natLog = AppLog.getLogger("io.ebean.cache.NATKEY"); - private final BeanDescriptor desc; + final BeanDescriptor desc; private final SpiCacheManager cacheManager; private final CacheOptions cacheOptions; /** @@ -44,13 +44,10 @@ final class BeanDescriptorCacheHelp { */ private final boolean cacheSharableBeans; private final boolean invalidateQueryCache; - private final Class beanType; + final Class beanType; private final String cacheName; private final BeanPropertyAssocOne[] propertiesOneImported; private final String[] naturalKey; - private final ServerCache beanCache; - private final ServerCache naturalKeyCache; - private final ServerCache queryCache; private final boolean noCaching; private final SpiCacheControl cacheControl; private final SpiCacheRegion cacheRegion; @@ -64,6 +61,15 @@ final class BeanDescriptorCacheHelp { */ private boolean cacheNotifyOnDelete; + static BeanDescriptorCacheHelp create(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + if ((cacheOptions.isEnableQueryCache() || cacheOptions.isEnableBeanCache()) && cacheManager.isTenantPartitionedCache()) { + return new BeanDescriptorCacheHelpPartitioned<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + } else { + return new BeanDescriptorCacheHelpFixed<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + } + } + BeanDescriptorCacheHelp(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { this.desc = desc; @@ -75,38 +81,34 @@ final class BeanDescriptorCacheHelp { this.cacheSharableBeans = cacheSharableBeans; this.propertiesOneImported = propertiesOneImported; this.naturalKey = cacheOptions.getNaturalKey(); - if (!cacheOptions.isEnableQueryCache()) { - this.queryCache = null; - } else { - this.queryCache = cacheManager.getQueryCache(beanType); - } - - if (cacheOptions.isEnableBeanCache()) { - this.beanCache = cacheManager.getBeanCache(beanType); - if (cacheOptions.getNaturalKey() != null) { - this.naturalKeyCache = cacheManager.getNaturalKeyCache(beanType); - } else { - this.naturalKeyCache = null; - } - } else { - this.beanCache = null; - this.naturalKeyCache = null; - } - this.noCaching = (beanCache == null && queryCache == null); + this.noCaching = !cacheOptions.isEnableQueryCache() && !cacheOptions.isEnableBeanCache(); if (noCaching) { this.cacheControl = DCacheControlNone.INSTANCE; this.cacheRegion = (invalidateQueryCache) ? cacheManager.getRegion(cacheOptions.getRegion()) : DCacheRegionNone.INSTANCE; } else { this.cacheRegion = cacheManager.getRegion(cacheOptions.getRegion()); - this.cacheControl = new DCacheControl(cacheRegion, (beanCache != null), (naturalKeyCache != null), (queryCache != null)); + this.cacheControl = new DCacheControl(cacheRegion, + cacheOptions.isEnableBeanCache(), + cacheOptions.isEnableBeanCache() && cacheOptions.getNaturalKey() != null, + cacheOptions.isEnableQueryCache()); } } + abstract boolean hasBeanCache(); + + abstract boolean hasQueryCache(); + + abstract ServerCache queryCache(); + + abstract ServerCache naturalKeyCache(); + + abstract ServerCache beanCache(); + /** * Derive the cache notify flags. */ void deriveNotifyFlags() { - cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null); + cacheNotifyOnAll = (invalidateQueryCache || hasBeanCache() || hasQueryCache()); cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes(); if (log.isLoggable(DEBUG)) { if (cacheNotifyOnAll || cacheNotifyOnDelete) { @@ -184,11 +186,11 @@ final class BeanDescriptorCacheHelp { * Clear the query cache. */ void queryCacheClear() { - if (queryCache != null) { + if (hasQueryCache()) { if (queryLog.isLoggable(DEBUG)) { queryLog.log(DEBUG, " CLEAR {0}", cacheName); } - queryCache.clear(); + queryCache().clear(); } } @@ -196,7 +198,7 @@ final class BeanDescriptorCacheHelp { * Add query cache clear to the changeSet. */ private void queryCacheClear(CacheChangeSet changeSet) { - if (queryCache != null) { + if (hasQueryCache()) { changeSet.addClearQuery(desc); } } @@ -205,10 +207,10 @@ final class BeanDescriptorCacheHelp { * Get a query result from the query cache. */ Object queryCacheGet(Object id) { - if (queryCache == null) { + if (!hasQueryCache()) { throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)"); } - Object queryResult = queryCache.get(id); + Object queryResult = queryCache().get(id); if (queryLog.isLoggable(DEBUG)) { if (queryResult == null) { queryLog.log(DEBUG, " GET {0}({1}) - cache miss", cacheName, id); @@ -223,13 +225,13 @@ final class BeanDescriptorCacheHelp { * Put a query result into the query cache. */ void queryCachePut(Object id, QueryCacheEntry entry) { - if (queryCache == null) { + if (!hasQueryCache()) { throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)"); } if (queryLog.isLoggable(DEBUG)) { queryLog.log(DEBUG, " PUT {0}({1})", cacheName, id); } - queryCache.put(id, entry); + queryCache().put(id, entry); } void manyPropRemove(String propertyName, String parentKey) { @@ -300,7 +302,7 @@ final class BeanDescriptorCacheHelp { */ void manyPropPut(BeanPropertyAssocMany many, Object details, String parentKey) { if (many.isElementCollection()) { - CachedBeanData data = (CachedBeanData) beanCache.get(parentKey); + CachedBeanData data = (CachedBeanData) beanCache().get(parentKey); if (data != null) { try { // add as JSON to bean cache @@ -312,7 +314,7 @@ final class BeanDescriptorCacheHelp { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " UPDATE {0}({1}) changes:{2}", cacheName, parentKey, changes); } - beanCache.put(parentKey, newData); + beanCache().put(parentKey, newData); } catch (IOException e) { log.log(ERROR, "Error updating L2 cache", e); } @@ -358,7 +360,7 @@ final class BeanDescriptorCacheHelp { if (ids.isEmpty()) { return new BeanCacheResult<>(); } - Map beanDataMap = beanCache.getAll(keys); + Map beanDataMap = beanCache().getAll(keys); if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet()); } @@ -380,7 +382,7 @@ final class BeanDescriptorCacheHelp { } // naturalKey -> Id map - Map naturalKeyMap = naturalKeyCache.getAll(keys); + Map naturalKeyMap = naturalKeyCache().getAll(keys); if (natLog.isLoggable(TRACE)) { natLog.log(TRACE, " MLOOKUP {0}({1}) - hits:{2}", cacheName, keys, naturalKeyMap); } @@ -397,7 +399,7 @@ final class BeanDescriptorCacheHelp { } Set ids = new HashSet<>(naturalKeyMap.values()); - Map beanDataMap = beanCache.getAll(ids); + Map beanDataMap = beanCache().getAll(ids); if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet()); } @@ -428,25 +430,15 @@ final class BeanDescriptorCacheHelp { desc.contextPut(context, id, bean); } - /** - * Return the beanCache creating it if necessary. - */ - private ServerCache getBeanCache() { - if (beanCache == null) { - throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); - } - return beanCache; - } - /** * Clear the bean cache. */ void beanCacheClear() { - if (beanCache != null) { + if (hasBeanCache()) { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " CLEAR {0}", cacheName); } - beanCache.clear(); + beanCache().clear(); } } @@ -516,13 +508,13 @@ final class BeanDescriptorCacheHelp { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " MPUT {0}({1})", cacheName, map.keySet()); } - getBeanCache().putAll(map); + beanCache().putAll(map); if (natKeys != null && !natKeys.isEmpty()) { if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, " MPUT {0}({1}, {2})", cacheName, Arrays.toString(naturalKey), natKeys.keySet()); } - naturalKeyCache.putAll(natKeys); + naturalKeyCache().putAll(natKeys); } } @@ -535,14 +527,14 @@ final class BeanDescriptorCacheHelp { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " PUT {0}({1}) data:{2}", cacheName, key, beanData); } - getBeanCache().put(key, beanData); + beanCache().put(key, beanData); if (naturalKey != null) { String naturalKey = calculateNaturalKey(beanData); if (naturalKey != null) { if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, " PUT {0}({1}, {2})", cacheName, naturalKey, key); } - naturalKeyCache.put(naturalKey, key); + naturalKeyCache().put(naturalKey, key); } } } @@ -564,7 +556,7 @@ final class BeanDescriptorCacheHelp { } CachedBeanData beanCacheGetData(String key) { - return (CachedBeanData) getBeanCache().get(key); + return (CachedBeanData) beanCache().get(key); } T beanCacheGet(String key, boolean unmodifiable, PersistenceContext context) { @@ -579,7 +571,7 @@ final class BeanDescriptorCacheHelp { * Return a bean from the bean cache. */ private T beanCacheGetInternal(String key, boolean unmodifiable, PersistenceContext context) { - CachedBeanData data = (CachedBeanData) getBeanCache().get(key); + CachedBeanData data = (CachedBeanData) beanCache().get(key); if (data == null) { if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " GET {0}({1}) - cache miss", cacheName, key); @@ -684,11 +676,11 @@ final class BeanDescriptorCacheHelp { * Remove a bean from the cache given its Id. */ void beanCacheApplyInvalidate(Collection keys) { - if (beanCache != null) { + if (hasBeanCache()) { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " MREMOVE {0}({1})", cacheName, keys); } - beanCache.removeAll(new HashSet<>(keys)); + beanCache().removeAll(new HashSet<>(keys)); } for (BeanPropertyAssocOne imported : propertiesOneImported) { imported.cacheClear(); @@ -704,7 +696,7 @@ final class BeanDescriptorCacheHelp { ebis.put(desc.cacheKeyForBean(ebi.owner()), ebi); } - Map hits = getBeanCache().getAll(ebis.keySet()); + Map hits = beanCache().getAll(ebis.keySet()); if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " MLOAD {0}({1}) - got hits ({2})", cacheName, ebis.keySet(), hits.size()); } @@ -737,7 +729,7 @@ final class BeanDescriptorCacheHelp { * Returns true if it managed to populate/load the single bean from the cache. */ boolean beanCacheLoad(EntityBean bean, EntityBeanIntercept ebi, String key, PersistenceContext context) { - CachedBeanData cacheData = (CachedBeanData) getBeanCache().get(key); + CachedBeanData cacheData = (CachedBeanData) beanCache().get(key); if (cacheData == null) { if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " LOAD {0}({1}) - cache miss", cacheName, key); @@ -775,7 +767,7 @@ final class BeanDescriptorCacheHelp { changeSet.addInvalidate(desc); } else { queryCacheClear(changeSet); - if (beanCache != null) { + if (hasBeanCache()) { changeSet.addBeanRemoveMany(desc, ids); } cacheDeleteImported(true, null, changeSet); @@ -793,7 +785,7 @@ final class BeanDescriptorCacheHelp { changeSet.addInvalidate(desc); } else { queryCacheClear(changeSet); - if (beanCache != null) { + if (hasBeanCache()) { changeSet.addBeanRemove(desc, id); } cacheDeleteImported(true, deleteRequest.entityBean(), changeSet); @@ -831,7 +823,7 @@ final class BeanDescriptorCacheHelp { } else { queryCacheClear(changeSet); - if (beanCache == null) { + if (!hasBeanCache()) { // query caching only return; } @@ -877,7 +869,7 @@ final class BeanDescriptorCacheHelp { void cacheNaturalKeyPut(String key, String newKey) { if (newKey != null) { - naturalKeyCache.put(newKey, key); + naturalKeyCache().put(newKey, key); } } @@ -885,7 +877,7 @@ final class BeanDescriptorCacheHelp { * Apply changes to the bean cache entry. */ void cacheBeanUpdate(String key, Map changes, boolean updateNaturalKey, long version) { - ServerCache cache = getBeanCache(); + ServerCache cache = beanCache(); CachedBeanData existingData = (CachedBeanData) cache.get(key); if (existingData != null) { long currentVersion = existingData.getVersion(); @@ -910,7 +902,7 @@ final class BeanDescriptorCacheHelp { if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, ".. update {0} REMOVE({1}) - old key for ({2})", cacheName, oldKey, key); } - naturalKeyCache.remove(oldKey); + naturalKeyCache().remove(oldKey); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java new file mode 100644 index 000000000..d6b366e5b --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java @@ -0,0 +1,63 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.cache.ServerCache; +import io.ebeaninternal.server.cache.SpiCacheManager; +import io.ebeaninternal.server.core.CacheOptions; + +/** + * BeanDescriptorCacheHelp implementation for non-tenant-partitioned (fixed) caches. + * Cache instances are obtained once at construction time and reused for all requests. + * + * @param The entity bean type + */ +final class BeanDescriptorCacheHelpFixed extends BeanDescriptorCacheHelp { + + private final ServerCache beanCache; + private final ServerCache naturalKeyCache; + private final ServerCache queryCache; + + BeanDescriptorCacheHelpFixed(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + if (!cacheOptions.isEnableQueryCache()) { + this.queryCache = null; + } else { + this.queryCache = cacheManager.getQueryCache(beanType); + } + if (cacheOptions.isEnableBeanCache()) { + this.beanCache = cacheManager.getBeanCache(beanType); + this.naturalKeyCache = (cacheOptions.getNaturalKey() != null) ? cacheManager.getNaturalKeyCache(beanType) : null; + } else { + this.beanCache = null; + this.naturalKeyCache = null; + } + } + + @Override + boolean hasBeanCache() { + return beanCache != null; + } + + @Override + boolean hasQueryCache() { + return queryCache != null; + } + + @Override + ServerCache queryCache() { + return queryCache; + } + + @Override + ServerCache naturalKeyCache() { + return naturalKeyCache; + } + + @Override + ServerCache beanCache() { + if (beanCache == null) { + throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); + } + return beanCache; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java new file mode 100644 index 000000000..499656198 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java @@ -0,0 +1,62 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.cache.ServerCache; +import io.ebeaninternal.server.cache.SpiCacheManager; +import io.ebeaninternal.server.core.CacheOptions; + +import java.util.function.Supplier; + +/** + * BeanDescriptorCacheHelp implementation for tenant-partitioned caches. + * Cache instances are looked up on every access via the cacheManager (which resolves the + * correct tenant-namespaced cache using the current tenant id from the tenant provider). + * + * @param The entity bean type + */ +final class BeanDescriptorCacheHelpPartitioned extends BeanDescriptorCacheHelp { + + private final Supplier beanCacheSupplier; + private final Supplier naturalKeyCacheSupplier; + private final Supplier queryCacheSupplier; + + BeanDescriptorCacheHelpPartitioned(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + this.queryCacheSupplier = cacheOptions.isEnableQueryCache() ? () -> cacheManager.getQueryCache(beanType) : null; + if (cacheOptions.isEnableBeanCache()) { + this.beanCacheSupplier = () -> cacheManager.getBeanCache(beanType); + this.naturalKeyCacheSupplier = (cacheOptions.getNaturalKey() != null) ? () -> cacheManager.getNaturalKeyCache(beanType) : null; + } else { + this.beanCacheSupplier = null; + this.naturalKeyCacheSupplier = null; + } + } + + @Override + boolean hasBeanCache() { + return beanCacheSupplier != null; + } + + @Override + boolean hasQueryCache() { + return queryCacheSupplier != null; + } + + @Override + ServerCache queryCache() { + return queryCacheSupplier.get(); + } + + @Override + ServerCache naturalKeyCache() { + return naturalKeyCacheSupplier.get(); + } + + @Override + ServerCache beanCache() { + if (beanCacheSupplier == null) { + throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); + } + return beanCacheSupplier.get(); + } +} diff --git a/tests/test-java16/pom.xml b/tests/test-java16/pom.xml index 03f3bc553..498a35903 100644 --- a/tests/test-java16/pom.xml +++ b/tests/test-java16/pom.xml @@ -5,7 +5,7 @@ tests io.ebean - 18.0.0 + 18.1.0 test-java16 @@ -18,7 +18,7 @@ io.ebean ebean-h2 - 18.0.0 + 18.1.0 @@ -30,14 +30,14 @@ io.ebean ebean-test - 18.0.0 + 18.1.0 test io.ebean querybean-generator - 18.0.0 + 18.1.0 provided