File: System\ServiceModel\Security\TimeBoundedCache.cs
Project: ndp\cdf\src\WCF\ServiceModel\System.ServiceModel.csproj (System.ServiceModel)
//-----------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//-----------------------------------------------------------------------------
 
namespace System.ServiceModel.Security 
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Runtime;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.Threading;
 
    // NOTE: this class does minimum argument checking as it is all internal 
    class TimeBoundedCache 
    {
        static Action<object> purgeCallback;
        ReaderWriterLock cacheLock;
        Hashtable entries;
        // if there are less than lowWaterMark entries, no purging is done
        int lowWaterMark;
        int maxCacheItems;
        DateTime nextPurgeTimeUtc;
        TimeSpan purgeInterval;
        PurgingMode purgingMode;
        IOThreadTimer purgingTimer;
        bool doRemoveNotification;
 
        protected TimeBoundedCache(int lowWaterMark, int maxCacheItems, IEqualityComparer keyComparer, PurgingMode purgingMode, TimeSpan purgeInterval, bool doRemoveNotification)
        {
            this.entries = new Hashtable(keyComparer);
            this.cacheLock = new ReaderWriterLock();
            this.lowWaterMark = lowWaterMark;
            this.maxCacheItems = maxCacheItems;
            this.purgingMode = purgingMode;
            this.purgeInterval = purgeInterval;
            this.doRemoveNotification = doRemoveNotification;
            this.nextPurgeTimeUtc = DateTime.UtcNow.Add(this.purgeInterval);
        }
        
        public int Count
        {
            get
            {
                return this.entries.Count;
            }
        }
 
        static Action<object> PurgeCallback
        {
            get
            {
                if (purgeCallback == null)
                {
                    purgeCallback = new Action<object>(PurgeCallbackStatic);
                }
                return purgeCallback;
            }
        }
 
        protected int Capacity
        {
            get
            {
                return this.maxCacheItems;
            }
        }
 
        protected Hashtable Entries
        {
            get
            {
                return this.entries;
            }
        }
 
        protected ReaderWriterLock CacheLock
        {
            get
            {
                return this.cacheLock;
            }
        }
 
        protected bool TryAddItem(object key, object item, DateTime expirationTime, bool replaceExistingEntry)
        {
            return this.TryAddItem(key, new ExpirableItem(item, expirationTime), replaceExistingEntry);
        }
 
        void CancelTimerIfNeeded()
        {
            if (this.Count == 0 && this.purgingTimer != null)
            {
                this.purgingTimer.Cancel();
                this.purgingTimer = null;
            }
        }
 
        void StartTimerIfNeeded()
        {
            if (this.purgingMode != PurgingMode.TimerBasedPurge)
            {
                return;
            }
            if (this.purgingTimer == null)
            {
                this.purgingTimer = new IOThreadTimer(PurgeCallback, this, false);
                this.purgingTimer.Set(this.purgeInterval);
            }
        }
 
        protected bool TryAddItem(object key, IExpirableItem item, bool replaceExistingEntry)
        {
            bool lockHeld = false;
            try
            {
                try { }
                finally
                {
                    this.cacheLock.AcquireWriterLock(-1);
                    lockHeld = true;
                }
                PurgeIfNeeded();
                EnforceQuota();
                IExpirableItem currentItem = this.entries[key] as IExpirableItem;
                if (currentItem == null || IsExpired(currentItem))
                {
                    this.entries[key] = item;
                }
                else if (!replaceExistingEntry)
                {
                    return false;
                }
                else
                {
                    this.entries[key] = item;
                }
                if (currentItem != null && doRemoveNotification)
                {
                    this.OnRemove(ExtractItem(currentItem));
                }
                StartTimerIfNeeded();
                return true;
            }
            finally
            {
                if (lockHeld)
                {
                    this.cacheLock.ReleaseWriterLock();
                }
            }
        }
 
        protected bool TryReplaceItem(object key, object item, DateTime expirationTime)
        {
            bool lockHeld = false;
            try
            {
                try { }
                finally
                {
                    this.cacheLock.AcquireWriterLock(-1);
                    lockHeld = true;
                }
                PurgeIfNeeded();
                EnforceQuota();
                IExpirableItem currentItem = this.entries[key] as IExpirableItem;
                if (currentItem == null || IsExpired(currentItem))
                {
                    return false;
                }
                else
                {
                    this.entries[key] = new ExpirableItem(item, expirationTime);
                    if (currentItem != null && doRemoveNotification)
                    {
                        this.OnRemove(ExtractItem(currentItem));
                    }
                    StartTimerIfNeeded();
                    return true;
                }
            }
            finally
            {
                if (lockHeld)
                {
                    this.cacheLock.ReleaseWriterLock();
                }
            }
        }
 
        protected void ClearItems()
        {
            bool lockHeld = false;
            try
            {
                try { }
                finally
                {
                    this.cacheLock.AcquireWriterLock(-1);
                    lockHeld = true;
                }
 
                int count = this.entries.Count;
                if (doRemoveNotification)
                {
                    foreach (IExpirableItem item in this.entries.Values)
                    {
                        OnRemove(ExtractItem(item));
                    }
                }
                this.entries.Clear();
                CancelTimerIfNeeded();
            }
            finally
            {
                if (lockHeld)
                {
                    this.cacheLock.ReleaseWriterLock();
                }
            }
        }
 
        protected object GetItem(object key)
        {
            bool lockHeld = false;
            try
            {
                try { }
                finally
                {
                    this.cacheLock.AcquireReaderLock(-1);
                    lockHeld = true;
                }
                IExpirableItem item = this.entries[key] as IExpirableItem;
                if (item == null)
                {
                    return null;
                }
                else if (IsExpired(item))
                {
                    // this is a stale item
                    return null;
                }
                else
                {
                    return ExtractItem(item);
                }
            }
            finally
            {
                if (lockHeld)
                {
                    this.cacheLock.ReleaseReaderLock();
                }
            }
        }
 
        protected virtual ArrayList OnQuotaReached(Hashtable cacheTable)
        {
            this.ThrowQuotaReachedException();
            return null;
        }
 
        protected virtual void OnRemove(object item)
        {
        }
 
        protected bool TryRemoveItem(object key)
        {
            bool lockHeld = false;
            try
            {
                try { }
                finally
                {
                    this.cacheLock.AcquireWriterLock(-1);
                    lockHeld = true;
                }
                PurgeIfNeeded();
                IExpirableItem currentItem = this.entries[key] as IExpirableItem;
                bool result = (currentItem != null) && !IsExpired(currentItem);
                if (currentItem != null)
                {
                    this.entries.Remove(key);
                    if (doRemoveNotification)
                    {
                        this.OnRemove(ExtractItem(currentItem));
                    }
                    CancelTimerIfNeeded();
                }
                return result;
            }
            finally
            {
                if (lockHeld)
                {
                    this.cacheLock.ReleaseWriterLock();
                }
            }
        }
 
 
        void EnforceQuota()
        {
            if (!(this.cacheLock.IsWriterLockHeld == true))
            {
                // we failfast here because if we don't have the lock we could corrupt the cache
                Fx.Assert("Cache write lock is not held.");
                DiagnosticUtility.FailFast("Cache write lock is not held.");
            }
            if (this.Count >= this.maxCacheItems)
            {
                ArrayList keysToBeRemoved;
                keysToBeRemoved = this.OnQuotaReached(this.entries);
                if (keysToBeRemoved != null)
                {
                    for (int i = 0; i < keysToBeRemoved.Count; ++i)
                    {
                        this.entries.Remove(keysToBeRemoved[i]);
                    }
                    
                }
                CancelTimerIfNeeded();
                if (this.Count >= this.maxCacheItems)
                {
                    this.ThrowQuotaReachedException();
                }
            }
        }
 
        protected object ExtractItem(IExpirableItem val)
        {
            ExpirableItem wrapper = (val as ExpirableItem);
            if (wrapper != null)
            {
                return wrapper.Item;
            }
            else
            {
                return val;
            }
        }
 
        bool IsExpired(IExpirableItem item)
        {
            Fx.Assert(item.ExpirationTime == DateTime.MaxValue || item.ExpirationTime.Kind == DateTimeKind.Utc, "");
            return (item.ExpirationTime <= DateTime.UtcNow);
        }
 
        bool ShouldPurge()
        {
            if (this.Count >= this.maxCacheItems)
            {
                return true;
            }
            else if (this.purgingMode == PurgingMode.AccessBasedPurge && DateTime.UtcNow > this.nextPurgeTimeUtc && this.Count > this.lowWaterMark)
            {
                return true;
            }
            else 
            {
                return false;
            }
        }
 
        void PurgeIfNeeded()
        {
            if (!(this.cacheLock.IsWriterLockHeld == true))
            {
                // we failfast here because if we don't have the lock we could corrupt the cache
                Fx.Assert("Cache write lock is not held.");
                DiagnosticUtility.FailFast("Cache write lock is not held.");
            }
            if (ShouldPurge())
            {
                PurgeStaleItems();
            }
        }
 
        /// <summary>
        /// This method must be called from within a writer lock
        /// </summary>
        void PurgeStaleItems()
        {
            if (!(this.cacheLock.IsWriterLockHeld == true))
            {
                // we failfast here because if we don't have the lock we could corrupt the cache
                Fx.Assert("Cache write lock is not held.");
                DiagnosticUtility.FailFast("Cache write lock is not held.");
            }
            ArrayList expiredItems = new ArrayList();
            foreach (object key in this.entries.Keys)
            {
                IExpirableItem item = this.entries[key] as IExpirableItem;
                if (IsExpired(item))
                {
                    // this is a stale item. Remove!
                    this.OnRemove(ExtractItem(item));
                    expiredItems.Add(key);
                }
            }
            for (int i = 0; i < expiredItems.Count; ++i)
            {
                this.entries.Remove(expiredItems[i]);
            }
            CancelTimerIfNeeded();
            this.nextPurgeTimeUtc = DateTime.UtcNow.Add(this.purgeInterval);
        }
 
        void ThrowQuotaReachedException()
        {
            string message = SR.GetString(SR.CacheQuotaReached, this.maxCacheItems);
            Exception inner = new QuotaExceededException(message);
            throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new CommunicationException(message, inner));
        }
 
        static void PurgeCallbackStatic(object state)
        {
            TimeBoundedCache self = (TimeBoundedCache)state;
 
            bool lockHeld = false;
            try
            {
                try { }
                finally
                {
                    self.cacheLock.AcquireWriterLock(-1);
                    lockHeld = true;
                }
 
                if (self.purgingTimer == null)
                {
                    return;
                }
                self.PurgeStaleItems();
                if (self.Count > 0 && self.purgingTimer != null)
                {
                    self.purgingTimer.Set(self.purgeInterval);
                }
            }
            finally
            {
                if (lockHeld)
                {
                    self.cacheLock.ReleaseWriterLock();
                }
            }
        }
 
        internal interface IExpirableItem
        {
            DateTime ExpirationTime { get; }
        }
 
        internal class ExpirableItemComparer : IComparer<IExpirableItem>
        {
            static ExpirableItemComparer instance;
 
            public static ExpirableItemComparer Default
            {
                get
                {
                    if (instance == null)
                    {
                        instance = new ExpirableItemComparer();
                    }
                    return instance;
                }
            }
 
            // positive, if item1 will expire before item2. 
            public int Compare(IExpirableItem item1, IExpirableItem item2)
            {
                if (ReferenceEquals(item1, item2))
                {
                    return 0;
                }
                Fx.Assert(item1.ExpirationTime.Kind == item2.ExpirationTime.Kind, "");
                if (item1.ExpirationTime < item2.ExpirationTime)
                {
                    return 1;
                }
                else if (item1.ExpirationTime > item2.ExpirationTime)
                {
                    return -1;
                }
                else
                {
                    return 0;
                }
            }
        }
 
        internal sealed class ExpirableItem : IExpirableItem
        {
            DateTime expirationTime;
            object item;
 
            public ExpirableItem(object item, DateTime expirationTime)
            {
                this.item = item;
                Fx.Assert( expirationTime == DateTime.MaxValue || expirationTime.Kind == DateTimeKind.Utc, "");
                this.expirationTime = expirationTime;
            }
 
            public DateTime ExpirationTime { get { return this.expirationTime; } }
            public object Item { get { return this.item; } }
        }
    }
 
    enum PurgingMode
    {
        TimerBasedPurge,
        AccessBasedPurge
    }
}