Request-Scoped Caching
Request-Scoped Caching
Cortex can cache objects for the duration of an HTTP request to improve performance. Cached objects are automatically evicted from cache when the request is completed. Request-scoped caching is controlled by adding @CacheResultand @CacheRemove annotations to methods in Cortex repository classes.
To enable request-scoped caching on a repository's methods, you must first ensure that AspectJ can detect your repository to weave the repository with caching annotations, and then you must apply the annotations.
Enabling Caching on a Repository
As of Cortex 6.18 repositories use the @Component annotation. To enable request-scoped caching on repositories, ensure the following prerequisites are met:
- Ensure that your repository is located in package following the naming pattern: *.integration.epcommerce.repository, where * is any prefix. For example, com.elasticpath.rest.resource.integration.epcommerce.repository would be an appropriate package.
- Ensure that your repository's class name ends with RepositoryImpl. For example, CarsRepositoryImpl.
@Named Legacy Repositories
Legacy repositories using the @Named annotation require the above steps, as well as the following steps:
If the package name does not start with com.elasticpath.rest, then the package name must be added to the <context:component-scan/> element in resources\OSGI-INF\blueprint\ext-applicationContext-repositories-integration.xml:
<context:component-scan base-package="my.pkg.with.custom.repos" scope-resolver="org.springframework.context.annotation.Jsr330ScopeMetadataResolver"/commerce-legacy/>
When adding multiple packages to <context:component-scan/>, the values should be comma separated:
<context:component-scan base-package="my.pkg.with.custom.repos,my.pkg.with.custom.repos2" scope-resolver="org.springframework.context.annotation.Jsr330ScopeMetadataResolver"/commerce-legacy/>
Caching Method Responses
@CacheResult public Customer getCustomer(CustomerId customerId){}
In most cases, the @CacheResult annotation is the only thing needed to enable caching.
For Helix programming model repositories using the @Component annotation, the AspectJ weaving process occurs at run time when the resource's URI is accessed. For legacy repositories using the @Named annotation, AspectJ weaving occurs at load time when Cortex starts up.
Invalidating Caches on Methods
To invalidate cached values when running update or delete methods, annotate the methods with the @CacheRemove annotation. @CacheRemove requires either an object of the class to be invalidated or an array of class objects. The value to invalidate equals the return type of the corresponding get method.
@CacheRemove(typesToInvalidate = Customer.class) public void deleteCustomer(CustomerId customerId) {} @CacheRemove(typesToInvalidate = Customer.class) public void deleteAllCustomers() {}
Return-type | typesToInvalidate | Description |
---|---|---|
Store | Store.class | No generic type used thus class type can be directly derived from the return type. |
ExecutionResult<Store> | Store.class | ExecutionResult wrapper is omitted, the type-hint rule for the declared generic type applies. |
Map<String, List<String>> | Map.class | Nested generic type definitions are flatten and class type. |
ExecutionResult<List<String>> | List.class | When defining ExecutionResult in combination with a nested generic type omitting ExecutionResult and generic type flattening applies. |
Tuning Request-Scoped Caching Performance
To achieve the best request-scoped caching performance, use the tuning techniques described below.
Defining Key Variants
Key variants have to be unique otherwise inconsistencies might occur.
class Customer { String storeCode,userId; //key variant - unique identifier String guid //key variant - unique identifier } @CacheResult//first call goes here public Customer findCustomerByUserId(String storeCode, String userId) {} @CacheResult//second call goes here, leverages key variant caching public Customer findCustomerByGuid(String guid) {}To enable the cache to use key variant optimization, register the domain object and its key variants by implementing the CacheKeyVariants interface and registering this interface with the CacheKeyManager. For example:
@Singleton @Named public final class CustomerCacheKeyVariants implements CacheKeyVariants<Customer> { @Inject public CustomerCacheKeyVariants( @Named("cacheKeyManager") final CacheKeyManager cacheKeyManager) { //register the CacheKeyVariant to use it cacheKeyManager.register(this); } @Override public Collection<Object[]> get(final Customer customer) { return Arrays.asList( // the keys have be unique identifier, otherwise inconsistencies might occur new Object[]{customer.guid}, new Object[]{customer.storeCode, customer.userId} ); } @Override public Class<Customer> getType() { return Customer.class; } }
How Cache Invalidation Works
Request-scoped caches are divided into cache regions, each region represents the class that instantiated the cache. Cached values are only available to the class that originated the cache. The same applies for invalidation, only the originating class can invalidate the cache.
Getting the Best Cache Invalidation Performance
We achieve the best performance results by invalidating the least amount of cache entries as possible. Below is a list of some important cache invalidation approaches.
We match the invalidation method parameters, so only cached entries with the same input parameters are removed from the cached.
@CacheResult() public Customer getCustomerById(CustomerId customerId){} @CacheRemove(typesToInvalidate = Customer.class) public void deleteCustomer(CustomerId customerId){} //both method have the same input parameter //when calling deleteCustomer(CustomerId(123)) only the //value associated to CustomerId(123) will be removed
We use partial parameter matches, so invalidation method parameters are a subset of the cached method parameters.
@CacheResult() public Customer getCustomerByFullName(String name,String preName){} @CacheRemove(typesToInvalidate = Customer.class) public void deleteCustomer(String preName){} //parameters of delete are subset of the get method //when calling deleteCustomer("Albert") all Customers with the preName Albert will be removed
@CacheResult(uniqueIdentifier="customerId") public Customer getCustomerById(CustomerId customerId){} @CacheRemove(typesToInvalidate = Customer.class, uniqueIdentifier="customerId") public void deleteCustomer(CustomerId customerId){} //all Customer objects registered with the defined unique identifier will be removed
By not using any of the above strategies, all cached entries in a region can be invalidated and evicted, causing performance hit.
@CacheResult() public Customer getCustomerById(String storeCode,String userId){} @CacheRemove(typesToInvalidate = Customer.class) public void removeCustomer(String customerName){} //In this case all cached Customer objects will be removed from the cache.
Avoiding Cache Inconsistencies
Treat Cache Values as Immutable Objects
Modifying cached values is a bad practice that can lead to inconsistencies. Either use immutable types as values, which is the preferred way, or treat cached values as if they were immutable and create new objects on mutation.
Avoiding Method Signature Collision
@CacheResult(uniqueIdentifier = "default-customer-address") public Address getDefaultCustomerAddress(CustomerId customerId) {} @CacheResult(uniqueIdentifier = "shipping-address") public Address getShippingAddress(CustomerId customerId) {}
Manual Caching
In cases where annotation caching isn't appropriate, you can cache manually. To use manual caching, inject an instance of the cacheKeyBasedRequestCache bean.
@Named("cacheKeyBasedRequestCache") private TypedRequestCache<CacheKey> requestCache public void method() { String value = "cache me"; requestCache.putCacheEntry(new CacheKey(this.getClass(),"my-cache-entry", String.class), value); //do some operations requestCache.invalidateCacheEntries(new CacheKey(this.getClass(),"my-cache-entry", String.class))}
Logging Caching
<logger name="com.elasticpath.rest.resource.integration.epcommerce.repository" level="TRACE"/commerce-legacy/>Cache logging dramatically decreases cache performance as it will log a lot of information.
<logger name="com.elasticpath.rest.cache.guava.impl" level="TRACE"/commerce-legacy/>
Configuring Request-Scoped Caching
By default, the request-scoped cache is configured to evict cached entries after 60 sec and when the number of cached entries reaches 2147483647. Parameters are configurable via the RelOS Request Cache OSGi configuration in the Apache Felix Web Console. Caches are destroyed and rebuilt when configuration values are updated through the Felix console.
Cache Metrics Monitoring
Request-scoped cache metrics are exposed as MBeans over Cortex's JMX API. The metrics Mbeans are identified by the naming pattern EP-RelOS.*.RequestCache.*.
- evictionCount – number of times entries have been evicted
- hitCount – number of times entries have been looked up and found
- hitRate – ratio of cache request which were hits
- loadCount – number of times entries have been looked up
- missCount – number of times entries have been looked up and missed
- missRate – ratio of cache requests which weren't hits
- requestCount – number of times entries have been looked up