C# – MemoryCache AbsoluteExpiration acting strange

cmemorycachenet

I'm trying to use a MemoryCache in .net 4.5 to keep track of and automatically update various items, but it seems like no matter what I set as an AbsoluteExpiration it will always only expire in 15 seconds or more.

I want the cache items to expire every 5 seconds, but it always expires in at least 15 seconds, and if I move the expiration time out, it will end up being something like 15 seconds + my refresh interval, but never less than 15 seconds.

Is there some internal timer resolution that I'm not seeing? I looked through a bit of the reflected System.Runtime.Caching.MemoryCache code and nothing stood out to me, and I haven't been able to find anybody else who has this issue out on the internet.

I have a very basic example below that illustrates the problem.

What I want is for CacheEntryUpdate to be hit every 5 seconds or so and update with new data, but, as I've said, it only ever gets hit in 15+ seconds.

static MemoryCache MemCache;
static int RefreshInterval = 5000;

protected void Page_Load(object sender, EventArgs e)
{
    if (MemCache == null)
        MemCache = new MemoryCache("MemCache");

    if (!MemCache.Contains("cacheItem"))
    {
        var cacheObj = new object();
        var policy = new CacheItemPolicy
        {
            UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
            AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
        };
        var cacheItem = new CacheItem("cacheItem", cacheObj);
        MemCache.Set("cacheItem", cacheItem, policy);
    }
}

private void CacheEntryUpdate(CacheEntryUpdateArguments args)
{
    var cacheItem = MemCache.GetCacheItem(args.Key);
    var cacheObj = cacheItem.Value;

    cacheItem.Value = cacheObj;
    args.UpdatedCacheItem = cacheItem;
    var policy = new CacheItemPolicy
    {
        UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
        AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
    };
    args.UpdatedCacheItemPolicy = policy;
}

Best Answer

I've figured it out. There's an internal static readonly TimeSpan on System.Runtime.Caching.CacheExpires called _tsPerBucket that is hardcoded at 20 seconds.

Apparently, this field is what's used on the internal timers that run and check to see if cache items are expired.

I'm working around this by overwriting the value using reflection and clearing the default MemoryCache instance to reset everything. It seems to work, even if it is a giant hack.

Here's the updated code:

static MemoryCache MemCache;
static int RefreshInterval = 1000;

protected void Page_Load(object sender, EventArgs e)
{
    if (MemCache == null)
    {
        const string assembly = "System.Runtime.Caching, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
        var type = Type.GetType("System.Runtime.Caching.CacheExpires, " + assembly, true, true);
        var field = type.GetField("_tsPerBucket", BindingFlags.Static | BindingFlags.NonPublic);
        field.SetValue(null, TimeSpan.FromSeconds(1));

        type = typeof(MemoryCache);
        field = type.GetField("s_defaultCache", BindingFlags.Static | BindingFlags.NonPublic);
        field.SetValue(null, null);

        MemCache = new MemoryCache("MemCache");
    }

    if (!MemCache.Contains("cacheItem"))
    {
        var cacheObj = new object();
        var policy = new CacheItemPolicy
        {
            UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
            AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
        };
        var cacheItem = new CacheItem("cacheItem", cacheObj);
        MemCache.Set("cacheItem", cacheItem, policy);
    }
}

private void CacheEntryUpdate(CacheEntryUpdateArguments args)
{
    var cacheItem = MemCache.GetCacheItem(args.Key);
    var cacheObj = cacheItem.Value;

    cacheItem.Value = cacheObj;
    args.UpdatedCacheItem = cacheItem;
    var policy = new CacheItemPolicy
    {
        UpdateCallback = new CacheEntryUpdateCallback(CacheEntryUpdate),
        AbsoluteExpiration = DateTimeOffset.UtcNow.AddMilliseconds(RefreshInterval)
    };
    args.UpdatedCacheItemPolicy = policy;
}