File: System\Caching\MemoryCacheStore.cs
Project: ndp\fx\src\Caching\System.Runtime.Caching.csproj (System.Runtime.Caching)
// <copyright file="MemoryCacheStore.cs" company="Microsoft">
//   Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
// </copyright>
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Threading;
using System.Diagnostics;
using System.Security;
using System.Security.Permissions;
using System.Diagnostics.CodeAnalysis;
 
namespace System.Runtime.Caching {
    internal sealed class MemoryCacheStore : IDisposable {
        const int INSERT_BLOCK_WAIT = 10000;
        const int MAX_COUNT = Int32.MaxValue / 2;
 
        private Hashtable _entries;
        private Object _entriesLock;
        private CacheExpires _expires;
        private CacheUsage _usage;
        private int _disposed;
        private ManualResetEvent _insertBlock;
        private volatile bool _useInsertBlock;
        private MemoryCache _cache;
        private PerfCounters _perfCounters;
 
        internal MemoryCacheStore(MemoryCache cache, PerfCounters perfCounters) {
            _cache = cache;
            _perfCounters = perfCounters;
            _entries = new Hashtable(new MemoryCacheEqualityComparer());
            _entriesLock = new Object();
            _expires = new CacheExpires(this);
            _usage = new CacheUsage(this);
            InitDisposableMembers();
        }
 
        // private members        
 
        private void AddToCache(MemoryCacheEntry entry) {
 
            // add outside of lock
            if (entry == null) {
                return;
            }
 
            if (entry.HasExpiration()) {
                _expires.Add(entry);
            }
 
            if (entry.HasUsage()
                && (!entry.HasExpiration() || entry.UtcAbsExp - DateTime.UtcNow >= CacheUsage.MIN_LIFETIME_FOR_USAGE)) {
                _usage.Add(entry);
            }
 
            // One last sanity check to be sure we didn't fall victim to an Add ----
            if (!entry.CompareExchangeState(EntryState.AddedToCache, EntryState.AddingToCache)) {
                if (entry.InExpires()) {
                    _expires.Remove(entry);
                }
 
                if (entry.InUsage()) {
                    _usage.Remove(entry);
                }
            }
 
            entry.CallNotifyOnChanged();
            if (_perfCounters != null) {
                _perfCounters.Increment(PerfCounterName.Entries);
                _perfCounters.Increment(PerfCounterName.Turnover);
            }
        }
 
        private void InitDisposableMembers() {
            _insertBlock = new ManualResetEvent(true);
            _expires.EnableExpirationTimer(true);
        }
 
 
        private void RemoveFromCache(MemoryCacheEntry entry, CacheEntryRemovedReason reason, bool delayRelease = false) {
            // release outside of lock
            if (entry != null) {
                if (entry.InExpires()) {
                    _expires.Remove(entry);
                }
 
                if (entry.InUsage()) {
                    _usage.Remove(entry);
                }
 
                Dbg.Assert(entry.State == EntryState.RemovingFromCache, "entry.State = EntryState.RemovingFromCache");
 
                entry.State = EntryState.RemovedFromCache;
                if (!delayRelease) {
                    entry.Release(_cache, reason);
                }
                if (_perfCounters != null) {
                    _perfCounters.Decrement(PerfCounterName.Entries);
                    _perfCounters.Increment(PerfCounterName.Turnover);
                }
            }
        }
 
        // 'updatePerfCounters' defaults to true since this method is called by all Get() operations
        // to update both the performance counters and the sliding expiration. Callers that perform
        // nested sliding expiration updates (like a MemoryCacheEntry touching its update sentinel)
        // can pass false to prevent these from unintentionally showing up in the perf counters.
        internal void UpdateExpAndUsage(MemoryCacheEntry entry, bool updatePerfCounters = true) {
            if (entry != null) {
                if (entry.InUsage() || entry.SlidingExp > TimeSpan.Zero) {
                    DateTime utcNow = DateTime.UtcNow;
                    entry.UpdateSlidingExp(utcNow, _expires);
                    entry.UpdateUsage(utcNow, _usage);
                }
 
                // DevDiv #67021: If this entry has an update sentinel, the sliding expiration is actually associated
                // with that sentinel, not with this entry. We need to update the sentinel's sliding expiration to
                // keep the sentinel from expiring, which in turn would force a removal of this entry from the cache.
                entry.UpdateSlidingExpForUpdateSentinel();
 
                if (updatePerfCounters && _perfCounters != null) {
                    _perfCounters.Increment(PerfCounterName.Hits);
                    _perfCounters.Increment(PerfCounterName.HitRatio);
                    _perfCounters.Increment(PerfCounterName.HitRatioBase);
                }
            }
            else {
                if (updatePerfCounters && _perfCounters != null) {
                    _perfCounters.Increment(PerfCounterName.Misses);
                    _perfCounters.Increment(PerfCounterName.HitRatioBase);
                }
            }
        }
 
