Social Icons

twitter google plus linkedin rss feed

Pages

28.5.20

Synchronous and Asychronous ThreadSafe Blitzkrieg Caching

Update! You can download BlitzCache from nuget now


Caching is necessary
Over the years I have used a lot of caching. In fact, I consider that some things like user permissions should normally have a cache of at least one minute.

Blitzkrieg Caching
Even when a method is cached there are cases when it is called again before it has finished the first time and this results in a new request to the database, and this time much slower. This is what I call The Blitzkrieg Scenario.

The slowest the query the more probabilities you have for this to happen and the worse the impact. I have seen too many times SQL Server freeze in the struggle of replying to the exact same query while the query is already being executed...

Ideally at least in my mind a cached method should only calculate its value once per cache period. To achieve this we could use a lock... But if I am caching different calls I want more than one call to be executed at the same time, exactly one time per cache key in parallel. This is why I created the LockDictionary class.

The LockDictionary
Instead of having a lock in my cache service that will lock all the parallel calls indiscriminately I have a dictionary of locks to lock by cache key.

public static class LockDictionary
{
    private static readonly object dictionaryLock = new object();
    private static readonly Dictionary<string, object> locks = new Dictionary<string, object>();

    public static object Get(string key)
    {
        if (!locks.ContainsKey(key))
        {
            lock (dictionaryLock)
            {
                if (!locks.ContainsKey(key)) locks.Add(key, new object());
            }
        }

        return locks[key];
    }
}
With this I can very easily select what I want to lock

GetBlitzkriegLocking
Now I can check if something is cached and return it or lock that call in particular and calculate the value of the function passed as a parameter.

public class CacheService : ICacheService
{
  private readonly IMemoryCache memoryCache;

  public CacheService(IMemoryCache memoryCache)
  {
      this.memoryCache = memoryCache;
  }

  public T GetBlitzkriegLocking<T>(string cacheKey, Func<T> function, double milliseconds)
  {
      if (memoryCache.TryGetValue(cacheKey, out T result)) return result;
      lock (LockDictionary.Get(cacheKey))
      {
          if (memoryCache.TryGetValue(cacheKey, out result)) return result;

          result = function.Invoke();
          memoryCache.Set(cacheKey, result, DateTime.Now.AddMilliseconds(milliseconds));
      }

      return result;
  }
}
And how do I use it?
var completionInfo = cacheService.GetBlitzkriegLocking($"CompletionInfo-{legalEntityDto.Id}", () => GetCompletionInfoDictionary(legalEntityDto), 500));
//Look ma, I am caching this for just 500 milliseconds and it really makes a difference
I find this method extremely useful but sometimes the function I am calling needs to be awaited... And you Cannot await in the body of a lock statement. What do I do?

The SemaphoreDictionary
Semaphores do allow you to await whatever you need, in fact they themselves are awaitable. If we translate the LockDictionary class to use semaphores it looks like this:

public static class SemaphoreDictionary
{
    private static readonly object dictionaryLock = new object();
    private static Dictionary<string, SemaphoreSlim> locks = new Dictionary<string, SemaphoreSlim>();

    public static SemaphoreSlim Get(string key)
    {
        if (!locks.ContainsKey(key))
        {
            lock (dictionaryLock)
            {
                if (!locks.ContainsKey(key)) locks.Add(key, new SemaphoreSlim(1,1));
            }
        }

        return locks[key];
    }
}
And using this I can await calls while I am locking stuff.


The Awaitable GetBlitzkriegLocking

The main rule about semaphores is that you must make sure you release them or they will be locked forever. Catching the error is optional though.
public async Task<T> GetBlitzkriegLocking<T>(string cacheKey, Func<Task<T>> function, double milliseconds)
{
    if (memoryCache.TryGetValue(cacheKey, out T result)) return result;

    var semaphore = SemaphoreDictionary.Get(cacheKey);

    try
    {
        await semaphore.WaitAsync();
        if (!memoryCache.TryGetValue(cacheKey, out result))
        {
            result = await function.Invoke();
            memoryCache.Set(cacheKey, result, DateTime.Now.AddMilliseconds(milliseconds));
        }
    }
    finally
    {
        semaphore.Release();
    }

    return result;
}
And how do I use it?
await cache.GetBlitzkriegLocking($"RequestPermissions-{userSid}-{workplace}", () => RequestPermissionsAsync(userSid, workplace), 60 * 1000);
Please give these methods a try and let me know how you would improve them. I am using them every now and then and I really enjoy how they simplify the code. I hope you like them too.

No comments:

Post a Comment