46

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;
}
Jared
  • 1,180
  • 1
  • 9
  • 14
  • 1
    Why do you want to cache the object(s) for such a short amount of time? There doesn't seem to be much value to using a cache if the objects are going to be evicted in 5-15 seconds. – nateirvin Sep 27 '12 at 22:56
  • I found even more strange behavior: if you set RemovedCallback on CacheItemPolicy, that delay reduces to 10 seconds – Sasha Jun 20 '14 at 10:20
  • Update to my previous comment: not always 10 seconds, - sometimes it is 0, sometimes 20. But very often exactly 10 seconds – Sasha Jun 20 '14 at 11:58

4 Answers4

49

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;
}
Jared
  • 1,180
  • 1
  • 9
  • 14
  • Having trouble getting this to work, can anybody provide some context? I dont understand where this code needs to live in order to work. thanks! – kmdsax Jan 23 '14 at 19:54
  • @kmdsax the code should live wherever your cache lives? in the example, i just had it inside an aspx page. in reality, we have a singleton cache object that runs the if (MemCache == null) block code above on initialization. the reflection code should apply the updated timer resolution for the lifetime of the application that it's in, so you should only have to run it at the start of your cache – Jared Jan 23 '14 at 22:47
  • 11
    The polling interval seems to be configurable, see here: http://msdn.microsoft.com/en-us/library/system.runtime.caching.configuration.memorycacheelement.pollinginterval(v=vs.110).aspx – MatteoSp Jan 28 '14 at 14:31
  • 1
    you should update the polling interval not write to private field using reflection... – skyde Mar 28 '18 at 21:21
10

Would you be willing/able to change from the older System.Runtime.Caching to the new Microsft.Extensions.Caching? version 1.x supports netstandard 1.3 and net451. If so then the improved API would support the usage you describe without hackery with reflection.

The MemoryCacheOptions object has a property ExpirationScanFrequency to allow you to control the scan frequency of the cache cleanup, see https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.memorycacheoptions.expirationscanfrequency?view=aspnetcore-2.0

Be aware that there is no longer expiration based on timers (this is a performance design decision), and so now memory pressure or calling one of the Get() based methods for the cached items are now the triggers for expiration. However you can force time based expiration using cancellation tokens, see this SO answer for an example https://stackoverflow.com/a/47949111/3140853.

alastairtree
  • 3,960
  • 32
  • 49
  • It has been a long time since I've touched any of this, but if this is the best way to implement what I was originally asking for then I am willing to accept your solution ‍♂️ – Jared Jun 22 '20 at 15:47
4

To MatteoSp - the pollingInterval in the configuration or the NameValueCollection in the constructor is a different timer. It is an interval that when called will use the two other config properties to determine if memory is at a level that requires entries to be removed using the Trim method.

user487779
  • 550
  • 5
  • 12
  • 1
    Had the same experience here. Changing the polling interval to one second, for example, did not affect how soon items were removed from the cache or updated. – Ryan Burbidge Dec 04 '14 at 19:09
1

An updated version basing on @Jared's answer. Insread of modify the default MemoryCache instance, here creates a new one.

class FastExpiringCache
{
    public static MemoryCache Default { get; } = Create();

    private static MemoryCache Create()
    {
        MemoryCache instance = null;
        Assembly assembly = typeof(CacheItemPolicy).Assembly;
        Type type = assembly.GetType("System.Runtime.Caching.CacheExpires");
        if( type != null)
        {
            FieldInfo field = type.GetField("_tsPerBucket", BindingFlags.Static | BindingFlags.NonPublic);
            if(field != null && field.FieldType == typeof(TimeSpan))
            {
                TimeSpan originalValue = (TimeSpan)field.GetValue(null);
                field.SetValue(null, TimeSpan.FromSeconds(3));
                instance = new MemoryCache("FastExpiringCache");
                field.SetValue(null, originalValue); // reset to original value
            }
        }
        return instance ?? new MemoryCache("FastExpiringCache");
    }
}
Mr.Wang from Next Door
  • 13,670
  • 12
  • 64
  • 97