C# – SlidingExpiration and MemoryCache

c

Looking at the documentation for MemoryCache I expected that if an object was accessed within the Expiration period the period would be refreshed. To be honest I think I inferred from the name 'Sliding' as much as anything.

However, it appears from this test

   [Test]
    public void SlidingExpiryNotRefreshedOnTouch()
    {
        var memoryCache = new MemoryCache("donkey")
        {
            {
                "1",
                "jane",
                new CacheItemPolicy {SlidingExpiration = TimeSpan.FromSeconds(1) }
            }
        };
        var enumerable = Enumerable.Repeat("1", 100)
            .TakeWhile((id, index) =>
            {
                Thread.Sleep(100);
                return memoryCache.Get(id) != null; // i.e. it still exists
            })
            .Select((id, index) => (index+2)*100.0/1000); // return the elapsed time
        var expires = enumerable.Last(); // gets the last existing entry 
        expires.Should().BeGreaterThan(1.0);
    }

It fails and exhibits the behavior that the object is ejected once the TimeSpan is complete whether or not the object has been accessed. The Linq query is executed at the enumerable.Last(); statement, at which point it will only take while the cache has not expired. As soon as it stops the last item in the list will indicate how long the item lived in the cache for.

For Clarity This question is about the behaviour of MemoryCache. Not the linq query.

Is this anyone else's expectation (i.e. that the expiration does not slide with each touch)?
Is there a mode that extends the lifetime of objects that are 'touched'?

Update I found even if I wrote a wrapper around the cache and re-added the object back to the cache every time I retrieved it, with another SlidingExpiration its still only honored the initial setting. To get it to work the way I desired I had to physically remove it from the cache before re-adding it! This could cause undesirable race conditions in a multi-threaded environment.

Best Answer

 ... new CacheItemPolicy {SlidingExpiration = TimeSpan.FromSeconds(1) }

This is not adequately documented in MSDN. You were a bit unlucky, 1 second is not enough. By a hair, use 2 seconds and you'll see it works just like you hoped it would. Tinker some more with FromMilliseconds() and you'll see that ~1.2 seconds is the happy minimum in this program.

Explaining this is rather convoluted, I have to talk about how MemoryCache avoids having to update the sliding timer every single time you access the cache. Which is relatively expensive, as you might imagine. Let's take a shortcut and take you to the relevant Reference Source code. Small enough to paste here:

    internal void UpdateSlidingExp(DateTime utcNow, CacheExpires expires) {
        if (_slidingExp > TimeSpan.Zero) {
            DateTime utcNewExpires = utcNow + _slidingExp;
            if (utcNewExpires - _utcAbsExp >= CacheExpires.MIN_UPDATE_DELTA || utcNewExpires < _utcAbsExp) {
                expires.UtcUpdate(this, utcNewExpires);
            }
        }
    }

CacheExpires.MIN_UPDATE_DELTA is the crux, it prevents UtcUpdate() from being called. Or to put it another way, at least MIN_UPDATE_DELTA worth of time has to pass before it will update the sliding timer. The CacheExpired class is not indexed by the Reference Source, a hint that they are not entirely happy about the way it works :) But a decent decompiler can show you:

static CacheExpires()
{
    MIN_UPDATE_DELTA = new TimeSpan(0, 0, 1);
    MIN_FLUSH_INTERVAL = new TimeSpan(0, 0, 1);
    // etc...
}

In other words, hard-coded to 1 second. With no way to change it right now, that's pretty ugly. It takes ~1.2 seconds for the SlidingExpiration value in this test program because Thread.Sleep(100) does not actually sleep for 100 milliseconds, it takes a bit more. Or to put it another way, it will be the 11th Get() call that gets the sliding timer to slide in this test program. You didn't get that far.

Well, this ought to be documented but I'd guess this is subject to change. For now, you'll need to assume that a practical sliding expiration time should be at least 2 seconds.