Auto Refresh Cache Thread Safe

In this article we will build a high performance, thread-safe, auto-refreshing cache type (on the top of the .NET ObjectCache class) with the following specifications:

  • The cache should work in any type of application (Console application, ASP.NET application, …).
  • The cache will work with both reference and value types.
  • The cache is type-safe (no casting from Object).
  • The cache will take either a Func<T> delegate or a Func<Task<T>> delegate to automatically refresh the cached object at a specific periodic interval (since the item being cached must be expensive to retrieve then it makes sense to have the function retrieving it returns a task for better performance).
  • In either case, retrieving and refreshing the item will always run on a thread from the thread-pool (not the calling thread).
  • The delegate for retrieving the item will be called only once (till next refresh) no matter how many threads are simultaneously trying to insert or retrieve the item from the cache.
  • Refreshing the item in the cache will not block other threads trying to retrieve the item from the cache (they will be served the current cached item till the retrieve delegate returns the new item).
  • The cache type will provide the ability to pre-warm the cache.
  • The cache type will allow the caller to specify a timeout when retrieving an item from the cache.
  • The cache type will provide asynchronous methods (return a task) for retrieving an item from the cache (remember the retrieving delegate could still be running in the background when a thread asks for an item from the cache).
  • The cache type should handle the case when the retrieving delegate fails to retrieve the item during the auto-update.

Here is my implementation of such type in C#:

using System;

using System.Threading;

using System.Threading.Tasks;

using System.Runtime.Caching;

namespace AutoRefreshCache

{

public static class TaskUtils

{

public static async Task<T> TimeoutAfter<T>(this Task<T> task, int millisecondsTimeout)

{

if (task.IsCompleted || (millisecondsTimeout < 0) || (millisecondsTimeout == Timeout.Infinite)) //the last condition is unnecessary since Timeout.Infinite = -1

{

return task.Result;

}

var completedTask = await Task.WhenAny(task, Task.Delay(millisecondsTimeout));

if (task.IsCompleted)

{ //return task result even if it completes immediately after the timeout

return task.Result;

}

throw new TimeoutException();

}

public static async Task<T> ThrowIfNullResult<T>(this Task<T> task)

{

T result = await task;

object obj = result;

if (null == obj) { throw new Exception(); }

return result;

}

public static async Task<T> NotifyIfFailed<T>(this Task<T> task, Action<Exception> onFailedAction)

{

try

{

return await task;

}

catch (Exception ex)

{

if (null != onFailedAction) { onFailedAction(ex); }

throw;

}

}

}

public class CachedItemInfo<T>

{

public string Key { get; }

public TimeSpan ExpirationInterval { get; }

internal Func<Task<T>> LoadTaskFunc { get; }

public bool TreatNullResultAsError { get; }

public int UpdateMillisecondsTimeout { get; }

public TimeSpan FailureTryAfterInterval { get; }

public CachedItemInfo(string key, TimeSpan expirationInterval, Func<Task<T>> loadTaskFunc, TimeSpan failureTryAfterInterval,

int updateMillisecondsTimeout = Timeout.Infinite, bool treatNullResultAsError = true)

{

Key = key;

ExpirationInterval = expirationInterval;

if (treatNullResultAsError)

{

LoadTaskFunc = () => loadTaskFunc().ThrowIfNullResult();

}

else

{

LoadTaskFunc = loadTaskFunc;

}

FailureTryAfterInterval = failureTryAfterInterval;

UpdateMillisecondsTimeout = updateMillisecondsTimeout;

}

public static CachedItemInfo<T> CreateCachedItemInfo(string key, TimeSpan slidingExpiration, Func<T> loadFunc, TimeSpan failureTryAfterInterval,

int updateMillisecondsTimeout = Timeout.Infinite, bool treatNullResultAsError = true) =>

new CachedItemInfo<T>(key, slidingExpiration, () => Task.Run(loadFunc), failureTryAfterInterval, updateMillisecondsTimeout, treatNullResultAsError);

}

class AutoCachedItem<T>

{

class ExpensiveObject

{

internal DateTime LoadTimeStamp { get; }

internal T CachedObject { get; }

internal ExpensiveObject(T val) { CachedObject = val; LoadTimeStamp = DateTime.Now; }

}

Lazy<Task<ExpensiveObject>> LazyTask { get; }

internal CachedItemInfo<T> Info { get; }

internal AutoCachedItem(CachedItemInfo<T> info)

{

Info = info;

LazyTask = new Lazy<Task<ExpensiveObject>>(async () => new ExpensiveObject(await Info.LoadTaskFunc()));

}

internal AutoCachedItem<T> NewCopy() => new AutoCachedItem<T>(Info);

internal async Task<T> GetItemAsync(Action<Exception> onFailedAction, int millisecondsTimeout = Timeout.Infinite) =>

(await LazyTask.Value.NotifyIfFailed(onFailedAction).TimeoutAfter(millisecondsTimeout)).CachedObject;

internal async Task<DateTime> GetLoadTimeAsync(Action<Exception> onFailedAction, int millisecondsTimeout = Timeout.Infinite) =>

(await LazyTask.Value.NotifyIfFailed(onFailedAction).TimeoutAfter(millisecondsTimeout)).LoadTimeStamp;

internal void PreWarm() { var _ = LazyTask.Value; }

internal CacheItemPolicy GetPolicy() =>

new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.Add(Info.ExpirationInterval), UpdateCallback = OnItemUpdate };

internal CacheItemPolicy GetFailedPolicy() =>

new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.Now.Add(Info.FailureTryAfterInterval), UpdateCallback = OnItemUpdate };

