/* * Copyright (c) 2022, Sjofn LLC * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.co nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.Linq; namespace LibreMetaverse { public interface ICacheDictionaryRemovalStrategy { /// /// Initialize the strategy and pass the maximum number of allowed items /// /// The maximum number of allowed items void Initialize(int maxSize); /// /// Notify the strategy that a key was added to the base collection /// /// The key that was added void KeyAdded(TKey key); /// /// Notify the strategy that a key was removed from the base collection /// /// The key that was removed void KeyRemoved(TKey key); /// /// Notify the strategy that a key was accessed (retrieved by the user) in the base collection /// /// The key that was retrieved void KeyAccessed(TKey key); /// /// Notify the strategy that the base collection was cleared /// void Clear(); /// /// Get the most appropriate key to remove, this is called when the base collection runs out of space /// /// The key that the base collection will remove TKey GetKeyToRemove(); } public class CacheDictionary : IDictionary { private readonly Dictionary _data; private readonly int _maxSize; private readonly ICacheDictionaryRemovalStrategy _removalStrategy; public CacheDictionary(int maxSize, ICacheDictionaryRemovalStrategy removalStrategy) { if (maxSize == 0) throw new ArgumentException("maxSize must be a positive integer value"); _maxSize = maxSize; _removalStrategy = removalStrategy ?? throw new ArgumentNullException(nameof(removalStrategy)); _data = new Dictionary(); _removalStrategy.Initialize(maxSize); } #region IDictionaty Implementation public void Add(TKey key, TValue value) { if (_data.ContainsKey(key)) _data.Add(key, value); //I want to throw the same exception as the internal dictionary for this case. if (_data.Count == _maxSize) { TKey keyToRemove = _removalStrategy.GetKeyToRemove(); if (_data.ContainsKey(keyToRemove)) _data.Remove(keyToRemove); else throw new Exception( $"Could not find a valid key to remove from cache, key = {(key == null ? "null" : key.ToString())}"); } _data.Add(key, value); _removalStrategy.KeyAdded(key); } public bool ContainsKey(TKey key) { return _data.ContainsKey(key); } public ICollection Keys => _data.Keys; public bool Remove(TKey key) { bool result = _data.Remove(key); if (result) _removalStrategy.KeyRemoved(key); return result; } public bool TryGetValue(TKey key, out TValue value) { bool result = _data.TryGetValue(key, out value); if (result) _removalStrategy.KeyAccessed(key); return result; } public ICollection Values => _data.Values; public TValue this[TKey key] { get { TValue result = _data[key]; _removalStrategy.KeyAccessed(key); return result; } set { _data[key] = value; _removalStrategy.KeyAccessed(key); } } public void Add(KeyValuePair item) { Add(item.Key, item.Value); } public void Clear() { _data.Clear(); _removalStrategy.Clear(); } public bool Contains(KeyValuePair item) { return _data.Contains(item); } public void CopyTo(KeyValuePair[] array, int arrayIndex) { ((IDictionary)_data).CopyTo(array, arrayIndex); } public int Count => _data.Count; public bool IsReadOnly => ((IDictionary)_data).IsReadOnly; public bool Remove(KeyValuePair item) { bool result = ((IDictionary)_data).Remove(item); if (result) _removalStrategy.KeyRemoved(item.Key); return result; } public IEnumerator> GetEnumerator() { return new CacheDictionaryEnumerator(_data.GetEnumerator(), _removalStrategy); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return new CacheDictionaryEnumerator(_data.GetEnumerator(), _removalStrategy); } #endregion public class CacheDictionaryEnumerator : IEnumerator> { private IEnumerator> _innerEnumerator; private readonly ICacheDictionaryRemovalStrategy _removalStrategy; internal CacheDictionaryEnumerator(IEnumerator> innerEnumerator, ICacheDictionaryRemovalStrategy removalStrategy) { _innerEnumerator = innerEnumerator ?? throw new ArgumentNullException(nameof(innerEnumerator)); _removalStrategy = removalStrategy ?? throw new ArgumentNullException(nameof(removalStrategy)); } public KeyValuePair Current { get { KeyValuePair result = _innerEnumerator.Current; _removalStrategy.KeyAccessed(result.Key); return result; } } public void Dispose() { _innerEnumerator.Dispose(); _innerEnumerator = null; } object System.Collections.IEnumerator.Current => this.Current; public bool MoveNext() { return _innerEnumerator.MoveNext(); } public void Reset() { _innerEnumerator.Reset(); } } } /// /// A removal strategy that removes some item. /// /// public class EmptyRemovalStrategy : ICacheDictionaryRemovalStrategy { private HashSet _currentKeys; public void Initialize(int maxSize) { _currentKeys = new HashSet(); } public void KeyAdded(TKey key) { if (!_currentKeys.Contains(key)) _currentKeys.Add(key); } public void KeyRemoved(TKey key) { if (_currentKeys.Contains(key)) _currentKeys.Remove(key); } public void KeyAccessed(TKey key) { } public TKey GetKeyToRemove() { if (_currentKeys.Count == 0) throw new IndexOutOfRangeException("No key to remove because the internal collection is empty"); TKey key = _currentKeys.First(); _currentKeys.Remove(key); return key; } public void Clear() { _currentKeys.Clear(); } } /// /// Remove the most recently used (MRU) item from the cache /// /// public class MruRemovalStrategy : ICacheDictionaryRemovalStrategy { private List _items; public void Initialize(int maxSize) { _items = new List(maxSize); } public void KeyAdded(TKey key) { _items.Add(key); } public void KeyRemoved(TKey key) { _items.Remove(key); } public void KeyAccessed(TKey key) { _items.Remove(key); _items.Add(key); } public void Clear() { _items.Clear(); } public TKey GetKeyToRemove() { if (_items.Count == 0) throw new IndexOutOfRangeException("No key to remove because the internal collection is empty"); TKey key = _items.Last(); _items.Remove(key); return key; } } /// /// Removes the least recently used (LRU) item in the cache /// /// public class LruRemovalStrategy : ICacheDictionaryRemovalStrategy { private List _items; public void Initialize(int maxSize) { _items = new List(maxSize); } public void KeyAdded(TKey key) { _items.Add(key); } public void KeyRemoved(TKey key) { _items.Remove(key); } public void KeyAccessed(TKey key) { _items.Remove(key); _items.Add(key); } public void Clear() { _items.Clear(); } public TKey GetKeyToRemove() { if (_items.Count == 0) throw new IndexOutOfRangeException("No key to remove because the internal collection is empty"); TKey key = _items.First(); _items.Remove(key); return key; } } }