Improvements under the hood – Task-based caching in Xperience 13

   —   

Welcome to the first episode of our short article series presenting Kentico Xperience 13 technical features, which might not be visible at first sight.

Today I will talk about improvements in caching with regard to task-based asynchronous programming.

CacheHelper

The traditional approach to caching in Xperience starts with the use of the CacheHelper class. While the helper worked well with synchronous code, there were certain glitches with respect to asynchronous code.

Problem

Let me start with a code snippet first and then elaborate on it

var data = CacheHelper.Cache(cacheSettings => { var result = LoadDataSynchronously(); // Modify cache dependency based on the actual data retrieved cacheSettings.CacheDependency = GetCacheDependency(result); return result; }, new CacheSettings(TimeSpan.FromMinutes(10).TotalMinutes, "my", "custom", "cache", "key"));

The code simply retrieves data to be cached and sets the CacheDependency property of the CacheSettings based on the actual data. Now while this works as expected in synchronous code, rewriting the same to task-based asynchronous code starts showing some strange behavior.

var data = await CacheHelper.Cache(async cacheSettings => { var result = await LoadDataAsync().ConfigureAwait(false); // POI: Async method call // Modify cache dependency based on the actual data retrieved cacheSettings.CacheDependency = GetCacheDependency(result); return result; }, new CacheSettings(TimeSpan.FromMinutes(10).TotalMinutes, "my", "custom", "cache", "key")).ConfigureAwait(false);

The signature of the CacheHelper.Cache method

TData Cache<TData>(Func<CacheSettings, TData> loadMethod, CacheSettings settings)

propagates the task resulting from the asynchronous lambda expression to its return value, so at first glance the CacheHelper.Cache call can be awaited. However, internally there is no await in the Cache method as it was not designed with asynchronous code in mind.

As a result, we observe two things. First, instead of caching just TData, we actually cache a handle to the underlying asynchronous task – Task<TData>.

Second, the Cache method can finish its execution before the asynchronous lambda actually executes. Inherently, the data (a handle to Task<TData>) is stored into the cache with the original CacheSettings, without the modification of CacheDependency. There is actually a race condition, meaning if the asynchronous task finishes before Cache finishes, the cache settings are correctly modified.

Solution

The solution to the problem seems obvious – await the lambda expression in Cache. While this does the trick, it is tricky to implement with respect to the internals of the Cache method. Since the cache is based on CachedSection, which is based on mutexes to ensure single load behavior in parallel web server environment, the asynchronous code causes loss of ownership of the corresponding mutex upon awaiting the asynchronous code (unless the continuation runs in the same thread).

To deal with that, we have introduced a new implementation, whose internals are driven by tasks only, so mutexes are no longer required. The usage looks as one would expect

var data = await CacheHelper.CacheAsync(async cacheSettings => { var result = await LoadDataAsync().ConfigureAwait(false); // Modify cache dependency based on the actual data retrieved cacheSettings.CacheDependency = GetCacheDependency(result); return result; }, new CacheSettings(TimeSpan.FromMinutes(10).TotalMinutes, "my", "custom", "cache", "key")).ConfigureAwait(false);

All that is needed is to call CacheAsync for task-based data loads.

Brand new IProgressiveCache interface

Let me showcase one more addition to caching in Xperience 13. Being used to CacheHelper, you might have noticed its drawback. The API is based on static methods, which can be troublesome when writing tests, especially unit tests.

To overcome this, we are introducing the IProgressiveCache interface. In conjunction with dependency injection, the interface allows caching to be injected into custom classes with all the benefits that this has for testing. The interface contains methods for both synchronous and asynchronous caching.

To demonstrate on the previous example, corresponding code (including the dependency injection) would be

private IProgressiveCache ProgressiveCache { get; } // Assuming the class is managed by IoC public MyClass(IProgressiveCache progressiveCache) { ProgressiveCache = progressiveCache; } private async Task<object> GetDataAsync() { var data = await ProgressiveCache.LoadAsync(async cacheSettings => { var result = await LoadDataAsync(); // Modify cache dependency based on the actual data retrieved cacheSettings.CacheDependency = GetCacheDependency(result); return result; }, new CacheSettings(TimeSpan.FromMinutes(10).TotalMinutes, "my", "custom", "cache", "key")); return data; }

In the future, we expect the interface-based approach to become the standard for all caching.

Summary

To wrap it up, there are 2 new key concepts related to caching in Kentico Xperience 13.

The CMS.Helpers.CacheHelper class was enriched by CacheAsync to support asynchronous code caching.

The CMS.Helpers.IProgressiveCache interface was introduced as a convenient alternative to the static Cache/CacheAsync methods of the CacheHelper. The interface aims for easier unit testing and should be the preferred approach.

Share this article on   LinkedIn

Marek Fesar

Hi, I am the Principal Technical Leader here at Kentico and I primarily focus on the design, architecture and core of our solution.