Add tenant-partitioned L2 caches (#2956) (#3809)

* Add tenant-partitioned L2 caches (#2956)

 When `tenantPartitionedCache` is enabled, each tenant gets its own
 cache namespace (keys include the tenant id), improving cache-hit
 ratio by preventing cross-tenant key collisions.

 Refactor BeanDescriptorCacheHelp to abstract base class with two
 concrete subclasses:
 - BeanDescriptorCacheHelpFixed - static cache refs (original behaviour)
 - BeanDescriptorCacheHelpPartitioned - per-request Supplier<> lookup
   so the correct tenant-scoped cache is resolved on each access

 Fix background-thread invalidation: clear(name) in partitioned mode
 now scans all matching cache entries by key prefix rather than calling
 tenantProvider.currentId() which is null/wrong on background executor
 threads.

 Add SpiCacheManager.clearTenant(tenantId) to allow removal of all
 cache entries for a deactivated tenant, preventing unbounded memory
 growth in long-running multi-tenant deployments.

 Also load cache settings (cacheMaxSize, cacheMaxIdleTime, etc.) from
 application properties - these were previously missing from loadSettings().

* Fix versions in test-java16

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
This commit is contained in:
Rob Bygrave
2026-07-01 20:04:08 +12:00
committed by GitHub
co-authored by robin.bygrave
parent 3d2cc2d4af
commit 66bcfd107d
11 changed files with 348 additions and 88 deletions
@@ -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.
*/
@@ -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");
@@ -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;
}
}
@@ -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<String> 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<String> 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<String, ServerCache> 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<String, Set<String>> 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) {
@@ -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);
}
}
@@ -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);
}
@@ -320,7 +320,7 @@ public class BeanDescriptor<T> implements BeanType<T>, 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);
@@ -27,7 +27,7 @@ import static java.lang.System.Logger.Level.*;
*
* @param <T> The entity bean type
*/
final class BeanDescriptorCacheHelp<T> {
abstract class BeanDescriptorCacheHelp<T> {
private static final System.Logger log = CoreLog.internal;
@@ -36,7 +36,7 @@ final class BeanDescriptorCacheHelp<T> {
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<T> desc;
final BeanDescriptor<T> desc;
private final SpiCacheManager cacheManager;
private final CacheOptions cacheOptions;
/**
@@ -44,13 +44,10 @@ final class BeanDescriptorCacheHelp<T> {
*/
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<T> {
*/
private boolean cacheNotifyOnDelete;
static <T> BeanDescriptorCacheHelp<T> create(BeanDescriptor<T> 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<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
this.desc = desc;
@@ -75,38 +81,34 @@ final class BeanDescriptorCacheHelp<T> {
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<T> {
* 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<T> {
* 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<T> {
* 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<T> {
* 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<T> {
*/
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<T> {
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<T> {
if (ids.isEmpty()) {
return new BeanCacheResult<>();
}
Map<Object, Object> beanDataMap = beanCache.getAll(keys);
Map<Object, Object> 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<T> {
}
// naturalKey -> Id map
Map<Object, Object> naturalKeyMap = naturalKeyCache.getAll(keys);
Map<Object, Object> 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<T> {
}
Set<Object> ids = new HashSet<>(naturalKeyMap.values());
Map<Object, Object> beanDataMap = beanCache.getAll(ids);
Map<Object, Object> 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<T> {
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<T> {
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<T> {
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<T> {
}
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<T> {
* 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<T> {
* Remove a bean from the cache given its Id.
*/
void beanCacheApplyInvalidate(Collection<String> 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<T> {
ebis.put(desc.cacheKeyForBean(ebi.owner()), ebi);
}
Map<Object, Object> hits = getBeanCache().getAll(ebis.keySet());
Map<Object, Object> 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<T> {
* 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<T> {
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<T> {
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<T> {
} else {
queryCacheClear(changeSet);
if (beanCache == null) {
if (!hasBeanCache()) {
// query caching only
return;
}
@@ -877,7 +869,7 @@ final class BeanDescriptorCacheHelp<T> {
void cacheNaturalKeyPut(String key, String newKey) {
if (newKey != null) {
naturalKeyCache.put(newKey, key);
naturalKeyCache().put(newKey, key);
}
}
@@ -885,7 +877,7 @@ final class BeanDescriptorCacheHelp<T> {
* Apply changes to the bean cache entry.
*/
void cacheBeanUpdate(String key, Map<String, Object> 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<T> {
if (natLog.isLoggable(DEBUG)) {
natLog.log(DEBUG, ".. update {0} REMOVE({1}) - old key for ({2})", cacheName, oldKey, key);
}
naturalKeyCache.remove(oldKey);
naturalKeyCache().remove(oldKey);
}
}
}
@@ -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 <T> The entity bean type
*/
final class BeanDescriptorCacheHelpFixed<T> extends BeanDescriptorCacheHelp<T> {
private final ServerCache beanCache;
private final ServerCache naturalKeyCache;
private final ServerCache queryCache;
BeanDescriptorCacheHelpFixed(BeanDescriptor<T> 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;
}
}
@@ -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 <T> The entity bean type
*/
final class BeanDescriptorCacheHelpPartitioned<T> extends BeanDescriptorCacheHelp<T> {
private final Supplier<ServerCache> beanCacheSupplier;
private final Supplier<ServerCache> naturalKeyCacheSupplier;
private final Supplier<ServerCache> queryCacheSupplier;
BeanDescriptorCacheHelpPartitioned(BeanDescriptor<T> 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();
}
}
+4 -4
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>tests</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.1.0</version>
</parent>
<artifactId>test-java16</artifactId>
@@ -18,7 +18,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -30,14 +30,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
<scope>provided</scope>
</dependency>