zimdanen
zimdanen

Reputation: 5626

Caching in an asynchronous service

So, I've run into an interesting situation. Let's say I have a rarely-changing set of data, so I don't want to get it on every call to a high-volume service. I will typically cache this data for a period of time (depending on how rarely changing it really is). This is pretty straightforward:

private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
    T value = (T)HttpRuntime.Cache.Get(key);
    if (value == null)
    {
        lock (lockObject)
        {
            value = (T)HttpRuntime.Cache.Get(key);
            if (value == null)
            {
                value = getToSet();
                HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
            }
        }
    }
    return value;
}

If the getToSet function fails, I never Insert, so it gets attempted again on the next call.

However, if I use this same pattern in an asynchronous service, the getToSet function returns a Task<> - and now I've cached a Task that returns a failed result for a period of time. Assuming that the time to call getToSet is not negligible, how do I prevent caching a failed result while still not blocking a thread during the data retrieval?

Short but complete example below; this will cache a failed result for one minute, during which time every call to GetData will fail, and then the service will start returning 5 for every call.

using System;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Web;
using System.Web.Caching;

namespace TaskCacheIssue
{
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        Task<int> GetData();
    }

    public class Service1 : IService1
    {

        public async Task<int> GetData()
        {
            return await GetDataFromCache();
        }

        private static bool first = true;
        private static readonly object deadbolt = new object();
        private async Task<int> GetDataFromCache()
        {
            return await GetOrSet(
                "key",
                async () => await GetDataFromSomewhereElse(),
                1,
                deadbolt);
        }

        private async Task<int> GetDataFromSomewhereElse()
        {
            // This is actually a longer-running data retrieval.
            if (first)
            {
                first = false;
                throw new Exception("FIRST!");
            }
            return 5;
        }

        private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
        {
            T value = (T)HttpRuntime.Cache.Get(key);
            if (value == null)
            {
                lock (lockObject)
                {
                    value = (T)HttpRuntime.Cache.Get(key);
                    if (value == null)
                    {
                        value = getToSet();
                        HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
                    }
                }
            }
            return value;
        }

    }
}

Upvotes: 3

Views: 201

Answers (2)

friggle
friggle

Reputation: 3471

Instead of exclusively evaluating for null, additionally check if the cached value is a failed task.

private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
    T value = (T)HttpRuntime.Cache.Get(key);
    if (value == null || IsCanceledOrFaultedTask(value))
    {
        lock (lockObject)
        {
            value = (T)HttpRuntime.Cache.Get(key);
            if (value == null || IsCanceledOrFaultedTask(value))
            {
                value = getToSet();
                HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
            }
        }
    }
    return value;
}

private bool IsCanceledOrFaultedTask(object target)
{
    if (target is Task)
    {
        var task = (Task)target;
        return (task.IsCanceled || task.IsFaulted);
    }

    return false;
}

Upvotes: 1

usr
usr

Reputation: 171178

When retrieving a value from the cache and awaiting it check the result. If it is failed clear the cache and insert a new value into it.

Upvotes: 1

Related Questions