Reputation: 5626
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
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
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