diff --git a/dotCMS/src/enterprise/java/com/dotcms/enterprise/cache/provider/CacheProviderAPIImpl.java b/dotCMS/src/enterprise/java/com/dotcms/enterprise/cache/provider/CacheProviderAPIImpl.java index fc438771d775..337b031828e6 100644 --- a/dotCMS/src/enterprise/java/com/dotcms/enterprise/cache/provider/CacheProviderAPIImpl.java +++ b/dotCMS/src/enterprise/java/com/dotcms/enterprise/cache/provider/CacheProviderAPIImpl.java @@ -47,20 +47,27 @@ import com.dotcms.enterprise.LicenseUtil; import com.dotcms.enterprise.license.LicenseLevel; -import com.dotmarketing.business.cache.provider.CacheProviderStats; -import com.dotmarketing.business.cache.provider.caffine.CaffineCache; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.DotStateException; import com.dotmarketing.business.cache.CacheOSGIService; import com.dotmarketing.business.cache.provider.CacheProvider; +import com.dotmarketing.business.cache.provider.CacheProviderStats; +import com.dotmarketing.business.cache.provider.caffine.CaffineCache; import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; -import com.dotmarketing.util.UtilMethods; -import com.dotmarketing.util.WebKeys; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.collect.Lists; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; +import io.vavr.control.Try; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; /** * @author Jonathan Gamba @@ -70,125 +77,21 @@ public class CacheProviderAPIImpl implements CacheProviderAPI, CacheOSGIService private static final String CACHE_POOL_DEFAULT_CHAIN = "cache.default.chain"; - private static String defaultProvidersNoLicense;//Default providers with not ee lincense - private static String defaultProviders;//Default providers with license - - private final Map> configuredChainsPerRegion;//List of registered providers per region - private final Map providersClasses;//List of ALL the registered providers - private final Map defaultProvidersNoLicenseClasses;//List of the default registered providers + // we use a cache for providers because ConcurrentHashMap has a recursion problem in its computeIfAbsent method + private static final Cache> configuredChainsPerRegion = Caffeine.newBuilder().maximumSize(10000).build(); + private final List noLicenseProviders = List.of(CaffineCache.class.getCanonicalName()); - private boolean isLicenseInitialized = false; public CacheProviderAPIImpl () { - configuredChainsPerRegion = new ConcurrentHashMap<>(); - providersClasses = new ConcurrentHashMap<>(); - defaultProvidersNoLicenseClasses = new ConcurrentHashMap<>(); - - /* - This class stores in memory the initialized providers in order to reduce the performance impact as much as possible. - Keep in mind this class is heavily used!. - */ - - //Reading the default providers properties - defaultProvidersNoLicense = CaffineCache.class.getCanonicalName(); - defaultProviders = Config.getStringProperty(CACHE_POOL_DEFAULT_CHAIN, defaultProvidersNoLicense); - - //Lets try to find out what Providers we want to use - Iterator keysIterator = Config.subset("cache"); - while ( keysIterator.hasNext() ) { - - //Check if it is a region chain property and read the property - String propertyKey = keysIterator.next(); - if ( propertyKey.endsWith(".chain") ) { - - String poolChainClassNames = Config.getStringProperty("cache." + propertyKey, ""); - - //Now iterate each CacheProvider on this region - String[] chainArray = poolChainClassNames.split(","); - for ( String providerClassName : chainArray ) { - - providerClassName = providerClassName.trim(); - - //First lets search if we already instantiated this provider - if ( !providersClasses.containsKey(providerClassName) ) { - - try { - - CacheProvider cacheProviderInstance; - try { - //Create a new instance of the provider - cacheProviderInstance = getInstanceFor(providerClassName); - } catch ( ClassNotFoundException e ) { - Logger.error(this, "Unable to get the class reference for the CacheProvider [" + providerClassName + "].", e); - //Lets continue with the next given CacheProvider implementation - continue; - } - - //Add the new provider instance to the list for registered providers - providersClasses.put(providerClassName, cacheProviderInstance); - - } catch ( InstantiationException | IllegalAccessException e ) { - Logger.error(this, "Error instantiating CacheProvider [" + providerClassName + "].", e); - //Lets continue with the next given CacheProvider implementation - } - } - - } - } - - } - - /* - Get the list of default CacheProviders when no license is set, we do this for easy and quick access - */ - String[] chainArray = defaultProvidersNoLicense.split(","); - for ( String providerClassName : chainArray ) { - - providerClassName = providerClassName.trim(); - - CacheProvider cacheProviderInstance = null; - - //Search in the already saved Providers - if ( providersClasses.containsKey(providerClassName) ) { - cacheProviderInstance = providersClasses.get(providerClassName); - } else { - try { - //Create a new instance of the provider - cacheProviderInstance = getInstanceFor(providerClassName); - } catch ( Exception e ) { - Logger.error(this, "Unable to get the class reference for the CacheProvider [" + providerClassName + "].", e); - //Lets continue with the next given CacheProvider implementation - } - } - - if ( cacheProviderInstance != null ) { - //Now lets fill in the default CacheProviders list for easy access - defaultProvidersNoLicenseClasses.put(providerClassName, cacheProviderInstance); - } else { - Logger.error(this, "CacheProvider not found [" + providerClassName + "]."); - } - } - - //Registering this service to the OSGI context - registerBundleService(); } - /** - * Creates an instance of a given class name - * - * @param providerClassName - * @return - * @throws IllegalAccessException - * @throws InstantiationException - * @throws ClassNotFoundException - */ - private CacheProvider getInstanceFor ( String providerClassName ) throws IllegalAccessException, InstantiationException, ClassNotFoundException { - //Look for the provider class - Class providerClass = (Class) Class.forName(providerClassName); - //Create a new instance of the provider - return providerClass.newInstance(); + private Optional getInstanceFor ( String providerClassName ) { + return Try.of(() -> { + Class providerClass = (Class) Class.forName(providerClassName.trim()); + return providerClass.newInstance(); + }).onFailure(e -> Logger.error(this, "Error creating CacheProvider [" + providerClassName + "].", e)).toJavaOptional(); } /** @@ -196,120 +99,92 @@ private CacheProvider getInstanceFor ( String providerClassName ) throws Illegal * * @return */ - private Boolean isValidLicense () { - - if ( !isLicenseInitialized ) { - - /* - Validate if we can get use the LicenseUtil, the CacheLocator and CacheProviders - are one of the first elements to be created, using the LicenseUtil here on a clean install - can throw errors as the DB could not be even been loaded or a server id file could not be created - and we don't want to stop the execution here for those expected cases. - */ - String serverId = APILocator.getServerAPI().readServerId(); - if ( serverId == null ) { - //We can continue, probably a first start - Logger.debug(this, "Unable to get License level [server id is null]."); - return false; - } - - //We can finally call directly the LicenseUtil.getLevel() method, the cluster_server was finally created! - isLicenseInitialized = true; - } + private boolean isCommunity () { - if ( LicenseUtil.getLevel() >= LicenseLevel.STANDARD.level ) { + /* + Validate if we can get use the LicenseUtil, the CacheLocator and CacheProviders + are one of the first elements to be created, using the LicenseUtil here on a clean install + can throw errors as the DB could not be even been loaded or a server id file could not be created + and we don't want to stop the execution here for those expected cases. + */ + String serverId = APILocator.getServerAPI().readServerId(); + if ( serverId == null ) { + //We can continue, probably a first start + Logger.debug(this, "Unable to get License level [server id is null]."); return true; } - return false; + return LicenseUtil.getLevel() <= LicenseLevel.COMMUNITY.level ; + } /** * Return all the registered CacheProviders, there are cases when is required to iterate over all the Providers, like on * a flush or a shutdown * - * @return */ private List getAllProviders () { - //If no license return the default providers - if ( !isValidLicense() ) { - return new ArrayList<>(defaultProvidersNoLicenseClasses.values()); + Map providers = new HashMap<>(); + + for(List providerList : configuredChainsPerRegion.asMap().values()){ + providerList.forEach(p->providers.put(p.getClass().getCanonicalName(),p)); + } + return new ArrayList<>(providers.values()); + + + } + + + + private List getProviderNamesPerRegion(String group){ + if ( isCommunity() || null == group ) { + return noLicenseProviders; + } + //Read from the properties the cache chain to use for this region, if nothing found the default chain will be used + String[] poolChainClassNames = Config.getStringArrayProperty("cache." + group.toLowerCase() + ".chain", + new String[0]); + + if(poolChainClassNames.length>0){ + return Arrays.asList(poolChainClassNames); } - //Or else return the complete list of providers - return new ArrayList<>(providersClasses.values()); + return Arrays.asList(Config.getStringArrayProperty(CACHE_POOL_DEFAULT_CHAIN,new String[]{ CaffineCache.class.getCanonicalName()})); + + } + /** * Returns the list of CacheProviders to use for a given group, this class stores in * memory the initialized providers in order to impact as much as possible performance, keep in mind this class * is heavily used!. * * @param group - * @return */ private List getProvidersForRegion ( String group ) { //The case is very simple here, no license no chance to modify any region chain - if ( !isValidLicense() ) { - return new ArrayList<>(defaultProvidersNoLicenseClasses.values()); - } - - List providersForRegion = new ArrayList<>(); - List poolChainClassNames; - //First lets check if we already searched for this group in the past - if ( configuredChainsPerRegion.containsKey(group) ) { - poolChainClassNames = configuredChainsPerRegion.get(group); - } else { - - //Read from the properties the cache chain to use for this region, if nothing found the default chain will be used - String poolChainClassNamesValue = Config.getStringProperty("cache." + group + ".chain", defaultProviders); - poolChainClassNames = Arrays.asList(poolChainClassNamesValue.split(",")); - - //Added to the map, we want to avoid extra validations and checks as much as we can - configuredChainsPerRegion.put(group, poolChainClassNames); - } - - //Now iterate for each requested CacheProvider - for ( String providerClassName : poolChainClassNames ) { - - CacheProvider cacheProviderInstance = null; - - //First lets search if we already requested this provider in the past - if ( providersClasses.containsKey(providerClassName) ) { - cacheProviderInstance = providersClasses.get(providerClassName); + return configuredChainsPerRegion.get(group, k -> { + List providers = new ArrayList<>(); + for(String providerClassName : getProviderNamesPerRegion(group)){ + getInstanceFor(providerClassName).ifPresent(providers::add); } + return List.copyOf(initProviders(providers)); + }); - if ( cacheProviderInstance != null ) { - if ( !providersForRegion.contains(cacheProviderInstance) ) { - - try { - /* - Check if the Provider was not already initialized, if not is probably because - the user didn't had a license and now have it (so he is using more providers now), or had a license and he removed it - */ - if ( !cacheProviderInstance.isInitialized() ) { - cacheProviderInstance.init(); - } - - //Add to the list of providers to return - providersForRegion.add(cacheProviderInstance); - - } catch ( Exception e ) { - //On Error we don't want to stop the execution of the other providers - Logger.error(this.getClass(), "Error initializing CacheProvider [" + cacheProviderInstance.getName() + "].", e); - } - } - } else { - Logger.error(this, "CacheProvider not found [" + providerClassName + "]."); - } - } + } - return providersForRegion; + List initProviders(List cacheProviders) { + cacheProviders.forEach(provider -> + Try.run(provider::init).onFailure( + e -> Logger.error(this, "Error initializing CacheProvider [" + provider.getName() + "]." + e.getMessage(), e)) + ); + return cacheProviders; } + /** * Determines whether all Cache Providers are distributed */ @@ -341,13 +216,7 @@ private boolean determineDistributed(List providers) { * Registers this CacheOSGIService to the OSGI Context, in order to be use for OSGI plugins */ public void registerBundleService () { - - if (System.getProperty(WebKeys.OSGI_ENABLED) != null ) { - //Register the CacheOSGIService - // BundleContext context = HostActivator.instance().getBundleContext(); - // Hashtable props = new Hashtable<>(); - // context.registerService(CacheOSGIService.class.getName(), this, props); - } + // not implemented } @@ -491,6 +360,10 @@ public void removeAll (boolean ignoreDistributed) { Logger.error(this.getClass(), "Error flushing CacheProvider [" + provider.getName() + "].", e); } } + configuredChainsPerRegion.invalidateAll(); + shutdownProviders(providers); + + } @Override @@ -522,9 +395,9 @@ public Set getGroups () { public List getStats () { //Getting the list of all the cache providers - List providers = getAllProviders(); + List ret = new ArrayList<>(); - for ( CacheProvider provider : providers ) { + for ( CacheProvider provider : getAllProviders() ) { try { CacheProviderStats providerStat = provider.getStats(); @@ -544,58 +417,15 @@ public List getStats () { @Override public void shutdown () { - //Getting the list of all the cache providers - Set providers = new HashSet<>(providersClasses.values()); - providers.addAll(defaultProvidersNoLicenseClasses.values()); + shutdownProviders(List.copyOf(getAllProviders())); + } + private void shutdownProviders(Collection providers) { for ( CacheProvider provider : providers ) { - - try { - if ( provider.isInitialized() ) { - provider.shutdown(); - } - } catch ( Exception e ) { - //On Error we don't want to stop the execution of the other providers - Logger.error(this.getClass(), "Error shutting down CacheProvider [" + provider.getName() + "].", e); - } + Try.run(provider::shutdown).onFailure(e -> Logger.error(this, "Error shutting down CacheProvider [" + provider.getName() + "].", e)); } } - /** - * Internal comparator class used to order stat results (getStats()) - */ - public class CacheComparator implements Comparator> { - - public int compare ( Map o1, Map o2 ) { - - if ( o1 == null && o2 != null ) return 1; - if ( o1 != null && o2 == null ) return -1; - if ( o1 == null && o2 == null ) return 0; - - String group1 = (String) o1.get("region"); - String group2 = (String) o2.get("region"); - - if ( !UtilMethods.isSet(group1) && !UtilMethods.isSet(group2) ) { - return 0; - } else if ( UtilMethods.isSet(group1) && !UtilMethods.isSet(group2) ) { - return -1; - } else if ( !UtilMethods.isSet(group1) && UtilMethods.isSet(group2) ) { - return 1; - } else if ( group1.equals(group2) ) { - return 0; - } else if ( group1.startsWith(WORKING_CACHE_PREFIX) && group2.startsWith(LIVE_CACHE_PREFIX) ) { - return 1; - } else if ( group1.startsWith(LIVE_CACHE_PREFIX) && group2.startsWith(WORKING_CACHE_PREFIX) ) { - return -1; - } else if ( !group1.startsWith(LIVE_CACHE_PREFIX) && !group1.startsWith(WORKING_CACHE_PREFIX) && (group2.startsWith(LIVE_CACHE_PREFIX) || group2.startsWith(WORKING_CACHE_PREFIX)) ) { - return -1; - } else if ( (group1.startsWith(LIVE_CACHE_PREFIX) || group1.startsWith(WORKING_CACHE_PREFIX)) && !group2.startsWith(LIVE_CACHE_PREFIX) && !group2.startsWith(WORKING_CACHE_PREFIX) ) { - return 1; - } else { // neither group1 nor group2 are live or working - return group1.compareToIgnoreCase(group2); - } - } - } -} \ No newline at end of file +} diff --git a/dotCMS/src/main/java/com/dotcms/cache/CacheValue.java b/dotCMS/src/main/java/com/dotcms/cache/CacheValue.java new file mode 100644 index 000000000000..8a2e78f31105 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/cache/CacheValue.java @@ -0,0 +1,19 @@ +package com.dotcms.cache; + +import java.io.Serializable; + +public class CacheValue implements Serializable { + + public final Object value; + public final long ttlInMillis; + + public CacheValue(Object value, long ttlInMillis) { + this.value = value; + this.ttlInMillis = ttlInMillis <= 0 ? Long.MAX_VALUE : ttlInMillis; + } + + public CacheValue(Object value) { + this.value = value; + this.ttlInMillis = Long.MAX_VALUE; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/cache/DynamicTTLCache.java b/dotCMS/src/main/java/com/dotcms/cache/DynamicTTLCache.java new file mode 100644 index 000000000000..a9c4f29ade68 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/cache/DynamicTTLCache.java @@ -0,0 +1,96 @@ +package com.dotcms.cache; + + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.validation.constraints.NotNull; + + +/** + * This class constructs a cache with dynamic TTLs for each key. + * Relies on Caffeine for the underlying cache implementation. + * @param + * @param + */ +public class DynamicTTLCache { + + public final long defaultTTLInMillis; + private final Cache cache; + + public DynamicTTLCache(long maxCapacity) { + this(maxCapacity, Long.MAX_VALUE); + } + + + public DynamicTTLCache(long maxCapacity, long defaultTTLInMillis) { + this.defaultTTLInMillis = defaultTTLInMillis; + this.cache = Caffeine.newBuilder() + .initialCapacity((int) maxCapacity) + .expireAfter(new Expiry() { + @Override + public long expireAfterCreate(@NotNull K key, @NotNull CacheValue value, long currentTime) { + return TimeUnit.MILLISECONDS.toNanos(value.ttlInMillis); + } + + @Override + public long expireAfterUpdate(K key, CacheValue value, long currentTime, + long currentDuration) { + return currentDuration; + } + + @Override + public long expireAfterRead(K key, CacheValue value, long currentTime, long currentDuration) { + return currentDuration; + } + }) + .maximumSize(maxCapacity) + .build(); + } + + public void put(K key, V value, long ttlInMillis) { + + if(value instanceof CacheValue) { + cache.put(key, (CacheValue) value); + }else{ + cache.put(key, new CacheValue(value, ttlInMillis )); + } + + } + + public void put(K key, V value) { + this.put(key, value, defaultTTLInMillis); + } + + public V getIfPresent(K key) { + CacheValue cacheValue = cache.getIfPresent(key); + + return cacheValue != null ? (V) cacheValue.value : null; + } + + + public void invalidate(K key) { + cache.invalidate(key); + } + + public void invalidateAll() { + + cache.invalidateAll(); + } + + public CacheStats stats() { + return cache.stats(); + } + + public long estimatedSize() { + return cache.estimatedSize(); + } + + public Map asMap() { + return cache.asMap(); + } + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheProvider.java b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheProvider.java index 3388260b250c..bb3faaa7e727 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheProvider.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheProvider.java @@ -1,5 +1,6 @@ package com.dotmarketing.business.cache.provider; +import com.dotmarketing.util.Logger; import java.io.Serializable; import java.util.Set; @@ -67,6 +68,11 @@ public abstract class CacheProvider implements Serializable { */ public abstract boolean isInitialized () throws Exception; + public void put(String group, String key, Object content, long ttlMillis){ + Logger.warn(this.getClass(), "This cache implementation does not support per object TTL, ignoring it."); + put(group, key, content); + } + /** * Adds the given content to the given region and for the given key * @@ -132,4 +138,4 @@ public abstract class CacheProvider implements Serializable { */ public abstract void shutdown (); -} \ No newline at end of file +} diff --git a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheStats.java b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheStats.java index 4c3474ab1000..5b55dfee700f 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheStats.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/CacheStats.java @@ -27,12 +27,14 @@ public class CacheStats { public CacheStats(){} - public void addStat(String statName, String value) { + public CacheStats addStat(String statName, String value) { stats.put(statName, value); + return this; } - public void addStat(String statName, long value) { + public CacheStats addStat(String statName, long value) { stats.put(statName, value+""); + return this; } public Set getStatColumns() { @@ -45,4 +47,4 @@ public String getStatValue(String columnName) { -} \ No newline at end of file +} diff --git a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/caffine/CaffineCache.java b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/caffine/CaffineCache.java index f26c41573ae2..0747a89a3024 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/caffine/CaffineCache.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/caffine/CaffineCache.java @@ -1,65 +1,62 @@ package com.dotmarketing.business.cache.provider.caffine; -import com.dotcms.cache.Expirable; + +import com.dotcms.cache.DynamicTTLCache; import com.dotcms.enterprise.cache.provider.CacheProviderAPI; import com.dotmarketing.business.DotStateException; import com.dotmarketing.business.cache.provider.CacheProvider; import com.dotmarketing.business.cache.provider.CacheProviderStats; -import com.dotmarketing.business.cache.provider.CacheSizingUtil; import com.dotmarketing.business.cache.provider.CacheStats; import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.Expiry; -import com.github.benmanes.caffeine.cache.LoadingCache; -import com.google.common.collect.ImmutableSet; -import io.vavr.control.Try; import java.text.DecimalFormat; import java.text.NumberFormat; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicBoolean; /** - * In-Memory Cache implementation using https://github.com/ben-manes/caffeine + * In-Memory Cache implementation using ... *

