AsyncCache for faster caching

As requested, the code…

Usage after the code.

using ServiceStack.Caching;
using System;
using System.Threading.Tasks;

namespace Ludu.Base
{

  /// <summary>
  /// Base class for asynchronously caching data. After a cache-expiry, the data will be fetched asynchronously.
  /// While this fethcing is busy, the originally cached data is returned.
  /// </summary>
  /// <typeparam name="T">Type held by the cache</typeparam>
  public class AsyncCache<T> where T : class
  {
    private readonly Func<T> GetData;

    public AsyncCache(Func<T> getData, TimeSpan? lifeTime = null, T initialData = null)
    {
      GetData = getData;
      this.lifeTime = lifeTime ?? TimeSpan.FromMinutes(10);
      if (initialData != null)
      {
        InMemoryData = initialData;
        CurrentState = State.OnLine;
        RefreshedOn = DateTime.UtcNow;
      }
    }

    private enum State
    {
      Empty,
      OnLine,
      Refreshing
    }

    private T InMemoryData { get; set; }
    private volatile State CurrentState = State.Empty;
    private volatile object StateLock = new object();
    private DateTime RefreshedOn = DateTime.MinValue;
    private readonly TimeSpan lifeTime;

    public T Data
    {
      get
      {
        switch (CurrentState)
        {
          case State.OnLine: // Simple check on time spent in cache vs lifetime
            var timeSpentInCache = (DateTime.UtcNow - RefreshedOn);
            if (timeSpentInCache > lifeTime)
            {
              lock (StateLock)
              {
                if (CurrentState == State.OnLine)
                {
                  CurrentState = State.Refreshing;
                  Task.Factory.StartNew(Refresh);
                }
              }
            }
            break;

          case State.Empty: // Initial load : blocking to all callers

            lock (StateLock)
            {
              if (CurrentState == State.Empty)
              {
                InMemoryData = GetData(); // actually retrieve data
                RefreshedOn = DateTime.UtcNow;
                CurrentState = State.OnLine;
              }
            }
            return InMemoryData;
        }

        return InMemoryData;
      }
    }

    private void Refresh()
    {
      var dt = GetData(); // actually retrieve data from inheritor
      lock (StateLock)
      {
        RefreshedOn = DateTime.UtcNow;
        CurrentState = State.OnLine;
        InMemoryData = dt;
      }
    }

    public void Invalidate()
    {
      lock (StateLock)
      {
        RefreshedOn = DateTime.MinValue;
        CurrentState = State.OnLine;
      }
    }

    public static T Get(ICacheClient cache, string cacheKey, Func<T> getData)
    {
      return Get(cache, cacheKey, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(10), getData);
    }

    public static T Get(ICacheClient cache, string cacheKey, TimeSpan lifeTime, TimeSpan cacheTime, Func<T> getData)
    {
      var cachedData = cache.Get<AsyncCache<T>>(cacheKey);
      if (cachedData == null)
      {
        lock (getData)
        {
          // double check - voorkomt dat de lock elke keer moet worden geplaatst
          cachedData = cache.Get<AsyncCache<T>>(cacheKey);
          if (cachedData == null)
          {
            cachedData = new AsyncCache<T>(getData, lifeTime);
            cache.Set<AsyncCache<T>>(cacheKey, cachedData, cacheTime);
          }
        }
      }
      return cachedData.Data;
    }
  }

}

Usage:

public List<Tag> GetByIdCampaignCached(int idCampaign)
{
  string cacheKey = UrnId.CreateWithParts("TagsByIdCampaign", idCampaign.ToString());
  return AsyncCache<List<Tag>>.Get(cacheClient, cacheKey, TimeSpan.FromSeconds(5), TimeSpan.FromHours(1), () =>
  {
    return GetByIdCampaign(idCampaign);
  });
}

This will cache the call to GetByIdCampaign for 5 seconds. Since getting these tags is costly - it takes about 2 seconds - a consecutive call will return the PREVIOUSLY requested data and thus limits the wait time after the cache expired. The second timespan (1 hour) is the time the cached object will remain active. After this time the whole caching object will be discarded and will need to be fully retrieved (initial performance hit)

To see this in action, see: http://www.ohnespass.de and http://www.zondergein.nl/

3 Likes

If you decide to implement this gem in SS, please add it as a parameter to the new caching attributes. We were thinking of doing that on our own codebase, but if it is implemented in the library… :wink:

Also, the code was first published here:

Credits where credits are due.

Very nice! That is one snappy page for the amount of content, nicely done! :+1:

Thx! It’s actually an older version of the caching mechanism. This one is even better: http://www.zondergein.nl/
It also demonstrates how ‘slow’ various plugins (like Facebook) are.