private void OnItemUpdate(CacheEntryUpdateArguments args)

{

var newItem = NewCopy();

try

{

T _ = newItem.GetItemAsync(null, Info.UpdateMillisecondsTimeout).Result;

args.UpdatedCacheItem = new CacheItem(args.Key, newItem);

args.UpdatedCacheItemPolicy = newItem.GetPolicy();

}

catch (Exception)

{

args.UpdatedCacheItem = new CacheItem(args.Key, this); //use same old item

args.UpdatedCacheItemPolicy = newItem.GetFailedPolicy();

}

}

}

public class AutoRefreshCache

{

private readonly ObjectCache Cache;

private readonly object Lock = new object(); //protects the Cache object

public AutoRefreshCache(ObjectCache cache)

{

Cache = cache;

}

public AutoRefreshCache() : this(MemoryCache.Default) { }

private AutoCachedItem<T> SetupAndReturnItem<T>(CachedItemInfo<T> info, bool preWarm = false, bool refresh = false)

{

AutoCachedItem<T> item = null;

lock (Lock)

{

if ((!Cache.Contains(info.Key)) || refresh)

{

item = new AutoCachedItem<T>(info);

Cache.Set(info.Key, item, item.GetPolicy());

}

else

{

item = Cache.Get(info.Key) as AutoCachedItem<T>;

}

}

if (preWarm) { item.PreWarm(); }

return item;

}

public void SetupItem<T>(CachedItemInfo<T> info, bool preWarm = false) { SetupAndReturnItem(info, preWarm); }

private AutoCachedItem<T> GetItem<T>(CachedItemInfo<T> info)

{

var obj = Cache.Get(info.Key);

if (obj == null) //item is not in the cache

{

obj = SetupAndReturnItem(info, true);

}

return obj as AutoCachedItem<T>;

}

private void OnFailed(string key)

{

Remove(key);

}

public Task<T> GetAsync<T>(CachedItemInfo<T> info, int millisecondsTimeout = Timeout.Infinite) =>

GetItem(info).GetItemAsync(ex => OnFailed(info.Key), millisecondsTimeout);

public T Get<T>(CachedItemInfo<T> info, int millisecondsTimeout = Timeout.Infinite) =>

GetAsync(info, millisecondsTimeout).Result;

public Task<DateTime> GetLoadTimeAsync<T>(CachedItemInfo<T> info, int millisecondsTimeout = Timeout.Infinite) =>

GetItem(info).GetLoadTimeAsync(ex => OnFailed(info.Key), millisecondsTimeout);

public DateTime GetLoadTime<T>(CachedItemInfo<T> info, int millisecondsTimeout = Timeout.Infinite) =>

GetLoadTimeAsync(info, millisecondsTimeout).Result;

public void Remove(string key)

{

lock (Lock)

{

Cache.Remove(key);

}

}

public void RefreshNow<T>(CachedItemInfo<T> info)

{

SetupAndReturnItem(info, true, true);

}

}

}

 

Here is an example of how to use the AutoRefreshCache type:

//Singleton Pattern

public static class MyAppCache

{

private static AutoRefreshCache cacheInstance = new AutoRefreshCache();

static MyAppCache()

{

//nothing – static constructor to ensure the class is not marked with beforefieldinit by the compiler

//to ensure laziness of creating the static members

}

public static AutoRefreshCache GetInstance() => cacheInstance;

}

public static async Task<MyExpensiveObject> LoadExpensiveObjectAsync()

{

//Load the expensive object

}

CachedItemInfo<MyExpensiveObject> GetItem1CacheInfo() => new CachedItemInfo<MyExpensiveObject>(“MyApp.key1”, TimeSpan.FromMinutes(30), LoadExpensiveObjectAsync, TimeSpan.FromMinutes(5));

MyExpensiveObject x = MyAppCache.GetInstance().Get(GetItem1CacheInfo());