From e8d1113f58ca6debc0f64dac3bb5d5d4b543c125 Mon Sep 17 00:00:00 2001 From: Robert Baillie Date: Tue, 26 Apr 2022 16:21:21 +0100 Subject: [PATCH] Added ageing capabilities to SessionCache and NullCache Added ageing methods to the Cache Adaptor interface so it's now universal --- TODO.txt | 4 +- .../fflib-extension/caching/ICacheAdaptor.cls | 2 + .../fflib-extension/caching/NullCache.cls | 10 + .../fflib-extension/caching/SessionCache.cls | 57 ++++- .../caching/tests/NullCacheTest.cls | 19 ++ .../caching/tests/OrgCacheTest.cls | 4 +- .../caching/tests/SessionCacheTest.cls | 221 ++++++++++++++++++ 7 files changed, 309 insertions(+), 8 deletions(-) diff --git a/TODO.txt b/TODO.txt index 31969268f02..9e8494d0699 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,10 +1,8 @@ TODO: * Combine the DateTimeUtils and DateLiterals where appropriate * Document TestDateTimeUtils and how to set now and today - * Test ageing using dates on the OrgCache - * Implement ageing into the SessionCache - * Add the aged get definitions into ICacheAdaptor * Add 'maximum age' into CachedSoqlExecutor + * Consider a superclass for OrgCache and SessionCache? diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls index a145dcd70c1..46154eee093 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/ICacheAdaptor.cls @@ -12,6 +12,8 @@ public interface ICacheAdaptor Boolean hasAccessToCache(); Boolean isACache(); Object get( String key ); + Object get( String key, DateTime minimumDateTime ); + Object get( String key, Long maximumAgeInSeconds ); void put( String key, Object value, Integer lifespan ); Boolean contains( String key ); void remove( String key ); diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls index 7e3d189cc44..41add1bda1d 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/NullCache.cls @@ -22,6 +22,16 @@ public inherited sharing class NullCache implements ICacheAdaptor return null; } + public Object get( String key, DateTime minimumDateTime ) + { + return null; + } + + public Object get( String key, Long maximumAgeInSeconds ) + { + return null; + } + public Set getKeys() { return new Set(); diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls index beb482d1b90..d14f295dea1 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/SessionCache.cls @@ -3,7 +3,6 @@ * * All users are assumed to have access to the cache. */ -// TODO: add age capabilities public inherited sharing class SessionCache implements ICacheAdaptor { private static final String PARTITION_NAME = 'core'; @@ -35,9 +34,58 @@ public inherited sharing class SessionCache implements ICacheAdaptor * @return Object The cached object, if it exists */ public Object get( String key ) + { + Long maximumAgeInSeconds = null; + return get( key, maximumAgeInSeconds ); + } + + /** + * Retrieve the cache entry with the given key, but only if it was created after the stated date time + * + * If the entry does not exist in the cache (I.E. is a "cache miss"), will return null + * + * @param String The key for the object to retrieve + * @return Object The cached object, if it exists + */ + public Object get( String key, DateTime minimumDateTime ) + { + Long maximumAgeInSeconds; + if ( minimumDateTime != null ) + { + maximumAgeInSeconds = DateTimeUtils.getEpochTime() - DateTimeUtils.getEpochTime( minimumDateTime ); + } + return get( key, maximumAgeInSeconds ); + } + + /** + * Retrieve the cache entry with the given key, but only if it is younger than the stated number of seconds + * + * If the entry does not exist in the cache (I.E. is a "cache miss"), will return null + * + * @param String The key for the object to retrieve + * @return Object The cached object, if it exists + */ + public Object get( String key, Long maximumAgeInSeconds ) { Contract.requires( String.isNotBlank( key ), 'get called with a blank key' ); - return Cache.Session.get( key ); + + Object rawCacheValue = Cache.Session.get( createFullyQualifiedKey( key ) ); + + if ( rawCacheValue instanceOf CacheEntry ) + { + CacheEntry cacheValue = (CacheEntry)rawCacheValue; + if ( maximumAgeInSeconds == null || cacheValue.isYoungerThanOrEqualTo( maximumAgeInSeconds ) ) + { + return cacheValue.getValue(); + } + } + + if ( maximumAgeInSeconds == null ) // if we don't care about the age, then return it + { + return rawCacheValue; + } + + return null; // if there is no age, or it is too old, regard it as a cache miss } /** @@ -66,7 +114,9 @@ public inherited sharing class SessionCache implements ICacheAdaptor Contract.requires( lifespan >= 0, 'put called with a negative lifespan' ); // Note that the maximum is handled by Salesforce, just in case it increases - Cache.Session.put( key, value, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace + CacheEntry cacheEntry = new CacheEntry( value ); + + Cache.Session.put( key, cacheEntry, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace } /** @@ -112,6 +162,7 @@ public inherited sharing class SessionCache implements ICacheAdaptor return Cache.SessionPartition.createFullyQualifiedPartition( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME ); } + @testVisible private String createFullyQualifiedKey( String key ) { return Cache.SessionPartition.createFullyQualifiedKey( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME, key ); diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls index e1edc93de90..b2f4ed0dc5c 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/NullCacheTest.cls @@ -1,6 +1,9 @@ @isTest private without sharing class NullCacheTest { + private final static Long NULL_AGE = null; + private final static DateTime NULL_MIN_DATE_TIME = null; + @isTest private static void hasAccessToCache_whenCalled_returnsTrue() // NOPMD: Test method name format { @@ -25,6 +28,22 @@ private without sharing class NullCacheTest System.assertEquals( null, got, 'get, when called, will return null' ); } + @isTest + private static void get_maxAge_whenCalled_returnsNull() // NOPMD: Test method name format + { + NullCache cache = new NullCache(); + Object got = cache.get( null, NULL_AGE ); + System.assertEquals( null, got, 'get, when called with a max age, will return null' ); + } + + @isTest + private static void get_minDate_whenCalled_returnsNull() // NOPMD: Test method name format + { + NullCache cache = new NullCache(); + Object got = cache.get( null, NULL_MIN_DATE_TIME ); + System.assertEquals( null, got, 'get, when called with a min datetime, will return null' ); + } + @isTest private static void put_whenCalled_doesNothing() // NOPMD: Test method name format { diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls index 1112e87f32c..ed7b2506daa 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/OrgCacheTest.cls @@ -4,7 +4,7 @@ private without sharing class OrgCacheTest private final static Integer DEFAULT_LIFESPAN = 1000; private final static DateTime BASE_TIME = DateTime.newInstanceGmt( 1974, 08, 24, 22, 45, 00 ); private final static Long NULL_AGE = null; - private final static DateTime NULL_MAX_DATE_TIME = null; + private final static DateTime NULL_MIN_DATE_TIME = null; @isTest private static void hasAccessToCache_whenCalledByUserWithCacheAccess_returnsTrue() // NOPMD: Test method name format @@ -314,7 +314,7 @@ private without sharing class OrgCacheTest Cache.Org.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject ); Test.startTest(); - Object got = cacheInstance.get( 'key', NULL_MAX_DATE_TIME ); + Object got = cacheInstance.get( 'key', NULL_MIN_DATE_TIME ); Test.stopTest(); System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when no time is specified and the cached value has no age, will return the value' ); diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls index 15ede758276..ea8fbdcb4ec 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/caching/tests/SessionCacheTest.cls @@ -2,6 +2,9 @@ private without sharing class SessionCacheTest { private final static Integer DEFAULT_LIFESPAN = 1000; + private final static DateTime BASE_TIME = DateTime.newInstanceGmt( 1974, 08, 24, 22, 45, 00 ); + private final static Long NULL_AGE = null; + private final static DateTime NULL_MIN_DATE_TIME = null; @isTest private static void hasAccessToCache_whenCalled_returnsTrue() // NOPMD: Test method name format @@ -38,6 +41,224 @@ private without sharing class SessionCacheTest System.assertEquals( expected, got, 'get, when called with a key that is in the cache, will return it' ); } + @isTest + private static void get_maxAge_whenTheEntryIsTooOld_returnsNull() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + TestDateTimeUtils.addToCurrentTime( 100 ); + Object got = cache.get( 'key', 50 ); + Test.stopTest(); + + System.assertEquals( null, got, 'get, passing maximumAge, when the entry is older than the stated number of seconds, will return null' ); + } + + @isTest + private static void get_maxAge_whenTheEntryIsExactlyRight_returnsTheValue() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + TestDateTimeUtils.addToCurrentTime( 100 ); + Object got = cache.get( 'key', 100 ); + Test.stopTest(); + + System.assertEquals( cachedObject, got, 'get, passing maximumAge, when the entry is exactly the stated number of seconds, will return the cached value' ); + } + + @isTest + private static void get_maxAge_whenTheEntryIsYounger_returnsTheValue() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + TestDateTimeUtils.addToCurrentTime( 50 ); + Object got = cache.get( 'key', 100 ); + Test.stopTest(); + + System.assertEquals( cachedObject, got, 'get, passing maximumAge, when the entry is younger than the stated number of seconds, will return the cached value' ); + } + + @isTest + private static void get_maxAge_doesNotResetTheAge() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + TestDateTimeUtils.addToCurrentTime( 50 ); + cache.get( 'key', 100 ); + + TestDateTimeUtils.addToCurrentTime( 50 ); + cache.get( 'key', 100 ); + + TestDateTimeUtils.addToCurrentTime( 50 ); + Object got = cache.get( 'key', 100 ); + Test.stopTest(); + + System.assertEquals( null, got, 'get, passing maximumAge, does not reset the age of the cached entity, so when its too old it returns null' ); + } + + @isTest + private static void get_maxAge_whenNoAgeSpecifiedAndTheCachedValueHasNoAge_returnsTheValue() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cacheInstance = new SessionCache(); + + Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject ); + + Test.startTest(); + Object got = cacheInstance.get( 'key', NULL_AGE ); + Test.stopTest(); + + System.assertEquals( cachedObject, got, 'get, passing maximumAge, when no age is specified and the cached value has no age, will return the value' ); + } + + @isTest + private static void get_maxAge_whenAgeSpecifiedAndTheCachedValueHasNoAge_returnsNull() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cacheInstance = new SessionCache(); + + Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject ); + + Test.startTest(); + Object got = cacheInstance.get( 'key', 100 ); + Test.stopTest(); + + System.assertEquals( null, got, 'get, passing maximumAge, when an age is specified and the cached value has no age, will return null' ); + } + + @isTest + private static void get_minDateTime_whenTheEntryIsTooOld_returnsNull() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + Object got = cache.get( 'key', BASE_TIME.addSeconds( 50 ) ); + Test.stopTest(); + + System.assertEquals( null, got, 'get, passing minimumDateTime, when the entry is older than the stated time, will return null' ); + } + + @isTest + private static void get_minDateTime_whenTheEntryIsExactlyRight_returnsTheValue() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + Object got = cache.get( 'key', BASE_TIME ); + Test.stopTest(); + + System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when the entry is exactly the stated time, will return the cached value' ); + } + + @isTest + private static void get_minDateTime_whenTheEntryIsYounger_returnsTheValue() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + Object got = cache.get( 'key', BASE_TIME.addSeconds( -50 ) ); + Test.stopTest(); + + System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when the entry is younger than the stated time, will return the cached value' ); + } + + @isTest + private static void get_minDateTime_doesNotResetTheAge() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cache = new SessionCache(); + + TestDateTimeUtils.setCurrentDateTime( BASE_TIME ); + cache.put( 'key', cachedObject, DEFAULT_LIFESPAN ); + + Test.startTest(); + TestDateTimeUtils.addToCurrentTime( 50 ); + cache.get( 'key', BASE_TIME.addSeconds( -50 ) ); + + TestDateTimeUtils.addToCurrentTime( 50 ); + cache.get( 'key', BASE_TIME.addSeconds( -100 ) ); + + TestDateTimeUtils.addToCurrentTime( 50 ); + Object got = cache.get( 'key', BASE_TIME.addSeconds( 100 ) ); + Test.stopTest(); + + System.assertEquals( null, got, 'get, passing minimumDateTime, does not reset the age of the cached entity, so when its too old it returns null' ); + } + + @isTest + private static void get_minDateTime_whenNoAgeSpecifiedAndTheCachedValueHasNoAge_returnsTheValue() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cacheInstance = new SessionCache(); + + Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject ); + + Test.startTest(); + Object got = cacheInstance.get( 'key', NULL_MIN_DATE_TIME ); + Test.stopTest(); + + System.assertEquals( cachedObject, got, 'get, passing minimumDateTime, when no time is specified and the cached value has no age, will return the value' ); + } + + @isTest + private static void get_minDateTime_whenAgeSpecifiedAndTheCachedValueHasNoAge_returnsNull() // NOPMD: Test method name format + { + String cachedObject = 'thecachedthing'; + + SessionCache cacheInstance = new SessionCache(); + + Cache.Session.put( cacheInstance.createFullyQualifiedKey( 'key' ), cachedObject ); + + Test.startTest(); + Object got = cacheInstance.get( 'key', BASE_TIME.addSeconds( 100 ) ); + Test.stopTest(); + + System.assertEquals( null, got, 'get, passing minimumDateTime, when a time is specified and the cached value has no age, will return null' ); + } + + @isTest private static void put_whenCalledMultipleTimesWithTheSameKey_overwritesIt() // NOPMD: Test method name format {