        private void WaitInsertBlock() {
            _insertBlock.WaitOne(INSERT_BLOCK_WAIT, false);
        }
 
 
        // public/internal members
 
        internal CacheUsage Usage { get { return _usage; } }
 
        internal MemoryCacheEntry AddOrGetExisting(MemoryCacheKey key, MemoryCacheEntry entry) {
            if (_useInsertBlock && entry.HasUsage()) {
                WaitInsertBlock();
            }
            MemoryCacheEntry existingEntry = null;
            MemoryCacheEntry toBeReleasedEntry = null;
            bool added = false;
            lock (_entriesLock) {
                if (_disposed == 0) {
                    existingEntry = _entries[key] as MemoryCacheEntry;
                    // has it expired?
                    if (existingEntry != null && existingEntry.UtcAbsExp <= DateTime.UtcNow) {
                        toBeReleasedEntry = existingEntry;
                        toBeReleasedEntry.State = EntryState.RemovingFromCache;
                        existingEntry = null;
                    }
                    // can we add entry to the cache?
                    if (existingEntry == null) {
                        entry.State = EntryState.AddingToCache;
                        added = true;
                        _entries[key] = entry;
                    }
                }
            }
            // release outside of lock
            RemoveFromCache(toBeReleasedEntry, CacheEntryRemovedReason.Expired, delayRelease:true);
            if (added) {
                // add outside of lock
                AddToCache(entry);
            }
            // update outside of lock
            UpdateExpAndUsage(existingEntry);
            
            // Dev10 861163: Call Release after the new entry has been completely added so 
            // that the CacheItemRemovedCallback can take a dependency on the newly inserted item.
            if (toBeReleasedEntry != null) {
                toBeReleasedEntry.Release(_cache, CacheEntryRemovedReason.Expired);
            }
            return existingEntry;
        }
 
        internal void BlockInsert() {
            _insertBlock.Reset();
            _useInsertBlock = true;
        }
 
        internal void CopyTo(IDictionary h) {
            lock (_entriesLock) {
                if (_disposed == 0) {
                    foreach (DictionaryEntry e in _entries) {
                        MemoryCacheKey key = e.Key as MemoryCacheKey;
                        MemoryCacheEntry entry = e.Value as MemoryCacheEntry;
                        if (entry.UtcAbsExp > DateTime.UtcNow) {
                            h[key.Key] = entry.Value;
                        }
                    }
                }
            }
        }
 
        internal int Count {
            get {
                return _entries.Count;
 
            }
        }
 
        public void Dispose() {
            if (Interlocked.Exchange(ref _disposed, 1) == 0) {
                // disable CacheExpires timer
                _expires.EnableExpirationTimer(false);
                // build array list of entries
                ArrayList entries = new ArrayList(_entries.Count);
                lock (_entriesLock) {
                    foreach (DictionaryEntry e in _entries) {
                        MemoryCacheEntry entry = e.Value as MemoryCacheEntry;
                        entries.Add(entry);
                    }
                    foreach (MemoryCacheEntry entry in entries) {
                        MemoryCacheKey key = entry as MemoryCacheKey;
                        entry.State = EntryState.RemovingFromCache;
                        _entries.Remove(key);
                    }
                }
                // release entries outside of lock
                foreach (MemoryCacheEntry entry in entries) {
                    RemoveFromCache(entry, CacheEntryRemovedReason.CacheSpecificEviction);
                }
 
                // MemoryCacheStatistics has been disposed, and therefore nobody should be using
                // _insertBlock except for potential threads in WaitInsertBlock (which won't care if we call Close).
                Dbg.Assert(_useInsertBlock == false, "_useInsertBlock == false");
                _insertBlock.Close();
 
 
                // Don't need to call GC.SuppressFinalize(this) for sealed types without finalizers.
            }
        }
 
