Skip to content

Commit

Permalink
Merge pull request apex-enterprise-patterns#38 from OrtooApps/feature…
Browse files Browse the repository at this point in the history
…/timestamped-cache-entries

Feature/timestamped cache entries
  • Loading branch information
rob-baillie-ortoo committed Apr 27, 2022
2 parents a6c4484 + cd363c5 commit 933fe55
Show file tree
Hide file tree
Showing 24 changed files with 1,612 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Represents an entry that exists in the cache.
*
* Is dated based on the creation time so that entries can later be checked for their staleness
*
* @group Cache
*/
public inherited sharing class CacheEntry
{
Long createdOnEpoch;
Object value;

private Long ageInSeconds
{
get
{
return ortoo_DateLiterals.epochTime - this.createdOnEpoch;
}
}

public CacheEntry( Object value )
{
this.value = value;
this.createdOnEpoch = ortoo_DateLiterals.epochTime;
}

/**
* States if the current entry is younger than or equal to the the stated age in seconds
*
* @param Long The age to compare against, in seconds
* @return Boolean Is the current entry younger or equal to the given age
*/
public Boolean isYoungerThanOrEqualTo( Long compareAgeInSeconds )
{
Contract.requires( compareAgeInSeconds != null, 'isYoungerThanOrEqualTo called with a null compareAgeInSeconds' );

return ageInSeconds <= compareAgeInSeconds;
}

/**
* Returns the current value of the entry
*
* @return Object The value of the entry
*/
public Object getValue()
{
return this.value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>54.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*
* If used, it is recommended that triggers are added to those objects, or code added to the UI that updates the objects
* referenced in the SOQL that invalidate the cache.
*
* @group Cache
*/
public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of too many public methods caused by inner classes
{
Expand Down Expand Up @@ -54,6 +56,48 @@ public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of t
* @return List<Sobject> The records that match
*/
public List<Sobject> query( String soql )
{
return query( soql, new SimpleCacheRetriever( cacheWrapper ) );
}

/**
* Perform the given query, first checking if the Org Platform Cache Partition contains results for that SOQL.
* If so, the cached versions are returned.
* If not, the query is executed against the database and the result cached.
* If, for any reason, a cache read or write cannot be performed, the method will continue without an exception.
* Errors can be seen in the configured log destination (uses LoggerService)
*
* @param String The SOQL to return the results for
* @param Long The maximum age that the retrieved entry is allowed to be before a cache miss is recorded
* @return List<Sobject> The records that match
*/
public List<Sobject> query( String soql, Long maximumAgeInSeconds )
{
Contract.requires( maximumAgeInSeconds != null, 'query called with a null maximumAgeInSeconds' );

return query( soql, new MaximumAgeCacheRetriever( cacheWrapper, maximumAgeInSeconds ) );
}

/**
* Perform the given query, first checking if the Org Platform Cache Partition contains results for that SOQL.
* If so, the cached versions are returned.
* If not, the query is executed against the database and the result cached.
* If, for any reason, a cache read or write cannot be performed, the method will continue without an exception.
* Errors can be seen in the configured log destination (uses LoggerService)
*
* @param String The SOQL to return the results for
* @param DateTime The minimum (most recent) datetime that the retrieved entry is allowed to have been created on
* before it is registered as a cache miss
* @return List<Sobject> The records that match
*/
public List<Sobject> query( String soql, DateTime minimumDateTimeAdded )
{
Contract.requires( minimumDateTimeAdded != null, 'query called with a null minimumDateTimeAdded' );

return query( soql, new MinimimDateAddedCacheRetriever( cacheWrapper, minimumDateTimeAdded ) );
}

private List<Sobject> query( String soql, ICacheRetriever cacheRetriever )
{
Contract.requires( soql != null, 'query called with a null soql' );

Expand All @@ -64,7 +108,7 @@ public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of t
{
if ( cacheWrapper.hasAccessToCache() )
{
returnValues = (List<Sobject>)cacheWrapper.get( key );
returnValues = (List<Sobject>)cacheRetriever.get( key );
}
else
{
Expand Down Expand Up @@ -121,4 +165,57 @@ public inherited sharing class CachedSoqlExecutor //NOPMD: incorrect report of t
{
return EncodingUtil.convertToHex( Crypto.generateDigest( 'SHA1', Blob.valueOf( soql ) ) );
}

private interface ICacheRetriever
{
List<Object> get( String key );
}

private inherited sharing class SimpleCacheRetriever implements ICacheRetriever
{
ICacheAdaptor cacheWrapper;

private SimpleCacheRetriever( ICacheAdaptor cacheWrapper )
{
this.cacheWrapper = cacheWrapper;
}

public List<Object> get( String key )
{
return (List<Object>)cacheWrapper.get( key );
}
}

private inherited sharing class MaximumAgeCacheRetriever implements ICacheRetriever
{
ICacheAdaptor cacheWrapper;
Long maximumAge;

private MaximumAgeCacheRetriever( ICacheAdaptor cacheWrapper, Long maximumAge )
{
this.cacheWrapper = cacheWrapper;
this.maximumAge = maximumAge;
}

public List<Object> get( String key )
{
return (List<Object>)cacheWrapper.get( key, maximumAge );
}
}

private inherited sharing class MinimimDateAddedCacheRetriever implements ICacheRetriever
{
ICacheAdaptor cacheWrapper;
DateTime minimumDateAdded;
private MinimimDateAddedCacheRetriever( ICacheAdaptor cacheWrapper, DateTime minimumDateAdded )
{
this.cacheWrapper = cacheWrapper;
this.minimumDateAdded = minimumDateAdded;
}

public List<Object> get( String key )
{
return (List<Object>)cacheWrapper.get( key, minimumDateAdded );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
* * Cache.Session / Cache.SessionPartition
*
* This allows for the types of cache to be used interchangably / and dynamically
*
* @group Cache
*/
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 );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Allows for the use of code that will automatically interact with a cache and be able to switch that off dynamically and simply.
*
* All users are assumed to be allowed to use the cache, but it describes itself as 'not a cache' and effectively does nothing.
*
* @group Cache
*/
public inherited sharing class NullCache implements ICacheAdaptor
{
Expand All @@ -22,6 +24,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<String> getKeys()
{
return new Set<String>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
*
* Note: The lifespan of a given object is reset whenever any object in that object's list is written.
* That is, either the whole of the list is aged out, or none of it is.
*
* Note: This implementation does not include age based stalesness on retrieval, but could be enhanced to, if required.
* See the alternative 'get' options that are available on ICacheAdaptor
*
* @group Cache
*/
public inherited sharing class ObjectCache
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* custom permission as defined in 'CAN_ACCESS_CACHE_PERMISSION'.
*
* Attempting to access the cache without this permission will result in an OrgCache.AccessViolationException
*
* @group Cache
*/
public inherited sharing class OrgCache implements ICacheAdaptor
{
Expand Down Expand Up @@ -50,12 +52,46 @@ public inherited sharing class OrgCache implements ICacheAdaptor
* Retrieve the cache entry with the given key.
*
* If the user does not have access to the cache, will throw an AccessViolationException.
* If the entry does not exist in the cache, will return null
* 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 = 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 user does not have access to the cache, will throw an AccessViolationException.
* 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 = ortoo_DateLiterals.epochTime - DateTimeUtils.convertToEpochTime( 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 user does not have access to the cache, will throw an AccessViolationException.
* 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' );

Expand All @@ -64,10 +100,27 @@ public inherited sharing class OrgCache implements ICacheAdaptor
throw new AccessViolationException( Label.ortoo_core_org_cache_access_violation )
.setErrorCode( FrameworkErrorCodes.CACHE_ACCESS_VIOLATION )
.addContext( 'method', 'get' )
.addContext( 'key', key );
.addContext( 'key', key )
.addContext( 'maximumAgeInSeconds', maximumAgeInSeconds );
}

return Cache.Org.get( createFullyQualifiedKey( key ) );
Object rawCacheValue = Cache.Org.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
}

/**
Expand Down Expand Up @@ -114,7 +167,9 @@ public inherited sharing class OrgCache implements ICacheAdaptor
.addContext( 'value', value );
}

Cache.Org.put( createFullyQualifiedKey( key ), value, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace
CacheEntry cacheEntry = new CacheEntry( value );

Cache.Org.put( createFullyQualifiedKey( key ), cacheEntry, lifespan, Cache.Visibility.NAMESPACE, true ); // immutable outside of namespace
}

/**
Expand Down Expand Up @@ -187,6 +242,7 @@ public inherited sharing class OrgCache implements ICacheAdaptor
return Cache.OrgPartition.createFullyQualifiedPartition( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME );
}

@testVisible
private String createFullyQualifiedKey( String key )
{
return Cache.OrgPartition.createFullyQualifiedKey( PackageUtils.NAMESPACE_PREFIX, PARTITION_NAME, key );
Expand Down
Loading

0 comments on commit 933fe55

Please sign in to comment.