Cache Events

And implementing self-refreshable cache in Kentico 12 MVC

Did you know?

Some time ago I discovered that in Kentico 12 there is a new Global Event type called CacheEvents. It fires every time some item has been removed from the cache and you can simply subscribe to it:

CacheEvents.CacheItemRemoved.Execute += (sender, args) => { /* do something */ };

And the good thing is that CacheItemRemovedEventArgs contain Key, Value and Reason it's been removed. There are the following reasons defined in the CMS:

/// <summary>
/// Specifies the reason an item was removed from the cache.
/// </summary>
public enum CMSCacheItemRemovedReason
{
    /// <summary>
    /// The item is removed from the cache because of method remove or replace.
    /// </summary>
    Removed,
    /// <summary>
    /// The item is removed from the cache because it expired.
    /// </summary>
    Expired,
    /// <summary>
    /// The item is removed from the cache because the system removed it to free memory.
    /// </summary>
    Underused,
    /// <summary>
    /// The item is removed from the cache because the cache dependency associated with it changed.
    /// </summary>
    DependencyChanged,
}

So, what fancy can we do with it? Let's see!

Self-refreshable cache

Imagine that we have an extensive operation to get the data displayed on the website. For example, it could be ratings and reviews data received from the 3rd party API call. It wouldn't make sense to store this data in the database, otherwise we would need to sync it from time to time. Therefore we will just cache it for some time! Look at the Normal cache timeline below: on Request 1 our website caches the data (and Response 1 is sent with a delay), but on Request 2 the website can respond immediately as long as it has a valid cache.

What could be the problem here? Well, cache tend to expire. And typically it expires silently. So on Request 3 the website refreshes the cache and the process repeats. But what if the website could have refreshed the cache automatically without waiting the next request to come? Look at the Refreshable cache timeline. Note that the delay between Request 3 and Response 3 has become less.

 

Proof of concept

As usual, the full code of the example can be found on GitHub. Just download it, copy to the default DancingGoat MVC website, include missing files in the solution in Visual Studio and rebuild.

Let's start with CacheExpirationCallbacks class:

public static class CacheExpirationCallbacks
{
    private static readonly ConcurrentDictionary<string, Action> Callbacks =
        new ConcurrentDictionary<string, Action>(StringComparer.OrdinalIgnoreCase);

    public static void Register(string key, Action callback)
    {
        if (!Callbacks.ContainsKey(key))
        {
            Callbacks.AddOrUpdate(key, callback, (k, oldCallback) => callback);
        }
    }

    public static void Call(string key)
    {
        if (Callbacks.TryGetValue(key, out var callback))
        {
            var callbackCopy = callback;
            callbackCopy?.Invoke();
        }
    }
}

It stores the dictionary of cache keys and a set of callbacks. When data stored against this cache key expires - the corresponding registered callbacks will be called. And they will be called by CacheEventsModule:

public class CacheEventsModule : Module
{
    public CacheEventsModule()
        : base("CacheEventsModule")
    {
    }

    protected override void OnInit()
    {
        CacheEvents.CacheItemRemoved.Execute += CacheItemRemovedOnExecute;
    }

    private void CacheItemRemovedOnExecute(object sender, CMSEventArgs<CacheItemRemovedEventArgs> e)
    {
        CacheExpirationCallbacks.Call(e.Parameter.Key);
    }
}

Let's have a closer look at CachingRepositoryDecorator.cs. It contains the following GetCachedResult method:

private object GetCachedResult(IInvocation invocation, string dependencyCacheKey)
{
    var cacheKey = GetCacheItemKey(invocation);
    var cacheSettings = CreateCacheSettings(cacheKey, dependencyCacheKey);
    Func<Object> provideData = () =>
    {
        invocation.Proceed();
        return invocation.ReturnValue;
    };

    return CacheHelper.Cache(provideData, cacheSettings);
}

This method creates association between the cache key and the cached data so that on the next request if we have some data cached by this key we will return it. But actually, here we also have a method of obtaining cached data! It means we can subscribe to cache expiration event now and pass it a method to refresh the cache when the time comes:

private object GetCachedResult(IInvocation invocation, string dependencyCacheKey)
{
    var cacheKey = GetCacheItemKey(invocation);
    var cacheSettings = CreateCacheSettings(cacheKey, dependencyCacheKey);
    Func<Object> provideData = () =>
    {
        invocation.Proceed();
        return invocation.ReturnValue;
    };

    CacheExpirationCallbacks.Register(cacheKey, () =>
    {
        CacheHelper.Cache(() => invocation.MethodInvocationTarget.Invoke(invocation.InvocationTarget, invocation.Arguments), cacheSettings);
    });

    return CacheHelper.Cache(provideData, cacheSettings);
}

With CacheExpirationCallbacks.Register(..) call we are just saying: "Hey, system! When something expires on cacheKey, could you call this method again to refresh the data?".

But if you run this example now, the system will crash after some time with some nullref exceptions. And there are reasons for it:

  • When we register a callback, it keeps a reference to one of the IRepository objects (invocation.InvocationTarget), it's method (invocation.MethodInvocationTarget) and method's parameters (invocation.Arguments)
  • Most of the repositories contain the call .OnSite(SiteContext.CurrentSiteName) which resolves the current site name using the current request context.
  • When the system calls this method again from the callback, unfortunately, the current request context is null.

But we can fix it moving this into the private field (mSiteName) and init in the constructor. In this case repository will become request-independent (well, it's not 100% true, but let's leave it for now):

public class KenticoHomeRepository : IHomeRepository
{
    private readonly string mCultureName;
    private readonly string mSiteName;
    private readonly bool mLatestVersionEnabled;


    /// <summary>
    /// Initializes a new instance of the <see cref="KenticoHomeRepository"/> class that returns home page sections in the specified language. 
    /// If the requested page doesn't exist in specified language then its default culture version is returned.
    /// </summary>
    /// <param name="cultureName">The name of a culture.</param>
    /// <param name="latestVersionEnabled">Indicates whether the repository will provide the most recent version of pages.</param>
    public KenticoHomeRepository(string cultureName, bool latestVersionEnabled)
    {
        mCultureName = cultureName;
        mLatestVersionEnabled = latestVersionEnabled;
        mSiteName = SiteContext.CurrentSiteName;
    }


    /// <summary>
    /// Returns an enumerable collection of home page sections ordered by a position in the content tree.
    /// </summary>
    public IEnumerable<HomeSection> GetHomeSections()
    {
        return HomeSectionProvider.GetHomeSections()
            .LatestVersion(mLatestVersionEnabled)
            .Published(!mLatestVersionEnabled)
            .OnSite(mSiteName)
            .Culture(mCultureName)
            .CombineWithDefaultCulture()
            .OrderBy("NodeOrder")
            .ToList();
    }


    /// <summary>
    /// Returns an object representing the home page.
    /// </summary>
    public Home GetHomePage()
    {
        // some long cache filling
        Thread.Sleep(10000);

        return HomeProvider.GetHomes()
            .LatestVersion(mLatestVersionEnabled)
            .Published(!mLatestVersionEnabled)
            .OnSite(mSiteName)
            .Culture(mCultureName)
            .CombineWithDefaultCulture()
            .TopN(1);
    }
}

In the example code on GitHub all necessary repositories are fixed this way. And there is a Thread.Sleep(10000) also to simulate some long cache data population. Let's finish the example with setting some low cache duration value in Web.config (1 minute):

<add key="RepositoryCacheItemDuration" value="600" />

That's it! We can open Home page now and notice 10 seconds delay bacause of Thread.Sleep in KenticoHomeRepository.GetHomePage(). Typically, if we wait 1 minute (as set in web.config) cache of this data should expire and the next request should have 10 seconds delay. But not this time. System will refresh the cache for us automatically when it expires. There is still a chance if request comes when cache is refreshing there will be a delay, but this chance is significatly lower now.

Final note

Kentico 12 brought a convenient CacheEvents class. Using it allows to build the caching system which can refresh itself automatically. But there is one thing to keep in mind before doing it: all your repositories must be independent from request context as they will be called in a separate thread where the request will not exist! Remember me saying that repository fix was not 100% perfect? Well, in DancingGoat MVC example all the repositories are registered with per-request life-time. Example above works well as a proof of concept, but the current version of code should not be used in production as when you website runs these repositories should be GCed at some point. And the approach described in this article should work much better with singleton repositories.

Happy coding!