//----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // //----------------------------------------------------------------------- namespace System.IdentityModel.Tokens { using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IdentityModel.Diagnostics; using System.IdentityModel.Tokens; /// /// An MRU cache (Most Recently Used). /// /// /// Thread safe. Critsec around each method. /// A LinkedList is used to track MRU for fast purge. /// A Dictionary is used for fast keyed lookup. /// Grows until it reaches this.maximumSize, then purges down to this.sizeAfterPurge. /// internal class MruSessionSecurityTokenCache : SessionSecurityTokenCache { #pragma warning disable 1591 public const int DefaultTokenCacheSize = 20000; public static readonly TimeSpan DefaultPurgeInterval = TimeSpan.FromMinutes(15); #pragma warning restore 1591 private DateTime nextPurgeTime = DateTime.UtcNow + DefaultPurgeInterval; private Dictionary items; private int maximumSize; private CacheEntry mruEntry; private LinkedList mruList; private int sizeAfterPurge; private object syncRoot = new object(); private object purgeLock = new object(); /// /// Constructor to create an instance of this class. /// /// /// Uses the default maximum cache size. /// public MruSessionSecurityTokenCache() : this(DefaultTokenCacheSize) { } /// /// Constructor to create an instance of this class. /// /// Defines the maximum size of the cache. public MruSessionSecurityTokenCache(int maximumSize) : this(maximumSize, null) { } /// /// Constructor to create an instance of this class. /// /// Defines the maximum size of the cache. /// The method used for comparing cache entries. public MruSessionSecurityTokenCache(int maximumSize, IEqualityComparer comparer) : this((maximumSize / 5) * 4, maximumSize, comparer) { } /// /// Constructor to create an instance of this class. /// /// /// If the cache size exceeds , /// the cache will be resized to by removing least recently used items. /// /// Defines the maximum size of the cache. public MruSessionSecurityTokenCache(int sizeAfterPurge, int maximumSize) : this(sizeAfterPurge, maximumSize, null) { } /// /// Constructor to create an instance of this class. /// /// Specifies the size to which the cache is purged after it reaches . /// Specifies the maximum size of the cache. /// Specifies the method used for comparing cache entries. public MruSessionSecurityTokenCache(int sizeAfterPurge, int maximumSize, IEqualityComparer comparer) { if (sizeAfterPurge < 0) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ArgumentException(SR.GetString(SR.ID0008), "sizeAfterPurge")); } if (sizeAfterPurge >= maximumSize) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ArgumentException(SR.GetString(SR.ID0009), "sizeAfterPurge")); } // null comparer is ok this.items = new Dictionary(maximumSize, comparer); this.maximumSize = maximumSize; this.mruList = new LinkedList(); this.sizeAfterPurge = sizeAfterPurge; this.mruEntry = new CacheEntry(); } /// /// Gets the maximum size of the cache /// public int MaximumSize { get { return this.maximumSize; } } /// /// Deletes the specified cache entry from the MruCache. /// /// Specifies the key for the entry to be deleted. /// The is null. public override void Remove(SessionSecurityTokenCacheKey key) { if (key == null) { return; } lock (this.syncRoot) { CacheEntry entry; if (this.items.TryGetValue(key, out entry)) { this.items.Remove(key); this.mruList.Remove(entry.Node); if (object.ReferenceEquals(this.mruEntry.Node, entry.Node)) { this.mruEntry.Value = null; this.mruEntry.Node = null; } } } } /// /// Attempts to add an entry to the cache or update an existing one. /// /// The key for the entry to be added. /// The security token to be added to the cache. /// The expiration time for this entry. public override void AddOrUpdate(SessionSecurityTokenCacheKey key, SessionSecurityToken value, DateTime expirationTime) { if (key == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("key"); } lock (this.syncRoot) { this.Purge(); this.Remove(key); // Add the new entry to the cache and make it the MRU element CacheEntry entry = new CacheEntry(); entry.Node = this.mruList.AddFirst(key); entry.Value = value; this.items.Add(key, entry); this.mruEntry = entry; } } /// /// Returns the Session Security Token corresponding to the specified key exists in the cache. Also if it exists, marks it as MRU. /// /// Specifies the key for the entry to be retrieved. /// Returns the Session Security Token from the cache if found, otherwise, null. public override SessionSecurityToken Get(SessionSecurityTokenCacheKey key) { if (key == null) { return null; } // If found, make the entry most recently used SessionSecurityToken sessionToken = null; CacheEntry entry; bool found; lock (this.syncRoot) { // first check our MRU item if (this.mruEntry.Node != null && key != null && key.Equals(this.mruEntry.Node.Value)) { return this.mruEntry.Value; } found = this.items.TryGetValue(key, out entry); if (found) { sessionToken = entry.Value; // Move the node to the head of the MRU list if it's not already there if (this.mruList.Count > 1 && !object.ReferenceEquals(this.mruList.First, entry.Node)) { this.mruList.Remove(entry.Node); this.mruList.AddFirst(entry.Node); this.mruEntry = entry; } } } return sessionToken; } /// /// Deletes matching cache entries from the MruCache. /// /// Specifies the endpointId for the entries to be deleted. /// Specifies the contextId for the entries to be deleted. public override void RemoveAll(string endpointId, System.Xml.UniqueId contextId) { if (null == contextId || string.IsNullOrEmpty(endpointId)) { return; } Dictionary entriesToDelete = new Dictionary(); SessionSecurityTokenCacheKey key = new SessionSecurityTokenCacheKey(endpointId, contextId, null); key.IgnoreKeyGeneration = true; lock (this.syncRoot) { foreach (SessionSecurityTokenCacheKey itemKey in this.items.Keys) { if (itemKey.Equals(key)) { entriesToDelete.Add(itemKey, this.items[itemKey]); } } foreach (SessionSecurityTokenCacheKey itemKey in entriesToDelete.Keys) { this.items.Remove(itemKey); CacheEntry entry = entriesToDelete[itemKey]; this.mruList.Remove(entry.Node); if (object.ReferenceEquals(this.mruEntry.Node, entry.Node)) { this.mruEntry.Value = null; this.mruEntry.Node = null; } } } } /// /// Attempts to remove all entries with a matching endpoint Id from the cache. /// /// The endpoint id for the entry to be removed. public override void RemoveAll(string endpointId) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new NotImplementedException(SR.GetString(SR.ID4294))); } /// /// Returns all the entries that match the given key. /// /// The endpoint id for the entries to be retrieved. /// The context id for the entries to be retrieved. /// A collection of all the matching entries, an empty collection of no match found. public override IEnumerable GetAll(string endpointId, System.Xml.UniqueId contextId) { Collection tokens = new Collection(); if (null == contextId || string.IsNullOrEmpty(endpointId)) { return tokens; } CacheEntry entry; SessionSecurityTokenCacheKey key = new SessionSecurityTokenCacheKey(endpointId, contextId, null); key.IgnoreKeyGeneration = true; lock (this.syncRoot) { foreach (SessionSecurityTokenCacheKey itemKey in this.items.Keys) { if (itemKey.Equals(key)) { entry = this.items[itemKey]; // Move the node to the head of the MRU list if it's not already there if (this.mruList.Count > 1 && !object.ReferenceEquals(this.mruList.First, entry.Node)) { this.mruList.Remove(entry.Node); this.mruList.AddFirst(entry.Node); this.mruEntry = entry; } tokens.Add(entry.Value); } } } return tokens; } /// /// This method must not be called from within a read or writer lock as a deadlock will occur. /// Checks the time a decides if a cleanup needs to occur. /// private void Purge() { if (this.items.Count >= this.maximumSize) { // If the cache is full, purge enough LRU items to shrink the // cache down to the low watermark int countToPurge = this.maximumSize - this.sizeAfterPurge; for (int i = 0; i < countToPurge; i++) { SessionSecurityTokenCacheKey keyRemove = this.mruList.Last.Value; this.mruList.RemoveLast(); this.items.Remove(keyRemove); } if (DiagnosticUtility.ShouldTrace(TraceEventType.Information)) { TraceUtility.TraceString( TraceEventType.Information, SR.GetString( SR.ID8003, this.maximumSize, this.sizeAfterPurge)); } } } public class CacheEntry { public SessionSecurityToken Value { get; set; } public LinkedListNode Node { get; set; } } } }