//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner Microsoft // @backupOwner Microsoft //------------------------------------------------------------------------------ namespace System.Data.Common.QueryCache { using System; using System.Collections.Generic; using System.Data.EntityClient; using System.Data.Metadata.Edm; using System.Data.Objects.Internal; using System.Diagnostics; using System.Threading; using System.Data.Common.Internal.Materialization; using System.Data.Entity.Util; /// /// Provides Query Execution Plan Caching Service /// /// /// Thread safe. /// Dispose must be called as there is no finalizer for this class /// internal class QueryCacheManager : IDisposable { #region Constants/Default values for configuration parameters /// /// Default high mark for starting sweeping process /// default value: 80% of MaxNumberOfEntries /// const float DefaultHighMarkPercentageFactor = 0.8f; // 80% of MaxLimit /// /// Recycler timer period /// const int DefaultRecyclerPeriodInMilliseconds = 60 * 1000; #endregion #region Fields /// /// cache lock object /// private readonly object _cacheDataLock = new object(); /// /// cache data /// private readonly Dictionary _cacheData = new Dictionary(32); /// /// soft maximum number of entries in the cache /// private readonly int _maxNumberOfEntries; /// /// high mark of the number of entries to trigger the sweeping process /// private readonly int _sweepingTriggerHighMark; /// /// Eviction timer /// private readonly EvictionTimer _evictionTimer; #endregion #region Construction and Initialization /// /// Constructs a new Query Cache Manager instance, with default values for all 'configurable' parameters. /// /// A new instance of configured with default entry count, load factor and recycle period internal static QueryCacheManager Create() { return new QueryCacheManager(AppSettings.QueryCacheSize, DefaultHighMarkPercentageFactor, DefaultRecyclerPeriodInMilliseconds); } /// /// Cache Constructor /// /// /// Maximum number of entries that the cache should contain. /// /// /// The number of entries that must be present, as a percentage, before entries should be removed /// according to the eviction policy. /// Must be greater than 0 and less than or equal to 1.0 /// /// /// The interval, in milliseconds, at which the number of entries will be compared to the load factor /// and eviction carried out if necessary. /// private QueryCacheManager(int maximumSize, float loadFactor, int recycleMillis) { Debug.Assert(maximumSize > 0, "Maximum size must be greater than zero"); Debug.Assert(loadFactor > 0 && loadFactor <= 1, "Load factor must be greater than 0.0 and less than or equal to 1.0"); Debug.Assert(recycleMillis >= 0, "Recycle period in milliseconds must not be negative"); // // Load hardcoded defaults // this._maxNumberOfEntries = maximumSize; // // set sweeping high mark trigger value // this._sweepingTriggerHighMark = (int)(_maxNumberOfEntries * loadFactor); // // Initialize Recycler // this._evictionTimer = new EvictionTimer(this, recycleMillis); } #endregion #region 'External' interface /// /// Adds new entry to the cache using "abstract" cache context and /// value; returns an existing entry if the key is already in the /// dictionary. /// /// /// /// The existing entry in the dicitionary if already there; /// inQueryCacheEntry if none was found and inQueryCacheEntry /// was added instead. /// /// true if the output entry was already found; false if it had to be added. internal bool TryLookupAndAdd(QueryCacheEntry inQueryCacheEntry, out QueryCacheEntry outQueryCacheEntry) { Debug.Assert(null != inQueryCacheEntry, "qEntry must not be null"); outQueryCacheEntry = null; lock (_cacheDataLock) { if (!_cacheData.TryGetValue(inQueryCacheEntry.QueryCacheKey, out outQueryCacheEntry)) { // // add entry to cache data // _cacheData.Add(inQueryCacheEntry.QueryCacheKey, inQueryCacheEntry); if (_cacheData.Count > _sweepingTriggerHighMark) { _evictionTimer.Start(); } return false; } else { outQueryCacheEntry.QueryCacheKey.UpdateHit(); return true; } } } /// /// Lookup service for a cached value. /// internal bool TryCacheLookup(TK key, out TE value) where TK : QueryCacheKey { Debug.Assert(null != key, "key must not be null"); value = default(TE); // // invoke internal lookup // QueryCacheEntry qEntry = null; bool bHit = TryInternalCacheLookup(key, out qEntry); // // if it is a hit, 'extract' the entry strong type cache value // if (bHit) { value = (TE)qEntry.GetTarget(); } return bHit; } /// /// Clears the Cache /// internal void Clear() { lock (_cacheDataLock) { _cacheData.Clear(); } } #endregion #region Private Members /// /// lookup service /// /// /// /// true if cache hit, false if cache miss private bool TryInternalCacheLookup( QueryCacheKey queryCacheKey, out QueryCacheEntry queryCacheEntry ) { Debug.Assert(null != queryCacheKey, "queryCacheKey must not be null"); queryCacheEntry = null; bool bHit = false; // // lock the cache for the minimal possible period // lock (_cacheDataLock) { bHit = _cacheData.TryGetValue(queryCacheKey, out queryCacheEntry); } // // if cache hit // if (bHit) { // // update hit mark in cache key // queryCacheEntry.QueryCacheKey.UpdateHit(); } return bHit; } /// /// Recycler handler. This method is called directly by the eviction timer. /// It should take no action beyond invoking the method on the /// cache manager instance passed as . /// /// The cache manager instance on which the 'recycle' handler should be invoked private static void CacheRecyclerHandler(object state) { ((QueryCacheManager)state).SweepCache(); } /// /// Aging factor /// private static readonly int[] _agingFactor = {1,1,2,4,8,16}; private static readonly int AgingMaxIndex = _agingFactor.Length - 1; /// /// Sweeps the cache removing old unused entries. /// This method implements the query cache eviction policy. /// private void SweepCache() { if (!this._evictionTimer.Suspend()) { // Return of false from .Suspend means that the manager and timer have been disposed. return; } bool disabledEviction = false; lock (_cacheDataLock) { // // recycle only if entries exceeds the high mark factor // if (_cacheData.Count > _sweepingTriggerHighMark) { // // sweep the cache // uint evictedEntriesCount = 0; List cacheKeys = new List(_cacheData.Count); cacheKeys.AddRange(_cacheData.Keys); for (int i = 0; i < cacheKeys.Count; i++) { // // if entry was not used in the last time window, then evict the entry // if (0 == cacheKeys[i].HitCount) { _cacheData.Remove(cacheKeys[i]); evictedEntriesCount++; } // // otherwise, age the entry in a progressive scheme // else { int agingIndex = unchecked(cacheKeys[i].AgingIndex + 1); if (agingIndex > AgingMaxIndex) { agingIndex = AgingMaxIndex; } cacheKeys[i].AgingIndex = agingIndex; cacheKeys[i].HitCount = cacheKeys[i].HitCount >> _agingFactor[agingIndex]; } } } else { _evictionTimer.Stop(); disabledEviction = true; } } if (!disabledEviction) { this._evictionTimer.Resume(); } } #endregion #region IDisposable Members /// /// Dispose instance /// /// Dispose must be called as there are no finalizers for this class public void Dispose() { // Technically, calling GC.SuppressFinalize is not required because the class does not // have a finalizer, but it does no harm, protects against the case where a finalizer is added // in the future, and prevents an FxCop warning. GC.SuppressFinalize(this); if (this._evictionTimer.Stop()) { this.Clear(); } } #endregion /// /// Periodically invokes cache cleanup logic on a specified instance, /// and allows this periodic callback to be suspended, resumed or stopped in a thread-safe way. /// private sealed class EvictionTimer { /// Used to control multi-threaded accesses to this instance private readonly object _sync = new object(); /// The required interval between invocations of the cache cleanup logic private readonly int _period; /// The underlying QueryCacheManger that the callback will act on private readonly QueryCacheManager _cacheManager; /// The underlying that implements the periodic callback private Timer _timer; internal EvictionTimer(QueryCacheManager cacheManager, int recyclePeriod) { this._cacheManager = cacheManager; this._period = recyclePeriod; } internal void Start() { lock (_sync) { if (_timer == null) { this._timer = new Timer(QueryCacheManager.CacheRecyclerHandler, _cacheManager, _period, _period); } } } /// /// Permanently stops the eviction timer. /// It will no longer generate periodic callbacks and further calls to , , or , /// though thread-safe, will have no effect. /// /// /// If this eviction timer has already been stopped (using the method), returns false; /// otherwise, returns true to indicate that the call successfully stopped and cleaned up the underlying timer instance. /// /// /// Thread safe. May be called regardless of the current state of the eviction timer. /// Once stopped, an eviction timer cannot be restarted with the method. /// internal bool Stop() { lock (_sync) { if (this._timer != null) { this._timer.Dispose(); this._timer = null; return true; } else { return false; } } } /// /// Pauses the operation of the eviction timer. /// /// /// If this eviction timer has already been stopped (using the method), returns false; /// otherwise, returns true to indicate that the call successfully suspended the inderlying /// and no further periodic callbacks will be generated until the method is called. /// /// /// Thread-safe. May be called regardless of the current state of the eviction timer. /// Once suspended, an eviction timer may be resumed or stopped. /// internal bool Suspend() { lock (_sync) { if (this._timer != null) { this._timer.Change(Timeout.Infinite, Timeout.Infinite); return true; } else { return false; } } } /// /// Causes this eviction timer to generate periodic callbacks, provided it has not been permanently stopped (using the method). /// /// /// Thread-safe. May be called regardless of the current state of the eviction timer. /// internal void Resume() { lock (_sync) { if (this._timer != null) { this._timer.Change(this._period, this._period); } } } } } }