1

I use a System.Runtime.MemoryCache in a web services integration layer to keep some product lists which are external and slow to retrieve. However, I want to refresh those as they expire to free my callers from waiting on it. For that reason I am prewarming this cache using IIS7.5 and have a RemovedCallback to reload the data when expired.

However, what happens when the web pool process dies gracefully? MemoryCache is disposable, so it will kick my object out, at which point I'll try to cram a new instance back, whilst putting the entire process on hold. Is there any way for me to safely detect that I should not reload data?

internal class ProductCache {
    private static object _lock = new object();
    private static string _cid = Guid.NewGuid().ToString();

    public static Product[] data 
    {
        get
        {
            lock (_lock)
            {
                if (!MemoryCache.Default.Contains(_cid))
                {
                    Product[] p;
                    try
                    {
                        // load data into p
                    }
                    catch (System.Exception e)
                    {
                        Helper.SafeSendExceptionEmail("Integrator.Caching", e);
                        throw e;
                    }
                    MemoryCache.Default.Add(_cid, p, new CacheItemPolicy()
                    {
                        AbsoluteExpiration = DateTimeOffset.Now.AddHours(8),
                        RemovedCallback = arg =>
                        {
                            // query again to force reload
                            var d = data;
                        }
                    });
                }
                return (Product[])MemoryCache.Default[_cid];
            }
        }
    }
}
mmix
  • 6,057
  • 3
  • 39
  • 65
  • `RemovedCallback` gets a `CacheEntryRemovedArguments` instance which has a `RemovedReason`. I'm assuming (but have not verified) that that's only `RemovedReason.Expired` when the entry actually expires. – Jeroen Mostert Feb 17 '15 at 10:07
  • It can be Evicted too, if memory gets constrained, etc. I don't see a "I am shutting down, so I kicked you out" reason in the list. There is one vague reason (`CacheSpecificEviction = 4, // A cache entry was evicted for as reason that is defined by a particular cache`), but that seems too ambiguous... – mmix Feb 17 '15 at 10:17
  • But you are only interested in reloading if the entry *expires*, right? You don't need to know if the cache is shutting down, just if your entry is removed due to expiration. You don't want to reload it if it's removed for any other reason period (imagine the mess you get if the cache tries to remove entries due to memory pressure and you chuck them back in!) – Jeroen Mostert Feb 17 '15 at 10:19
  • I have to chuck them back in, even if it forces other data out, its a critical piece of info. Worst case scenario, this and other data in cache will constantly get reloaded and push each other out. Speed is a nice-to-have, working-anyway-even-if-we-have -to-wait is a must :) memory pressure might also be temporary but if I give up on first eviction I will never get back on the horse. – mmix Feb 17 '15 at 10:25
  • 1
    Your worst case scenario is actually crippling -- not only are you out of memory but your application also eats 100% CPU while doing nothing useful except cycling items. If it's so critical that you have the data resident in memory, you probably don't want a best-effort cache in the first place -- write your own class that keeps the information resident and just periodically reloads it. That way you're not trying to fit a square peg in a round hole. – Jeroen Mostert Feb 17 '15 at 10:33
  • Look, that scenario is purely academic, I am merely doing a best effort to respect it as we will never run out of memory unless there is a leak. I would much rather not build my own cache if there is one to use. Anyway, it seems that `CacheSpecificEviction` is the one I am looking for, I am just digging through `MemoryCacheStore` source. – mmix Feb 17 '15 at 10:48
  • 1
    What I'm trying to get at is this: a cache should speed things up, but not affect functional correctness if it's removed altogether. Therefore, it should be totally legitimate to have a conservative implementation that re-inserts entries only when they're known to have expired, and not for any other removal reason (like someone explicitly calling `Remove`). If you're "digging through the source" to get at the exact implementation details, you are already relying way more on particular behavior than you have any sane reason to. Hence my advice to write a class that does precisely what you want. – Jeroen Mostert Feb 17 '15 at 10:51
  • I somewhat agree with you, I am just trying to avoid reinventing the hot water. Writing my own timers, managing expiration, etc, why do all that if its already done for me. Now I see that many aspect of this are moot (Remove is impossible due to obscurity, so Eviction and Expiration are the only possible non-terminal reasons), but that also became obvious by implementation analysis. I'll just leave the question, someone might have a different use for it. – mmix Feb 17 '15 at 11:06

1 Answers1

1

Ok, by digging through MemoryCache and MemoryCacheStore source, it seems that cache is auto-disposed on domain unload at which point it disposes of all its stores, which in turn remove cache items with CacheSpecificEviction reason. This reason is not used anywhere else so it must represent the "I'm dying" reason (they could have been more clear in the docs though)

    public void Dispose()
    {
        if (Interlocked.Exchange(ref this._disposed, 1) == 0)
        {
            this._expires.EnableExpirationTimer(false);
            ArrayList list = new ArrayList(this._entries.Count);
            lock (this._entriesLock)
            {
                foreach (DictionaryEntry entry in this._entries)
                {
                    MemoryCacheEntry entry2 = entry.Value as MemoryCacheEntry;
                    list.Add(entry2);
                }
                foreach (MemoryCacheEntry entry3 in list)
                {
                    MemoryCacheKey key = entry3;
                    entry3.State = EntryState.RemovingFromCache;
                    this._entries.Remove(key);
                }
            }
            foreach (MemoryCacheEntry entry4 in list)
            {
                this.RemoveFromCache(entry4, CacheEntryRemovedReason.CacheSpecificEviction, false);
            }
            this._insertBlock.Close();
        }
    }
mmix
  • 6,057
  • 3
  • 39
  • 65