- * Supports key-specific time invalidations by providing an {@link Expirable} object in the - * {@link #put(String, String, Object)} method with the desired TTL. + * Supports key-specific time invalidations by using the method + * {@link #put(String, String, Object, long)} method with the desired TTL. *

* A group-wide invalidation time can also be set by config properties. *

* i.e., for the "graphqlquerycache" group *

* cache.graphqlquerycache.chain=com.dotmarketing.business.cache.provider.caffine.CaffineCache + * cache.graphqlquerycache.size=10000 * cache.graphqlquerycache.seconds=15 + * + * An individual object's TTL will override the group's TTL. + * + * */ public class CaffineCache extends CacheProvider { - private static final long serialVersionUID = 1348649382678659786L; - - private Boolean isInitialized = false; - - static final String DEFAULT_CACHE = CacheProviderAPI.DEFAULT_CACHE; - - private final ConcurrentHashMap> groups = - new ConcurrentHashMap<>(); - private Set availableCaches; + private static final String CACHE_PREFIX = "cache."; + private static final String SIZE_POSTFIX = ".size"; + private static final String SECONDS_POSTFIX = ".seconds"; + private static final String DEFAULT_CACHE = CacheProviderAPI.DEFAULT_CACHE; + private static final long serialVersionUID = 1L; + // we use a cache for groups because concurrentHashMap has a recusion problem in its computeIfAbsent method + private static final Cache> groups = Caffeine.newBuilder().maximumSize(10000).build(); + private static final AtomicBoolean isInitialized = new AtomicBoolean(false); @Override public String getName() { - return "Caffine Memory Cache"; + return "Caffeine Cache Provider"; } @Override public String getKey() { - return "LocalCaffineMem"; + return "CaffineCache"; } @Override @@ -69,163 +66,136 @@ public boolean isDistributed() { @Override public void init() { - HashSet _availableCaches = new HashSet<>(); - _availableCaches.add(DEFAULT_CACHE); - - Iterator it = Config.getKeys(); - while (it.hasNext()) { - - String key = it.next(); - if (key == null) { - continue; - } - - if (key.startsWith("cache.")) { - - String cacheName = key.split("\\.")[1]; - if (key.endsWith(".size")) { - int inMemory = Config.getIntProperty(key, 0); - _availableCaches.add(cacheName.toLowerCase()); - Logger.debug(this.getClass(), - "***\t Cache Config Memory : " + cacheName + ": " + inMemory); - } - - } + if(!isInitialized.getAndSet(true)){ + groups.invalidateAll(); + Logger.info(this.getClass(), "===== Initializing [" + getName() + "]."); } - this.availableCaches = ImmutableSet.copyOf(_availableCaches); - isInitialized = true; } @Override public boolean isInitialized() throws Exception { - return isInitialized; + return isInitialized.get(); } @Override - public void put(String group, String key, Object content) { - // Get the cache for the given group - Cache cache = getCache(group); - // Add the given content to the group and for a given key - cache.put(key, content); + public void put(String group, String key, Object content, long ttlMillis) { + if ( key == null || content == null) { + return; + } + DynamicTTLCache cache = getCache(group); + cache.put(key, content, ttlMillis); + } @Override - public Object get(String group, String key) { - // Get the cache for the given group - Cache cache = getCache(group); - // Get the content from the group and for a given key - return cache.getIfPresent(key); + public void put(String group, String key, Object content) { + put(group, key, content, Long.MAX_VALUE); + } @Override - public void remove(String group) { - - // Get the cache for the given group - Cache cache = getCache(group); + public Object get(String group, String key) { + return getCache(group).getIfPresent(key); - // Invalidates the Cache for the given group - cache.invalidateAll(); - // Remove this group from the global list of cache groups - groups.remove(group); } @Override public void remove(String group, String key) { - // Get the cache for the given group - Cache cache = getCache(group); - // Invalidates from Cache a key from a given group - cache.invalidate(key); + getCache(group).invalidate(key); } @Override - public void removeAll() { - - Set currentGroups = new HashSet<>(); - currentGroups.addAll(getGroups()); - - for (String group : currentGroups) { - remove(group); + public void remove(String group) { + Logger.debug(this.getClass(), "===== Calling remove for [" + getName() + + "] - " + group); + if (group == null) { + return; } - - groups.clear(); + groups.invalidate(group.toLowerCase()); } @Override - public Set getGroups() { - return groups.keySet(); + public void removeAll() { + groups.invalidateAll(); } @Override public Set getKeys(String group) { + return getCache(group).asMap().keySet(); + } + + @Override + public Set getGroups() { + return groups.asMap().keySet(); + } + + private long getTTLMillis(String group) { + if("system_cache".equals(group)){ + return UtilMethods.isSet(System.getenv("DOT_CACHE_SYSTEM_GROUP_SECONDS")) + ? Long.parseLong(System.getenv("DOT_CACHE_SYSTEM_GROUP_SECONDS")) + : Long.MAX_VALUE; + } - Set keys = new HashSet<>(); + long seconds = Config.getLongProperty(CACHE_PREFIX + group + SECONDS_POSTFIX, + Config.getLongProperty(CACHE_PREFIX + DEFAULT_CACHE + SECONDS_POSTFIX, -1)); - Cache cache = getCache(group); - Map m = cache.asMap(); + return seconds < 0 ? Long.MAX_VALUE : seconds * 1000; + } - if (m != null) { - keys = m.keySet(); + private int getMaxSize(String group) { + if("system_cache".equals(group)){ + return UtilMethods.isSet(System.getenv("DOT_CACHE_SYSTEM_GROUP_SIZE")) + ? Integer.parseInt(System.getenv("DOT_CACHE_SYSTEM_GROUP_SIZE")) + : 5000; } - return keys; + return Config.getIntProperty(CACHE_PREFIX + group + SIZE_POSTFIX, + Config.getIntProperty(CACHE_PREFIX + DEFAULT_CACHE + SIZE_POSTFIX, 1000)); + } + @Override public CacheProviderStats getStats() { CacheStats providerStats = new CacheStats(); CacheProviderStats ret = new CacheProviderStats(providerStats, getName()); - Set currentGroups = new HashSet<>(); - currentGroups.addAll(getGroups()); + Set currentGroups = new TreeSet<>(getGroups()); + NumberFormat nf = DecimalFormat.getInstance(); DecimalFormat pf = new DecimalFormat("##.##%"); - - CacheSizingUtil sizer = new CacheSizingUtil(); - for (String group : currentGroups) { - final CacheStats stats = new CacheStats(); + CacheStats stats = new CacheStats(); - final Cache foundCache = getCache(group); + DynamicTTLCache foundCache = getCache(group); - final boolean isDefault = (Config.getIntProperty("cache." + group + ".size", -1) == -1); + long size = getMaxSize(group); - final int size = isDefault ? Config.getIntProperty("cache." + DEFAULT_CACHE + ".size") - : (Config.getIntProperty("cache." + group + ".size", -1) != -1) - ? Config.getIntProperty("cache." + group + ".size") - : Config.getIntProperty("cache." + DEFAULT_CACHE + ".size"); + long millis = foundCache.defaultTTLInMillis; - final int seconds = - isDefault ? Config.getIntProperty("cache." + DEFAULT_CACHE + ".seconds", -1) - : Config.getIntProperty("cache." + group + ".seconds", -1); + String duration = millis == Long.MAX_VALUE ? "" : " | ttl:" + nf.format(millis / 1000) + "s"; com.github.benmanes.caffeine.cache.stats.CacheStats cstats = foundCache.stats(); - stats.addStat(CacheStats.REGION, group); - stats.addStat(CacheStats.REGION_DEFAULT, isDefault + ""); - stats.addStat(CacheStats.REGION_CONFIGURED_SIZE, - "size:" + nf.format(size) + " / " + (seconds == -1? "infinity": seconds + "s") ); - stats.addStat(CacheStats.REGION_SIZE, nf.format(foundCache.estimatedSize())); - stats.addStat(CacheStats.REGION_LOAD, - nf.format(cstats.missCount() + cstats.hitCount())); - stats.addStat(CacheStats.REGION_HITS, nf.format(cstats.hitCount())); - stats.addStat(CacheStats.REGION_HIT_RATE, pf.format(cstats.hitRate())); - stats.addStat(CacheStats.REGION_AVG_LOAD_TIME, - nf.format(cstats.averageLoadPenalty() / 1000000) + " ms"); - stats.addStat(CacheStats.REGION_EVICTIONS, nf.format(cstats.evictionCount())); - - long averageObjectSize = Try.of(() -> sizer.averageSize(foundCache.asMap())) - .getOrElse(-1L); - long totalObjectSize = averageObjectSize * foundCache.estimatedSize(); - - stats.addStat(CacheStats.REGION_MEM_PER_OBJECT, - "

" + String.format("%010d", averageObjectSize) + "
" - + UtilMethods.prettyByteify(averageObjectSize)); - stats.addStat(CacheStats.REGION_MEM_TOTAL_PRETTY, - "
" + String.format("%010d", totalObjectSize) + "
" - + UtilMethods.prettyByteify(totalObjectSize)); + stats.addStat(CacheStats.REGION, group) + .addStat(CacheStats.REGION_DEFAULT, "false") + .addStat(CacheStats.REGION_CONFIGURED_SIZE, "size:" + nf.format(size) + duration ) + .addStat(CacheStats.REGION_SIZE, nf.format(foundCache.estimatedSize())) + .addStat(CacheStats.REGION_LOAD, nf.format(cstats.missCount() + cstats.hitCount())) + .addStat(CacheStats.REGION_HITS, nf.format(cstats.hitCount())) + .addStat(CacheStats.REGION_HIT_RATE, pf.format(cstats.hitRate())) + .addStat(CacheStats.REGION_AVG_LOAD_TIME, nf.format(cstats.averageLoadPenalty() / 1000000) + " ms") + .addStat(CacheStats.REGION_EVICTIONS, nf.format(cstats.evictionCount())); + ret.addStatRecord(stats); + } + + if (currentGroups.isEmpty()) { + CacheStats stats = new CacheStats(); + stats.addStat(CacheStats.REGION, "n/a"); + stats.addStat(CacheStats.REGION_SIZE, 0); ret.addStatRecord(stats); } @@ -235,87 +205,30 @@ public CacheProviderStats getStats() { @Override public void shutdown() { Logger.info(this.getClass(), "===== Calling shutdown [" + getName() + "]."); - isInitialized = false; + isInitialized.set(false); } - private Cache getCache(String cacheName) { + private DynamicTTLCache getCache(String cacheName) { if (cacheName == null) { throw new DotStateException("Null cache region passed in"); } - cacheName = cacheName.toLowerCase(); - Cache cache = groups.get(cacheName); - - // init cache if it does not exist - if (cache == null) { - synchronized (cacheName.intern()) { - cache = groups.get(cacheName); - if (cache == null) { - - boolean separateCache = (Config.getBooleanProperty( - "cache.separate.caches.for.non.defined.regions", true) - || availableCaches.contains(cacheName) - || DEFAULT_CACHE.equals(cacheName)); - - if (separateCache) { - int size = Config.getIntProperty("cache." + cacheName + ".size", -1); - - if (size == -1) { - size = Config.getIntProperty("cache." + DEFAULT_CACHE + ".size", 100); - } - - final int defaultTTL = Config.getIntProperty( - "cache." + cacheName + ".seconds", -1); - - Logger.debug(this.getClass(), - "***\t Building Cache : " + cacheName + ", size:" + size - + ",Concurrency:" - + Config.getIntProperty("cache.concurrencylevel", 32)); - - cache = Caffeine.newBuilder() - .maximumSize(size) - .recordStats() - .expireAfter(new Expiry() { - public long expireAfterCreate(String key, Object value, - long currentTime) { - long ttlInSeconds; - - if (value instanceof Expirable - && ((Expirable) value).getTtl() > 0) { - ttlInSeconds = ((Expirable) value).getTtl(); - } else if (defaultTTL > 0) { - ttlInSeconds = defaultTTL; - } else { - ttlInSeconds = Long.MAX_VALUE; - } - - return TimeUnit.SECONDS.toNanos(ttlInSeconds); - } - - public long expireAfterUpdate(String key, Object value, - long currentTime, long currentDuration) { - return currentDuration; - } - - public long expireAfterRead(String key, Object value, - long currentTime, long currentDuration) { - return currentDuration; - } - }) - .build(key -> null); - - groups.put(cacheName, cache); - - } else { - Logger.debug(this.getClass(), - "***\t No Cache for : " + cacheName + ", using " + DEFAULT_CACHE); - cache = getCache(DEFAULT_CACHE); - groups.put(cacheName, cache); - } - } - } + DynamicTTLCache cache = groups.getIfPresent(cacheName); + if(cache != null){ + return cache; } - return cache; + final int maxSize = getMaxSize(cacheName); + final long ttlSeconds = getTTLMillis(cacheName); + synchronized (groups) { + return groups.get(cacheName, k -> + + new DynamicTTLCache<>(maxSize, ttlSeconds) + ); + } + + + } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/timedcache/TimedCacheProvider.java b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/timedcache/TimedCacheProvider.java index cde5eef3a339..023344c6a38b 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/timedcache/TimedCacheProvider.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/cache/provider/timedcache/TimedCacheProvider.java @@ -1,42 +1,19 @@ package com.dotmarketing.business.cache.provider.timedcache; -import java.text.DecimalFormat; -import java.text.NumberFormat; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -import com.dotcms.enterprise.cache.provider.CacheProviderAPI; -import com.dotmarketing.business.DotStateException; -import com.dotmarketing.business.cache.provider.CacheProvider; -import com.dotmarketing.business.cache.provider.CacheProviderStats; -import com.dotmarketing.business.cache.provider.CacheStats; -import com.dotmarketing.util.Config; -import com.dotmarketing.util.Logger; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; - +import com.dotmarketing.business.cache.provider.caffine.CaffineCache; /** - * @author Jonathan Gamba Date: 9/2/15 + * Provides a timed cache implementation using Caffeine. + * @deprecated + * This class is deprecated and will be removed in a future version - use CaffineCache instead as it + * provides a more robust and efficient implementation. + *

Use {@link com.dotmarketing.business.cache.provider.caffine.CaffineCache} instead. */ -public class TimedCacheProvider extends CacheProvider { +@Deprecated(since = "24.12", forRemoval = false) +public class TimedCacheProvider extends CaffineCache { private static final long serialVersionUID = 1L; - private Boolean isInitialized = false; - - private final ConcurrentHashMap> groups = new ConcurrentHashMap<>(); - - static final String DEFAULT_CACHE = CacheProviderAPI.DEFAULT_CACHE; - - - private final HashSet availableCaches = new HashSet<>(); - - private final int DEFAULT_TIMEOUT = 100; - @Override public String getName() { return "Timed Cache Provider"; @@ -47,219 +24,7 @@ public String getKey() { return "Timed Cache Provider"; } - @Override - public boolean isDistributed() { - return false; - } - - @Override - public void init() { - groups.clear(); - Logger.info(this.getClass(), "===== Initializing [" + getName() + "]."); - availableCaches.add(DEFAULT_CACHE); - Iterator it = Config.getKeys(); - while (it.hasNext()) { - String key = it.next(); - if (key == null) { - continue; - } - if (key.startsWith("cache.")) { - String cacheName = key.split("\\.")[1]; - if (key.endsWith(".size")) { - int inMemory = Config.getIntProperty(key, 0); - availableCaches.add(cacheName.toLowerCase()); - Logger.info(this.getClass(), "***\t Cache Config Memory : " - + cacheName + ": " + inMemory); - } - } - } - - isInitialized = true; - } - - @Override - public boolean isInitialized () throws Exception { - return isInitialized; - } - - @Override - public void put(String group, String key, Object content) { - if (group == null || key == null || content == null) { - return; - } - Cache cache = getCache(group); - cache.put(key, content); - } - - @Override - public Object get(String group, String key) { - // Get the cache for the given group - Cache cache = getCache(group); - return cache.getIfPresent(key); - - } - - @Override - public void remove(String group, String key) { - - Cache cache = getCache(group); - // Invalidates from Cache a key from a given group - cache.invalidate(key); - } - - @Override - public void remove(String group) { - Logger.debug(this.getClass(), "===== Calling remove for [" + getName() - + "] - " + cacheKey(group, "")); - // Get the cache for the given group - Cache cache = getCache(group); - // Invalidates the Cache for the given group - cache.invalidateAll(); - // Remove this group from the global list of cache groups - groups.remove(group); - } - @Override - public void removeAll() { - this.init(); - } - - @Override - public Set getKeys(String group) { - Cache cache = getCache(group); - return cache.asMap().keySet(); - } - - @Override - public Set getGroups() { - return groups.keySet(); - } - - @Override - public CacheProviderStats getStats() { - - CacheStats providerStats = new CacheStats(); - CacheProviderStats ret = new CacheProviderStats(providerStats,getName()); - - Set currentGroups = new HashSet<>(); - currentGroups.addAll(getGroups()); - - NumberFormat nf = DecimalFormat.getInstance(); - DecimalFormat pf = new DecimalFormat("##.##%"); - for (String group : currentGroups) { - CacheStats stats = new CacheStats(); - - Cache foundCache = getCache(group); - - - boolean isDefault = (Config.getIntProperty("cache." + group + ".size", -1) == -1 && Config.getIntProperty("cache." + group + ".seconds", -1) == -1); - - - int size = isDefault ? Config.getIntProperty("cache." + DEFAULT_CACHE + ".size", 100) - : (Config.getIntProperty("cache." + group + ".size", -1) != -1) - ? Config.getIntProperty("cache." + group + ".size") - : Config.getIntProperty("cache." + DEFAULT_CACHE + ".size", 100); - - int seconds = isDefault ? Config.getIntProperty("cache." + DEFAULT_CACHE + ".seconds", 100) - : (Config.getIntProperty("cache." + group + ".seconds", -1) != -1) - ? Config.getIntProperty("cache." + group + ".seconds") - : Config.getIntProperty("cache." + DEFAULT_CACHE + ".seconds", 100); - - - - com.github.benmanes.caffeine.cache.stats.CacheStats cstats = foundCache.stats(); - stats.addStat(CacheStats.REGION, group); - stats.addStat(CacheStats.REGION_DEFAULT, isDefault + ""); - stats.addStat(CacheStats.REGION_CONFIGURED_SIZE, "size:" + nf.format(size) + " / " + seconds + "s"); - stats.addStat(CacheStats.REGION_SIZE, nf.format(foundCache.estimatedSize())); - stats.addStat(CacheStats.REGION_LOAD, nf.format(cstats.missCount()+cstats.hitCount())); - stats.addStat(CacheStats.REGION_HITS, nf.format(cstats.hitCount())); - stats.addStat(CacheStats.REGION_HIT_RATE, pf.format(cstats.hitRate())); - stats.addStat(CacheStats.REGION_AVG_LOAD_TIME, nf.format(cstats.averageLoadPenalty()/1000000) + " ms"); - stats.addStat(CacheStats.REGION_EVICTIONS, nf.format(cstats.evictionCount())); - - - ret.addStatRecord(stats); - } - - if(currentGroups.isEmpty()) { - CacheStats stats = new CacheStats(); - stats.addStat(CacheStats.REGION, "n/a"); - stats.addStat(CacheStats.REGION_SIZE, 0); - ret.addStatRecord(stats); - } - - return ret; - } - - @Override - public void shutdown() { - Logger.info(this.getClass(), "===== Calling shutdown [" + getName()+ "]."); - isInitialized = false; - } - - private String cacheKey(String group, String key) { - return (group + ":" + key).toLowerCase(); - } - - private Cache getCache(String cacheName) { - if (cacheName == null) { - throw new DotStateException("Null cache region passed in"); - } - cacheName = cacheName.toLowerCase(); - Cache cache = groups.get(cacheName); - // init cache if it does not exist - if (cache == null) { - synchronized (cacheName.intern()) { - cache = groups.get(cacheName); - // init cache if it does not exist - if (cache == null) { - synchronized (cacheName.intern()) { - cache = groups.get(cacheName); - if (cache == null) { - - boolean separateCache = (Config.getBooleanProperty( - "cache.separate.caches.for.non.defined.regions", true) - || availableCaches.contains(cacheName) - || DEFAULT_CACHE.equals(cacheName)); - - if (separateCache) { - int size = Config.getIntProperty("cache." + cacheName + ".size", -1); - int seconds = Config.getIntProperty("cache." + cacheName + ".seconds", -1); - if (size == -1) { - size = Config.getIntProperty("cache." + DEFAULT_CACHE + ".size", 100); - } - if (seconds == -1) { - seconds = Config.getIntProperty("cache." + DEFAULT_CACHE + ".seconds", 100); - } - Logger.infoEvery(this.getClass(), - "***\t Building Cache : " + cacheName + ", size:" + size + ", seconds:" + seconds - + ",Concurrency:" - + Config.getIntProperty("cache.concurrencylevel", 32), 60000); - cache = Caffeine.newBuilder() - .maximumSize(size) - .expireAfterWrite(seconds, TimeUnit.SECONDS) - .recordStats() - //.softValues() - .build(); - - - groups.put(cacheName, cache); - - } else { - Logger.infoEvery(this.getClass(), - "***\t No Cache for : " + cacheName + ", using " + DEFAULT_CACHE, 60000); - cache = getCache(DEFAULT_CACHE); - groups.put(cacheName, cache); - } - } - } - } - - } - } - return cache; - } diff --git a/dotCMS/src/main/resources/dotmarketing-config.properties b/dotCMS/src/main/resources/dotmarketing-config.properties index aa82eeed3aee..dd3ffe1d0d6e 100644 --- a/dotCMS/src/main/resources/dotmarketing-config.properties +++ b/dotCMS/src/main/resources/dotmarketing-config.properties @@ -389,8 +389,6 @@ asset.cache.control.max.days=30 ##################### dotCMS Cache Configuration ##################### cache.default.chain =com.dotmarketing.business.cache.provider.caffine.CaffineCache cache.contentletcache.chain =com.dotmarketing.business.cache.provider.caffine.CaffineCache,com.dotmarketing.business.cache.provider.h22.H22Cache -cache.vanityurlcache.chain =com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider -cache.cachedvanityurlgroup.chain=com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider @@ -402,16 +400,13 @@ cache.velocitycache.chain =com.dotmarketing.business.cache.provider.caffine.Ca cache.velocitymacrocache.chain =com.dotmarketing.business.cache.provider.caffine.CaffineCache,com.dotmarketing.business.cache.provider.h22.H22Cache ## Cache API Token lookups for a hour (invalidations still apply) -cache.apitokencache.chain=com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider cache.apitokencache.seconds=3600 ## Cache Elasticsearch Queries for an hour - this cache gets in validated whenever a contentlet is checked in -cache.esquerycache.chain=com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider cache.esquerycache.seconds=3600 cache.esquerycache.size=10000 ## Cache Elasticsearch Queries for an hour - this cache gets in validated whenever a contentlet is checked in -cache.esquerycountcache.chain=com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider cache.esquerycountcache.seconds=3600 cache.esquerycountcache.size=10000 @@ -422,7 +417,6 @@ cache.graphqlquerycache.size=5000 ## Cache doesUserHavePermission calls for 60 seconds -cache.permissionshortlived.chain=com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider cache.permissionshortlived.seconds=60 cache.permissionshortlived.size=100000 diff --git a/dotCMS/src/test/java/com/dotcms/cache/DynamicTTLCacheTest.java b/dotCMS/src/test/java/com/dotcms/cache/DynamicTTLCacheTest.java new file mode 100644 index 000000000000..184001ee25c4 --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/cache/DynamicTTLCacheTest.java @@ -0,0 +1,90 @@ +package com.dotcms.cache; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +public class DynamicTTLCacheTest { + + + private DynamicTTLCache cache; + + @BeforeEach + public void setUp() { + cache = new DynamicTTLCache<>(100, 5000); // maxCapacity = 100, defaultTTLInMillis = 5000 + } + + @Test + public void testPutAndGetIfPresent() { + cache.put("key1", "value1", 2000); + assertEquals("value1", cache.getIfPresent("key1")); + } + + @Test + public void testGetIfPresentAfterTTLExpires() throws InterruptedException { + cache.put("key2", "value2", 1000); + assertNotNull(cache.getIfPresent("key2")); + TimeUnit.MILLISECONDS.sleep(1500); + assertNull(cache.getIfPresent("key2")); + } + + @Test + public void testInvalidate() { + cache.put("key3", "value3", 2000); + cache.invalidate("key3"); + assertNull(cache.getIfPresent("key3")); + } + + @Test + public void testInvalidateAll() { + cache.put("key4", "value4", 2000); + cache.put("key5", "value5", 2000); + cache.invalidateAll(); + assertNull(cache.getIfPresent("key4")); + assertNull(cache.getIfPresent("key5")); + } + + @Test + public void testEstimatedSize() { + cache.invalidateAll(); + cache.put("key6", "value6", 2000); + cache.put("key7", "value7", 2000); + assertEquals(2, cache.estimatedSize()); + } + + @Test + public void testStats() { + cache.put("key8", "value8", 2000); + assertNotNull(cache.getIfPresent("key8")); + + assertNotNull(cache.stats()); + } + + @Test + public void testCacheExpiry()throws InterruptedException { + cache.put("key8", "value8"); + TimeUnit.MILLISECONDS.sleep(6000); + assertNull(cache.getIfPresent("key8")); + + } + + + @Test + public void testCacheTTLGreaterThanObjectTTL()throws InterruptedException { + // cache has a 5000ms timeout + cache.put("10Seconds", "10Seconds", 10000); + TimeUnit.MILLISECONDS.sleep(7000); + assertNotNull(cache.getIfPresent("10Seconds")); + TimeUnit.MILLISECONDS.sleep(4000); + assertNull(cache.getIfPresent("10Seconds")); + } + + + + +} diff --git a/dotcms-integration/src/test/resources/dotmarketing-config.properties b/dotcms-integration/src/test/resources/dotmarketing-config.properties index 784625d984fc..fd49c4489495 100644 --- a/dotcms-integration/src/test/resources/dotmarketing-config.properties +++ b/dotcms-integration/src/test/resources/dotmarketing-config.properties @@ -515,8 +515,7 @@ asset.cache.control.max.days=30 cache.default.chain =com.dotmarketing.business.cache.provider.caffine.CaffineCache cache.contentletcache.chain =com.dotmarketing.business.cache.provider.caffine.CaffineCache,com.dotmarketing.business.cache.provider.h22.H22Cache cache.velocitycache.chain =com.dotmarketing.business.cache.provider.caffine.CaffineCache,com.dotmarketing.business.cache.provider.h22.H22Cache -cache.vanityurlcache.chain =com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider -cache.cachedvanityurlgroup.chain=com.dotmarketing.business.cache.provider.timedcache.TimedCacheProvider + CACHE_INVALIDATION_TRANSPORT_CLASS=com.dotmarketing.business.cache.transport.NullTransport @@ -881,4 +880,4 @@ DELETE_CONTENT_TYPE_ASYNC_WITH_JOB=false secrets.scripting.enabled=true FEATURE_FLAG_DUMMY_FALSE=false -FEATURE_FLAG_DUMMY_TRUE=true \ No newline at end of file +FEATURE_FLAG_DUMMY_TRUE=true