        internal MemoryCacheEntry Get(MemoryCacheKey key) {
            MemoryCacheEntry entry = _entries[key] as MemoryCacheEntry;
            // has it expired?
            if (entry != null && entry.UtcAbsExp <= DateTime.UtcNow) {
                Remove(key, entry, CacheEntryRemovedReason.Expired);
                entry = null;
            }
            // update outside of lock
            UpdateExpAndUsage(entry);
            return entry;
        }
 
        internal MemoryCacheEntry Remove(MemoryCacheKey key, MemoryCacheEntry entryToRemove, CacheEntryRemovedReason reason) {
            MemoryCacheEntry entry = null;
            lock (_entriesLock) {
                if (_disposed == 0) {
                    // get current entry
                    entry = _entries[key] as MemoryCacheEntry;
                    // remove if it matches the entry to be removed (but always remove if entryToRemove is null)
                    if (entryToRemove == null || Object.ReferenceEquals(entry, entryToRemove)) {
                        // Dev10 865887: MemoryCache.Remove("\ue637\ud22a\u3e17") causes NullReferenceEx
                        if (entry != null) {
                            entry.State = EntryState.RemovingFromCache;
                            _entries.Remove(key);
                        }
                    }
                    else {
                        entry = null;
                    }
                }
            }
            // release outside of lock
            RemoveFromCache(entry, reason);
            return entry;
        }
 
        internal void Set(MemoryCacheKey key, MemoryCacheEntry entry) {
            if (_useInsertBlock && entry.HasUsage()) {
                WaitInsertBlock();
            }
            MemoryCacheEntry existingEntry = null;
            bool added = false;
            lock (_entriesLock) {
                if (_disposed == 0) {
                    existingEntry = _entries[key] as MemoryCacheEntry;
                    if (existingEntry != null) {
                        existingEntry.State = EntryState.RemovingFromCache;
                    }
                    entry.State = EntryState.AddingToCache;
                    added = true;
                    _entries[key] = entry;
                }
            }
 
            CacheEntryRemovedReason reason = CacheEntryRemovedReason.Removed;
            if (existingEntry != null) {
                if (existingEntry.UtcAbsExp <= DateTime.UtcNow) {
                    reason = CacheEntryRemovedReason.Expired;
                }
                RemoveFromCache(existingEntry, reason, delayRelease:true);
            }
            if (added) {
                AddToCache(entry);
            }
 
            // Dev10 861163: Call Release after the new entry has been completely added so 
            // that the CacheItemRemovedCallback can take a dependency on the newly inserted item.
            if (existingEntry != null) {
                existingEntry.Release(_cache, reason);
            }
        }
 
        internal long TrimInternal(int percent) {
            Dbg.Assert(percent <= 100, "percent <= 100");
 
            int count = Count;
            int toTrim = 0;
            // do we need to drop a percentage of entries?
            if (percent > 0) {
                toTrim = (int)Math.Ceiling(((long)count * (long)percent) / 100D);
                // would this leave us above MAX_COUNT?
                int minTrim = count - MAX_COUNT;
                if (toTrim < minTrim) {
                    toTrim = minTrim;
                }
            }
            // do we need to trim?
            if (toTrim <= 0 || _disposed == 1) {
                return 0;
            }
            int trimmed = 0; // total number of entries trimmed
            int trimmedOrExpired = 0;
#if DBG
            int beginTotalCount = count;
#endif
 
            trimmedOrExpired = _expires.FlushExpiredItems(true);
            if (trimmedOrExpired < toTrim) {
                trimmed = _usage.FlushUnderUsedItems(toTrim - trimmedOrExpired);
                trimmedOrExpired += trimmed;
            }
 
            if (trimmed > 0 && _perfCounters != null) {
                // Update values for perfcounters
                _perfCounters.IncrementBy(PerfCounterName.Trims, trimmed);
            }
 
#if DBG
            Dbg.Trace("MemoryCacheStore", "TrimInternal:"
                        + " beginTotalCount=" + beginTotalCount
                        + ", endTotalCount=" + count
                        + ", percent=" + percent
                        + ", trimmed=" + trimmed);
#endif
            return trimmedOrExpired;
 
        }
 
        internal void UnblockInsert() {
            _useInsertBlock = false;
            _insertBlock.Set();
        }
    }
}