diff --git a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs index 08f3c297..fdb8a6f8 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs @@ -1,169 +1,169 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using BitFaster.Caching.Atomic; -using FluentAssertions; -using Xunit; - -namespace BitFaster.Caching.UnitTests.Atomic -{ - public class AsyncAtomicFactoryTests - { - [Fact] - public void DefaultCtorValueIsNotCreated() - { - var a = new AsyncAtomicFactory(); - - a.IsValueCreated.Should().BeFalse(); - a.ValueIfCreated.Should().Be(0); - } - - [Fact] - public void WhenValuePassedToCtorValueIsStored() - { - var a = new AsyncAtomicFactory(1); - - a.ValueIfCreated.Should().Be(1); - a.IsValueCreated.Should().BeTrue(); - } - - [Fact] - public async Task WhenValueCreatedValueReturned() - { - var a = new AsyncAtomicFactory(); - (await a.GetValueAsync(1, k => Task.FromResult(2))).Should().Be(2); - - a.ValueIfCreated.Should().Be(2); - a.IsValueCreated.Should().BeTrue(); - } - - [Fact] - public async Task WhenValueCreatedWithArgValueReturned() - { - var a = new AsyncAtomicFactory(); - (await a.GetValueAsync(1, (k, a) => Task.FromResult(k + a), 7)).Should().Be(8); - - a.ValueIfCreated.Should().Be(8); - a.IsValueCreated.Should().BeTrue(); - } - - [Fact] - public async Task WhenValueCreatedGetValueReturnsOriginalValue() - { - var a = new AsyncAtomicFactory(); - await a.GetValueAsync(1, k => Task.FromResult(2)); - (await a.GetValueAsync(1, k => Task.FromResult(3))).Should().Be(2); - } - - [Fact] - public async Task WhenValueCreatedArgGetValueReturnsOriginalValue() - { - var a = new AsyncAtomicFactory(); - await a.GetValueAsync(1, (k, a) => Task.FromResult(k + a), 7); - (await a.GetValueAsync(1, (k, a) => Task.FromResult(k + a), 9)).Should().Be(8); - } - - [Fact] - public async Task WhenValueCreateThrowsValueIsNotStored() - { - var a = new AsyncAtomicFactory(); - - Func getOrAdd = async () => { await a.GetValueAsync(1, k => throw new ArithmeticException()); }; - - await getOrAdd.Should().ThrowAsync(); - - (await a.GetValueAsync(1, k => Task.FromResult(3))).Should().Be(3); - } - - [Fact] - public async Task WhenCallersRunConcurrentlyResultIsFromWinner() - { - var enter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var resume = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var atomicFactory = new AsyncAtomicFactory(); - var result = 0; - var winnerCount = 0; - - var first = atomicFactory.GetValueAsync(1, async k => - { - enter.SetResult(true); - await resume.Task; - - result = 1; - Interlocked.Increment(ref winnerCount); - return 1; - }); - - var second = atomicFactory.GetValueAsync(1, async k => - { - enter.SetResult(true); - await resume.Task; - - result = 2; - Interlocked.Increment(ref winnerCount); - return 2; - }); - - await enter.Task; - resume.SetResult(true); - - (await first).Should().Be(result); - (await second).Should().Be(result); - - winnerCount.Should().Be(1); +using System; +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching.Atomic; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Atomic +{ + public class AsyncAtomicFactoryTests + { + [Fact] + public void DefaultCtorValueIsNotCreated() + { + var a = new AsyncAtomicFactory(); + + a.IsValueCreated.Should().BeFalse(); + a.ValueIfCreated.Should().Be(0); + } + + [Fact] + public void WhenValuePassedToCtorValueIsStored() + { + var a = new AsyncAtomicFactory(1); + + a.ValueIfCreated.Should().Be(1); + a.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public async Task WhenValueCreatedValueReturned() + { + var a = new AsyncAtomicFactory(); + (await a.GetValueAsync(1, k => Task.FromResult(2))).Should().Be(2); + + a.ValueIfCreated.Should().Be(2); + a.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public async Task WhenValueCreatedWithArgValueReturned() + { + var a = new AsyncAtomicFactory(); + (await a.GetValueAsync(1, (k, a) => Task.FromResult(k + a), 7)).Should().Be(8); + + a.ValueIfCreated.Should().Be(8); + a.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public async Task WhenValueCreatedGetValueReturnsOriginalValue() + { + var a = new AsyncAtomicFactory(); + await a.GetValueAsync(1, k => Task.FromResult(2)); + (await a.GetValueAsync(1, k => Task.FromResult(3))).Should().Be(2); + } + + [Fact] + public async Task WhenValueCreatedArgGetValueReturnsOriginalValue() + { + var a = new AsyncAtomicFactory(); + await a.GetValueAsync(1, (k, a) => Task.FromResult(k + a), 7); + (await a.GetValueAsync(1, (k, a) => Task.FromResult(k + a), 9)).Should().Be(8); + } + + [Fact] + public async Task WhenValueCreateThrowsValueIsNotStored() + { + var a = new AsyncAtomicFactory(); + + Func getOrAdd = async () => { await a.GetValueAsync(1, k => throw new ArithmeticException()); }; + + await getOrAdd.Should().ThrowAsync(); + + (await a.GetValueAsync(1, k => Task.FromResult(3))).Should().Be(3); + } + + [Fact] + public async Task WhenCallersRunConcurrentlyResultIsFromWinner() + { + var enter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var resume = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var atomicFactory = new AsyncAtomicFactory(); + var result = 0; + var winnerCount = 0; + + var first = atomicFactory.GetValueAsync(1, async k => + { + enter.SetResult(true); + await resume.Task; + + result = 1; + Interlocked.Increment(ref winnerCount); + return 1; + }); + + var second = atomicFactory.GetValueAsync(1, async k => + { + enter.SetResult(true); + await resume.Task; + + result = 2; + Interlocked.Increment(ref winnerCount); + return 2; + }); + + await enter.Task; + resume.SetResult(true); + + (await first).Should().Be(result); + (await second).Should().Be(result); + + winnerCount.Should().Be(1); + } + + [Fact] + public void WhenValueNotCreatedHashCodeIsZero() + { + new AsyncAtomicFactory() + .GetHashCode() + .Should().Be(0); + } + + [Fact] + public void WhenValueCreatedHashCodeIsValueHashCode() + { + new AsyncAtomicFactory(1) + .GetHashCode() + .Should().Be(1); + } + + [Fact] + public void WhenValueNotCreatedEqualsFalse() + { + var a = new AsyncAtomicFactory(); + var b = new AsyncAtomicFactory(); + + a.Equals(b).Should().BeFalse(); } - [Fact] - public void WhenValueNotCreatedHashCodeIsZero() - { - new AsyncAtomicFactory() - .GetHashCode() - .Should().Be(0); - } - - [Fact] - public void WhenValueCreatedHashCodeIsValueHashCode() - { - new AsyncAtomicFactory(1) - .GetHashCode() - .Should().Be(1); - } - - [Fact] - public void WhenValueNotCreatedEqualsFalse() - { - var a = new AsyncAtomicFactory(); - var b = new AsyncAtomicFactory(); - - a.Equals(b).Should().BeFalse(); - } - - [Fact] - public void WhenOtherValueNotCreatedEqualsFalse() - { - var a = new AsyncAtomicFactory(1); - var b = new AsyncAtomicFactory(); - - a.Equals(b).Should().BeFalse(); - } - - [Fact] - public void WhenArgNullEqualsFalse() - { - new AsyncAtomicFactory(1) - .Equals(null) - .Should().BeFalse(); - } - - [Fact] - public void WhenArgObjectValuesAreSameEqualsTrue() - { - object other = new AsyncAtomicFactory(1); - - new AsyncAtomicFactory(1) - .Equals(other) - .Should().BeTrue(); - } - } -} + [Fact] + public void WhenOtherValueNotCreatedEqualsFalse() + { + var a = new AsyncAtomicFactory(1); + var b = new AsyncAtomicFactory(); + + a.Equals(b).Should().BeFalse(); + } + + [Fact] + public void WhenArgNullEqualsFalse() + { + new AsyncAtomicFactory(1) + .Equals(null) + .Should().BeFalse(); + } + + [Fact] + public void WhenArgObjectValuesAreSameEqualsTrue() + { + object other = new AsyncAtomicFactory(1); + + new AsyncAtomicFactory(1) + .Equals(other) + .Should().BeTrue(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs index d2b58d96..4dca7fbe 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs @@ -1,316 +1,316 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using BitFaster.Caching.Lru; -using BitFaster.Caching.Atomic; -using FluentAssertions; -using Xunit; -using Moq; - -namespace BitFaster.Caching.UnitTests.Atomic -{ - public class AtomicFactoryAsyncCacheTests - { - private const int capacity = 6; - private readonly ConcurrentLru> innerCache = new(capacity); - private readonly AtomicFactoryAsyncCache cache; - - private List> removedItems = new(); - private List> updatedItems = new(); - +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Atomic; +using FluentAssertions; +using Xunit; +using Moq; + +namespace BitFaster.Caching.UnitTests.Atomic +{ + public class AtomicFactoryAsyncCacheTests + { + private const int capacity = 6; + private readonly ConcurrentLru> innerCache = new(capacity); + private readonly AtomicFactoryAsyncCache cache; + + private List> removedItems = new(); + private List> updatedItems = new(); + public AtomicFactoryAsyncCacheTests() { - cache = new(innerCache); - } - - [Fact] - public void WhenInnerCacheIsNullCtorThrows() - { - Action constructor = () => { var x = new AtomicFactoryAsyncCache(null); }; - - constructor.Should().Throw(); - } - - [Fact] - public void WhenCreatedCapacityPropertyWrapsInnerCache() - { - this.cache.Policy.Eviction.Value.Capacity.Should().Be(capacity); - } - - [Fact] - public void WhenItemIsAddedCountIsCorrect() - { - this.cache.Count.Should().Be(0); - - this.cache.AddOrUpdate(2, 2); - - this.cache.Count.Should().Be(1); - } - - [Fact] - public async Task WhenItemIsAddedThenLookedUpMetricsAreCorrect() - { - this.cache.AddOrUpdate(1, 1); - await this.cache.GetOrAddAsync(1, k => Task.FromResult(k)); - - this.cache.Metrics.Value.Misses.Should().Be(0); - this.cache.Metrics.Value.Hits.Should().Be(1); - } - - [Fact] - public async Task WhenItemIsAddedWithArgValueIsCorrect() - { - await this.cache.GetOrAddAsync(1, (k, a) => Task.FromResult(k + a), 2); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(3); - } - - [Fact] - public void WhenNoInnerEventsNoOuterEvents() - { - var inner = new Mock>>(); - inner.SetupGet(c => c.Events).Returns(Optional>>.None); - - var cache = new AtomicFactoryAsyncCache(inner.Object); - - cache.Events.HasValue.Should().BeFalse(); - } - - [Fact] - public void WhenRemovedEventHandlerIsRegisteredItIsFired() - { - this.cache.Events.Value.ItemRemoved += OnItemRemoved; - - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(1); - - this.removedItems.First().Key.Should().Be(1); - } - -// backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenUpdatedEventHandlerIsRegisteredItIsFired() - { - this.cache.Events.Value.ItemUpdated += OnItemUpdated; - - this.cache.AddOrUpdate(1, 2); - this.cache.AddOrUpdate(1, 3); - - this.updatedItems.First().Key.Should().Be(1); - this.updatedItems.First().OldValue.Should().Be(2); - this.updatedItems.First().NewValue.Should().Be(3); - } -#endif - - [Fact] - public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() - { - this.cache.AddOrUpdate(1, 1); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(1); - } - - [Fact] - public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() - { - this.cache.AddOrUpdate(1, 1); - this.cache.AddOrUpdate(1, 2); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(2); - } - - [Fact] - public void WhenClearedItemsAreRemoved() - { - this.cache.AddOrUpdate(1, 1); - - this.cache.Clear(); - - this.cache.Count.Should().Be(0); - } - - [Fact] - public void WhenItemDoesNotExistTryGetReturnsFalse() - { - this.cache.TryGet(1, out var value).Should().BeFalse(); - } - - [Fact] - public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() - { - await this.cache.GetOrAddAsync(1, k => Task.FromResult(k)); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(1); - } - - [Fact] - public void WhenCacheContainsValuesTrim1RemovesColdestValue() - { - this.cache.AddOrUpdate(0, 0); - this.cache.AddOrUpdate(1, 1); - this.cache.AddOrUpdate(2, 2); - - this.cache.Policy.Eviction.Value.Trim(1); - - this.cache.TryGet(0, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenKeyDoesNotExistTryRemoveReturnsFalse() - { - this.cache.TryRemove(1).Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryRemoveReturnsTrue() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(1).Should().BeTrue(); - } - - [Fact] - public void WhenKeyDoesNotExistTryUpdateReturnsFalse() - { - this.cache.TryUpdate(1, 1).Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryUpdateReturnsTrue() - { - this.cache.AddOrUpdate(1, 1); - - this.cache.TryUpdate(1, 2).Should().BeTrue(); - this.cache.TryGet(1, out var value); - value.Should().Be(2); - } - - [Fact] - public void WhenItemsAddedKeysContainsTheKeys() - { - cache.Count.Should().Be(0); - cache.AddOrUpdate(1, 1); - cache.AddOrUpdate(2, 2); - cache.Keys.Should().BeEquivalentTo(new[] { 1, 2 }); - } - - [Fact] - public void WhenItemsAddedGenericEnumerateContainsKvps() - { - cache.Count.Should().Be(0); - cache.AddOrUpdate(1, 1); - cache.AddOrUpdate(2, 2); - cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); - } - - [Fact] - public void WhenItemsAddedEnumerateContainsKvps() - { - cache.Count.Should().Be(0); - cache.AddOrUpdate(1, 1); - cache.AddOrUpdate(2, 2); - - var enumerable = (IEnumerable)cache; - enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); - } - - [Fact] - public async Task WhenFactoryThrowsEmptyValueIsNotCounted() - { - try - { - await cache.GetOrAddAsync(1, k => throw new ArithmeticException()); - } - catch { } - - cache.Count.Should().Be(0); - } - - [Fact] - public async Task WhenFactoryThrowsEmptyValueIsNotEnumerable() - { - try - { - await cache.GetOrAddAsync(1, k => throw new ArithmeticException()); - } - catch { } - - // IEnumerable.Count() instead of Count property - cache.Count().Should().Be(0); - } - - [Fact] - public async Task WhenFactoryThrowsEmptyKeyIsNotEnumerable() - { - try - { - await cache.GetOrAddAsync(1, k => throw new ArithmeticException()); - } - catch { } - - cache.Keys.Count().Should().Be(0); + cache = new(innerCache); + } + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryAsyncCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCreatedCapacityPropertyWrapsInnerCache() + { + this.cache.Policy.Eviction.Value.Capacity.Should().Be(capacity); + } + + [Fact] + public void WhenItemIsAddedCountIsCorrect() + { + this.cache.Count.Should().Be(0); + + this.cache.AddOrUpdate(2, 2); + + this.cache.Count.Should().Be(1); + } + + [Fact] + public async Task WhenItemIsAddedThenLookedUpMetricsAreCorrect() + { + this.cache.AddOrUpdate(1, 1); + await this.cache.GetOrAddAsync(1, k => Task.FromResult(k)); + + this.cache.Metrics.Value.Misses.Should().Be(0); + this.cache.Metrics.Value.Hits.Should().Be(1); + } + + [Fact] + public async Task WhenItemIsAddedWithArgValueIsCorrect() + { + await this.cache.GetOrAddAsync(1, (k, a) => Task.FromResult(k + a), 2); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(3); + } + + [Fact] + public void WhenNoInnerEventsNoOuterEvents() + { + var inner = new Mock>>(); + inner.SetupGet(c => c.Events).Returns(Optional>>.None); + + var cache = new AtomicFactoryAsyncCache(inner.Object); + + cache.Events.HasValue.Should().BeFalse(); + } + + [Fact] + public void WhenRemovedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemRemoved += OnItemRemoved; + + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1); + + this.removedItems.First().Key.Should().Be(1); + } + +// backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenUpdatedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemUpdated += OnItemUpdated; + + this.cache.AddOrUpdate(1, 2); + this.cache.AddOrUpdate(1, 3); + + this.updatedItems.First().Key.Should().Be(1); + this.updatedItems.First().OldValue.Should().Be(2); + this.updatedItems.First().NewValue.Should().Be(3); + } +#endif + + [Fact] + public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() + { + this.cache.AddOrUpdate(1, 1); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() + { + this.cache.AddOrUpdate(1, 1); + this.cache.AddOrUpdate(1, 2); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(2); + } + + [Fact] + public void WhenClearedItemsAreRemoved() + { + this.cache.AddOrUpdate(1, 1); + + this.cache.Clear(); + + this.cache.Count.Should().Be(0); + } + + [Fact] + public void WhenItemDoesNotExistTryGetReturnsFalse() + { + this.cache.TryGet(1, out var value).Should().BeFalse(); + } + + [Fact] + public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() + { + await this.cache.GetOrAddAsync(1, k => Task.FromResult(k)); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void WhenCacheContainsValuesTrim1RemovesColdestValue() + { + this.cache.AddOrUpdate(0, 0); + this.cache.AddOrUpdate(1, 1); + this.cache.AddOrUpdate(2, 2); + + this.cache.Policy.Eviction.Value.Trim(1); + + this.cache.TryGet(0, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistTryRemoveReturnsFalse() + { + this.cache.TryRemove(1).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryRemoveReturnsTrue() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1).Should().BeTrue(); + } + + [Fact] + public void WhenKeyDoesNotExistTryUpdateReturnsFalse() + { + this.cache.TryUpdate(1, 1).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryUpdateReturnsTrue() + { + this.cache.AddOrUpdate(1, 1); + + this.cache.TryUpdate(1, 2).Should().BeTrue(); + this.cache.TryGet(1, out var value); + value.Should().Be(2); + } + + [Fact] + public void WhenItemsAddedKeysContainsTheKeys() + { + cache.Count.Should().Be(0); + cache.AddOrUpdate(1, 1); + cache.AddOrUpdate(2, 2); + cache.Keys.Should().BeEquivalentTo(new[] { 1, 2 }); + } + + [Fact] + public void WhenItemsAddedGenericEnumerateContainsKvps() + { + cache.Count.Should().Be(0); + cache.AddOrUpdate(1, 1); + cache.AddOrUpdate(2, 2); + cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); + } + + [Fact] + public void WhenItemsAddedEnumerateContainsKvps() + { + cache.Count.Should().Be(0); + cache.AddOrUpdate(1, 1); + cache.AddOrUpdate(2, 2); + + var enumerable = (IEnumerable)cache; + enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); + } + + [Fact] + public async Task WhenFactoryThrowsEmptyValueIsNotCounted() + { + try + { + await cache.GetOrAddAsync(1, k => throw new ArithmeticException()); + } + catch { } + + cache.Count.Should().Be(0); + } + + [Fact] + public async Task WhenFactoryThrowsEmptyValueIsNotEnumerable() + { + try + { + await cache.GetOrAddAsync(1, k => throw new ArithmeticException()); + } + catch { } + + // IEnumerable.Count() instead of Count property + cache.Count().Should().Be(0); + } + + [Fact] + public async Task WhenFactoryThrowsEmptyKeyIsNotEnumerable() + { + try + { + await cache.GetOrAddAsync(1, k => throw new ArithmeticException()); + } + catch { } + + cache.Keys.Count().Should().Be(0); } // backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenRemovedValueIsReturned() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(1, out var value); - - value.Should().Be(1); - } - - [Fact] - public void WhenNotRemovedValueIsDefault() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(2, out var value); - - value.Should().Be(0); - } - - [Fact] - public void WhenRemoveKeyValueAndValueDoesntMatchDontRemove() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(new KeyValuePair(1, 2)).Should().BeFalse(); - } - - [Fact] - public void WhenRemoveKeyValueAndValueDoesMatchThenRemove() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); - } - - [Fact] - public void WhenRemoveKeyValueAndValueIsNotCreatedDoesNotRemove() - { - // seed the inner cache with an not yet created value - this.innerCache.AddOrUpdate(1, new AsyncAtomicFactory()); - - // try to remove with the default value (0) - this.cache.TryRemove(new KeyValuePair(1, 0)).Should().BeFalse(); - } -#endif - - private void OnItemRemoved(object sender, ItemRemovedEventArgs e) - { - this.removedItems.Add(e); - } - - private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) - { - this.updatedItems.Add(e); - } - } -} +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenRemovedValueIsReturned() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1, out var value); + + value.Should().Be(1); + } + + [Fact] + public void WhenNotRemovedValueIsDefault() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(2, out var value); + + value.Should().Be(0); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesntMatchDontRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 2)).Should().BeFalse(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesMatchThenRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueIsNotCreatedDoesNotRemove() + { + // seed the inner cache with an not yet created value + this.innerCache.AddOrUpdate(1, new AsyncAtomicFactory()); + + // try to remove with the default value (0) + this.cache.TryRemove(new KeyValuePair(1, 0)).Should().BeFalse(); + } +#endif + + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) + { + this.removedItems.Add(e); + } + + private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) + { + this.updatedItems.Add(e); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs index 8c047ec6..0c8bd66c 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryCacheTests.cs @@ -1,311 +1,311 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using BitFaster.Caching.Lru; -using BitFaster.Caching.Atomic; -using FluentAssertions; -using Xunit; -using Moq; - -namespace BitFaster.Caching.UnitTests.Atomic -{ - public class AtomicFactoryCacheTests - { - private const int capacity = 6; - private readonly ConcurrentLru> innerCache; - private readonly AtomicFactoryCache cache; - - private List> removedItems = new(); - private List> updatedItems = new(); - +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Atomic; +using FluentAssertions; +using Xunit; +using Moq; + +namespace BitFaster.Caching.UnitTests.Atomic +{ + public class AtomicFactoryCacheTests + { + private const int capacity = 6; + private readonly ConcurrentLru> innerCache; + private readonly AtomicFactoryCache cache; + + private List> removedItems = new(); + private List> updatedItems = new(); + public AtomicFactoryCacheTests() { innerCache = new ConcurrentLru>(capacity); - cache = new(innerCache); - } - - [Fact] - public void WhenInnerCacheIsNullCtorThrows() - { - Action constructor = () => { var x = new AtomicFactoryCache(null); }; - - constructor.Should().Throw(); - } - - [Fact] - public void WhenCreatedCapacityPropertyWrapsInnerCache() - { - this.cache.Policy.Eviction.Value.Capacity.Should().Be(capacity); - } - - [Fact] - public void WhenItemIsAddedCountIsCorrect() - { - this.cache.Count.Should().Be(0); - - this.cache.AddOrUpdate(2, 2); - - this.cache.Count.Should().Be(1); - } - - [Fact] - public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() - { - this.cache.AddOrUpdate(1, 1); - this.cache.GetOrAdd(1, k => k); - - this.cache.Metrics.Value.Misses.Should().Be(0); - this.cache.Metrics.Value.Hits.Should().Be(1); - } - - [Fact] - public void WhenItemIsAddedWithArgValueIsCorrect() - { - this.cache.GetOrAdd(1, (k, a) => k + a, 2); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(3); - } - - [Fact] - public void WhenRemovedEventHandlerIsRegisteredItIsFired() - { - this.cache.Events.Value.ItemRemoved += OnItemRemoved; - - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(1); - - this.removedItems.First().Key.Should().Be(1); + cache = new(innerCache); + } + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCreatedCapacityPropertyWrapsInnerCache() + { + this.cache.Policy.Eviction.Value.Capacity.Should().Be(capacity); + } + + [Fact] + public void WhenItemIsAddedCountIsCorrect() + { + this.cache.Count.Should().Be(0); + + this.cache.AddOrUpdate(2, 2); + + this.cache.Count.Should().Be(1); + } + + [Fact] + public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() + { + this.cache.AddOrUpdate(1, 1); + this.cache.GetOrAdd(1, k => k); + + this.cache.Metrics.Value.Misses.Should().Be(0); + this.cache.Metrics.Value.Hits.Should().Be(1); + } + + [Fact] + public void WhenItemIsAddedWithArgValueIsCorrect() + { + this.cache.GetOrAdd(1, (k, a) => k + a, 2); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(3); + } + + [Fact] + public void WhenRemovedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemRemoved += OnItemRemoved; + + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1); + + this.removedItems.First().Key.Should().Be(1); } // backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenRemovedValueIsReturned() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(1, out var value); - - value.Should().Be(1); - } - - [Fact] - public void WhenNotRemovedValueIsDefault() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(2, out var value); - - value.Should().Be(0); - } - - [Fact] - public void WhenRemoveKeyValueAndValueDoesntMatchDontRemove() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(new KeyValuePair(1, 2)).Should().BeFalse(); - } - - [Fact] - public void WhenRemoveKeyValueAndValueDoesMatchThenRemove() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); - } - - [Fact] - public void WhenRemoveKeyValueAndValueIsNotCreatedDoesNotRemove() - { - // seed the inner cache with an not yet created value - this.innerCache.AddOrUpdate(1, new AtomicFactory()); - - // try to remove with the default value (0) - this.cache.TryRemove(new KeyValuePair(1, 0)).Should().BeFalse(); - } - - [Fact] - public void WhenUpdatedEventHandlerIsRegisteredItIsFired() - { - this.cache.Events.Value.ItemUpdated += OnItemUpdated; - - this.cache.AddOrUpdate(1, 2); - this.cache.AddOrUpdate(1, 3); - - this.updatedItems.First().Key.Should().Be(1); - this.updatedItems.First().OldValue.Should().Be(2); - this.updatedItems.First().NewValue.Should().Be(3); - } -#endif - [Fact] - public void WhenNoInnerEventsNoOuterEvents() - { - var inner = new Mock>>(); - inner.SetupGet(c => c.Events).Returns(Optional>>.None); - - var cache = new AtomicFactoryCache(inner.Object); - - cache.Events.HasValue.Should().BeFalse(); - } - - [Fact] - public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() - { - this.cache.AddOrUpdate(1, 1); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(1); - } - - [Fact] - public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() - { - this.cache.AddOrUpdate(1, 1); - this.cache.AddOrUpdate(1, 2); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(2); - } - - [Fact] - public void WhenClearedItemsAreRemoved() - { - this.cache.AddOrUpdate(1, 1); - - this.cache.Clear(); - - this.cache.Count.Should().Be(0); - } - - [Fact] - public void WhenItemDoesNotExistTryGetReturnsFalse() - { - this.cache.TryGet(1, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenKeyDoesNotExistGetOrAddAddsValue() - { - this.cache.GetOrAdd(1, k => k); - - this.cache.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be(1); - } - - [Fact] - public void WhenCacheContainsValuesTrim1RemovesColdestValue() - { - this.cache.AddOrUpdate(0, 0); - this.cache.AddOrUpdate(1, 1); - this.cache.AddOrUpdate(2, 2); - - this.cache.Policy.Eviction.Value.Trim(1); - - this.cache.TryGet(0, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenKeyDoesNotExistTryRemoveReturnsFalse() - { - this.cache.TryRemove(1).Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryRemoveReturnsTrue() - { - this.cache.AddOrUpdate(1, 1); - this.cache.TryRemove(1).Should().BeTrue(); - } - - [Fact] - public void WhenKeyDoesNotExistTryUpdateReturnsFalse() - { - this.cache.TryUpdate(1, 1).Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryUpdateReturnsTrue() - { - this.cache.AddOrUpdate(1, 1); - - this.cache.TryUpdate(1, 2).Should().BeTrue(); - this.cache.TryGet(1, out var value); - value.Should().Be(2); - } - - [Fact] - public void WhenItemsAddedKeysContainsTheKeys() - { - cache.Count.Should().Be(0); - cache.AddOrUpdate(1, 1); - cache.AddOrUpdate(2, 2); - cache.Keys.Should().BeEquivalentTo(new[] { 1, 2 }); - } - - [Fact] - public void WhenItemsAddedGenericEnumerateContainsKvps() - { - cache.Count.Should().Be(0); - cache.AddOrUpdate(1, 1); - cache.AddOrUpdate(2, 2); - cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); - } - - [Fact] - public void WhenItemsAddedEnumerateContainsKvps() - { - cache.Count.Should().Be(0); - cache.AddOrUpdate(1, 1); - cache.AddOrUpdate(2, 2); - - var enumerable = (IEnumerable)cache; - enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); - } - - [Fact] - public void WhenFactoryThrowsEmptyValueIsNotCounted() - { - try - { - cache.GetOrAdd(1, _ => throw new Exception()); - } - catch { } - - cache.Count.Should().Be(0); - } - - [Fact] - public void WhenFactoryThrowsEmptyValueIsNotEnumerable() - { - try - { - cache.GetOrAdd(1, k => throw new Exception()); - } - catch { } - - // IEnumerable.Count() instead of Count property - cache.Count().Should().Be(0); - } - - [Fact] - public void WhenFactoryThrowsEmptyKeyIsNotEnumerable() - { - try - { - cache.GetOrAdd(1, k => throw new Exception()); - } - catch { } - - cache.Keys.Count().Should().Be(0); - } - - private void OnItemRemoved(object sender, ItemRemovedEventArgs e) - { - this.removedItems.Add(e); - } - - private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) - { - this.updatedItems.Add(e); - } - } -} +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenRemovedValueIsReturned() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1, out var value); + + value.Should().Be(1); + } + + [Fact] + public void WhenNotRemovedValueIsDefault() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(2, out var value); + + value.Should().Be(0); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesntMatchDontRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 2)).Should().BeFalse(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesMatchThenRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueIsNotCreatedDoesNotRemove() + { + // seed the inner cache with an not yet created value + this.innerCache.AddOrUpdate(1, new AtomicFactory()); + + // try to remove with the default value (0) + this.cache.TryRemove(new KeyValuePair(1, 0)).Should().BeFalse(); + } + + [Fact] + public void WhenUpdatedEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.Value.ItemUpdated += OnItemUpdated; + + this.cache.AddOrUpdate(1, 2); + this.cache.AddOrUpdate(1, 3); + + this.updatedItems.First().Key.Should().Be(1); + this.updatedItems.First().OldValue.Should().Be(2); + this.updatedItems.First().NewValue.Should().Be(3); + } +#endif + [Fact] + public void WhenNoInnerEventsNoOuterEvents() + { + var inner = new Mock>>(); + inner.SetupGet(c => c.Events).Returns(Optional>>.None); + + var cache = new AtomicFactoryCache(inner.Object); + + cache.Events.HasValue.Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() + { + this.cache.AddOrUpdate(1, 1); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() + { + this.cache.AddOrUpdate(1, 1); + this.cache.AddOrUpdate(1, 2); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(2); + } + + [Fact] + public void WhenClearedItemsAreRemoved() + { + this.cache.AddOrUpdate(1, 1); + + this.cache.Clear(); + + this.cache.Count.Should().Be(0); + } + + [Fact] + public void WhenItemDoesNotExistTryGetReturnsFalse() + { + this.cache.TryGet(1, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistGetOrAddAddsValue() + { + this.cache.GetOrAdd(1, k => k); + + this.cache.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void WhenCacheContainsValuesTrim1RemovesColdestValue() + { + this.cache.AddOrUpdate(0, 0); + this.cache.AddOrUpdate(1, 1); + this.cache.AddOrUpdate(2, 2); + + this.cache.Policy.Eviction.Value.Trim(1); + + this.cache.TryGet(0, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistTryRemoveReturnsFalse() + { + this.cache.TryRemove(1).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryRemoveReturnsTrue() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1).Should().BeTrue(); + } + + [Fact] + public void WhenKeyDoesNotExistTryUpdateReturnsFalse() + { + this.cache.TryUpdate(1, 1).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryUpdateReturnsTrue() + { + this.cache.AddOrUpdate(1, 1); + + this.cache.TryUpdate(1, 2).Should().BeTrue(); + this.cache.TryGet(1, out var value); + value.Should().Be(2); + } + + [Fact] + public void WhenItemsAddedKeysContainsTheKeys() + { + cache.Count.Should().Be(0); + cache.AddOrUpdate(1, 1); + cache.AddOrUpdate(2, 2); + cache.Keys.Should().BeEquivalentTo(new[] { 1, 2 }); + } + + [Fact] + public void WhenItemsAddedGenericEnumerateContainsKvps() + { + cache.Count.Should().Be(0); + cache.AddOrUpdate(1, 1); + cache.AddOrUpdate(2, 2); + cache.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); + } + + [Fact] + public void WhenItemsAddedEnumerateContainsKvps() + { + cache.Count.Should().Be(0); + cache.AddOrUpdate(1, 1); + cache.AddOrUpdate(2, 2); + + var enumerable = (IEnumerable)cache; + enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair(1, 1), new KeyValuePair(2, 2) }); + } + + [Fact] + public void WhenFactoryThrowsEmptyValueIsNotCounted() + { + try + { + cache.GetOrAdd(1, _ => throw new Exception()); + } + catch { } + + cache.Count.Should().Be(0); + } + + [Fact] + public void WhenFactoryThrowsEmptyValueIsNotEnumerable() + { + try + { + cache.GetOrAdd(1, k => throw new Exception()); + } + catch { } + + // IEnumerable.Count() instead of Count property + cache.Count().Should().Be(0); + } + + [Fact] + public void WhenFactoryThrowsEmptyKeyIsNotEnumerable() + { + try + { + cache.GetOrAdd(1, k => throw new Exception()); + } + catch { } + + cache.Keys.Count().Should().Be(0); + } + + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) + { + this.removedItems.Add(e); + } + + private void OnItemUpdated(object sender, ItemUpdatedEventArgs e) + { + this.updatedItems.Add(e); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryTests.cs index 85987dfe..eae14265 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryTests.cs @@ -1,163 +1,163 @@ - -using System.Threading; -using System.Threading.Tasks; -using BitFaster.Caching.Atomic; -using FluentAssertions; -using Xunit; - -namespace BitFaster.Caching.UnitTests.Atomic -{ - public class AtomicFactoryTests - { - [Fact] - public void DefaultCtorValueIsNotCreated() - { - var a = new AtomicFactory(); - - a.IsValueCreated.Should().BeFalse(); - a.ValueIfCreated.Should().Be(0); - } - - [Fact] - public void WhenValuePassedToCtorValueIsStored() - { - var a = new AtomicFactory(1); - - a.ValueIfCreated.Should().Be(1); - a.IsValueCreated.Should().BeTrue(); - } - - [Fact] - public void WhenValueCreatedValueReturned() - { - var a = new AtomicFactory(); - a.GetValue(1, k => 2).Should().Be(2); - - a.ValueIfCreated.Should().Be(2); - a.IsValueCreated.Should().BeTrue(); - } - - [Fact] - public void WhenValueCreatedWithArgValueReturned() - { - var a = new AtomicFactory(); - a.GetValue(1, (k, a) => k + a, 7).Should().Be(8); - - a.ValueIfCreated.Should().Be(8); - a.IsValueCreated.Should().BeTrue(); - } - - [Fact] - public void WhenValueCreatedGetValueReturnsOriginalValue() - { - var a = new AtomicFactory(); - a.GetValue(1, k => 2); - a.GetValue(1, k => 3).Should().Be(2); - } - - [Fact] - public void WhenValueCreatedArgGetValueReturnsOriginalValue() - { - var a = new AtomicFactory(); - a.GetValue(1, (k, a) => k + a, 7); - a.GetValue(1, (k, a) => k + a, 9).Should().Be(8); - } - - [Fact] + +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching.Atomic; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Atomic +{ + public class AtomicFactoryTests + { + [Fact] + public void DefaultCtorValueIsNotCreated() + { + var a = new AtomicFactory(); + + a.IsValueCreated.Should().BeFalse(); + a.ValueIfCreated.Should().Be(0); + } + + [Fact] + public void WhenValuePassedToCtorValueIsStored() + { + var a = new AtomicFactory(1); + + a.ValueIfCreated.Should().Be(1); + a.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public void WhenValueCreatedValueReturned() + { + var a = new AtomicFactory(); + a.GetValue(1, k => 2).Should().Be(2); + + a.ValueIfCreated.Should().Be(2); + a.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public void WhenValueCreatedWithArgValueReturned() + { + var a = new AtomicFactory(); + a.GetValue(1, (k, a) => k + a, 7).Should().Be(8); + + a.ValueIfCreated.Should().Be(8); + a.IsValueCreated.Should().BeTrue(); + } + + [Fact] + public void WhenValueCreatedGetValueReturnsOriginalValue() + { + var a = new AtomicFactory(); + a.GetValue(1, k => 2); + a.GetValue(1, k => 3).Should().Be(2); + } + + [Fact] + public void WhenValueCreatedArgGetValueReturnsOriginalValue() + { + var a = new AtomicFactory(); + a.GetValue(1, (k, a) => k + a, 7); + a.GetValue(1, (k, a) => k + a, 9).Should().Be(8); + } + + [Fact] public void WhenValueNotCreatedHashCodeIsZero() { - new AtomicFactory() - .GetHashCode() - .Should().Be(0); + new AtomicFactory() + .GetHashCode() + .Should().Be(0); } - [Fact] + [Fact] public void WhenValueCreatedHashCodeIsValueHashCode() { - new AtomicFactory(1) - .GetHashCode() - .Should().Be(1); + new AtomicFactory(1) + .GetHashCode() + .Should().Be(1); } - [Fact] + [Fact] public void WhenValueNotCreatedEqualsFalse() { - var a = new AtomicFactory(); - var b = new AtomicFactory(); - - a.Equals(b).Should().BeFalse(); + var a = new AtomicFactory(); + var b = new AtomicFactory(); + + a.Equals(b).Should().BeFalse(); } - [Fact] + [Fact] public void WhenOtherValueNotCreatedEqualsFalse() { - var a = new AtomicFactory(1); - var b = new AtomicFactory(); - - a.Equals(b).Should().BeFalse(); + var a = new AtomicFactory(1); + var b = new AtomicFactory(); + + a.Equals(b).Should().BeFalse(); } - [Fact] + [Fact] public void WhenArgNullEqualsFalse() { - new AtomicFactory(1) - .Equals(null) - .Should().BeFalse(); + new AtomicFactory(1) + .Equals(null) + .Should().BeFalse(); } - [Fact] + [Fact] public void WhenArgObjectValuesAreSameEqualsTrue() { object other = new AtomicFactory(1); - new AtomicFactory(1) - .Equals(other) - .Should().BeTrue(); - } - - [Fact] - public async Task WhenCallersRunConcurrentlyResultIsFromWinner() - { - var enter = new ManualResetEvent(false); - var resume = new ManualResetEvent(false); - - var atomicFactory = new AtomicFactory(); - var result = 0; - var winnerCount = 0; - - Task first = Task.Run(() => - { - return atomicFactory.GetValue(1, k => - { - enter.Set(); - resume.WaitOne(); - - result = 1; - Interlocked.Increment(ref winnerCount); - return 1; - }); - }); - - Task second = Task.Run(() => - { - return atomicFactory.GetValue(1, k => - { - enter.Set(); - resume.WaitOne(); - - result = 2; - Interlocked.Increment(ref winnerCount); - return 2; - }); - }); - - enter.WaitOne(); - resume.Set(); - - (await first).Should().Be(result); - (await second).Should().Be(result); - - winnerCount.Should().Be(1); - } - } -} + new AtomicFactory(1) + .Equals(other) + .Should().BeTrue(); + } + + [Fact] + public async Task WhenCallersRunConcurrentlyResultIsFromWinner() + { + var enter = new ManualResetEvent(false); + var resume = new ManualResetEvent(false); + + var atomicFactory = new AtomicFactory(); + var result = 0; + var winnerCount = 0; + + Task first = Task.Run(() => + { + return atomicFactory.GetValue(1, k => + { + enter.Set(); + resume.WaitOne(); + + result = 1; + Interlocked.Increment(ref winnerCount); + return 1; + }); + }); + + Task second = Task.Run(() => + { + return atomicFactory.GetValue(1, k => + { + enter.Set(); + resume.WaitOne(); + + result = 2; + Interlocked.Increment(ref winnerCount); + return 2; + }); + }); + + enter.WaitOne(); + resume.Set(); + + (await first).Should().Be(result); + (await second).Should().Be(result); + + winnerCount.Should().Be(1); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Atomic/ConcurrentDictionaryExtensionTests.cs b/BitFaster.Caching.UnitTests/Atomic/ConcurrentDictionaryExtensionTests.cs index f5076a20..bec9a5f7 100644 --- a/BitFaster.Caching.UnitTests/Atomic/ConcurrentDictionaryExtensionTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/ConcurrentDictionaryExtensionTests.cs @@ -1,62 +1,62 @@ - -using System.Collections.Concurrent; + +using System.Collections.Concurrent; using System.Collections.Generic; -using BitFaster.Caching.Atomic; -using FluentAssertions; -using Xunit; - -namespace BitFaster.Caching.UnitTests.Atomic -{ - public class ConcurrentDictionaryExtensionTests - { - private ConcurrentDictionary> dictionary = new ConcurrentDictionary>(); - - [Fact] - public void WhenItemIsAddedItCanBeRetrieved() - { - dictionary.GetOrAdd(1, k => k); - - dictionary.TryGetValue(1, out int value).Should().BeTrue(); - value.Should().Be(1); +using BitFaster.Caching.Atomic; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Atomic +{ + public class ConcurrentDictionaryExtensionTests + { + private ConcurrentDictionary> dictionary = new ConcurrentDictionary>(); + + [Fact] + public void WhenItemIsAddedItCanBeRetrieved() + { + dictionary.GetOrAdd(1, k => k); + + dictionary.TryGetValue(1, out int value).Should().BeTrue(); + value.Should().Be(1); } - [Fact] - public void WhenItemIsAddedWithArgItCanBeRetrieved() - { - dictionary.GetOrAdd(1, (k,a) => k + a, 2); - - dictionary.TryGetValue(1, out int value).Should().BeTrue(); - value.Should().Be(3); + [Fact] + public void WhenItemIsAddedWithArgItCanBeRetrieved() + { + dictionary.GetOrAdd(1, (k,a) => k + a, 2); + + dictionary.TryGetValue(1, out int value).Should().BeTrue(); + value.Should().Be(3); } - [Fact] + [Fact] public void WhenKeyDoesNotExistTryGetReturnsFalse() { - dictionary.TryGetValue(1, out int _).Should().BeFalse(); + dictionary.TryGetValue(1, out int _).Should().BeFalse(); } - [Fact] - public void WhenItemIsAddedItCanBeRemovedByKey() - { - dictionary.GetOrAdd(1, k => k); - - dictionary.TryRemove(1, out int value).Should().BeTrue(); - value.Should().Be(1); + [Fact] + public void WhenItemIsAddedItCanBeRemovedByKey() + { + dictionary.GetOrAdd(1, k => k); + + dictionary.TryRemove(1, out int value).Should().BeTrue(); + value.Should().Be(1); + } + + [Fact] + public void WhenItemIsAddedItCanBeRemovedByKvp() + { + dictionary.GetOrAdd(1, k => k); + + dictionary.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); + dictionary.TryGetValue(1, out _).Should().BeFalse(); } - [Fact] - public void WhenItemIsAddedItCanBeRemovedByKvp() - { - dictionary.GetOrAdd(1, k => k); - - dictionary.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); - dictionary.TryGetValue(1, out _).Should().BeFalse(); - } - - [Fact] + [Fact] public void WhenKeyDoesNotExistTryRemoveReturnsFalse() { - dictionary.TryRemove(1, out int _).Should().BeFalse(); - } - } -} + dictionary.TryRemove(1, out int _).Should().BeFalse(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Buffers/MpscBoundedBufferTests.cs b/BitFaster.Caching.UnitTests/Buffers/MpscBoundedBufferTests.cs index 60ada589..a4a65242 100644 --- a/BitFaster.Caching.UnitTests/Buffers/MpscBoundedBufferTests.cs +++ b/BitFaster.Caching.UnitTests/Buffers/MpscBoundedBufferTests.cs @@ -1,155 +1,155 @@ -using System; -using BitFaster.Caching.Buffers; -using FluentAssertions; +using System; +using BitFaster.Caching.Buffers; +using FluentAssertions; using Xunit; -namespace BitFaster.Caching.UnitTests.Buffers -{ - public class MpscBoundedBufferTests - { +namespace BitFaster.Caching.UnitTests.Buffers +{ + public class MpscBoundedBufferTests + { private readonly MpscBoundedBuffer buffer = new MpscBoundedBuffer(10); - - [Fact] - public void WhenSizeIsLessThan1CtorThrows() - { - Action constructor = () => { var x = new MpscBoundedBuffer(-1); }; - - constructor.Should().Throw(); - } - - [Fact] - public void SizeIsPowerOfTwo() - { - buffer.Capacity.Should().Be(16); - } - - [Fact] - public void WhenBufferIsEmptyCountIsZero() - { - buffer.Count.Should().Be(0); - } - - [Fact] - public void WhenBufferHasOneItemCountIsOne() - { - // head < tail - buffer.TryAdd("1"); - buffer.Count.Should().Be(1); - } - - [Fact] - public void WhenBufferHas15ItemCountIs15() - { - buffer.TryAdd("1").Should().Be(BufferStatus.Success); - buffer.TryTake(out var _).Should().Be(BufferStatus.Success); - - for (var i = 0; i < 15; i++) - { - buffer.TryAdd("0").Should().Be(BufferStatus.Success); - } - - // head = 1, tail = 0 : head > tail - buffer.Count.Should().Be(15); - } - - [Fact] - public void WhenBufferIsFullTryAddIsFalse() - { - for (var i = 0; i < 16; i++) - { - buffer.TryAdd(i.ToString()).Should().Be(BufferStatus.Success); - } - - buffer.TryAdd("666").Should().Be(BufferStatus.Full); - } - - [Fact] - public void WhenBufferIsEmptyTryTakeIsFalse() - { - buffer.TryTake(out var _).Should().Be(BufferStatus.Empty); - } - - [Fact] - public void WhenItemAddedItCanBeTaken() - { - buffer.TryAdd("123").Should().Be(BufferStatus.Success); - buffer.TryTake(out var item).Should().Be(BufferStatus.Success); - item.Should().Be("123"); - } - - [Fact] - public void WhenItemsAreAddedClearRemovesItems() - { - buffer.TryAdd("1"); - buffer.TryAdd("2"); - - buffer.Count.Should().Be(2); - - buffer.Clear(); - - buffer.Count.Should().Be(0); - buffer.TryTake(out var _).Should().Be(BufferStatus.Empty); - } - - [Fact] - public void WhenBufferEmptyDrainReturnsZero() - { - var outputBuffer = new string[16]; - var output = new ArraySegment(outputBuffer); - - buffer.DrainTo(output).Should().Be(0); - } - -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenBufferContainsItemsDrainArrayTakesItems() - { - buffer.TryAdd("1"); - buffer.TryAdd("2"); - buffer.TryAdd("3"); - - var outputBuffer = new string[16]; - - buffer.DrainTo(outputBuffer.AsSpan()).Should().Be(3); - - outputBuffer[0].Should().Be("1"); - outputBuffer[1].Should().Be("2"); - outputBuffer[2].Should().Be("3"); - } -#endif - - [Fact] - public void WhenBufferContainsItemsDrainSegmentTakesItems() - { - buffer.TryAdd("1"); - buffer.TryAdd("2"); - buffer.TryAdd("3"); - - var outputBuffer = new string[16]; - var output = new ArraySegment(outputBuffer); - - buffer.DrainTo(output).Should().Be(3); - - outputBuffer[0].Should().Be("1"); - outputBuffer[1].Should().Be("2"); - outputBuffer[2].Should().Be("3"); - } - - [Fact] - public void WhenSegmentUsesOffsetItemsDrainedToOffset() - { - buffer.TryAdd("1"); - buffer.TryAdd("2"); - buffer.TryAdd("3"); - - var outputBuffer = new string[16]; - var output = new ArraySegment(outputBuffer, 6, 10); - - buffer.DrainTo(output).Should().Be(3); - - outputBuffer[6].Should().Be("1"); - outputBuffer[7].Should().Be("2"); - outputBuffer[8].Should().Be("3"); - } - } -} + + [Fact] + public void WhenSizeIsLessThan1CtorThrows() + { + Action constructor = () => { var x = new MpscBoundedBuffer(-1); }; + + constructor.Should().Throw(); + } + + [Fact] + public void SizeIsPowerOfTwo() + { + buffer.Capacity.Should().Be(16); + } + + [Fact] + public void WhenBufferIsEmptyCountIsZero() + { + buffer.Count.Should().Be(0); + } + + [Fact] + public void WhenBufferHasOneItemCountIsOne() + { + // head < tail + buffer.TryAdd("1"); + buffer.Count.Should().Be(1); + } + + [Fact] + public void WhenBufferHas15ItemCountIs15() + { + buffer.TryAdd("1").Should().Be(BufferStatus.Success); + buffer.TryTake(out var _).Should().Be(BufferStatus.Success); + + for (var i = 0; i < 15; i++) + { + buffer.TryAdd("0").Should().Be(BufferStatus.Success); + } + + // head = 1, tail = 0 : head > tail + buffer.Count.Should().Be(15); + } + + [Fact] + public void WhenBufferIsFullTryAddIsFalse() + { + for (var i = 0; i < 16; i++) + { + buffer.TryAdd(i.ToString()).Should().Be(BufferStatus.Success); + } + + buffer.TryAdd("666").Should().Be(BufferStatus.Full); + } + + [Fact] + public void WhenBufferIsEmptyTryTakeIsFalse() + { + buffer.TryTake(out var _).Should().Be(BufferStatus.Empty); + } + + [Fact] + public void WhenItemAddedItCanBeTaken() + { + buffer.TryAdd("123").Should().Be(BufferStatus.Success); + buffer.TryTake(out var item).Should().Be(BufferStatus.Success); + item.Should().Be("123"); + } + + [Fact] + public void WhenItemsAreAddedClearRemovesItems() + { + buffer.TryAdd("1"); + buffer.TryAdd("2"); + + buffer.Count.Should().Be(2); + + buffer.Clear(); + + buffer.Count.Should().Be(0); + buffer.TryTake(out var _).Should().Be(BufferStatus.Empty); + } + + [Fact] + public void WhenBufferEmptyDrainReturnsZero() + { + var outputBuffer = new string[16]; + var output = new ArraySegment(outputBuffer); + + buffer.DrainTo(output).Should().Be(0); + } + +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenBufferContainsItemsDrainArrayTakesItems() + { + buffer.TryAdd("1"); + buffer.TryAdd("2"); + buffer.TryAdd("3"); + + var outputBuffer = new string[16]; + + buffer.DrainTo(outputBuffer.AsSpan()).Should().Be(3); + + outputBuffer[0].Should().Be("1"); + outputBuffer[1].Should().Be("2"); + outputBuffer[2].Should().Be("3"); + } +#endif + + [Fact] + public void WhenBufferContainsItemsDrainSegmentTakesItems() + { + buffer.TryAdd("1"); + buffer.TryAdd("2"); + buffer.TryAdd("3"); + + var outputBuffer = new string[16]; + var output = new ArraySegment(outputBuffer); + + buffer.DrainTo(output).Should().Be(3); + + outputBuffer[0].Should().Be("1"); + outputBuffer[1].Should().Be("2"); + outputBuffer[2].Should().Be("3"); + } + + [Fact] + public void WhenSegmentUsesOffsetItemsDrainedToOffset() + { + buffer.TryAdd("1"); + buffer.TryAdd("2"); + buffer.TryAdd("3"); + + var outputBuffer = new string[16]; + var output = new ArraySegment(outputBuffer, 6, 10); + + buffer.DrainTo(output).Should().Be(3); + + outputBuffer[6].Should().Be("1"); + outputBuffer[7].Should().Be("2"); + outputBuffer[8].Should().Be("3"); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index b891225f..71b850ff 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -1,876 +1,876 @@ -using FluentAssertions; -using BitFaster.Caching.Lru; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; +using FluentAssertions; +using BitFaster.Caching.Lru; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; using System.Collections.Concurrent; using System.Reflection; -namespace BitFaster.Caching.UnitTests.Lru -{ - public class ConcurrentLruTests - { - private readonly ITestOutputHelper testOutputHelper; - private const int hotCap = 3; - private const int warmCap = 3; - private const int coldCap = 3; - private static readonly ICapacityPartition capacity = new EqualCapacityPartition(hotCap + warmCap + coldCap); - - private ConcurrentLru lru = new ConcurrentLru(1, capacity, EqualityComparer.Default); - private ValueFactory valueFactory = new ValueFactory(); - - private List> removedItems = new List>(); - private List> updatedItems = new List>(); - - private void OnLruItemRemoved(object sender, ItemRemovedEventArgs e) - { - removedItems.Add(e); - } - - private void OnLruItemUpdated(object sender, ItemUpdatedEventArgs e) - { - updatedItems.Add(e); - } - - public ConcurrentLruTests(ITestOutputHelper testOutputHelper) - { - this.testOutputHelper = testOutputHelper; - } - - [Fact] - public void WhenConcurrencyIsLessThan1CtorThrows() - { - Action constructor = () => { var x = new ConcurrentLru(0, 3, EqualityComparer.Default); }; - - constructor.Should().Throw(); - } - - [Fact] - public void WhenCapacityIsLessThan3CtorThrows() - { - Action constructor = () => { var x = new ConcurrentLru(1, 2, EqualityComparer.Default); }; - - constructor.Should().Throw(); - } - - [Fact] - public void WhenPartitionIsNullCtorThrows() - { - ICapacityPartition partition = null; - Action constructor = () => { var x = new ConcurrentLru(1, partition, EqualityComparer.Default); }; - - constructor.Should().Throw(); - } - - [Fact] - public void WhenPartitionIsInvalidThrows() - { - var p = new TestCapacityPartition { Cold = 2, Warm = 0, Hot = 2 }; - Action constructor = () => { var x = new ConcurrentLru(1, p, EqualityComparer.Default); }; - - constructor.Should().Throw(); - } - - [Fact] - public void WhenComparerIsNullCtorThrows() - { - Action constructor = () => { var x = new ConcurrentLru(1, 3, null); }; - - constructor.Should().Throw(); - } - - [Fact] - public void WhenCapacityIs4HotHasCapacity1AndColdHasCapacity1() - { - var lru = new ConcurrentLru(1, new EqualCapacityPartition(4), EqualityComparer.Default); - - for (int i = 0; i < 5; i++) - { - lru.GetOrAdd(i, x => x); - } - - lru.HotCount.Should().Be(1); - lru.ColdCount.Should().Be(1); - lru.Capacity.Should().Be(4); - } - - [Fact] - public void WhenCapacityIs5HotHasCapacity1AndColdHasCapacity2() - { - var lru = new ConcurrentLru(1, new EqualCapacityPartition(5), EqualityComparer.Default); - - for (int i = 0; i < 5; i++) - { - lru.GetOrAdd(i, x => x); - } - - lru.HotCount.Should().Be(1); - lru.ColdCount.Should().Be(2); - lru.Capacity.Should().Be(5); - } - - [Fact] - public void ConstructAddAndRetrieveWithDefaultCtorReturnsValue() - { - var x = new ConcurrentLru(3); - - x.GetOrAdd(1, k => k).Should().Be(1); - } - - [Fact] - public void WhenItemIsAddedCountIsCorrect() - { - lru.Count.Should().Be(0); - lru.GetOrAdd(1, valueFactory.Create); - lru.Count.Should().Be(1); - } - - [Fact] - public async Task WhenItemIsAddedCountIsCorrectAsync() - { - lru.Count.Should().Be(0); - await lru.GetOrAddAsync(0, valueFactory.CreateAsync); - lru.Count.Should().Be(1); - } - - [Fact] - public void WhenItemsAddedKeysContainsTheKeys() - { - lru.Count.Should().Be(0); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.Keys.Should().BeEquivalentTo(new[] { 1, 2 }); - } - - [Fact] - public void WhenItemsAddedGenericEnumerateContainsKvps() - { - lru.Count.Should().Be(0); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.Should().BeEquivalentTo(new[] { new KeyValuePair(1, "1"), new KeyValuePair(2, "2") }); - } - - [Fact] - public void WhenItemsAddedEnumerateContainsKvps() - { - lru.Count.Should().Be(0); - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - - var enumerable = (IEnumerable)lru; - enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair(1, "1"), new KeyValuePair(2, "2") }); - } - - [Fact] - public void FromColdWarmupFillsWarmQueue() - { - this.Warmup(); - - this.lru.Count.Should().Be(9); - } - - [Fact] - public void WhenItemExistsTryGetReturnsValueAndTrue() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - result.Should().Be(true); - value.Should().Be("1"); - } - - [Fact] - public void WhenItemDoesNotExistTryGetReturnsNullAndFalse() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(2, out var value); - - result.Should().Be(false); - value.Should().BeNull(); - } - - [Fact] - public void MetricsAreEnabled() - { - lru.Metrics.HasValue.Should().BeTrue(); - } - - [Fact] - public void WhenItemIsAddedThenRetrievedMetricHitRatioIsHalf() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - lru.Metrics.Value.HitRatio.Should().Be(0.5); - } - - [Fact] - public void WhenItemIsAddedThenRetrievedTotalIs2() - { - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - lru.Metrics.Value.Total.Should().Be(2); - } - - [Fact] - public void WhenRefToMetricsIsCapturedResultIsCorrect() - { - // this detects the case where the struct is copied. If the internal Data class - // doesn't work, this test fails. - var m = lru.Metrics; - - lru.GetOrAdd(1, valueFactory.Create); - bool result = lru.TryGet(1, out var value); - - m.Value.HitRatio.Should().Be(0.5); - } - - [Fact] - public void ExpireAfterWriteHasValueIsFalse() - { - this.lru.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); - } - - [Fact] - public void WhenKeyIsRequestedItIsCreatedAndCached() - { - var result1 = lru.GetOrAdd(1, valueFactory.Create); - var result2 = lru.GetOrAdd(1, valueFactory.Create); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public void WhenKeyIsRequestedWithArgItIsCreatedAndCached() - { - var result1 = lru.GetOrAdd(1, valueFactory.Create, "x"); - var result2 = lru.GetOrAdd(1, valueFactory.Create, "y"); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public async Task WhenKeyIsRequestedItIsCreatedAndCachedAsync() - { - var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync); - var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync() - { - var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "x"); - var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "y"); - - valueFactory.timesCalled.Should().Be(1); - result1.Should().Be(result2); - } - - [Fact] - public void WhenDifferentKeysAreRequestedValueIsCreatedForEach() - { - var result1 = lru.GetOrAdd(1, valueFactory.Create); - var result2 = lru.GetOrAdd(2, valueFactory.Create); - - valueFactory.timesCalled.Should().Be(2); - - result1.Should().Be("1"); - result2.Should().Be("2"); - } - - [Fact] - public async Task WhenDifferentKeysAreRequesteValueIsCreatedForEachAsync() - { - var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync); - var result2 = await lru.GetOrAddAsync(2, valueFactory.CreateAsync); - - valueFactory.timesCalled.Should().Be(2); - - result1.Should().Be("1"); - result2.Should().Be("2"); - } - - [Fact] - public void WhenValuesAreNotReadAndMoreKeysRequestedThanCapacityCountDoesNotIncrease() - { - this.Warmup(); - - var result = lru.GetOrAdd(1, valueFactory.Create); - - lru.Count.Should().Be(9); - valueFactory.timesCalled.Should().Be(10); - } - - [Fact] - public void WhenValuesAreReadAndMoreKeysRequestedThanCapacityCountIsBounded() - { - int capacity = hotCap + coldCap + warmCap; - for (int i = 0; i < capacity + 1; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - - // touch items already cached when they are still in hot - if (i > 0) - { - lru.GetOrAdd(i - 1, valueFactory.Create); - } - } - - lru.Count.Should().Be(capacity); - valueFactory.timesCalled.Should().Be(capacity + 1); - } - - [Fact] - public void WhenKeysAreContinuouslyRequestedInTheOrderTheyAreAddedCountIsBounded() - { - int capacity = hotCap + coldCap + warmCap; - for (int i = 0; i < capacity + 10; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - - // Touch all items already cached in hot, warm and cold. - // This is worst case scenario, since we touch them in the exact order they - // were added. - for (int j = 0; j < i; j++) - { - lru.GetOrAdd(j, valueFactory.Create); - } - - testOutputHelper.WriteLine($"Total: {lru.Count} Hot: {lru.HotCount} Warm: {lru.WarmCount} Cold: {lru.ColdCount}"); - lru.Count.Should().BeLessOrEqualTo(capacity + 1); - } +namespace BitFaster.Caching.UnitTests.Lru +{ + public class ConcurrentLruTests + { + private readonly ITestOutputHelper testOutputHelper; + private const int hotCap = 3; + private const int warmCap = 3; + private const int coldCap = 3; + private static readonly ICapacityPartition capacity = new EqualCapacityPartition(hotCap + warmCap + coldCap); + + private ConcurrentLru lru = new ConcurrentLru(1, capacity, EqualityComparer.Default); + private ValueFactory valueFactory = new ValueFactory(); + + private List> removedItems = new List>(); + private List> updatedItems = new List>(); + + private void OnLruItemRemoved(object sender, ItemRemovedEventArgs e) + { + removedItems.Add(e); + } + + private void OnLruItemUpdated(object sender, ItemUpdatedEventArgs e) + { + updatedItems.Add(e); + } + + public ConcurrentLruTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + [Fact] + public void WhenConcurrencyIsLessThan1CtorThrows() + { + Action constructor = () => { var x = new ConcurrentLru(0, 3, EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCapacityIsLessThan3CtorThrows() + { + Action constructor = () => { var x = new ConcurrentLru(1, 2, EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenPartitionIsNullCtorThrows() + { + ICapacityPartition partition = null; + Action constructor = () => { var x = new ConcurrentLru(1, partition, EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenPartitionIsInvalidThrows() + { + var p = new TestCapacityPartition { Cold = 2, Warm = 0, Hot = 2 }; + Action constructor = () => { var x = new ConcurrentLru(1, p, EqualityComparer.Default); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenComparerIsNullCtorThrows() + { + Action constructor = () => { var x = new ConcurrentLru(1, 3, null); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCapacityIs4HotHasCapacity1AndColdHasCapacity1() + { + var lru = new ConcurrentLru(1, new EqualCapacityPartition(4), EqualityComparer.Default); + + for (int i = 0; i < 5; i++) + { + lru.GetOrAdd(i, x => x); + } + + lru.HotCount.Should().Be(1); + lru.ColdCount.Should().Be(1); + lru.Capacity.Should().Be(4); + } + + [Fact] + public void WhenCapacityIs5HotHasCapacity1AndColdHasCapacity2() + { + var lru = new ConcurrentLru(1, new EqualCapacityPartition(5), EqualityComparer.Default); + + for (int i = 0; i < 5; i++) + { + lru.GetOrAdd(i, x => x); + } + + lru.HotCount.Should().Be(1); + lru.ColdCount.Should().Be(2); + lru.Capacity.Should().Be(5); + } + + [Fact] + public void ConstructAddAndRetrieveWithDefaultCtorReturnsValue() + { + var x = new ConcurrentLru(3); + + x.GetOrAdd(1, k => k).Should().Be(1); + } + + [Fact] + public void WhenItemIsAddedCountIsCorrect() + { + lru.Count.Should().Be(0); + lru.GetOrAdd(1, valueFactory.Create); + lru.Count.Should().Be(1); + } + + [Fact] + public async Task WhenItemIsAddedCountIsCorrectAsync() + { + lru.Count.Should().Be(0); + await lru.GetOrAddAsync(0, valueFactory.CreateAsync); + lru.Count.Should().Be(1); + } + + [Fact] + public void WhenItemsAddedKeysContainsTheKeys() + { + lru.Count.Should().Be(0); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.Keys.Should().BeEquivalentTo(new[] { 1, 2 }); + } + + [Fact] + public void WhenItemsAddedGenericEnumerateContainsKvps() + { + lru.Count.Should().Be(0); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.Should().BeEquivalentTo(new[] { new KeyValuePair(1, "1"), new KeyValuePair(2, "2") }); + } + + [Fact] + public void WhenItemsAddedEnumerateContainsKvps() + { + lru.Count.Should().Be(0); + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + + var enumerable = (IEnumerable)lru; + enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair(1, "1"), new KeyValuePair(2, "2") }); + } + + [Fact] + public void FromColdWarmupFillsWarmQueue() + { + this.Warmup(); + + this.lru.Count.Should().Be(9); + } + + [Fact] + public void WhenItemExistsTryGetReturnsValueAndTrue() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + result.Should().Be(true); + value.Should().Be("1"); + } + + [Fact] + public void WhenItemDoesNotExistTryGetReturnsNullAndFalse() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(2, out var value); + + result.Should().Be(false); + value.Should().BeNull(); + } + + [Fact] + public void MetricsAreEnabled() + { + lru.Metrics.HasValue.Should().BeTrue(); + } + + [Fact] + public void WhenItemIsAddedThenRetrievedMetricHitRatioIsHalf() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + lru.Metrics.Value.HitRatio.Should().Be(0.5); + } + + [Fact] + public void WhenItemIsAddedThenRetrievedTotalIs2() + { + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + lru.Metrics.Value.Total.Should().Be(2); + } + + [Fact] + public void WhenRefToMetricsIsCapturedResultIsCorrect() + { + // this detects the case where the struct is copied. If the internal Data class + // doesn't work, this test fails. + var m = lru.Metrics; + + lru.GetOrAdd(1, valueFactory.Create); + bool result = lru.TryGet(1, out var value); + + m.Value.HitRatio.Should().Be(0.5); + } + + [Fact] + public void ExpireAfterWriteHasValueIsFalse() + { + this.lru.Policy.ExpireAfterWrite.HasValue.Should().BeFalse(); + } + + [Fact] + public void WhenKeyIsRequestedItIsCreatedAndCached() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create); + var result2 = lru.GetOrAdd(1, valueFactory.Create); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public void WhenKeyIsRequestedWithArgItIsCreatedAndCached() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create, "x"); + var result2 = lru.GetOrAdd(1, valueFactory.Create, "y"); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public async Task WhenKeyIsRequestedItIsCreatedAndCachedAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync); + var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "x"); + var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "y"); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public void WhenDifferentKeysAreRequestedValueIsCreatedForEach() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create); + var result2 = lru.GetOrAdd(2, valueFactory.Create); + + valueFactory.timesCalled.Should().Be(2); + + result1.Should().Be("1"); + result2.Should().Be("2"); + } + + [Fact] + public async Task WhenDifferentKeysAreRequesteValueIsCreatedForEachAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync); + var result2 = await lru.GetOrAddAsync(2, valueFactory.CreateAsync); + + valueFactory.timesCalled.Should().Be(2); + + result1.Should().Be("1"); + result2.Should().Be("2"); + } + + [Fact] + public void WhenValuesAreNotReadAndMoreKeysRequestedThanCapacityCountDoesNotIncrease() + { + this.Warmup(); + + var result = lru.GetOrAdd(1, valueFactory.Create); + + lru.Count.Should().Be(9); + valueFactory.timesCalled.Should().Be(10); + } + + [Fact] + public void WhenValuesAreReadAndMoreKeysRequestedThanCapacityCountIsBounded() + { + int capacity = hotCap + coldCap + warmCap; + for (int i = 0; i < capacity + 1; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + + // touch items already cached when they are still in hot + if (i > 0) + { + lru.GetOrAdd(i - 1, valueFactory.Create); + } + } + + lru.Count.Should().Be(capacity); + valueFactory.timesCalled.Should().Be(capacity + 1); + } + + [Fact] + public void WhenKeysAreContinuouslyRequestedInTheOrderTheyAreAddedCountIsBounded() + { + int capacity = hotCap + coldCap + warmCap; + for (int i = 0; i < capacity + 10; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + + // Touch all items already cached in hot, warm and cold. + // This is worst case scenario, since we touch them in the exact order they + // were added. + for (int j = 0; j < i; j++) + { + lru.GetOrAdd(j, valueFactory.Create); + } + + testOutputHelper.WriteLine($"Total: {lru.Count} Hot: {lru.HotCount} Warm: {lru.WarmCount} Cold: {lru.ColdCount}"); + lru.Count.Should().BeLessOrEqualTo(capacity + 1); + } + } + + public class KeysInOrderTestDataGenerator : IEnumerable + { + private readonly List _data = new List + { + new object[] { new EqualCapacityPartition(hotCap + warmCap + coldCap) }, + new object[] { new EqualCapacityPartition(128) }, + new object[] { new EqualCapacityPartition(256) }, + new object[] { new EqualCapacityPartition(1024) }, + new object[] { new FavorWarmPartition(128) }, + new object[] { new FavorWarmPartition(256) }, + new object[] { new FavorWarmPartition(1024) }, + new object[] { new FavorWarmPartition(128, 0.6) }, + new object[] { new FavorWarmPartition(256, 0.6) }, + new object[] { new FavorWarmPartition(1024, 0.6) }, + }; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [Theory] + [ClassData(typeof(KeysInOrderTestDataGenerator))] + public void WhenKeysAreContinuouslyRequestedInTheOrderTheyAreAddedCountIsBounded2(ICapacityPartition p) + { + int capacity = p.Hot + p.Cold + p.Warm; + lru = new ConcurrentLru(capacity, p, EqualityComparer.Default); + + testOutputHelper.WriteLine($"Capacity: {lru.Capacity} (Hot: {p.Hot} Warm: {p.Warm} Cold: {p.Cold})"); + + for (int i = 0; i < capacity + 10; i++) + { + lru.GetOrAdd(i, valueFactory.Create); + + // Touch all items already cached in hot, warm and cold. + // This is worst case scenario, since we touch them in the exact order they + // were added. + for (int j = 0; j < i; j++) + { + lru.GetOrAdd(j, valueFactory.Create); + } + + lru.Count.Should().BeLessOrEqualTo(capacity + 1, $"Total: {lru.Count} Hot: {lru.HotCount} Warm: {lru.WarmCount} Cold: {lru.ColdCount}"); + } + } + + [Fact] + public void WhenValueIsNotTouchedAndExpiresFromHotValueIsBumpedToCold() + { + this.Warmup(); + + lru.GetOrAdd(0, valueFactory.Create); // Don't touch in hot + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + lru.GetOrAdd(7, valueFactory.Create); + lru.GetOrAdd(8, valueFactory.Create); + lru.GetOrAdd(9, valueFactory.Create); + + lru.TryGet(0, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenValueIsTouchedAndExpiresFromHotValueIsBumpedToWarm() + { + this.Warmup(); + + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(0, valueFactory.Create); // Touch in hot + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); + lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + lru.GetOrAdd(7, valueFactory.Create); + lru.GetOrAdd(8, valueFactory.Create); + lru.GetOrAdd(9, valueFactory.Create); + + lru.TryGet(0, out var value).Should().BeTrue(); + } + + [Fact] + public void WhenValueIsTouchedAndExpiresFromColdItIsBumpedToWarm() + { + this.Warmup(); + + lru.GetOrAdd(0, valueFactory.Create); + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); // push 0 to cold (not touched in hot) + + lru.GetOrAdd(0, valueFactory.Create); // Touch 0 in cold + + lru.GetOrAdd(4, valueFactory.Create); // fully cycle cold, this will evict 0 if it is not moved to warm + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + lru.GetOrAdd(7, valueFactory.Create); + lru.GetOrAdd(8, valueFactory.Create); + lru.GetOrAdd(9, valueFactory.Create); + + lru.TryGet(0, out var value).Should().BeTrue(); + } + + [Fact] + public void WhenValueIsNotTouchedAndExpiresFromColdItIsRemoved() + { + this.Warmup(); + + lru.GetOrAdd(0, valueFactory.Create); + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); // push 0 to cold (not touched in hot) + + // Don't touch 0 in cold + + lru.GetOrAdd(4, valueFactory.Create); // fully cycle cold, this will evict 0 if it is not moved to warm + lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); + lru.GetOrAdd(7, valueFactory.Create); + lru.GetOrAdd(8, valueFactory.Create); + lru.GetOrAdd(9, valueFactory.Create); + + lru.TryGet(0, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenValueIsNotTouchedAndExpiresFromWarmValueIsBumpedToCold() + { + this.Warmup(); + + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(0, valueFactory.Create); // Touch 0 in hot, it will promote to warm + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); // push 0 to warm + + // touch next 3 values, so they will promote to warm + lru.GetOrAdd(4, valueFactory.Create); lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); lru.GetOrAdd(6, valueFactory.Create); + + // push 4,5,6 to warm, 0 to cold + lru.GetOrAdd(7, valueFactory.Create); + lru.GetOrAdd(8, valueFactory.Create); + lru.GetOrAdd(9, valueFactory.Create); + + // verify 0 is present, but don't touch it + lru.Keys.Should().Contain(0); + + // push 7,8,9 to cold, evict 0 + lru.GetOrAdd(10, valueFactory.Create); + lru.GetOrAdd(11, valueFactory.Create); + lru.GetOrAdd(12, valueFactory.Create); + + lru.TryGet(0, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenValueIsTouchedAndExpiresFromWarmValueIsBumpedBackIntoWarm() + { + this.Warmup(); + + lru.GetOrAdd(0, valueFactory.Create); + lru.GetOrAdd(0, valueFactory.Create); // Touch 0 in hot, it will promote to warm + + lru.GetOrAdd(1, valueFactory.Create); + lru.GetOrAdd(2, valueFactory.Create); + lru.GetOrAdd(3, valueFactory.Create); // push 0 to warm + + // touch next 3 values, so they will promote to warm + lru.GetOrAdd(4, valueFactory.Create); lru.GetOrAdd(4, valueFactory.Create); + lru.GetOrAdd(5, valueFactory.Create); lru.GetOrAdd(5, valueFactory.Create); + lru.GetOrAdd(6, valueFactory.Create); lru.GetOrAdd(6, valueFactory.Create); + + // push 4,5,6 to warm, 0 to cold + lru.GetOrAdd(7, valueFactory.Create); + lru.GetOrAdd(8, valueFactory.Create); + lru.GetOrAdd(9, valueFactory.Create); + + // Touch 0 + lru.TryGet(0, out var value).Should().BeTrue(); + + // push 7,8,9 to cold, cycle 0 back to warm + lru.GetOrAdd(10, valueFactory.Create); + lru.GetOrAdd(11, valueFactory.Create); + lru.GetOrAdd(12, valueFactory.Create); + + lru.TryGet(0, out value).Should().BeTrue(); + } + + [Fact] + public void WhenValueExpiresItIsDisposed() + { + var lruOfDisposable = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + + for (int i = 0; i < 7; i++) + { + lruOfDisposable.GetOrAdd(i, disposableValueFactory.Create); + } + + disposableValueFactory.Items[0].IsDisposed.Should().BeTrue(); + + disposableValueFactory.Items[1].IsDisposed.Should().BeFalse(); + disposableValueFactory.Items[2].IsDisposed.Should().BeFalse(); + disposableValueFactory.Items[3].IsDisposed.Should().BeFalse(); + disposableValueFactory.Items[4].IsDisposed.Should().BeFalse(); + disposableValueFactory.Items[5].IsDisposed.Should().BeFalse(); + disposableValueFactory.Items[6].IsDisposed.Should().BeFalse(); + } + + [Fact] + public void WhenAddingNullValueCanBeAddedAndRemoved() + { + lru.GetOrAdd(1, _ => null).Should().BeNull(); + lru.AddOrUpdate(1, null); + lru.TryRemove(1).Should().BeTrue(); + } + + [Fact] + public void WhenValueEvictedItemRemovedEventIsFired() + { + var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); + lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; + + // First 6 adds + // hot[6, 5], warm[2, 1], cold[4, 3] + // => + // hot[8, 7], warm[1, 0], cold[6, 5], evicted[4, 3] + for (int i = 0; i < 8; i++) + { + lruEvents.GetOrAdd(i + 1, i => i + 1); + } + + removedItems.Count.Should().Be(2); + + removedItems[0].Key.Should().Be(1); + removedItems[0].Value.Should().Be(2); + removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted); + + removedItems[1].Key.Should().Be(4); + removedItems[1].Value.Should().Be(5); + removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted); + } + + [Fact] + public void WhenValuesAreEvictedEvictionMetricCountsEvicted() + { + this.Warmup(); + + this.lru.GetOrAdd(1, valueFactory.Create); + + this.lru.Metrics.Value.Evicted.Should().Be(1); + } + + [Fact] + public void WhenItemRemovedEventIsUnregisteredEventIsNotFired() + { + var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); + + lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; + lruEvents.Events.Value.ItemRemoved -= OnLruItemRemoved; + + for (int i = 0; i < 6; i++) + { + lruEvents.GetOrAdd(i + 1, i => i + 1); + } + + removedItems.Count.Should().Be(0); + } + + [Fact] + public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryRemove(1).Should().BeTrue(); + lru.TryGet(1, out var value).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryRemoveReturnsValue() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryRemove(1, out var value).Should().BeTrue(); + value.Should().Be("1"); } - public class KeysInOrderTestDataGenerator : IEnumerable + [Fact] + public void WhenItemIsRemovedItIsDisposed() { - private readonly List _data = new List + var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + + lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); + lruOfDisposable.TryRemove(1); + + disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenItemIsRemovedRemovedEventIsFired() + { + var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); + lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; + + lruEvents.GetOrAdd(1, i => i + 2); + + lruEvents.TryRemove(1).Should().BeTrue(); + + removedItems.Count().Should().Be(1); + removedItems[0].Key.Should().Be(1); + removedItems[0].Value.Should().Be(3); + removedItems[0].Reason.Should().Be(ItemRemovedReason.Removed); + } + + [Fact] + public void WhenKeyDoesNotExistTryRemoveReturnsFalse() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryRemove(2).Should().BeFalse(); + } + + [Fact] + public void WhenRepeatedlyAddingAndRemovingSameValueLruRemainsInConsistentState() + { + int capacity = hotCap + coldCap + warmCap; + for (int i = 0; i < capacity; i++) { - new object[] { new EqualCapacityPartition(hotCap + warmCap + coldCap) }, - new object[] { new EqualCapacityPartition(128) }, - new object[] { new EqualCapacityPartition(256) }, - new object[] { new EqualCapacityPartition(1024) }, - new object[] { new FavorWarmPartition(128) }, - new object[] { new FavorWarmPartition(256) }, - new object[] { new FavorWarmPartition(1024) }, - new object[] { new FavorWarmPartition(128, 0.6) }, - new object[] { new FavorWarmPartition(256, 0.6) }, - new object[] { new FavorWarmPartition(1024, 0.6) }, - }; + // Because TryRemove leaves the item in the queue, when it is eventually removed + // from the cold queue, it should not remove the newly created value. + lru.GetOrAdd(1, valueFactory.Create); + lru.TryGet(1, out var value).Should().BeTrue(); + lru.TryRemove(1); + } + } - public IEnumerator GetEnumerator() => _data.GetEnumerator(); + [Fact] + public void WhenKeyExistsTryUpdateUpdatesValueAndReturnsTrue() + { + lru.GetOrAdd(1, valueFactory.Create); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + lru.TryUpdate(1, "2").Should().BeTrue(); + + lru.TryGet(1, out var value); + value.Should().Be("2"); } - [Theory] - [ClassData(typeof(KeysInOrderTestDataGenerator))] - public void WhenKeysAreContinuouslyRequestedInTheOrderTheyAreAddedCountIsBounded2(ICapacityPartition p) - { - int capacity = p.Hot + p.Cold + p.Warm; - lru = new ConcurrentLru(capacity, p, EqualityComparer.Default); - - testOutputHelper.WriteLine($"Capacity: {lru.Capacity} (Hot: {p.Hot} Warm: {p.Warm} Cold: {p.Cold})"); - - for (int i = 0; i < capacity + 10; i++) - { - lru.GetOrAdd(i, valueFactory.Create); - - // Touch all items already cached in hot, warm and cold. - // This is worst case scenario, since we touch them in the exact order they - // were added. - for (int j = 0; j < i; j++) - { - lru.GetOrAdd(j, valueFactory.Create); - } + [Fact] + public void WhenKeyExistsTryUpdateDisposesOldValue() + { + var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + var newValue = new DisposableItem(); - lru.Count.Should().BeLessOrEqualTo(capacity + 1, $"Total: {lru.Count} Hot: {lru.HotCount} Warm: {lru.WarmCount} Cold: {lru.ColdCount}"); - } - } - - [Fact] - public void WhenValueIsNotTouchedAndExpiresFromHotValueIsBumpedToCold() - { - this.Warmup(); - - lru.GetOrAdd(0, valueFactory.Create); // Don't touch in hot - - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - lru.GetOrAdd(7, valueFactory.Create); - lru.GetOrAdd(8, valueFactory.Create); - lru.GetOrAdd(9, valueFactory.Create); - - lru.TryGet(0, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenValueIsTouchedAndExpiresFromHotValueIsBumpedToWarm() - { - this.Warmup(); - - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(0, valueFactory.Create); // Touch in hot - - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); - lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - lru.GetOrAdd(7, valueFactory.Create); - lru.GetOrAdd(8, valueFactory.Create); - lru.GetOrAdd(9, valueFactory.Create); - - lru.TryGet(0, out var value).Should().BeTrue(); - } - - [Fact] - public void WhenValueIsTouchedAndExpiresFromColdItIsBumpedToWarm() - { - this.Warmup(); - - lru.GetOrAdd(0, valueFactory.Create); - - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); // push 0 to cold (not touched in hot) - - lru.GetOrAdd(0, valueFactory.Create); // Touch 0 in cold - - lru.GetOrAdd(4, valueFactory.Create); // fully cycle cold, this will evict 0 if it is not moved to warm - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - lru.GetOrAdd(7, valueFactory.Create); - lru.GetOrAdd(8, valueFactory.Create); - lru.GetOrAdd(9, valueFactory.Create); - - lru.TryGet(0, out var value).Should().BeTrue(); - } - - [Fact] - public void WhenValueIsNotTouchedAndExpiresFromColdItIsRemoved() - { - this.Warmup(); - - lru.GetOrAdd(0, valueFactory.Create); - - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); // push 0 to cold (not touched in hot) - - // Don't touch 0 in cold - - lru.GetOrAdd(4, valueFactory.Create); // fully cycle cold, this will evict 0 if it is not moved to warm - lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); - lru.GetOrAdd(7, valueFactory.Create); - lru.GetOrAdd(8, valueFactory.Create); - lru.GetOrAdd(9, valueFactory.Create); - - lru.TryGet(0, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenValueIsNotTouchedAndExpiresFromWarmValueIsBumpedToCold() - { - this.Warmup(); - - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(0, valueFactory.Create); // Touch 0 in hot, it will promote to warm - - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); // push 0 to warm - - // touch next 3 values, so they will promote to warm - lru.GetOrAdd(4, valueFactory.Create); lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); lru.GetOrAdd(6, valueFactory.Create); - - // push 4,5,6 to warm, 0 to cold - lru.GetOrAdd(7, valueFactory.Create); - lru.GetOrAdd(8, valueFactory.Create); - lru.GetOrAdd(9, valueFactory.Create); - - // verify 0 is present, but don't touch it - lru.Keys.Should().Contain(0); - - // push 7,8,9 to cold, evict 0 - lru.GetOrAdd(10, valueFactory.Create); - lru.GetOrAdd(11, valueFactory.Create); - lru.GetOrAdd(12, valueFactory.Create); - - lru.TryGet(0, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenValueIsTouchedAndExpiresFromWarmValueIsBumpedBackIntoWarm() - { - this.Warmup(); - - lru.GetOrAdd(0, valueFactory.Create); - lru.GetOrAdd(0, valueFactory.Create); // Touch 0 in hot, it will promote to warm - - lru.GetOrAdd(1, valueFactory.Create); - lru.GetOrAdd(2, valueFactory.Create); - lru.GetOrAdd(3, valueFactory.Create); // push 0 to warm - - // touch next 3 values, so they will promote to warm - lru.GetOrAdd(4, valueFactory.Create); lru.GetOrAdd(4, valueFactory.Create); - lru.GetOrAdd(5, valueFactory.Create); lru.GetOrAdd(5, valueFactory.Create); - lru.GetOrAdd(6, valueFactory.Create); lru.GetOrAdd(6, valueFactory.Create); - - // push 4,5,6 to warm, 0 to cold - lru.GetOrAdd(7, valueFactory.Create); - lru.GetOrAdd(8, valueFactory.Create); - lru.GetOrAdd(9, valueFactory.Create); - - // Touch 0 - lru.TryGet(0, out var value).Should().BeTrue(); - - // push 7,8,9 to cold, cycle 0 back to warm - lru.GetOrAdd(10, valueFactory.Create); - lru.GetOrAdd(11, valueFactory.Create); - lru.GetOrAdd(12, valueFactory.Create); - - lru.TryGet(0, out value).Should().BeTrue(); - } - - [Fact] - public void WhenValueExpiresItIsDisposed() - { - var lruOfDisposable = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); - var disposableValueFactory = new DisposableValueFactory(); - - for (int i = 0; i < 7; i++) - { - lruOfDisposable.GetOrAdd(i, disposableValueFactory.Create); - } - - disposableValueFactory.Items[0].IsDisposed.Should().BeTrue(); - - disposableValueFactory.Items[1].IsDisposed.Should().BeFalse(); - disposableValueFactory.Items[2].IsDisposed.Should().BeFalse(); - disposableValueFactory.Items[3].IsDisposed.Should().BeFalse(); - disposableValueFactory.Items[4].IsDisposed.Should().BeFalse(); - disposableValueFactory.Items[5].IsDisposed.Should().BeFalse(); - disposableValueFactory.Items[6].IsDisposed.Should().BeFalse(); - } - - [Fact] - public void WhenAddingNullValueCanBeAddedAndRemoved() + lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); + lruOfDisposable.TryUpdate(1, newValue); + + disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenKeyDoesNotExistTryUpdateReturnsFalse() { - lru.GetOrAdd(1, _ => null).Should().BeNull(); - lru.AddOrUpdate(1, null); - lru.TryRemove(1).Should().BeTrue(); - } - - [Fact] - public void WhenValueEvictedItemRemovedEventIsFired() - { - var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); - lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; - - // First 6 adds - // hot[6, 5], warm[2, 1], cold[4, 3] - // => - // hot[8, 7], warm[1, 0], cold[6, 5], evicted[4, 3] - for (int i = 0; i < 8; i++) - { - lruEvents.GetOrAdd(i + 1, i => i + 1); - } - - removedItems.Count.Should().Be(2); - - removedItems[0].Key.Should().Be(1); - removedItems[0].Value.Should().Be(2); - removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted); - - removedItems[1].Key.Should().Be(4); - removedItems[1].Value.Should().Be(5); - removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted); - } - - [Fact] - public void WhenValuesAreEvictedEvictionMetricCountsEvicted() - { - this.Warmup(); - - this.lru.GetOrAdd(1, valueFactory.Create); - - this.lru.Metrics.Value.Evicted.Should().Be(1); - } - - [Fact] - public void WhenItemRemovedEventIsUnregisteredEventIsNotFired() - { - var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); - - lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; - lruEvents.Events.Value.ItemRemoved -= OnLruItemRemoved; - - for (int i = 0; i < 6; i++) - { - lruEvents.GetOrAdd(i + 1, i => i + 1); - } - - removedItems.Count.Should().Be(0); - } - - [Fact] - public void WhenKeyExistsTryRemoveRemovesItemAndReturnsTrue() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryRemove(1).Should().BeTrue(); - lru.TryGet(1, out var value).Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryRemoveReturnsValue() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryRemove(1, out var value).Should().BeTrue(); - value.Should().Be("1"); - } - - [Fact] - public void WhenItemIsRemovedItIsDisposed() - { - var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); - var disposableValueFactory = new DisposableValueFactory(); - - lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); - lruOfDisposable.TryRemove(1); - - disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); - } - - [Fact] - public void WhenItemIsRemovedRemovedEventIsFired() - { - var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); - lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; - - lruEvents.GetOrAdd(1, i => i + 2); - - lruEvents.TryRemove(1).Should().BeTrue(); - - removedItems.Count().Should().Be(1); - removedItems[0].Key.Should().Be(1); - removedItems[0].Value.Should().Be(3); - removedItems[0].Reason.Should().Be(ItemRemovedReason.Removed); - } - - [Fact] - public void WhenKeyDoesNotExistTryRemoveReturnsFalse() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryRemove(2).Should().BeFalse(); - } - - [Fact] - public void WhenRepeatedlyAddingAndRemovingSameValueLruRemainsInConsistentState() - { - int capacity = hotCap + coldCap + warmCap; - for (int i = 0; i < capacity; i++) - { - // Because TryRemove leaves the item in the queue, when it is eventually removed - // from the cold queue, it should not remove the newly created value. - lru.GetOrAdd(1, valueFactory.Create); - lru.TryGet(1, out var value).Should().BeTrue(); - lru.TryRemove(1); - } - } - - [Fact] - public void WhenKeyExistsTryUpdateUpdatesValueAndReturnsTrue() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryUpdate(1, "2").Should().BeTrue(); - - lru.TryGet(1, out var value); - value.Should().Be("2"); - } - - [Fact] - public void WhenKeyExistsTryUpdateDisposesOldValue() - { - var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); - var disposableValueFactory = new DisposableValueFactory(); - var newValue = new DisposableItem(); - - lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); - lruOfDisposable.TryUpdate(1, newValue); - - disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); - } - - [Fact] - public void WhenKeyDoesNotExistTryUpdateReturnsFalse() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryUpdate(2, "3").Should().BeFalse(); - } - -// backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenKeyExistsTryUpdateIncrementsUpdateCount() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryUpdate(1, "2").Should().BeTrue(); - - lru.Metrics.Value.Updated.Should().Be(1); - } - - [Fact] - public void WhenKeyDoesNotExistTryUpdateDoesNotIncrementCounter() - { - lru.GetOrAdd(1, valueFactory.Create); - - lru.TryUpdate(2, "3").Should().BeFalse(); - - lru.Metrics.Value.Updated.Should().Be(0); - } -#endif - [Fact] - public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() - { - lru.AddOrUpdate(1, "1"); - - lru.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be("1"); - } - - [Fact] - public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() - { - lru.AddOrUpdate(1, "1"); - lru.AddOrUpdate(1, "2"); - - lru.TryGet(1, out var value).Should().BeTrue(); - value.Should().Be("2"); - } - - [Fact] - public void WhenKeyExistsAddOrUpdateDisposesOldValue() - { - var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); - var disposableValueFactory = new DisposableValueFactory(); - var newValue = new DisposableItem(); - - lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); - lruOfDisposable.AddOrUpdate(1, newValue); - - disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); - } - - [Fact] - public void WhenKeyDoesNotExistAddOrUpdateMaintainsLruOrder() - { - lru.AddOrUpdate(1, "1"); - lru.AddOrUpdate(2, "2"); - lru.AddOrUpdate(3, "3"); - lru.AddOrUpdate(4, "4"); - - lru.HotCount.Should().Be(3); - lru.WarmCount.Should().Be(1); // items must have been enqueued and cycled for one of them to reach the warm queue - } - -// backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenItemExistsAddOrUpdateFiresUpdateEvent() - { - var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); - lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; - - lruEvents.AddOrUpdate(1, 2); - lruEvents.AddOrUpdate(2, 3); - - lruEvents.AddOrUpdate(1, 3); - - this.updatedItems.Count.Should().Be(1); - this.updatedItems[0].Key.Should().Be(1); - this.updatedItems[0].OldValue.Should().Be(2); - this.updatedItems[0].NewValue.Should().Be(3); - } - - [Fact] - public void WhenItemExistsTryUpdateFiresUpdateEvent() - { - var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); - lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; - - lruEvents.AddOrUpdate(1, 2); - lruEvents.AddOrUpdate(2, 3); - - lruEvents.TryUpdate(1, 3); - - this.updatedItems.Count.Should().Be(1); - this.updatedItems[0].Key.Should().Be(1); - this.updatedItems[0].OldValue.Should().Be(2); - this.updatedItems[0].NewValue.Should().Be(3); - } - - [Fact] - public void WhenItemUpdatedEventIsUnregisteredEventIsNotFired() - { - var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); - - lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; - lruEvents.Events.Value.ItemUpdated -= OnLruItemUpdated; - - lruEvents.AddOrUpdate(1, 2); - lruEvents.AddOrUpdate(1, 2); - lruEvents.AddOrUpdate(1, 2); - - updatedItems.Count.Should().Be(0); - } -#endif - - [Fact] - public void WhenCacheIsEmptyClearIsNoOp() - { - lru.Clear(); - lru.Count.Should().Be(0); - } - - [Fact] - public void WhenItemsExistClearRemovesAllItems() - { - lru.AddOrUpdate(1, "1"); - lru.AddOrUpdate(2, "2"); - - lru.Clear(); - - lru.Count.Should().Be(0); - - // verify queues are purged - lru.HotCount.Should().Be(0); - lru.WarmCount.Should().Be(0); - lru.ColdCount.Should().Be(0); + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryUpdate(2, "3").Should().BeFalse(); + } + +// backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenKeyExistsTryUpdateIncrementsUpdateCount() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryUpdate(1, "2").Should().BeTrue(); + + lru.Metrics.Value.Updated.Should().Be(1); + } + + [Fact] + public void WhenKeyDoesNotExistTryUpdateDoesNotIncrementCounter() + { + lru.GetOrAdd(1, valueFactory.Create); + + lru.TryUpdate(2, "3").Should().BeFalse(); + + lru.Metrics.Value.Updated.Should().Be(0); + } +#endif + [Fact] + public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() + { + lru.AddOrUpdate(1, "1"); + + lru.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be("1"); + } + + [Fact] + public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() + { + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(1, "2"); + + lru.TryGet(1, out var value).Should().BeTrue(); + value.Should().Be("2"); + } + + [Fact] + public void WhenKeyExistsAddOrUpdateDisposesOldValue() + { + var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); + var disposableValueFactory = new DisposableValueFactory(); + var newValue = new DisposableItem(); + + lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create); + lruOfDisposable.AddOrUpdate(1, newValue); + + disposableValueFactory.Items[1].IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenKeyDoesNotExistAddOrUpdateMaintainsLruOrder() + { + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + lru.AddOrUpdate(4, "4"); + + lru.HotCount.Should().Be(3); + lru.WarmCount.Should().Be(1); // items must have been enqueued and cycled for one of them to reach the warm queue + } + +// backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenItemExistsAddOrUpdateFiresUpdateEvent() + { + var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); + lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; + + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(2, 3); + + lruEvents.AddOrUpdate(1, 3); + + this.updatedItems.Count.Should().Be(1); + this.updatedItems[0].Key.Should().Be(1); + this.updatedItems[0].OldValue.Should().Be(2); + this.updatedItems[0].NewValue.Should().Be(3); + } + + [Fact] + public void WhenItemExistsTryUpdateFiresUpdateEvent() + { + var lruEvents = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); + lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; + + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(2, 3); + + lruEvents.TryUpdate(1, 3); + + this.updatedItems.Count.Should().Be(1); + this.updatedItems[0].Key.Should().Be(1); + this.updatedItems[0].OldValue.Should().Be(2); + this.updatedItems[0].NewValue.Should().Be(3); + } + + [Fact] + public void WhenItemUpdatedEventIsUnregisteredEventIsNotFired() + { + var lruEvents = new ConcurrentLru(1, 6, EqualityComparer.Default); + + lruEvents.Events.Value.ItemUpdated += OnLruItemUpdated; + lruEvents.Events.Value.ItemUpdated -= OnLruItemUpdated; + + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(1, 2); + lruEvents.AddOrUpdate(1, 2); + + updatedItems.Count.Should().Be(0); + } +#endif + + [Fact] + public void WhenCacheIsEmptyClearIsNoOp() + { + lru.Clear(); + lru.Count.Should().Be(0); + } + + [Fact] + public void WhenItemsExistClearRemovesAllItems() + { + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + + lru.Clear(); + + lru.Count.Should().Be(0); + + // verify queues are purged + lru.HotCount.Should().Be(0); + lru.WarmCount.Should().Be(0); + lru.ColdCount.Should().Be(0); } // This is a special case: @@ -895,353 +895,353 @@ public void WhenCacheIsSize3ItemsExistAndItemsAccessedClearRemovesAllItems() lru.Count.Should().Be(0); } - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - [InlineData(6)] - [InlineData(7)] - [InlineData(8)] - [InlineData(9)] - [InlineData(10)] - public void WhenItemsExistAndItemsAccessedClearRemovesAllItems(int itemCount) - { - // By default capacity is 9. Test all possible states of touched items - // in the cache. - - for (int i = 0; i < itemCount; i++) + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + public void WhenItemsExistAndItemsAccessedClearRemovesAllItems(int itemCount) + { + // By default capacity is 9. Test all possible states of touched items + // in the cache. + + for (int i = 0; i < itemCount; i++) { - lru.AddOrUpdate(i, "1"); + lru.AddOrUpdate(i, "1"); } // touch n items for (int i = 0; i < itemCount; i++) { - lru.TryGet(i, out _); - } - - lru.Clear(); - - this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys)); - - lru.Count.Should().Be(0); - - // verify queues are purged - lru.HotCount.Should().Be(0); - lru.WarmCount.Should().Be(0); - lru.ColdCount.Should().Be(0); - } - - [Fact] - public void WhenWarmThenClearedIsWarmIsReset() - { + lru.TryGet(i, out _); + } + + lru.Clear(); + + this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys)); + + lru.Count.Should().Be(0); + + // verify queues are purged + lru.HotCount.Should().Be(0); + lru.WarmCount.Should().Be(0); + lru.ColdCount.Should().Be(0); + } + + [Fact] + public void WhenWarmThenClearedIsWarmIsReset() + { for (int i = 0; i < 20; i++) - { - lru.GetOrAdd(i, k => k.ToString()); - } - - lru.Clear(); - lru.Count.Should().Be(0); - + { + lru.GetOrAdd(i, k => k.ToString()); + } + + lru.Clear(); + lru.Count.Should().Be(0); + for (int i = 0; i < 20; i++) - { - lru.GetOrAdd(i, k => k.ToString()); - } - - lru.Count.Should().Be(capacity.Hot + capacity.Warm + capacity.Cold); - } - - [Fact] - public void WhenItemsAreDisposableClearDisposesItemsOnRemove() - { - var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); - - var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList(); - - for (int i = 0; i < 4; i++) - { - lruOfDisposable.AddOrUpdate(i, items[i]); - } - - lruOfDisposable.Clear(); - - items.All(i => i.IsDisposed == true).Should().BeTrue(); - } - - [Fact] - public void WhenItemsArClearedAnEventIsFired() - { - var lruEvents = new ConcurrentLru(1, capacity, EqualityComparer.Default); - lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; - - for (int i = 0; i < 6; i++) - { - lruEvents.GetOrAdd(i + 1, i => i + 1); - } - - lruEvents.Clear(); - - removedItems.Count.Should().Be(6); - - for (int i = 0; i < 6; i++) - { - removedItems[i].Reason.Should().Be(ItemRemovedReason.Cleared); - } - } - - [Fact] - public void WhenTrimCountIsZeroThrows() - { - lru.Invoking(l => lru.Trim(0)).Should().Throw(); - } - - [Fact] - public void WhenTrimCountIsMoreThanCapacityThrows() - { - lru.Invoking(l => lru.Trim(hotCap + warmCap + coldCap + 1)).Should().Throw(); - } - - [Theory] - [InlineData(1, new[] { 9, 8, 7, 3, 2, 1, 6, 5 })] - [InlineData(2, new[] { 9, 8, 7, 3, 2, 1, 6 })] - [InlineData(3, new[] { 9, 8, 7, 3, 2, 1 })] - [InlineData(4, new[] { 9, 8, 7, 3, 2 })] - [InlineData(5, new[] { 9, 8, 7, 3 })] - [InlineData(6, new[] { 9, 8, 7 })] - [InlineData(7, new[] { 9, 8 })] - [InlineData(8, new[] { 9 })] - [InlineData(9, new int[] { })] - public void WhenColdItemsExistTrimRemovesExpectedItemCount(int trimCount, int[] expected) - { - Warmup(); - - // initial state: - // Hot = 9, 8, 7 - // Warm = 3, 2, 1 - // Cold = 6, 5, 4 - lru.AddOrUpdate(1, "1"); - lru.AddOrUpdate(2, "2"); - lru.AddOrUpdate(3, "3"); - lru.GetOrAdd(1, i => i.ToString()); - lru.GetOrAdd(2, i => i.ToString()); - lru.GetOrAdd(3, i => i.ToString()); - - lru.AddOrUpdate(4, "4"); - lru.AddOrUpdate(5, "5"); - lru.AddOrUpdate(6, "6"); - - lru.AddOrUpdate(7, "7"); - lru.AddOrUpdate(8, "8"); - lru.AddOrUpdate(9, "9"); - - lru.Trim(trimCount); - - lru.Keys.Should().BeEquivalentTo(expected); - } - - [Theory] - [InlineData(1, new[] { 6, 5, 4, 3, 2 })] - [InlineData(2, new[] { 6, 5, 4, 3 })] - [InlineData(3, new[] { 6, 5, 4 })] - [InlineData(4, new[] { 6, 5 })] - [InlineData(5, new[] { 6 })] - [InlineData(6, new int[] { })] - [InlineData(7, new int[] { })] - [InlineData(8, new int[] { })] - [InlineData(9, new int[] { })] - public void WhenHotAndWarmItemsExistTrimRemovesExpectedItemCount(int itemCount, int[] expected) - { - // initial state: - // Hot = 6, 5, 4 - // Warm = 3, 2, 1 - // Cold = - - lru.AddOrUpdate(1, "1"); - lru.AddOrUpdate(2, "2"); - lru.AddOrUpdate(3, "3"); - lru.GetOrAdd(1, i => i.ToString()); - lru.GetOrAdd(2, i => i.ToString()); - lru.GetOrAdd(3, i => i.ToString()); - - lru.AddOrUpdate(4, "4"); - lru.AddOrUpdate(5, "5"); - lru.AddOrUpdate(6, "6"); - - lru.Trim(itemCount); - - lru.Keys.Should().BeEquivalentTo(expected); - } - - [Theory] - [InlineData(1, new[] { 3, 2 })] - [InlineData(2, new[] { 3 })] - [InlineData(3, new int[] { })] - [InlineData(4, new int[] { })] - [InlineData(5, new int[] { })] - [InlineData(6, new int[] { })] - [InlineData(7, new int[] { })] - [InlineData(8, new int[] { })] - [InlineData(9, new int[] { })] - public void WhenHotItemsExistTrimRemovesExpectedItemCount(int itemCount, int[] expected) - { - // initial state: - // Hot = 3, 2, 1 - // Warm = - - // Cold = - - lru.AddOrUpdate(1, "1"); - lru.AddOrUpdate(2, "2"); - lru.AddOrUpdate(3, "3"); - - lru.Trim(itemCount); - - lru.Keys.Should().BeEquivalentTo(expected); - } - - [Theory] - [InlineData(1, new[] { 9, 8, 7, 6, 5, 4, 3, 2 })] - [InlineData(2, new[] { 9, 8, 7, 6, 5, 4, 3 })] - [InlineData(3, new[] { 9, 8, 7, 6, 5, 4 })] - [InlineData(4, new[] { 9, 8, 7, 6, 5 })] - [InlineData(5, new[] { 9, 8, 7, 6 })] - [InlineData(6, new[] { 9, 8, 7 })] - [InlineData(7, new[] { 9, 8 })] - [InlineData(8, new[] { 9 })] - [InlineData(9, new int[] { })] - public void WhenColdItemsAreTouchedTrimRemovesExpectedItemCount(int trimCount, int[] expected) - { - Warmup(); - - // initial state: - // Hot = 9, 8, 7 - // Warm = 3, 2, 1 - // Cold = 6*, 5*, 4* - lru.AddOrUpdate(1, "1"); - lru.AddOrUpdate(2, "2"); - lru.AddOrUpdate(3, "3"); - lru.GetOrAdd(1, i => i.ToString()); - lru.GetOrAdd(2, i => i.ToString()); - lru.GetOrAdd(3, i => i.ToString()); - - lru.AddOrUpdate(4, "4"); - lru.AddOrUpdate(5, "5"); - lru.AddOrUpdate(6, "6"); - - lru.AddOrUpdate(7, "7"); - lru.AddOrUpdate(8, "8"); - lru.AddOrUpdate(9, "9"); - - // touch all items in the cold queue - lru.GetOrAdd(4, i => i.ToString()); - lru.GetOrAdd(5, i => i.ToString()); - lru.GetOrAdd(6, i => i.ToString()); - - lru.Trim(trimCount); - - this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys)); - this.testOutputHelper.WriteLine("exp " + string.Join(" ", expected)); - - lru.Keys.Should().BeEquivalentTo(expected); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(4)] - [InlineData(5)] - [InlineData(6)] - [InlineData(7)] - [InlineData(8)] - [InlineData(9)] - [InlineData(10)] - public void WhenItemsExistAndItemsAccessedTrimRemovesAllItems(int itemCount) - { - // By default capacity is 9. Test all possible states of touched items - // in the cache. - + { + lru.GetOrAdd(i, k => k.ToString()); + } + + lru.Count.Should().Be(capacity.Hot + capacity.Warm + capacity.Cold); + } + + [Fact] + public void WhenItemsAreDisposableClearDisposesItemsOnRemove() + { + var lruOfDisposable = new ConcurrentLru(1, 6, EqualityComparer.Default); + + var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList(); + + for (int i = 0; i < 4; i++) + { + lruOfDisposable.AddOrUpdate(i, items[i]); + } + + lruOfDisposable.Clear(); + + items.All(i => i.IsDisposed == true).Should().BeTrue(); + } + + [Fact] + public void WhenItemsArClearedAnEventIsFired() + { + var lruEvents = new ConcurrentLru(1, capacity, EqualityComparer.Default); + lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; + + for (int i = 0; i < 6; i++) + { + lruEvents.GetOrAdd(i + 1, i => i + 1); + } + + lruEvents.Clear(); + + removedItems.Count.Should().Be(6); + + for (int i = 0; i < 6; i++) + { + removedItems[i].Reason.Should().Be(ItemRemovedReason.Cleared); + } + } + + [Fact] + public void WhenTrimCountIsZeroThrows() + { + lru.Invoking(l => lru.Trim(0)).Should().Throw(); + } + + [Fact] + public void WhenTrimCountIsMoreThanCapacityThrows() + { + lru.Invoking(l => lru.Trim(hotCap + warmCap + coldCap + 1)).Should().Throw(); + } + + [Theory] + [InlineData(1, new[] { 9, 8, 7, 3, 2, 1, 6, 5 })] + [InlineData(2, new[] { 9, 8, 7, 3, 2, 1, 6 })] + [InlineData(3, new[] { 9, 8, 7, 3, 2, 1 })] + [InlineData(4, new[] { 9, 8, 7, 3, 2 })] + [InlineData(5, new[] { 9, 8, 7, 3 })] + [InlineData(6, new[] { 9, 8, 7 })] + [InlineData(7, new[] { 9, 8 })] + [InlineData(8, new[] { 9 })] + [InlineData(9, new int[] { })] + public void WhenColdItemsExistTrimRemovesExpectedItemCount(int trimCount, int[] expected) + { + Warmup(); + + // initial state: + // Hot = 9, 8, 7 + // Warm = 3, 2, 1 + // Cold = 6, 5, 4 + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + lru.GetOrAdd(1, i => i.ToString()); + lru.GetOrAdd(2, i => i.ToString()); + lru.GetOrAdd(3, i => i.ToString()); + + lru.AddOrUpdate(4, "4"); + lru.AddOrUpdate(5, "5"); + lru.AddOrUpdate(6, "6"); + + lru.AddOrUpdate(7, "7"); + lru.AddOrUpdate(8, "8"); + lru.AddOrUpdate(9, "9"); + + lru.Trim(trimCount); + + lru.Keys.Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(1, new[] { 6, 5, 4, 3, 2 })] + [InlineData(2, new[] { 6, 5, 4, 3 })] + [InlineData(3, new[] { 6, 5, 4 })] + [InlineData(4, new[] { 6, 5 })] + [InlineData(5, new[] { 6 })] + [InlineData(6, new int[] { })] + [InlineData(7, new int[] { })] + [InlineData(8, new int[] { })] + [InlineData(9, new int[] { })] + public void WhenHotAndWarmItemsExistTrimRemovesExpectedItemCount(int itemCount, int[] expected) + { + // initial state: + // Hot = 6, 5, 4 + // Warm = 3, 2, 1 + // Cold = - + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + lru.GetOrAdd(1, i => i.ToString()); + lru.GetOrAdd(2, i => i.ToString()); + lru.GetOrAdd(3, i => i.ToString()); + + lru.AddOrUpdate(4, "4"); + lru.AddOrUpdate(5, "5"); + lru.AddOrUpdate(6, "6"); + + lru.Trim(itemCount); + + lru.Keys.Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(1, new[] { 3, 2 })] + [InlineData(2, new[] { 3 })] + [InlineData(3, new int[] { })] + [InlineData(4, new int[] { })] + [InlineData(5, new int[] { })] + [InlineData(6, new int[] { })] + [InlineData(7, new int[] { })] + [InlineData(8, new int[] { })] + [InlineData(9, new int[] { })] + public void WhenHotItemsExistTrimRemovesExpectedItemCount(int itemCount, int[] expected) + { + // initial state: + // Hot = 3, 2, 1 + // Warm = - + // Cold = - + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + + lru.Trim(itemCount); + + lru.Keys.Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(1, new[] { 9, 8, 7, 6, 5, 4, 3, 2 })] + [InlineData(2, new[] { 9, 8, 7, 6, 5, 4, 3 })] + [InlineData(3, new[] { 9, 8, 7, 6, 5, 4 })] + [InlineData(4, new[] { 9, 8, 7, 6, 5 })] + [InlineData(5, new[] { 9, 8, 7, 6 })] + [InlineData(6, new[] { 9, 8, 7 })] + [InlineData(7, new[] { 9, 8 })] + [InlineData(8, new[] { 9 })] + [InlineData(9, new int[] { })] + public void WhenColdItemsAreTouchedTrimRemovesExpectedItemCount(int trimCount, int[] expected) + { + Warmup(); + + // initial state: + // Hot = 9, 8, 7 + // Warm = 3, 2, 1 + // Cold = 6*, 5*, 4* + lru.AddOrUpdate(1, "1"); + lru.AddOrUpdate(2, "2"); + lru.AddOrUpdate(3, "3"); + lru.GetOrAdd(1, i => i.ToString()); + lru.GetOrAdd(2, i => i.ToString()); + lru.GetOrAdd(3, i => i.ToString()); + + lru.AddOrUpdate(4, "4"); + lru.AddOrUpdate(5, "5"); + lru.AddOrUpdate(6, "6"); + + lru.AddOrUpdate(7, "7"); + lru.AddOrUpdate(8, "8"); + lru.AddOrUpdate(9, "9"); + + // touch all items in the cold queue + lru.GetOrAdd(4, i => i.ToString()); + lru.GetOrAdd(5, i => i.ToString()); + lru.GetOrAdd(6, i => i.ToString()); + + lru.Trim(trimCount); + + this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys)); + this.testOutputHelper.WriteLine("exp " + string.Join(" ", expected)); + + lru.Keys.Should().BeEquivalentTo(expected); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + public void WhenItemsExistAndItemsAccessedTrimRemovesAllItems(int itemCount) + { + // By default capacity is 9. Test all possible states of touched items + // in the cache. + for (int i = 0; i < itemCount; i++) { - lru.AddOrUpdate(i, "1"); + lru.AddOrUpdate(i, "1"); } // touch n items for (int i = 0; i < itemCount; i++) { - lru.TryGet(i, out _); - } - - lru.Trim(Math.Min(itemCount, lru.Capacity)); - - this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys)); - - lru.Count.Should().Be(0); - - // verify queues are purged - lru.HotCount.Should().Be(0); - lru.WarmCount.Should().Be(0); - lru.ColdCount.Should().Be(0); - } - - [Fact] - public void WhenItemsAreDisposableTrimDisposesItems() - { - var lruOfDisposable = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); - - var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList(); - - for (int i = 0; i < 4; i++) - { - lruOfDisposable.AddOrUpdate(i, items[i]); - } - - lruOfDisposable.Trim(2); - - items[0].IsDisposed.Should().BeTrue(); - items[1].IsDisposed.Should().BeTrue(); - items[2].IsDisposed.Should().BeFalse(); - items[3].IsDisposed.Should().BeFalse(); - } - - [Fact] - public void WhenItemsAreTrimmedAnEventIsFired() - { - var lruEvents = new ConcurrentLru(1, capacity, EqualityComparer.Default); - lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; - - for (int i = 0; i < 6; i++) - { - lruEvents.GetOrAdd(i + 1, i => i + 1); - } - - lruEvents.Trim(2); - - removedItems.Count.Should().Be(2); - - removedItems[0].Key.Should().Be(1); - removedItems[0].Value.Should().Be(2); - removedItems[0].Reason.Should().Be(ItemRemovedReason.Trimmed); - - removedItems[1].Key.Should().Be(2); - removedItems[1].Value.Should().Be(3); - removedItems[1].Reason.Should().Be(ItemRemovedReason.Trimmed); - } - - - private void Warmup() - { - lru.GetOrAdd(-1, valueFactory.Create); - lru.GetOrAdd(-2, valueFactory.Create); - lru.GetOrAdd(-3, valueFactory.Create); - lru.GetOrAdd(-4, valueFactory.Create); - lru.GetOrAdd(-5, valueFactory.Create); - lru.GetOrAdd(-6, valueFactory.Create); - lru.GetOrAdd(-7, valueFactory.Create); - lru.GetOrAdd(-8, valueFactory.Create); - lru.GetOrAdd(-9, valueFactory.Create); - } - } - + lru.TryGet(i, out _); + } + + lru.Trim(Math.Min(itemCount, lru.Capacity)); + + this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys)); + + lru.Count.Should().Be(0); + + // verify queues are purged + lru.HotCount.Should().Be(0); + lru.WarmCount.Should().Be(0); + lru.ColdCount.Should().Be(0); + } + + [Fact] + public void WhenItemsAreDisposableTrimDisposesItems() + { + var lruOfDisposable = new ConcurrentLru(1, new EqualCapacityPartition(6), EqualityComparer.Default); + + var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList(); + + for (int i = 0; i < 4; i++) + { + lruOfDisposable.AddOrUpdate(i, items[i]); + } + + lruOfDisposable.Trim(2); + + items[0].IsDisposed.Should().BeTrue(); + items[1].IsDisposed.Should().BeTrue(); + items[2].IsDisposed.Should().BeFalse(); + items[3].IsDisposed.Should().BeFalse(); + } + + [Fact] + public void WhenItemsAreTrimmedAnEventIsFired() + { + var lruEvents = new ConcurrentLru(1, capacity, EqualityComparer.Default); + lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved; + + for (int i = 0; i < 6; i++) + { + lruEvents.GetOrAdd(i + 1, i => i + 1); + } + + lruEvents.Trim(2); + + removedItems.Count.Should().Be(2); + + removedItems[0].Key.Should().Be(1); + removedItems[0].Value.Should().Be(2); + removedItems[0].Reason.Should().Be(ItemRemovedReason.Trimmed); + + removedItems[1].Key.Should().Be(2); + removedItems[1].Value.Should().Be(3); + removedItems[1].Reason.Should().Be(ItemRemovedReason.Trimmed); + } + + + private void Warmup() + { + lru.GetOrAdd(-1, valueFactory.Create); + lru.GetOrAdd(-2, valueFactory.Create); + lru.GetOrAdd(-3, valueFactory.Create); + lru.GetOrAdd(-4, valueFactory.Create); + lru.GetOrAdd(-5, valueFactory.Create); + lru.GetOrAdd(-6, valueFactory.Create); + lru.GetOrAdd(-7, valueFactory.Create); + lru.GetOrAdd(-8, valueFactory.Create); + lru.GetOrAdd(-9, valueFactory.Create); + } + } + public class ConcurrentLruIntegrityChecker where I : LruItem where P : struct, IItemPolicy @@ -1251,38 +1251,38 @@ public class ConcurrentLruIntegrityChecker private readonly ConcurrentQueue hotQueue; private readonly ConcurrentQueue warmQueue; - private readonly ConcurrentQueue coldQueue; - - private static FieldInfo hotQueueField = typeof(ConcurrentLruCore).GetField("hotQueue", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo warmQueueField = typeof(ConcurrentLruCore).GetField("warmQueue", BindingFlags.NonPublic | BindingFlags.Instance); - private static FieldInfo coldQueueField = typeof(ConcurrentLruCore).GetField("coldQueue", BindingFlags.NonPublic | BindingFlags.Instance); - + private readonly ConcurrentQueue coldQueue; + + private static FieldInfo hotQueueField = typeof(ConcurrentLruCore).GetField("hotQueue", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo warmQueueField = typeof(ConcurrentLruCore).GetField("warmQueue", BindingFlags.NonPublic | BindingFlags.Instance); + private static FieldInfo coldQueueField = typeof(ConcurrentLruCore).GetField("coldQueue", BindingFlags.NonPublic | BindingFlags.Instance); + public ConcurrentLruIntegrityChecker(ConcurrentLruCore cache) { - this.cache = cache; - - // get queues via reflection - this.hotQueue = (ConcurrentQueue)hotQueueField.GetValue(cache); + this.cache = cache; + + // get queues via reflection + this.hotQueue = (ConcurrentQueue)hotQueueField.GetValue(cache); this.warmQueue = (ConcurrentQueue)warmQueueField.GetValue(cache); - this.coldQueue = (ConcurrentQueue)coldQueueField.GetValue(cache); - } - + this.coldQueue = (ConcurrentQueue)coldQueueField.GetValue(cache); + } + public void Validate() { // queue counters must be consistent with queues - this.hotQueue.Count.Should().Be(cache.HotCount, "hot queue has a corrupted count"); - this.warmQueue.Count.Should().Be(cache.WarmCount, "warm queue has a corrupted count"); - this.coldQueue.Count.Should().Be(cache.ColdCount, "cold queue has a corrupted count"); - - // cache contents must be consistent with queued items - ValidateQueue(cache, this.hotQueue, "hot"); - ValidateQueue(cache, this.warmQueue, "warm"); - ValidateQueue(cache, this.coldQueue, "cold"); - - // cache must be within capacity - cache.Count.Should().BeLessThanOrEqualTo(cache.Capacity + 1, "capacity out of valid range"); - } - + this.hotQueue.Count.Should().Be(cache.HotCount, "hot queue has a corrupted count"); + this.warmQueue.Count.Should().Be(cache.WarmCount, "warm queue has a corrupted count"); + this.coldQueue.Count.Should().Be(cache.ColdCount, "cold queue has a corrupted count"); + + // cache contents must be consistent with queued items + ValidateQueue(cache, this.hotQueue, "hot"); + ValidateQueue(cache, this.warmQueue, "warm"); + ValidateQueue(cache, this.coldQueue, "cold"); + + // cache must be within capacity + cache.Count.Should().BeLessThanOrEqualTo(cache.Capacity + 1, "capacity out of valid range"); + } + private void ValidateQueue(ConcurrentLruCore cache, ConcurrentQueue queue, string queueName) { foreach (var item in queue) @@ -1298,12 +1298,12 @@ private void ValidateQueue(ConcurrentLruCore cache, ConcurrentQue .Any(i => i.Key.Equals(item.Key) && !i.WasRemoved) .Should().BeTrue($"{queueName} removed item {item.Key} was not removed"); } - } + } else { - cache.TryGet(item.Key, out var value).Should().BeTrue($"{queueName} item {item.Key} was not present"); - } - } - } - } -} + cache.TryGet(item.Key, out var value).Should().BeTrue($"{queueName} item {item.Key} was not present"); + } + } + } + } +} diff --git a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs index 4feb3ee0..713cefa6 100644 --- a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs @@ -1,191 +1,191 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace BitFaster.Caching.Atomic -{ - /// - /// A class that provides simple, lightweight exactly once initialization for values - /// stored in a cache. - /// - /// The type of the key. - /// The type of the value. - [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public sealed class AsyncAtomicFactory : IEquatable> - { - private Initializer initializer; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private V value; - - /// - /// Initializes a new instance of the class. - /// - public AsyncAtomicFactory() - { - initializer = new Initializer(); - } - - /// - /// Initializes a new instance of the class with the - /// specified value. - /// - /// The value. - public AsyncAtomicFactory(V value) - { - this.value = value; - } - - /// - /// Gets the value. If is false, calling will force initialization via the parameter. - /// - /// The key associated with the value. - /// The value factory to use to create the value when it is not initialized. - /// The value. - public async ValueTask GetValueAsync(K key, Func> valueFactory) - { - if (initializer == null) - { - return value; - } - - return await CreateValueAsync(key, new AsyncValueFactory(valueFactory)).ConfigureAwait(false); - } - - /// - /// Gets the value. If is false, calling will force initialization via the parameter. - /// - /// The type of the value factory argument. - /// The key associated with the value. - /// The value factory to use to create the value when it is not initialized. - /// The value factory argument. - /// The value. - public async ValueTask GetValueAsync(K key, Func> valueFactory, TArg factoryArgument) - { - if (initializer == null) - { - return value; - } - - return await CreateValueAsync(key, new AsyncValueFactoryArg(valueFactory, factoryArgument)).ConfigureAwait(false); - } - - /// - /// Gets a value indicating whether the value has been initialized. - /// - public bool IsValueCreated => initializer == null; - - /// - /// Gets the value if it has been initialized, else default. - /// - public V ValueIfCreated - { - get - { - if (!IsValueCreated) - { - return default; - } - - return value; - } - } - - /// - public override bool Equals(object obj) - { - return Equals(obj as AsyncAtomicFactory); - } - - /// - public bool Equals(AsyncAtomicFactory other) - { - if (other is null || !IsValueCreated || !other.IsValueCreated) - { - return false; - } - - return EqualityComparer.Default.Equals(ValueIfCreated, other.ValueIfCreated); - } - - /// - public override int GetHashCode() - { - if (!IsValueCreated) - { - return 0; - } - - return ValueIfCreated.GetHashCode(); - } - - private async ValueTask CreateValueAsync(K key, TFactory valueFactory) where TFactory : struct, IAsyncValueFactory - { - var init = Volatile.Read(ref initializer); - - if (init != null) - { - value = await init.CreateValueAsync(key, valueFactory).ConfigureAwait(false); - Volatile.Write(ref initializer, null); - } - - return value; - } - - private class Initializer - { - private bool isInitialized; - private Task valueTask; - - public async ValueTask CreateValueAsync(K key, TFactory valueFactory) where TFactory : struct, IAsyncValueFactory - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var synchronizedTask = DoubleCheck(tcs.Task); - - if (ReferenceEquals(synchronizedTask, tcs.Task)) - { - try - { - var value = await valueFactory.CreateAsync(key).ConfigureAwait(false); - tcs.SetResult(value); - - return value; - } - catch (Exception ex) - { - Volatile.Write(ref isInitialized, false); - tcs.SetException(ex); - throw; - } - } - - return await synchronizedTask.ConfigureAwait(false); - } - -#pragma warning disable CA2002 // Do not lock on objects with weak identity - private Task DoubleCheck(Task value) - { - // Fast path - if (Volatile.Read(ref isInitialized)) - { - return valueTask; - } - - lock (this) - { - if (!isInitialized) - { - valueTask = value; - isInitialized = true; - } - } - - return valueTask; - } -#pragma warning restore CA2002 // Do not lock on objects with weak identity - } - } -} +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Atomic +{ + /// + /// A class that provides simple, lightweight exactly once initialization for values + /// stored in a cache. + /// + /// The type of the key. + /// The type of the value. + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + public sealed class AsyncAtomicFactory : IEquatable> + { + private Initializer initializer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private V value; + + /// + /// Initializes a new instance of the class. + /// + public AsyncAtomicFactory() + { + initializer = new Initializer(); + } + + /// + /// Initializes a new instance of the class with the + /// specified value. + /// + /// The value. + public AsyncAtomicFactory(V value) + { + this.value = value; + } + + /// + /// Gets the value. If is false, calling will force initialization via the parameter. + /// + /// The key associated with the value. + /// The value factory to use to create the value when it is not initialized. + /// The value. + public async ValueTask GetValueAsync(K key, Func> valueFactory) + { + if (initializer == null) + { + return value; + } + + return await CreateValueAsync(key, new AsyncValueFactory(valueFactory)).ConfigureAwait(false); + } + + /// + /// Gets the value. If is false, calling will force initialization via the parameter. + /// + /// The type of the value factory argument. + /// The key associated with the value. + /// The value factory to use to create the value when it is not initialized. + /// The value factory argument. + /// The value. + public async ValueTask GetValueAsync(K key, Func> valueFactory, TArg factoryArgument) + { + if (initializer == null) + { + return value; + } + + return await CreateValueAsync(key, new AsyncValueFactoryArg(valueFactory, factoryArgument)).ConfigureAwait(false); + } + + /// + /// Gets a value indicating whether the value has been initialized. + /// + public bool IsValueCreated => initializer == null; + + /// + /// Gets the value if it has been initialized, else default. + /// + public V ValueIfCreated + { + get + { + if (!IsValueCreated) + { + return default; + } + + return value; + } + } + + /// + public override bool Equals(object obj) + { + return Equals(obj as AsyncAtomicFactory); + } + + /// + public bool Equals(AsyncAtomicFactory other) + { + if (other is null || !IsValueCreated || !other.IsValueCreated) + { + return false; + } + + return EqualityComparer.Default.Equals(ValueIfCreated, other.ValueIfCreated); + } + + /// + public override int GetHashCode() + { + if (!IsValueCreated) + { + return 0; + } + + return ValueIfCreated.GetHashCode(); + } + + private async ValueTask CreateValueAsync(K key, TFactory valueFactory) where TFactory : struct, IAsyncValueFactory + { + var init = Volatile.Read(ref initializer); + + if (init != null) + { + value = await init.CreateValueAsync(key, valueFactory).ConfigureAwait(false); + Volatile.Write(ref initializer, null); + } + + return value; + } + + private class Initializer + { + private bool isInitialized; + private Task valueTask; + + public async ValueTask CreateValueAsync(K key, TFactory valueFactory) where TFactory : struct, IAsyncValueFactory + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var synchronizedTask = DoubleCheck(tcs.Task); + + if (ReferenceEquals(synchronizedTask, tcs.Task)) + { + try + { + var value = await valueFactory.CreateAsync(key).ConfigureAwait(false); + tcs.SetResult(value); + + return value; + } + catch (Exception ex) + { + Volatile.Write(ref isInitialized, false); + tcs.SetException(ex); + throw; + } + } + + return await synchronizedTask.ConfigureAwait(false); + } + +#pragma warning disable CA2002 // Do not lock on objects with weak identity + private Task DoubleCheck(Task value) + { + // Fast path + if (Volatile.Read(ref isInitialized)) + { + return valueTask; + } + + lock (this) + { + if (!isInitialized) + { + valueTask = value; + isInitialized = true; + } + } + + return valueTask; + } +#pragma warning restore CA2002 // Do not lock on objects with weak identity + } + } +} diff --git a/BitFaster.Caching/Atomic/AtomicFactory.cs b/BitFaster.Caching/Atomic/AtomicFactory.cs index 0ca92318..2873dbe5 100644 --- a/BitFaster.Caching/Atomic/AtomicFactory.cs +++ b/BitFaster.Caching/Atomic/AtomicFactory.cs @@ -1,108 +1,108 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; - -namespace BitFaster.Caching.Atomic -{ - /// - /// A class that provides simple, lightweight exactly once initialization for values - /// stored in a cache. - /// - /// The type of the key. - /// The type of the value. - [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public sealed class AtomicFactory : IEquatable> - { - private Initializer initializer; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private V value; - - /// - /// Initializes a new instance of the class. - /// - public AtomicFactory() - { - initializer = new Initializer(); - } - - /// - /// Initializes a new instance of the class with the - /// specified value. - /// - /// The value. - public AtomicFactory(V value) - { - this.value = value; - } - - /// - /// Gets the value. If is false, calling will force initialization via the parameter. - /// - /// The key associated with the value. - /// The value factory to use to create the value when it is not initialized. - /// The value. - public V GetValue(K key, Func valueFactory) - { - if (initializer == null) - { - return value; - } - - return CreateValue(key, new ValueFactory(valueFactory)); - } - - /// - /// Gets the value. If is false, calling will force initialization via the parameter. - /// - /// The type of the value factory argument. - /// The key associated with the value. - /// The value factory to use to create the value when it is not initialized. - /// The value factory argument. - /// The value. - public V GetValue(K key, Func valueFactory, TArg factoryArgument) - { - if (initializer == null) - { - return value; - } - - return CreateValue(key, new ValueFactoryArg(valueFactory, factoryArgument)); - } - - /// - /// Gets a value indicating whether the value has been initialized. - /// - public bool IsValueCreated => Volatile.Read(ref initializer) == null; - - /// - /// Gets the value if it has been initialized, else default. - /// - public V ValueIfCreated - { - get - { - if (!IsValueCreated) - { - return default; - } - - return value; - } - } - - private V CreateValue(K key, TFactory valueFactory) where TFactory : struct, IValueFactory - { - var init = Volatile.Read(ref initializer); - - if (init != null) - { - value = init.CreateValue(key, valueFactory); - Volatile.Write(ref initializer, null); // volatile write must occur after setting value - } - - return value; +using System.Diagnostics; +using System.Threading; + +namespace BitFaster.Caching.Atomic +{ + /// + /// A class that provides simple, lightweight exactly once initialization for values + /// stored in a cache. + /// + /// The type of the key. + /// The type of the value. + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + public sealed class AtomicFactory : IEquatable> + { + private Initializer initializer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private V value; + + /// + /// Initializes a new instance of the class. + /// + public AtomicFactory() + { + initializer = new Initializer(); + } + + /// + /// Initializes a new instance of the class with the + /// specified value. + /// + /// The value. + public AtomicFactory(V value) + { + this.value = value; + } + + /// + /// Gets the value. If is false, calling will force initialization via the parameter. + /// + /// The key associated with the value. + /// The value factory to use to create the value when it is not initialized. + /// The value. + public V GetValue(K key, Func valueFactory) + { + if (initializer == null) + { + return value; + } + + return CreateValue(key, new ValueFactory(valueFactory)); + } + + /// + /// Gets the value. If is false, calling will force initialization via the parameter. + /// + /// The type of the value factory argument. + /// The key associated with the value. + /// The value factory to use to create the value when it is not initialized. + /// The value factory argument. + /// The value. + public V GetValue(K key, Func valueFactory, TArg factoryArgument) + { + if (initializer == null) + { + return value; + } + + return CreateValue(key, new ValueFactoryArg(valueFactory, factoryArgument)); + } + + /// + /// Gets a value indicating whether the value has been initialized. + /// + public bool IsValueCreated => Volatile.Read(ref initializer) == null; + + /// + /// Gets the value if it has been initialized, else default. + /// + public V ValueIfCreated + { + get + { + if (!IsValueCreated) + { + return default; + } + + return value; + } + } + + private V CreateValue(K key, TFactory valueFactory) where TFactory : struct, IValueFactory + { + var init = Volatile.Read(ref initializer); + + if (init != null) + { + value = init.CreateValue(key, valueFactory); + Volatile.Write(ref initializer, null); // volatile write must occur after setting value + } + + return value; } /// @@ -134,26 +134,26 @@ public override int GetHashCode() } #pragma warning disable CA2002 // Do not lock on objects with weak identity - private class Initializer - { - private bool isInitialized; - private V value; - - public V CreateValue(K key, TFactory valueFactory) where TFactory : struct, IValueFactory - { - lock (this) - { - if (isInitialized) - { - return value; - } - - value = valueFactory.Create(key); - isInitialized = true; - return value; - } - } + private class Initializer + { + private bool isInitialized; + private V value; + + public V CreateValue(K key, TFactory valueFactory) where TFactory : struct, IValueFactory + { + lock (this) + { + if (isInitialized) + { + return value; + } + + value = valueFactory.Create(key); + isInitialized = true; + return value; + } + } } #pragma warning restore CA2002 // Do not lock on objects with weak identity - } -} + } +} diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 5d3e94ba..3ac0ad0a 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -1,212 +1,212 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; - -namespace BitFaster.Caching.Atomic -{ - /// - /// A cache decorator for working with wrapped values, giving exactly once initialization. - /// - /// The type of keys in the cache. - /// The type of values in the cache. - [DebuggerTypeProxy(typeof(AtomicFactoryAsyncCache<,>.AsyncCacheDebugView))] - [DebuggerDisplay("Count = {Count}")] - public sealed class AtomicFactoryAsyncCache : IAsyncCache - { - private readonly ICache> cache; - private readonly Optional> events; - - /// - /// Initializes a new instance of the AtomicFactoryAsyncCache class with the specified inner cache. - /// - /// The decorated cache. - public AtomicFactoryAsyncCache(ICache> cache) - { - if (cache == null) - Throw.ArgNull(ExceptionArgument.cache); - - this.cache = cache; - - if (cache.Events.HasValue) - { - this.events = new Optional>(new EventProxy(cache.Events.Value)); - } - else - { - this.events = Optional>.None(); - } - } - - /// - public int Count => AtomicEx.EnumerateCount(this.GetEnumerator()); - - /// - public Optional Metrics => cache.Metrics; - - /// - public Optional> Events => this.events; - - /// - public ICollection Keys => AtomicEx.FilterKeys>(this.cache, v => v.IsValueCreated); - - /// - public CachePolicy Policy => this.cache.Policy; - - /// - public void AddOrUpdate(K key, V value) - { - cache.AddOrUpdate(key, new AsyncAtomicFactory(value)); - } - - /// - public void Clear() - { - cache.Clear(); - } - - /// - public ValueTask GetOrAddAsync(K key, Func> valueFactory) - { - var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory()); - return synchronized.GetValueAsync(key, valueFactory); - } - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// An argument value to pass into valueFactory. - /// A task that represents the asynchronous GetOrAdd operation. - public ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) - { - var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory()); - return synchronized.GetValueAsync(key, valueFactory, factoryArgument); - } - - /// - public bool TryGet(K key, out V value) - { - AsyncAtomicFactory output; - var ret = cache.TryGet(key, out output); - - if (ret && output.IsValueCreated) - { - value = output.ValueIfCreated; - return true; - } - - value = default; - return false; - } - - /// - public bool TryRemove(K key) - { - return cache.TryRemove(key); +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Atomic +{ + /// + /// A cache decorator for working with wrapped values, giving exactly once initialization. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + [DebuggerTypeProxy(typeof(AtomicFactoryAsyncCache<,>.AsyncCacheDebugView))] + [DebuggerDisplay("Count = {Count}")] + public sealed class AtomicFactoryAsyncCache : IAsyncCache + { + private readonly ICache> cache; + private readonly Optional> events; + + /// + /// Initializes a new instance of the AtomicFactoryAsyncCache class with the specified inner cache. + /// + /// The decorated cache. + public AtomicFactoryAsyncCache(ICache> cache) + { + if (cache == null) + Throw.ArgNull(ExceptionArgument.cache); + + this.cache = cache; + + if (cache.Events.HasValue) + { + this.events = new Optional>(new EventProxy(cache.Events.Value)); + } + else + { + this.events = Optional>.None(); + } + } + + /// + public int Count => AtomicEx.EnumerateCount(this.GetEnumerator()); + + /// + public Optional Metrics => cache.Metrics; + + /// + public Optional> Events => this.events; + + /// + public ICollection Keys => AtomicEx.FilterKeys>(this.cache, v => v.IsValueCreated); + + /// + public CachePolicy Policy => this.cache.Policy; + + /// + public void AddOrUpdate(K key, V value) + { + cache.AddOrUpdate(key, new AsyncAtomicFactory(value)); + } + + /// + public void Clear() + { + cache.Clear(); + } + + /// + public ValueTask GetOrAddAsync(K key, Func> valueFactory) + { + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory()); + return synchronized.GetValueAsync(key, valueFactory); + } + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// An argument value to pass into valueFactory. + /// A task that represents the asynchronous GetOrAdd operation. + public ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) + { + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory()); + return synchronized.GetValueAsync(key, valueFactory, factoryArgument); + } + + /// + public bool TryGet(K key, out V value) + { + AsyncAtomicFactory output; + var ret = cache.TryGet(key, out output); + + if (ret && output.IsValueCreated) + { + value = output.ValueIfCreated; + return true; + } + + value = default; + return false; + } + + /// + public bool TryRemove(K key) + { + return cache.TryRemove(key); } // backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - /// - /// - ///If the value factory is still executing, returns false. - /// - public bool TryRemove(KeyValuePair item) - { - var kvp = new KeyValuePair>(item.Key, new AsyncAtomicFactory(item.Value)); - return cache.TryRemove(kvp); - } - - /// - /// - /// If the value factory is still executing, the default value will be returned. - /// - public bool TryRemove(K key, out V value) - { - if (cache.TryRemove(key, out var atomic)) - { - value = atomic.ValueIfCreated; - return true; - } - - value = default; - return false; - } -#endif - - /// - public bool TryUpdate(K key, V value) - { - return cache.TryUpdate(key, new AsyncAtomicFactory(value)); - } - - /// - public IEnumerator> GetEnumerator() - { - foreach (var kvp in this.cache) - { - if (kvp.Value.IsValueCreated) - { - yield return new KeyValuePair(kvp.Key, kvp.Value.ValueIfCreated); - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((AtomicFactoryAsyncCache)this).GetEnumerator(); - } - - private class EventProxy : CacheEventProxyBase, V> - { - public EventProxy(ICacheEvents> inner) - : base(inner) - { - } - - protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs> inner) - { - return new ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); - } - - protected override ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs> inner) - { - return new ItemUpdatedEventArgs(inner.Key, inner.OldValue.ValueIfCreated, inner.NewValue.ValueIfCreated); - } - } - - [ExcludeFromCodeCoverage] - internal class AsyncCacheDebugView - { - private readonly IAsyncCache cache; - - public AsyncCacheDebugView(IAsyncCache cache) - { - this.cache = cache; - } - - public KeyValuePair[] Items - { - get - { - var items = new KeyValuePair[cache.Count]; - - int index = 0; - foreach (var kvp in cache) - { - items[index++] = kvp; - } - return items; - } - } - - public ICacheMetrics Metrics => cache.Metrics.Value; - } - } -} +#if NETCOREAPP3_0_OR_GREATER + /// + /// + ///If the value factory is still executing, returns false. + /// + public bool TryRemove(KeyValuePair item) + { + var kvp = new KeyValuePair>(item.Key, new AsyncAtomicFactory(item.Value)); + return cache.TryRemove(kvp); + } + + /// + /// + /// If the value factory is still executing, the default value will be returned. + /// + public bool TryRemove(K key, out V value) + { + if (cache.TryRemove(key, out var atomic)) + { + value = atomic.ValueIfCreated; + return true; + } + + value = default; + return false; + } +#endif + + /// + public bool TryUpdate(K key, V value) + { + return cache.TryUpdate(key, new AsyncAtomicFactory(value)); + } + + /// + public IEnumerator> GetEnumerator() + { + foreach (var kvp in this.cache) + { + if (kvp.Value.IsValueCreated) + { + yield return new KeyValuePair(kvp.Key, kvp.Value.ValueIfCreated); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((AtomicFactoryAsyncCache)this).GetEnumerator(); + } + + private class EventProxy : CacheEventProxyBase, V> + { + public EventProxy(ICacheEvents> inner) + : base(inner) + { + } + + protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs> inner) + { + return new ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); + } + + protected override ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs> inner) + { + return new ItemUpdatedEventArgs(inner.Key, inner.OldValue.ValueIfCreated, inner.NewValue.ValueIfCreated); + } + } + + [ExcludeFromCodeCoverage] + internal class AsyncCacheDebugView + { + private readonly IAsyncCache cache; + + public AsyncCacheDebugView(IAsyncCache cache) + { + this.cache = cache; + } + + public KeyValuePair[] Items + { + get + { + var items = new KeyValuePair[cache.Count]; + + int index = 0; + foreach (var kvp in cache) + { + items[index++] = kvp; + } + return items; + } + } + + public ICacheMetrics Metrics => cache.Metrics.Value; + } + } +} diff --git a/BitFaster.Caching/Atomic/AtomicFactoryCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryCache.cs index 0433b682..8269368a 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryCache.cs @@ -1,107 +1,107 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; - -namespace BitFaster.Caching.Atomic -{ - /// - /// A cache decorator for working with wrapped values, giving exactly once initialization. - /// - /// The type of keys in the cache. - /// The type of values in the cache. - [DebuggerTypeProxy(typeof(CacheDebugView<,>))] - [DebuggerDisplay("Count = {Count}")] - public sealed class AtomicFactoryCache : ICache - { - private readonly ICache> cache; - private readonly Optional> events; - - /// - /// Initializes a new instance of the ScopedCache class with the specified inner cache. - /// - /// The decorated cache. - public AtomicFactoryCache(ICache> cache) - { - if (cache == null) - Throw.ArgNull(ExceptionArgument.cache); - - this.cache = cache; - - if (cache.Events.HasValue) - { - this.events = new Optional>(new EventProxy(cache.Events.Value)); - } - else - { - this.events = Optional>.None(); - } - } - - /// - public int Count => AtomicEx.EnumerateCount(this.GetEnumerator()); - - /// - public Optional Metrics => this.cache.Metrics; - - /// - public Optional> Events => this.events; - - /// - public ICollection Keys => AtomicEx.FilterKeys>(this.cache, v => v.IsValueCreated); - - /// - public CachePolicy Policy => this.cache.Policy; - - /// - public void AddOrUpdate(K key, V value) - { - this.cache.AddOrUpdate(key, new AtomicFactory(value)); - } - - /// - public void Clear() - { - this.cache.Clear(); - } - - /// - public V GetOrAdd(K key, Func valueFactory) - { - var atomicFactory = cache.GetOrAdd(key, _ => new AtomicFactory()); - return atomicFactory.GetValue(key, valueFactory); - } - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to generate a value for the key. - /// An argument value to pass into valueFactory. - /// The value for the key. This will be either the existing value for the key if the key is already - /// in the cache, or the new value if the key was not in the cache. - public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) - { - var atomicFactory = cache.GetOrAdd(key, _ => new AtomicFactory()); - return atomicFactory.GetValue(key, valueFactory, factoryArgument); - } - - /// - public bool TryGet(K key, out V value) - { - AtomicFactory output; - var ret = cache.TryGet(key, out output); - - if (ret && output.IsValueCreated) - { - value = output.ValueIfCreated; - return true; - } - - value = default; - return false; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace BitFaster.Caching.Atomic +{ + /// + /// A cache decorator for working with wrapped values, giving exactly once initialization. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + [DebuggerTypeProxy(typeof(CacheDebugView<,>))] + [DebuggerDisplay("Count = {Count}")] + public sealed class AtomicFactoryCache : ICache + { + private readonly ICache> cache; + private readonly Optional> events; + + /// + /// Initializes a new instance of the ScopedCache class with the specified inner cache. + /// + /// The decorated cache. + public AtomicFactoryCache(ICache> cache) + { + if (cache == null) + Throw.ArgNull(ExceptionArgument.cache); + + this.cache = cache; + + if (cache.Events.HasValue) + { + this.events = new Optional>(new EventProxy(cache.Events.Value)); + } + else + { + this.events = Optional>.None(); + } + } + + /// + public int Count => AtomicEx.EnumerateCount(this.GetEnumerator()); + + /// + public Optional Metrics => this.cache.Metrics; + + /// + public Optional> Events => this.events; + + /// + public ICollection Keys => AtomicEx.FilterKeys>(this.cache, v => v.IsValueCreated); + + /// + public CachePolicy Policy => this.cache.Policy; + + /// + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new AtomicFactory(value)); + } + + /// + public void Clear() + { + this.cache.Clear(); + } + + /// + public V GetOrAdd(K key, Func valueFactory) + { + var atomicFactory = cache.GetOrAdd(key, _ => new AtomicFactory()); + return atomicFactory.GetValue(key, valueFactory); + } + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to generate a value for the key. + /// An argument value to pass into valueFactory. + /// The value for the key. This will be either the existing value for the key if the key is already + /// in the cache, or the new value if the key was not in the cache. + public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) + { + var atomicFactory = cache.GetOrAdd(key, _ => new AtomicFactory()); + return atomicFactory.GetValue(key, valueFactory, factoryArgument); + } + + /// + public bool TryGet(K key, out V value) + { + AtomicFactory output; + var ret = cache.TryGet(key, out output); + + if (ret && output.IsValueCreated) + { + value = output.ValueIfCreated; + return true; + } + + value = default; + return false; } // backcompat: remove conditional compile @@ -110,74 +110,74 @@ public bool TryGet(K key, out V value) /// ///If the value factory is still executing, returns false. /// - public bool TryRemove(KeyValuePair item) - { - var kvp = new KeyValuePair>(item.Key, new AtomicFactory(item.Value)); - return cache.TryRemove(kvp); - } - - /// - /// - /// If the value factory is still executing, the default value will be returned. - /// - public bool TryRemove(K key, out V value) - { + public bool TryRemove(KeyValuePair item) + { + var kvp = new KeyValuePair>(item.Key, new AtomicFactory(item.Value)); + return cache.TryRemove(kvp); + } + + /// + /// + /// If the value factory is still executing, the default value will be returned. + /// + public bool TryRemove(K key, out V value) + { if (cache.TryRemove(key, out var atomic)) { value = atomic.ValueIfCreated; - return true; - } - - value = default; - return false; - } -#endif - - /// - public bool TryRemove(K key) - { - return cache.TryRemove(key); - } - - /// - public bool TryUpdate(K key, V value) - { - return cache.TryUpdate(key, new AtomicFactory(value)); - } - - /// - public IEnumerator> GetEnumerator() - { - foreach (var kvp in this.cache) - { - if (kvp.Value.IsValueCreated) - { - yield return new KeyValuePair(kvp.Key, kvp.Value.ValueIfCreated); - } - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((AtomicFactoryCache)this).GetEnumerator(); - } - - private class EventProxy : CacheEventProxyBase, V> - { - public EventProxy(ICacheEvents> inner) - : base(inner) - { - } - - protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs> inner) - { - return new ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); - } - - protected override ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs> inner) - { - return new ItemUpdatedEventArgs(inner.Key, inner.OldValue.ValueIfCreated, inner.NewValue.ValueIfCreated); - } - } - } -} + return true; + } + + value = default; + return false; + } +#endif + + /// + public bool TryRemove(K key) + { + return cache.TryRemove(key); + } + + /// + public bool TryUpdate(K key, V value) + { + return cache.TryUpdate(key, new AtomicFactory(value)); + } + + /// + public IEnumerator> GetEnumerator() + { + foreach (var kvp in this.cache) + { + if (kvp.Value.IsValueCreated) + { + yield return new KeyValuePair(kvp.Key, kvp.Value.ValueIfCreated); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((AtomicFactoryCache)this).GetEnumerator(); + } + + private class EventProxy : CacheEventProxyBase, V> + { + public EventProxy(ICacheEvents> inner) + : base(inner) + { + } + + protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs> inner) + { + return new ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); + } + + protected override ItemUpdatedEventArgs TranslateOnUpdated(ItemUpdatedEventArgs> inner) + { + return new ItemUpdatedEventArgs(inner.Key, inner.OldValue.ValueIfCreated, inner.NewValue.ValueIfCreated); + } + } + } +} diff --git a/BitFaster.Caching/Atomic/ConcurrentDictionaryExtensions.cs b/BitFaster.Caching/Atomic/ConcurrentDictionaryExtensions.cs index 333b8339..b8f3420f 100644 --- a/BitFaster.Caching/Atomic/ConcurrentDictionaryExtensions.cs +++ b/BitFaster.Caching/Atomic/ConcurrentDictionaryExtensions.cs @@ -1,27 +1,27 @@ -using System; -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; -namespace BitFaster.Caching.Atomic -{ +namespace BitFaster.Caching.Atomic +{ /// /// Convenience methods for using AtomicFactory with ConcurrentDictionary. - /// - public static class ConcurrentDictionaryExtensions - { + /// + public static class ConcurrentDictionaryExtensions + { /// /// Adds a key/value pair to the ConcurrentDictionary if the key does not already exist. Returns the new value, or the existing value if the key already exists. /// /// The ConcurrentDictionary to use. /// The key of the element to add. /// The function used to generate a value for the key. - /// The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value if the key was not in the dictionary. - public static V GetOrAdd(this ConcurrentDictionary> dictionary, K key, Func valueFactory) - { - var atomicFactory = dictionary.GetOrAdd(key, _ => new AtomicFactory()); - return atomicFactory.GetValue(key, valueFactory); - } - + /// The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value if the key was not in the dictionary. + public static V GetOrAdd(this ConcurrentDictionary> dictionary, K key, Func valueFactory) + { + var atomicFactory = dictionary.GetOrAdd(key, _ => new AtomicFactory()); + return atomicFactory.GetValue(key, valueFactory); + } + /// /// Adds a key/value pair to the ConcurrentDictionary by using the specified function and an argument if the key does not already exist, or returns the existing value if the key exists. /// @@ -29,33 +29,33 @@ public static V GetOrAdd(this ConcurrentDictionary> /// The key of the element to add. /// The function used to generate a value for the key. /// An argument value to pass into valueFactory. - /// The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value if the key was not in the dictionary. - public static V GetOrAdd(this ConcurrentDictionary> dictionary, K key, Func valueFactory, TArg factoryArgument) - { - var atomicFactory = dictionary.GetOrAdd(key, _ => new AtomicFactory()); - return atomicFactory.GetValue(key, valueFactory, factoryArgument); - } - + /// The value for the key. This will be either the existing value for the key if the key is already in the dictionary, or the new value if the key was not in the dictionary. + public static V GetOrAdd(this ConcurrentDictionary> dictionary, K key, Func valueFactory, TArg factoryArgument) + { + var atomicFactory = dictionary.GetOrAdd(key, _ => new AtomicFactory()); + return atomicFactory.GetValue(key, valueFactory, factoryArgument); + } + /// /// Attempts to get the value associated with the specified key from the ConcurrentDictionary. /// /// The ConcurrentDictionary to use. /// The key of the value to get. /// When this method returns, contains the object from the ConcurrentDictionary that has the specified key, or the default value of the type if the operation failed. - /// true if the key was found in the ConcurrentDictionary; otherwise, false. - public static bool TryGetValue(this ConcurrentDictionary> dictionary, K key, out V value) - { - AtomicFactory output; - var ret = dictionary.TryGetValue(key, out output); - - if (ret && output.IsValueCreated) - { - value = output.ValueIfCreated; - return true; - } - - value = default; - return false; + /// true if the key was found in the ConcurrentDictionary; otherwise, false. + public static bool TryGetValue(this ConcurrentDictionary> dictionary, K key, out V value) + { + AtomicFactory output; + var ret = dictionary.TryGetValue(key, out output); + + if (ret && output.IsValueCreated) + { + value = output.ValueIfCreated; + return true; + } + + value = default; + return false; } /// @@ -64,15 +64,15 @@ public static bool TryGetValue(this ConcurrentDictionaryThe ConcurrentDictionary to use. /// The KeyValuePair representing the key and value to remove. /// true if the object was removed successfully; otherwise, false. - public static bool TryRemove(this ConcurrentDictionary> dictionary, KeyValuePair item) - { + public static bool TryRemove(this ConcurrentDictionary> dictionary, KeyValuePair item) + { var kvp = new KeyValuePair>(item.Key, new AtomicFactory(item.Value)); #if NET6_0_OR_GREATER return dictionary.TryRemove(kvp); #else // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/ return ((ICollection>>)dictionary).Remove(kvp); -#endif +#endif } /// @@ -82,16 +82,16 @@ public static bool TryRemove(this ConcurrentDictionaryThe key of the element to remove and return. /// When this method returns, contains the object removed from the ConcurrentDictionary, or the default value of the TValue type if key does not exist. /// true if the object was removed successfully; otherwise, false. - public static bool TryRemove(this ConcurrentDictionary> dictionary, K key, out V value) - { + public static bool TryRemove(this ConcurrentDictionary> dictionary, K key, out V value) + { if (dictionary.TryRemove(key, out var atomic)) { value = atomic.ValueIfCreated; - return true; - } - - value = default; - return false; - } - } -} + return true; + } + + value = default; + return false; + } + } +} diff --git a/BitFaster.Caching/IAsyncCache.cs b/BitFaster.Caching/IAsyncCache.cs index 59299d60..11d5b171 100644 --- a/BitFaster.Caching/IAsyncCache.cs +++ b/BitFaster.Caching/IAsyncCache.cs @@ -1,113 +1,113 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace BitFaster.Caching -{ - /// - /// Represents a generic cache of key/value pairs. - /// - /// The type of keys in the cache. - /// The type of values in the cache. - public interface IAsyncCache : IEnumerable> - { - /// - /// Gets the number of items currently held in the cache. - /// - int Count { get; } - - /// - /// Gets the cache metrics, if configured. - /// - Optional Metrics { get; } - - /// - /// Gets the cache events, if configured. - /// - Optional> Events { get; } - - /// - /// Gets the cache policy. - /// - CachePolicy Policy { get; } - - /// - /// Gets a collection containing the keys in the cache. - /// - ICollection Keys { get; } - - /// - /// Attempts to get the value associated with the specified key from the cache. - /// - /// The key of the value to get. - /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed. - /// true if the key was found in the cache; otherwise, false. - bool TryGet(K key, out V value); - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// A task that represents the asynchronous GetOrAdd operation. - ValueTask GetOrAddAsync(K key, Func> valueFactory); - - // backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// An argument value to pass into valueFactory. - /// A task that represents the asynchronous GetOrAdd operation. +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// Represents a generic cache of key/value pairs. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public interface IAsyncCache : IEnumerable> + { + /// + /// Gets the number of items currently held in the cache. + /// + int Count { get; } + + /// + /// Gets the cache metrics, if configured. + /// + Optional Metrics { get; } + + /// + /// Gets the cache events, if configured. + /// + Optional> Events { get; } + + /// + /// Gets the cache policy. + /// + CachePolicy Policy { get; } + + /// + /// Gets a collection containing the keys in the cache. + /// + ICollection Keys { get; } + + /// + /// Attempts to get the value associated with the specified key from the cache. + /// + /// The key of the value to get. + /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed. + /// true if the key was found in the cache; otherwise, false. + bool TryGet(K key, out V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// A task that represents the asynchronous GetOrAdd operation. + ValueTask GetOrAddAsync(K key, Func> valueFactory); + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// An argument value to pass into valueFactory. + /// A task that represents the asynchronous GetOrAdd operation. ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) => this.GetOrAddAsync(key, k => valueFactory(k, factoryArgument)); - /// - /// Attempts to remove and return the value that has the specified key. - /// - /// The key of the element to remove. - /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. - /// true if the object was removed successfully; otherwise, false. - bool TryRemove(K key, out V value) => throw new NotSupportedException(); - - /// - /// Attempts to remove the specified key value pair. - /// - /// The item to remove. - /// true if the item was removed successfully; otherwise, false. - bool TryRemove(KeyValuePair item) => throw new NotSupportedException(); -#endif - - /// - /// Attempts to remove the value that has the specified key. - /// - /// The key of the element to remove. - /// true if the object was removed successfully; otherwise, false. - bool TryRemove(K key); - - /// - /// Attempts to update the value that has the specified key. - /// - /// The key of the element to update. - /// The new value. - /// true if the object was updated successfully; otherwise, false. - bool TryUpdate(K key, V value); - - /// - /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the - /// key already exists. - /// - /// The key of the element to update. - /// The new value. - void AddOrUpdate(K key, V value); - - /// - /// Removes all keys and values from the cache. - /// - void Clear(); - } -} + /// + /// Attempts to remove and return the value that has the specified key. + /// + /// The key of the element to remove. + /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key, out V value) => throw new NotSupportedException(); + + /// + /// Attempts to remove the specified key value pair. + /// + /// The item to remove. + /// true if the item was removed successfully; otherwise, false. + bool TryRemove(KeyValuePair item) => throw new NotSupportedException(); +#endif + + /// + /// Attempts to remove the value that has the specified key. + /// + /// The key of the element to remove. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key); + + /// + /// Attempts to update the value that has the specified key. + /// + /// The key of the element to update. + /// The new value. + /// true if the object was updated successfully; otherwise, false. + bool TryUpdate(K key, V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the + /// key already exists. + /// + /// The key of the element to update. + /// The new value. + void AddOrUpdate(K key, V value); + + /// + /// Removes all keys and values from the cache. + /// + void Clear(); + } +} diff --git a/BitFaster.Caching/ICache.cs b/BitFaster.Caching/ICache.cs index 00e16c96..f28e85db 100644 --- a/BitFaster.Caching/ICache.cs +++ b/BitFaster.Caching/ICache.cs @@ -1,114 +1,114 @@ -using System; -using System.Collections.Generic; - -namespace BitFaster.Caching -{ - /// - /// Represents a generic cache of key/value pairs. - /// - /// The type of keys in the cache. - /// The type of values in the cache. - public interface ICache : IEnumerable> - { - /// - /// Gets the number of items currently held in the cache. - /// - int Count { get; } - - /// - /// Gets the cache metrics, if configured. - /// - Optional Metrics { get; } - - /// - /// Gets the cache events, if configured. - /// - Optional> Events { get; } - - /// - /// Gets the cache policy. - /// - CachePolicy Policy { get; } - - /// - /// Gets a collection containing the keys in the cache. - /// - ICollection Keys { get; } - - /// - /// Attempts to get the value associated with the specified key from the cache. - /// - /// The key of the value to get. - /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed. - /// true if the key was found in the cache; otherwise, false. - bool TryGet(K key, out V value); - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The key of the element to add. - /// The factory function used to generate a value for the key. - /// The value for the key. This will be either the existing value for the key if the key is already - /// in the cache, or the new value if the key was not in the cache. - V GetOrAdd(K key, Func valueFactory); - - // backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to generate a value for the key. - /// An argument value to pass into valueFactory. - /// The value for the key. This will be either the existing value for the key if the key is already - /// in the cache, or the new value if the key was not in the cache. - V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) => this.GetOrAdd(key, k => valueFactory(k, factoryArgument)); - +using System; +using System.Collections.Generic; + +namespace BitFaster.Caching +{ + /// + /// Represents a generic cache of key/value pairs. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public interface ICache : IEnumerable> + { + /// + /// Gets the number of items currently held in the cache. + /// + int Count { get; } + + /// + /// Gets the cache metrics, if configured. + /// + Optional Metrics { get; } + + /// + /// Gets the cache events, if configured. + /// + Optional> Events { get; } + + /// + /// Gets the cache policy. + /// + CachePolicy Policy { get; } + + /// + /// Gets a collection containing the keys in the cache. + /// + ICollection Keys { get; } + + /// + /// Attempts to get the value associated with the specified key from the cache. + /// + /// The key of the value to get. + /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed. + /// true if the key was found in the cache; otherwise, false. + bool TryGet(K key, out V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The key of the element to add. + /// The factory function used to generate a value for the key. + /// The value for the key. This will be either the existing value for the key if the key is already + /// in the cache, or the new value if the key was not in the cache. + V GetOrAdd(K key, Func valueFactory); + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to generate a value for the key. + /// An argument value to pass into valueFactory. + /// The value for the key. This will be either the existing value for the key if the key is already + /// in the cache, or the new value if the key was not in the cache. + V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) => this.GetOrAdd(key, k => valueFactory(k, factoryArgument)); + /// /// Attempts to remove and return the value that has the specified key. /// /// The key of the element to remove. /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. /// true if the object was removed successfully; otherwise, false. - bool TryRemove(K key, out V value) => throw new NotSupportedException(); - + bool TryRemove(K key, out V value) => throw new NotSupportedException(); + /// /// Attempts to remove the specified key value pair. /// /// The item to remove. /// true if the item was removed successfully; otherwise, false. - bool TryRemove(KeyValuePair item) => throw new NotSupportedException(); -#endif - - /// - /// Attempts to remove the value that has the specified key. - /// - /// The key of the element to remove. - /// true if the object was removed successfully; otherwise, false. - bool TryRemove(K key); - - /// - /// Attempts to update the value that has the specified key. - /// - /// The key of the element to update. - /// The new value. - /// true if the object was updated successfully; otherwise, false. - bool TryUpdate(K key, V value); - - /// - /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the - /// key already exists. - /// - /// The key of the element to update. - /// The new value. - void AddOrUpdate(K key, V value); - - /// - /// Removes all keys and values from the cache. - /// - void Clear(); - } -} + bool TryRemove(KeyValuePair item) => throw new NotSupportedException(); +#endif + + /// + /// Attempts to remove the value that has the specified key. + /// + /// The key of the element to remove. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key); + + /// + /// Attempts to update the value that has the specified key. + /// + /// The key of the element to update. + /// The new value. + /// true if the object was updated successfully; otherwise, false. + bool TryUpdate(K key, V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the + /// key already exists. + /// + /// The key of the element to update. + /// The new value. + void AddOrUpdate(K key, V value); + + /// + /// Removes all keys and values from the cache. + /// + void Clear(); + } +} diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index fb550b32..0d47aeee 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -1,330 +1,330 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using BitFaster.Caching.Buffers; -using BitFaster.Caching.Counters; -using BitFaster.Caching.Lru; -using BitFaster.Caching.Scheduler; - -#if DEBUG -using System.Text; -#endif - -namespace BitFaster.Caching.Lfu -{ - /// - /// An approximate LFU based on the W-TinyLfu eviction policy. W-TinyLfu tracks items using a window LRU list, and - /// a main space LRU divided into protected and probation segments. Reads and writes to the cache are stored in buffers - /// and later applied to the policy LRU lists in batches under a lock. Each read and write is tracked using a compact - /// popularity sketch to probalistically estimate item frequency. Items proceed through the LRU lists as follows: - /// - /// New items are added to the window LRU. When acessed window items move to the window MRU position. - /// When the window is full, candidate items are moved to the probation segment in LRU order. - /// When the main space is full, the access frequency of each window candidate is compared - /// to probation victims in LRU order. The item with the lowest frequency is evicted until the cache size is within bounds. - /// When a probation item is accessed, it is moved to the protected segment. If the protected segment is full, - /// the LRU protected item is demoted to probation. - /// When a protected item is accessed, it is moved to the protected MRU position. - /// - /// The size of the admission window and main space are adapted over time to iteratively improve hit rate using a - /// hill climbing algorithm. A larger window favors workloads with high recency bias, whereas a larger main space - /// favors workloads with frequency bias. - /// - /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). - /// https://github.com/ben-manes/caffeine - [DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView))] - [DebuggerDisplay("Count = {Count}/{Capacity}")] - public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoundedPolicy - { - private const int MaxWriteBufferRetries = 64; - - /// - /// The default buffer size. - /// - public const int DefaultBufferSize = 128; - - private readonly ConcurrentDictionary> dictionary; - - private readonly StripedMpscBuffer> readBuffer; - private readonly MpscBoundedBuffer> writeBuffer; - - private readonly CacheMetrics metrics = new(); - - private readonly CmSketch cmSketch; - - private readonly LfuNodeList windowLru; - private readonly LfuNodeList probationLru; - private readonly LfuNodeList protectedLru; - - private readonly LfuCapacityPartition capacity; - - private readonly DrainStatus drainStatus = new(); - private readonly object maintenanceLock = new(); - - private readonly IScheduler scheduler; - private readonly Action drainBuffers; - +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching.Buffers; +using BitFaster.Caching.Counters; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Scheduler; + +#if DEBUG +using System.Text; +#endif + +namespace BitFaster.Caching.Lfu +{ + /// + /// An approximate LFU based on the W-TinyLfu eviction policy. W-TinyLfu tracks items using a window LRU list, and + /// a main space LRU divided into protected and probation segments. Reads and writes to the cache are stored in buffers + /// and later applied to the policy LRU lists in batches under a lock. Each read and write is tracked using a compact + /// popularity sketch to probalistically estimate item frequency. Items proceed through the LRU lists as follows: + /// + /// New items are added to the window LRU. When acessed window items move to the window MRU position. + /// When the window is full, candidate items are moved to the probation segment in LRU order. + /// When the main space is full, the access frequency of each window candidate is compared + /// to probation victims in LRU order. The item with the lowest frequency is evicted until the cache size is within bounds. + /// When a probation item is accessed, it is moved to the protected segment. If the protected segment is full, + /// the LRU protected item is demoted to probation. + /// When a protected item is accessed, it is moved to the protected MRU position. + /// + /// The size of the admission window and main space are adapted over time to iteratively improve hit rate using a + /// hill climbing algorithm. A larger window favors workloads with high recency bias, whereas a larger main space + /// favors workloads with frequency bias. + /// + /// Based on the Caffeine library by ben.manes@gmail.com (Ben Manes). + /// https://github.com/ben-manes/caffeine + [DebuggerTypeProxy(typeof(ConcurrentLfu<,>.LfuDebugView))] + [DebuggerDisplay("Count = {Count}/{Capacity}")] + public sealed class ConcurrentLfu : ICache, IAsyncCache, IBoundedPolicy + { + private const int MaxWriteBufferRetries = 64; + + /// + /// The default buffer size. + /// + public const int DefaultBufferSize = 128; + + private readonly ConcurrentDictionary> dictionary; + + private readonly StripedMpscBuffer> readBuffer; + private readonly MpscBoundedBuffer> writeBuffer; + + private readonly CacheMetrics metrics = new(); + + private readonly CmSketch cmSketch; + + private readonly LfuNodeList windowLru; + private readonly LfuNodeList probationLru; + private readonly LfuNodeList protectedLru; + + private readonly LfuCapacityPartition capacity; + + private readonly DrainStatus drainStatus = new(); + private readonly object maintenanceLock = new(); + + private readonly IScheduler scheduler; + private readonly Action drainBuffers; + private readonly LfuNode[] drainBuffer; - - /// - /// Initializes a new instance of the ConcurrentLfu class with the specified capacity. - /// - /// The capacity. - public ConcurrentLfu(int capacity) - : this(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default) - { - } - - /// - /// Initializes a new instance of the ConcurrentLfu class with the specified concurrencyLevel, capacity, scheduler, equality comparer and buffer size. - /// - /// The concurrency level. - /// The capacity. - /// The scheduler. - /// The equality comparer. - public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) - { - int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); - this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer); - - // cap concurrency at proc count * 2 - int readStripes = Math.Min(BitOps.CeilingPowerOfTwo(concurrencyLevel), BitOps.CeilingPowerOfTwo(Environment.ProcessorCount * 2)); - this.readBuffer = new StripedMpscBuffer>(readStripes, DefaultBufferSize); - - // Cap the write buffer to the cache size, or 128. Whichever is smaller. - int writeBufferSize = Math.Min(BitOps.CeilingPowerOfTwo(capacity), 128); - this.writeBuffer = new MpscBoundedBuffer>(writeBufferSize); - - this.cmSketch = new CmSketch(capacity, comparer); - this.windowLru = new LfuNodeList(); - this.probationLru = new LfuNodeList(); - this.protectedLru = new LfuNodeList(); - - this.capacity = new LfuCapacityPartition(capacity); - - this.scheduler = scheduler; - this.drainBuffers = () => this.DrainBuffers(); - - this.drainBuffer = new LfuNode[this.readBuffer.Capacity]; - } - - // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ - /// - public int Count => this.dictionary.Skip(0).Count(); - - /// - public int Capacity => this.capacity.Capacity; - - /// - public Optional Metrics => new(this.metrics); - - /// - public Optional> Events => Optional>.None(); - - /// - public CachePolicy Policy => new(new Optional(this), Optional.None()); - - /// - public ICollection Keys => this.dictionary.Keys; - - /// - /// Gets the scheduler. - /// - public IScheduler Scheduler => scheduler; - - /// - public void AddOrUpdate(K key, V value) - { - while (true) - { - if (TryUpdate(key, value)) - { - return; - } - - var node = new LfuNode(key, value); - if (this.dictionary.TryAdd(key, node)) - { - AfterWrite(node); - return; - } - } - } - - /// - public void Clear() - { - this.Trim(this.Count); - - lock (maintenanceLock) - { - this.cmSketch.Clear(); - this.readBuffer.Clear(); - this.writeBuffer.Clear(); - } - } - - /// - /// Trim the specified number of items from the cache. - /// - /// The number of items to remove. - public void Trim(int itemCount) - { - itemCount = Math.Min(itemCount, this.Count); - var candidates = new List>(itemCount); - - // TODO: this is LRU order eviction, Caffeine is based on frequency - lock (maintenanceLock) - { - // flush all buffers - Maintenance(); - - // walk in lru order, get itemCount keys to evict - TakeCandidatesInLruOrder(this.probationLru, candidates, itemCount); - TakeCandidatesInLruOrder(this.protectedLru, candidates, itemCount); - TakeCandidatesInLruOrder(this.windowLru, candidates, itemCount); - } - -#if NET6_0_OR_GREATER + + /// + /// Initializes a new instance of the ConcurrentLfu class with the specified capacity. + /// + /// The capacity. + public ConcurrentLfu(int capacity) + : this(Defaults.ConcurrencyLevel, capacity, new ThreadPoolScheduler(), EqualityComparer.Default) + { + } + + /// + /// Initializes a new instance of the ConcurrentLfu class with the specified concurrencyLevel, capacity, scheduler, equality comparer and buffer size. + /// + /// The concurrency level. + /// The capacity. + /// The scheduler. + /// The equality comparer. + public ConcurrentLfu(int concurrencyLevel, int capacity, IScheduler scheduler, IEqualityComparer comparer) + { + int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); + this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer); + + // cap concurrency at proc count * 2 + int readStripes = Math.Min(BitOps.CeilingPowerOfTwo(concurrencyLevel), BitOps.CeilingPowerOfTwo(Environment.ProcessorCount * 2)); + this.readBuffer = new StripedMpscBuffer>(readStripes, DefaultBufferSize); + + // Cap the write buffer to the cache size, or 128. Whichever is smaller. + int writeBufferSize = Math.Min(BitOps.CeilingPowerOfTwo(capacity), 128); + this.writeBuffer = new MpscBoundedBuffer>(writeBufferSize); + + this.cmSketch = new CmSketch(capacity, comparer); + this.windowLru = new LfuNodeList(); + this.probationLru = new LfuNodeList(); + this.protectedLru = new LfuNodeList(); + + this.capacity = new LfuCapacityPartition(capacity); + + this.scheduler = scheduler; + this.drainBuffers = () => this.DrainBuffers(); + + this.drainBuffer = new LfuNode[this.readBuffer.Capacity]; + } + + // No lock count: https://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/ + /// + public int Count => this.dictionary.Skip(0).Count(); + + /// + public int Capacity => this.capacity.Capacity; + + /// + public Optional Metrics => new(this.metrics); + + /// + public Optional> Events => Optional>.None(); + + /// + public CachePolicy Policy => new(new Optional(this), Optional.None()); + + /// + public ICollection Keys => this.dictionary.Keys; + + /// + /// Gets the scheduler. + /// + public IScheduler Scheduler => scheduler; + + /// + public void AddOrUpdate(K key, V value) + { + while (true) + { + if (TryUpdate(key, value)) + { + return; + } + + var node = new LfuNode(key, value); + if (this.dictionary.TryAdd(key, node)) + { + AfterWrite(node); + return; + } + } + } + + /// + public void Clear() + { + this.Trim(this.Count); + + lock (maintenanceLock) + { + this.cmSketch.Clear(); + this.readBuffer.Clear(); + this.writeBuffer.Clear(); + } + } + + /// + /// Trim the specified number of items from the cache. + /// + /// The number of items to remove. + public void Trim(int itemCount) + { + itemCount = Math.Min(itemCount, this.Count); + var candidates = new List>(itemCount); + + // TODO: this is LRU order eviction, Caffeine is based on frequency + lock (maintenanceLock) + { + // flush all buffers + Maintenance(); + + // walk in lru order, get itemCount keys to evict + TakeCandidatesInLruOrder(this.probationLru, candidates, itemCount); + TakeCandidatesInLruOrder(this.protectedLru, candidates, itemCount); + TakeCandidatesInLruOrder(this.windowLru, candidates, itemCount); + } + +#if NET6_0_OR_GREATER foreach (var candidate in CollectionsMarshal.AsSpan(candidates)) -#else - foreach (var candidate in candidates) -#endif - { - this.TryRemove(candidate.Key); - } - } - - private bool TryAdd(K key, V value) - { - var node = new LfuNode(key, value); - - if (this.dictionary.TryAdd(key, node)) - { - AfterWrite(node); - return true; - } - - Disposer.Dispose(node.Value); - return false; - } - - /// - public V GetOrAdd(K key, Func valueFactory) - { - while (true) - { - if (this.TryGet(key, out V value)) - { - return value; - } - - value = valueFactory(key); - if (this.TryAdd(key, value)) - { - return value; - } - } - } - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to generate a value for the key. - /// An argument value to pass into valueFactory. - /// The value for the key. This will be either the existing value for the key if the key is already - /// in the cache, or the new value if the key was not in the cache. - public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) - { - while (true) - { - if (this.TryGet(key, out V value)) - { - return value; - } - - value = valueFactory(key, factoryArgument); - if (this.TryAdd(key, value)) - { - return value; - } - } - } - - /// - public async ValueTask GetOrAddAsync(K key, Func> valueFactory) - { - while (true) - { - if (this.TryGet(key, out V value)) - { - return value; - } - - value = await valueFactory(key).ConfigureAwait(false); - if (this.TryAdd(key, value)) - { - return value; - } - } - } - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// An argument value to pass into valueFactory. - /// A task that represents the asynchronous GetOrAdd operation. - public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) - { - while (true) - { - if (this.TryGet(key, out V value)) - { - return value; - } - - value = await valueFactory(key, factoryArgument).ConfigureAwait(false); - if (this.TryAdd(key, value)) - { - return value; - } - } - } - - /// - public bool TryGet(K key, out V value) - { - if (this.dictionary.TryGetValue(key, out var node)) - { - bool delayable = this.readBuffer.TryAdd(node) != BufferStatus.Full; - - if (this.drainStatus.ShouldDrain(delayable)) - { - TryScheduleDrain(); - } - value = node.Value; - return true; - } - - this.metrics.requestMissCount.Increment(); - - value = default; - return false; +#else + foreach (var candidate in candidates) +#endif + { + this.TryRemove(candidate.Key); + } + } + + private bool TryAdd(K key, V value) + { + var node = new LfuNode(key, value); + + if (this.dictionary.TryAdd(key, node)) + { + AfterWrite(node); + return true; + } + + Disposer.Dispose(node.Value); + return false; + } + + /// + public V GetOrAdd(K key, Func valueFactory) + { + while (true) + { + if (this.TryGet(key, out V value)) + { + return value; + } + + value = valueFactory(key); + if (this.TryAdd(key, value)) + { + return value; + } + } + } + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to generate a value for the key. + /// An argument value to pass into valueFactory. + /// The value for the key. This will be either the existing value for the key if the key is already + /// in the cache, or the new value if the key was not in the cache. + public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) + { + while (true) + { + if (this.TryGet(key, out V value)) + { + return value; + } + + value = valueFactory(key, factoryArgument); + if (this.TryAdd(key, value)) + { + return value; + } + } + } + + /// + public async ValueTask GetOrAddAsync(K key, Func> valueFactory) + { + while (true) + { + if (this.TryGet(key, out V value)) + { + return value; + } + + value = await valueFactory(key).ConfigureAwait(false); + if (this.TryAdd(key, value)) + { + return value; + } + } + } + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// An argument value to pass into valueFactory. + /// A task that represents the asynchronous GetOrAdd operation. + public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) + { + while (true) + { + if (this.TryGet(key, out V value)) + { + return value; + } + + value = await valueFactory(key, factoryArgument).ConfigureAwait(false); + if (this.TryAdd(key, value)) + { + return value; + } + } + } + + /// + public bool TryGet(K key, out V value) + { + if (this.dictionary.TryGetValue(key, out var node)) + { + bool delayable = this.readBuffer.TryAdd(node) != BufferStatus.Full; + + if (this.drainStatus.ShouldDrain(delayable)) + { + TryScheduleDrain(); + } + value = node.Value; + return true; + } + + this.metrics.requestMissCount.Increment(); + + value = default; + return false; } /// @@ -332,8 +332,8 @@ public bool TryGet(K key, out V value) /// /// The item to remove. /// true if the item was removed successfully; otherwise, false. - public bool TryRemove(KeyValuePair item) - { + public bool TryRemove(KeyValuePair item) + { if (this.dictionary.TryGetValue(item.Key, out var node)) { if (EqualityComparer.Default.Equals(node.Value, item.Value)) @@ -348,13 +348,13 @@ public bool TryRemove(KeyValuePair item) #endif { node.WasRemoved = true; - AfterWrite(node); - return true; - } - } - } - - return false; + AfterWrite(node); + return true; + } + } + } + + return false; } /// @@ -362,340 +362,340 @@ public bool TryRemove(KeyValuePair item) /// /// The key of the element to remove. /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. - /// true if the object was removed successfully; otherwise, false. - public bool TryRemove(K key, out V value) - { - if (this.dictionary.TryRemove(key, out var node)) - { - node.WasRemoved = true; - AfterWrite(node); - value = node.Value; - return true; - } - - value = default; - return false; - } - - /// - public bool TryRemove(K key) - { - return this.TryRemove(key, out var _); - } - - /// - public bool TryUpdate(K key, V value) - { - if (this.dictionary.TryGetValue(key, out var node)) - { - node.Value = value; - - // It's ok for this to be lossy, since the node is already tracked - // and we will just lose ordering/hit count, but not orphan the node. - this.writeBuffer.TryAdd(node); - TryScheduleDrain(); - return true; - } - - return false; - } - - /// - /// Synchronously perform all pending policy maintenance. Drain the read and write buffers then - /// use the eviction policy to preserve bounded size and remove expired items. - /// - /// - /// Note: maintenance is automatically performed asynchronously immediately following a read or write. - /// It is not necessary to call this method, is provided purely to enable tests to reach a consistent state. - /// - public void DoMaintenance() - { - DrainBuffers(); - } - - /// Returns an enumerator that iterates through the cache. - /// An enumerator for the cache. - /// - /// The enumerator returned from the cache is safe to use concurrently with - /// reads and writes, however it does not represent a moment-in-time snapshot. - /// The contents exposed through the enumerator may contain modifications - /// made after was called. - /// - public IEnumerator> GetEnumerator() - { - foreach (var kvp in this.dictionary) - { - yield return new KeyValuePair(kvp.Key, kvp.Value.Value); - } - } - - private static void TakeCandidatesInLruOrder(LfuNodeList lru, List> candidates, int itemCount) - { - var curr = lru.First; - - while (candidates.Count < itemCount && curr != null) - { - // LRUs can contain items that are already removed, skip those + /// true if the object was removed successfully; otherwise, false. + public bool TryRemove(K key, out V value) + { + if (this.dictionary.TryRemove(key, out var node)) + { + node.WasRemoved = true; + AfterWrite(node); + value = node.Value; + return true; + } + + value = default; + return false; + } + + /// + public bool TryRemove(K key) + { + return this.TryRemove(key, out var _); + } + + /// + public bool TryUpdate(K key, V value) + { + if (this.dictionary.TryGetValue(key, out var node)) + { + node.Value = value; + + // It's ok for this to be lossy, since the node is already tracked + // and we will just lose ordering/hit count, but not orphan the node. + this.writeBuffer.TryAdd(node); + TryScheduleDrain(); + return true; + } + + return false; + } + + /// + /// Synchronously perform all pending policy maintenance. Drain the read and write buffers then + /// use the eviction policy to preserve bounded size and remove expired items. + /// + /// + /// Note: maintenance is automatically performed asynchronously immediately following a read or write. + /// It is not necessary to call this method, is provided purely to enable tests to reach a consistent state. + /// + public void DoMaintenance() + { + DrainBuffers(); + } + + /// Returns an enumerator that iterates through the cache. + /// An enumerator for the cache. + /// + /// The enumerator returned from the cache is safe to use concurrently with + /// reads and writes, however it does not represent a moment-in-time snapshot. + /// The contents exposed through the enumerator may contain modifications + /// made after was called. + /// + public IEnumerator> GetEnumerator() + { + foreach (var kvp in this.dictionary) + { + yield return new KeyValuePair(kvp.Key, kvp.Value.Value); + } + } + + private static void TakeCandidatesInLruOrder(LfuNodeList lru, List> candidates, int itemCount) + { + var curr = lru.First; + + while (candidates.Count < itemCount && curr != null) + { + // LRUs can contain items that are already removed, skip those if (!curr.WasRemoved) - { - candidates.Add(curr); - } - - curr = curr.Next; - } - } - - private void AfterWrite(LfuNode node) - { - for (int i = 0; i < MaxWriteBufferRetries; i++) - { - if (writeBuffer.TryAdd(node) == BufferStatus.Success) - { - ScheduleAfterWrite(); - return; - } - - TryScheduleDrain(); - } - - lock (this.maintenanceLock) - { - // aggressively try to exit the lock early before doing full maintenance - var status = BufferStatus.Contended; - while (status != BufferStatus.Full) - { - status = writeBuffer.TryAdd(node); - - if (status == BufferStatus.Success) - { - ScheduleAfterWrite(); - return; - } - } - - // if the write was dropped from the buffer, explicitly pass it to maintenance - Maintenance(node); - } - } - - private void ScheduleAfterWrite() - { - var spinner = new SpinWait(); - int status = this.drainStatus.NonVolatileRead(); - while (true) - { - switch (status) - { - case DrainStatus.Idle: - this.drainStatus.Cas(DrainStatus.Idle, DrainStatus.Required); - TryScheduleDrain(); - return; - case DrainStatus.Required: - TryScheduleDrain(); - return; - case DrainStatus.ProcessingToIdle: - if (this.drainStatus.Cas(DrainStatus.ProcessingToIdle, DrainStatus.ProcessingToRequired)) - { - return; - } - status = this.drainStatus.VolatileRead(); - break; - case DrainStatus.ProcessingToRequired: - return; - } - spinner.SpinOnce(); - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return ((ConcurrentLfu)this).GetEnumerator(); - } - - private void TryScheduleDrain() - { + { + candidates.Add(curr); + } + + curr = curr.Next; + } + } + + private void AfterWrite(LfuNode node) + { + for (int i = 0; i < MaxWriteBufferRetries; i++) + { + if (writeBuffer.TryAdd(node) == BufferStatus.Success) + { + ScheduleAfterWrite(); + return; + } + + TryScheduleDrain(); + } + + lock (this.maintenanceLock) + { + // aggressively try to exit the lock early before doing full maintenance + var status = BufferStatus.Contended; + while (status != BufferStatus.Full) + { + status = writeBuffer.TryAdd(node); + + if (status == BufferStatus.Success) + { + ScheduleAfterWrite(); + return; + } + } + + // if the write was dropped from the buffer, explicitly pass it to maintenance + Maintenance(node); + } + } + + private void ScheduleAfterWrite() + { + var spinner = new SpinWait(); + int status = this.drainStatus.NonVolatileRead(); + while (true) + { + switch (status) + { + case DrainStatus.Idle: + this.drainStatus.Cas(DrainStatus.Idle, DrainStatus.Required); + TryScheduleDrain(); + return; + case DrainStatus.Required: + TryScheduleDrain(); + return; + case DrainStatus.ProcessingToIdle: + if (this.drainStatus.Cas(DrainStatus.ProcessingToIdle, DrainStatus.ProcessingToRequired)) + { + return; + } + status = this.drainStatus.VolatileRead(); + break; + case DrainStatus.ProcessingToRequired: + return; + } + spinner.SpinOnce(); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((ConcurrentLfu)this).GetEnumerator(); + } + + private void TryScheduleDrain() + { if (this.drainStatus.NonVolatileRead() >= DrainStatus.ProcessingToIdle) { - return; - } - - bool lockTaken = false; - try - { - Monitor.TryEnter(maintenanceLock, ref lockTaken); - - if (lockTaken) - { - int status = this.drainStatus.NonVolatileRead(); - - if (status >= DrainStatus.ProcessingToIdle) - { - return; - } - - this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle); - scheduler.Run(this.drainBuffers); - } - } - finally - { - if (lockTaken) - { - Monitor.Exit(maintenanceLock); - } - } - } - - private void DrainBuffers() - { - bool done = false; - - while (!done) - { - lock (maintenanceLock) - { - done = Maintenance(); - } - - // don't run continuous foreground maintenance - if (!scheduler.IsBackground) - { - done = true; - } - } - - if (this.drainStatus.VolatileRead() == DrainStatus.Required) - { - TryScheduleDrain(); - } - } - - private bool Maintenance(LfuNode droppedWrite = null) - { - this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle); - - // Note: this is only Span on .NET Core 3.1+, else this is no-op and it is still an array - var buffer = this.drainBuffer.AsSpanOrArray(); - - // extract to a buffer before doing book keeping work, ~2x faster - int readCount = readBuffer.DrainTo(buffer); - - for (int i = 0; i < readCount; i++) - { - this.cmSketch.Increment(buffer[i].Key); - } - - for (int i = 0; i < readCount; i++) - { - OnAccess(buffer[i]); - } - - int writeCount = this.writeBuffer.DrainTo(buffer.AsSpanOrSegment()); - - for (int i = 0; i < writeCount; i++) - { - OnWrite(buffer[i]); - } - - // we are done only when both buffers are empty - var done = readCount == 0 & writeCount == 0; - - if (droppedWrite != null) - { - OnWrite(droppedWrite); - done = true; - } - - EvictEntries(); - this.capacity.OptimizePartitioning(this.metrics, this.cmSketch.ResetSampleSize); - ReFitProtected(); - - // Reset to idle if either - // 1. We drained both input buffers (all work done) - // 2. or scheduler is foreground (since don't run continuously on the foreground) - if ((done || !scheduler.IsBackground) && - (this.drainStatus.NonVolatileRead() != DrainStatus.ProcessingToIdle || - !this.drainStatus.Cas(DrainStatus.ProcessingToIdle, DrainStatus.Idle))) - { - this.drainStatus.NonVolatileWrite(DrainStatus.Required); - } - - return done; - } - - private void OnAccess(LfuNode node) - { - // there was a cache hit even if the item was removed or is not yet added. - this.metrics.requestHitCount++; - - // Node is added to read buffer while it is removed by maintenance, or it is read before it has been added. - if (node.list == null) - { - return; - } - - switch (node.Position) - { - case Position.Window: - this.windowLru.MoveToEnd(node); - break; - case Position.Probation: - PromoteProbation(node); - break; - case Position.Protected: - this.protectedLru.MoveToEnd(node); - break; - } - } - - private void OnWrite(LfuNode node) - { - // Nodes can be removed while they are in the write buffer, in which case they should - // not be added back into the LRU. - if (node.WasRemoved) - { - node.list?.Remove(node); - - if (!node.WasDeleted) - { - // if a write is in the buffer and is then removed in the buffer, it will enter OnWrite twice. - // we mark as deleted to avoid double counting/disposing it - this.metrics.evictedCount++; - Disposer.Dispose(node.Value); - node.WasDeleted = true; - } - - return; - } - - this.cmSketch.Increment(node.Key); - - // node can already be in one of the queues due to update - switch (node.Position) - { - case Position.Window: - if (node.list == null) - { - this.windowLru.AddLast(node); - } - else - { - this.windowLru.MoveToEnd(node); - this.metrics.updatedCount++; - } - break; - case Position.Probation: - PromoteProbation(node); - this.metrics.updatedCount++; - break; - case Position.Protected: - this.protectedLru.MoveToEnd(node); - this.metrics.updatedCount++; - break; - } - } - - private void PromoteProbation(LfuNode node) + return; + } + + bool lockTaken = false; + try + { + Monitor.TryEnter(maintenanceLock, ref lockTaken); + + if (lockTaken) + { + int status = this.drainStatus.NonVolatileRead(); + + if (status >= DrainStatus.ProcessingToIdle) + { + return; + } + + this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle); + scheduler.Run(this.drainBuffers); + } + } + finally + { + if (lockTaken) + { + Monitor.Exit(maintenanceLock); + } + } + } + + private void DrainBuffers() + { + bool done = false; + + while (!done) + { + lock (maintenanceLock) + { + done = Maintenance(); + } + + // don't run continuous foreground maintenance + if (!scheduler.IsBackground) + { + done = true; + } + } + + if (this.drainStatus.VolatileRead() == DrainStatus.Required) + { + TryScheduleDrain(); + } + } + + private bool Maintenance(LfuNode droppedWrite = null) + { + this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle); + + // Note: this is only Span on .NET Core 3.1+, else this is no-op and it is still an array + var buffer = this.drainBuffer.AsSpanOrArray(); + + // extract to a buffer before doing book keeping work, ~2x faster + int readCount = readBuffer.DrainTo(buffer); + + for (int i = 0; i < readCount; i++) + { + this.cmSketch.Increment(buffer[i].Key); + } + + for (int i = 0; i < readCount; i++) + { + OnAccess(buffer[i]); + } + + int writeCount = this.writeBuffer.DrainTo(buffer.AsSpanOrSegment()); + + for (int i = 0; i < writeCount; i++) + { + OnWrite(buffer[i]); + } + + // we are done only when both buffers are empty + var done = readCount == 0 & writeCount == 0; + + if (droppedWrite != null) + { + OnWrite(droppedWrite); + done = true; + } + + EvictEntries(); + this.capacity.OptimizePartitioning(this.metrics, this.cmSketch.ResetSampleSize); + ReFitProtected(); + + // Reset to idle if either + // 1. We drained both input buffers (all work done) + // 2. or scheduler is foreground (since don't run continuously on the foreground) + if ((done || !scheduler.IsBackground) && + (this.drainStatus.NonVolatileRead() != DrainStatus.ProcessingToIdle || + !this.drainStatus.Cas(DrainStatus.ProcessingToIdle, DrainStatus.Idle))) + { + this.drainStatus.NonVolatileWrite(DrainStatus.Required); + } + + return done; + } + + private void OnAccess(LfuNode node) + { + // there was a cache hit even if the item was removed or is not yet added. + this.metrics.requestHitCount++; + + // Node is added to read buffer while it is removed by maintenance, or it is read before it has been added. + if (node.list == null) + { + return; + } + + switch (node.Position) + { + case Position.Window: + this.windowLru.MoveToEnd(node); + break; + case Position.Probation: + PromoteProbation(node); + break; + case Position.Protected: + this.protectedLru.MoveToEnd(node); + break; + } + } + + private void OnWrite(LfuNode node) + { + // Nodes can be removed while they are in the write buffer, in which case they should + // not be added back into the LRU. + if (node.WasRemoved) + { + node.list?.Remove(node); + + if (!node.WasDeleted) + { + // if a write is in the buffer and is then removed in the buffer, it will enter OnWrite twice. + // we mark as deleted to avoid double counting/disposing it + this.metrics.evictedCount++; + Disposer.Dispose(node.Value); + node.WasDeleted = true; + } + + return; + } + + this.cmSketch.Increment(node.Key); + + // node can already be in one of the queues due to update + switch (node.Position) + { + case Position.Window: + if (node.list == null) + { + this.windowLru.AddLast(node); + } + else + { + this.windowLru.MoveToEnd(node); + this.metrics.updatedCount++; + } + break; + case Position.Probation: + PromoteProbation(node); + this.metrics.updatedCount++; + break; + case Position.Protected: + this.protectedLru.MoveToEnd(node); + this.metrics.updatedCount++; + break; + } + } + + private void PromoteProbation(LfuNode node) { if (node.list == null) { @@ -703,316 +703,316 @@ private void PromoteProbation(LfuNode node) return; } - this.probationLru.Remove(node); - this.protectedLru.AddLast(node); - node.Position = Position.Protected; - - // If the protected space exceeds its maximum, the LRU items are demoted to the probation space. - if (this.protectedLru.Count > this.capacity.Protected) - { - var demoted = this.protectedLru.First; - this.protectedLru.RemoveFirst(); - - demoted.Position = Position.Probation; - this.probationLru.AddLast(demoted); - } - } - - private void EvictEntries() - { - var candidate = EvictFromWindow(); - EvictFromMain(candidate); - } - - private LfuNode EvictFromWindow() - { - LfuNode first = null; - - while (this.windowLru.Count > this.capacity.Window) - { - var node = this.windowLru.First; - this.windowLru.RemoveFirst(); - - first ??= node; - - this.probationLru.AddLast(node); - node.Position = Position.Probation; - } - - return first; - } - - private ref struct EvictIterator - { - private readonly CmSketch sketch; - public LfuNode node; - public int freq; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public EvictIterator(CmSketch sketch, LfuNode node) - { - this.sketch = sketch; - this.node = node; - freq = node == null ? -1 : sketch.EstimateFrequency(node.Key); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Next() - { - node = node.Next; - - if (node != null) - { - freq = sketch.EstimateFrequency(node.Key); - } - } - } - - private void EvictFromMain(LfuNode candidateNode) - { - var victim = new EvictIterator(this.cmSketch, this.probationLru.First); // victims are LRU position in probation - var candidate = new EvictIterator(this.cmSketch, candidateNode); - - // first pass: admit candidates - while (this.windowLru.Count + this.probationLru.Count + this.protectedLru.Count > this.Capacity) - { - // bail when we run out of options - if (candidate.node == null | victim.node == null) - { - break; - } - - if (victim.node == candidate.node) - { - Evict(candidate.node); - break; - } - - // Evict the entry with the lowest frequency - if (candidate.freq > victim.freq) - { - var evictee = victim.node; - - // victim is initialized to first, and iterates forwards - victim.Next(); - candidate.Next(); - - Evict(evictee); - } - else - { - var evictee = candidate.node; - - // candidate is initialized to first cand, and iterates forwards - candidate.Next(); - - Evict(evictee); - } - } - - // 2nd pass: remove probation items in LRU order, evict lowest frequency - while (this.windowLru.Count + this.probationLru.Count + this.protectedLru.Count > this.Capacity) - { - var victim1 = this.probationLru.First; - var victim2 = victim1.Next; - - if (AdmitCandidate(victim1.Key, victim2.Key)) - { - Evict(victim2); - } - else - { - Evict(victim1); - } - } - } - - private bool AdmitCandidate(K candidateKey, K victimKey) - { - int victimFreq = this.cmSketch.EstimateFrequency(victimKey); - int candidateFreq = this.cmSketch.EstimateFrequency(candidateKey); - - //var (victimFreq, candidateFreq) = this.cmSketch.EstimateFrequency(victimKey, candidateKey); - - // TODO: random factor when candidate freq < 5 - return candidateFreq > victimFreq; - } - - private void Evict(LfuNode evictee) - { - this.dictionary.TryRemove(evictee.Key, out var _); - evictee.list.Remove(evictee); - Disposer.Dispose(evictee.Value); - this.metrics.evictedCount++; - } - - private void ReFitProtected() - { - // If hill climbing decreased protected, there may be too many items - // - demote overflow to probation. - while (this.protectedLru.Count > this.capacity.Protected) - { - var demoted = this.protectedLru.First; - this.protectedLru.RemoveFirst(); - - demoted.Position = Position.Probation; - this.probationLru.AddLast(demoted); - } - } - - [DebuggerDisplay("{Format(),nq}")] - private class DrainStatus - { - public const int Idle = 0; - public const int Required = 1; - public const int ProcessingToIdle = 2; - public const int ProcessingToRequired = 3; - - private PaddedInt drainStatus; // mutable struct, don't mark readonly - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ShouldDrain(bool delayable) - { - int status = this.NonVolatileRead(); - return status switch - { - Idle => !delayable, - Required => true, - ProcessingToIdle or ProcessingToRequired => false, - _ => false,// not reachable - }; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void VolatileWrite(int newStatus) - { - Volatile.Write(ref this.drainStatus.Value, newStatus); + this.probationLru.Remove(node); + this.protectedLru.AddLast(node); + node.Position = Position.Protected; + + // If the protected space exceeds its maximum, the LRU items are demoted to the probation space. + if (this.protectedLru.Count > this.capacity.Protected) + { + var demoted = this.protectedLru.First; + this.protectedLru.RemoveFirst(); + + demoted.Position = Position.Probation; + this.probationLru.AddLast(demoted); + } + } + + private void EvictEntries() + { + var candidate = EvictFromWindow(); + EvictFromMain(candidate); + } + + private LfuNode EvictFromWindow() + { + LfuNode first = null; + + while (this.windowLru.Count > this.capacity.Window) + { + var node = this.windowLru.First; + this.windowLru.RemoveFirst(); + + first ??= node; + + this.probationLru.AddLast(node); + node.Position = Position.Probation; + } + + return first; + } + + private ref struct EvictIterator + { + private readonly CmSketch sketch; + public LfuNode node; + public int freq; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public EvictIterator(CmSketch sketch, LfuNode node) + { + this.sketch = sketch; + this.node = node; + freq = node == null ? -1 : sketch.EstimateFrequency(node.Key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Next() + { + node = node.Next; + + if (node != null) + { + freq = sketch.EstimateFrequency(node.Key); + } + } + } + + private void EvictFromMain(LfuNode candidateNode) + { + var victim = new EvictIterator(this.cmSketch, this.probationLru.First); // victims are LRU position in probation + var candidate = new EvictIterator(this.cmSketch, candidateNode); + + // first pass: admit candidates + while (this.windowLru.Count + this.probationLru.Count + this.protectedLru.Count > this.Capacity) + { + // bail when we run out of options + if (candidate.node == null | victim.node == null) + { + break; + } + + if (victim.node == candidate.node) + { + Evict(candidate.node); + break; + } + + // Evict the entry with the lowest frequency + if (candidate.freq > victim.freq) + { + var evictee = victim.node; + + // victim is initialized to first, and iterates forwards + victim.Next(); + candidate.Next(); + + Evict(evictee); + } + else + { + var evictee = candidate.node; + + // candidate is initialized to first cand, and iterates forwards + candidate.Next(); + + Evict(evictee); + } + } + + // 2nd pass: remove probation items in LRU order, evict lowest frequency + while (this.windowLru.Count + this.probationLru.Count + this.protectedLru.Count > this.Capacity) + { + var victim1 = this.probationLru.First; + var victim2 = victim1.Next; + + if (AdmitCandidate(victim1.Key, victim2.Key)) + { + Evict(victim2); + } + else + { + Evict(victim1); + } + } + } + + private bool AdmitCandidate(K candidateKey, K victimKey) + { + int victimFreq = this.cmSketch.EstimateFrequency(victimKey); + int candidateFreq = this.cmSketch.EstimateFrequency(candidateKey); + + //var (victimFreq, candidateFreq) = this.cmSketch.EstimateFrequency(victimKey, candidateKey); + + // TODO: random factor when candidate freq < 5 + return candidateFreq > victimFreq; + } + + private void Evict(LfuNode evictee) + { + this.dictionary.TryRemove(evictee.Key, out var _); + evictee.list.Remove(evictee); + Disposer.Dispose(evictee.Value); + this.metrics.evictedCount++; + } + + private void ReFitProtected() + { + // If hill climbing decreased protected, there may be too many items + // - demote overflow to probation. + while (this.protectedLru.Count > this.capacity.Protected) + { + var demoted = this.protectedLru.First; + this.protectedLru.RemoveFirst(); + + demoted.Position = Position.Probation; + this.probationLru.AddLast(demoted); + } + } + + [DebuggerDisplay("{Format(),nq}")] + private class DrainStatus + { + public const int Idle = 0; + public const int Required = 1; + public const int ProcessingToIdle = 2; + public const int ProcessingToRequired = 3; + + private PaddedInt drainStatus; // mutable struct, don't mark readonly + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ShouldDrain(bool delayable) + { + int status = this.NonVolatileRead(); + return status switch + { + Idle => !delayable, + Required => true, + ProcessingToIdle or ProcessingToRequired => false, + _ => false,// not reachable + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void VolatileWrite(int newStatus) + { + Volatile.Write(ref this.drainStatus.Value, newStatus); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void NonVolatileWrite(int newStatus) - { - this.drainStatus.Value = newStatus; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Cas(int oldStatus, int newStatus) - { - return Interlocked.CompareExchange(ref this.drainStatus.Value, newStatus, oldStatus) == oldStatus; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int VolatileRead() - { - return Volatile.Read(ref this.drainStatus.Value); + public void NonVolatileWrite(int newStatus) + { + this.drainStatus.Value = newStatus; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int NonVolatileRead() - { - return this.drainStatus.Value; - } - - [ExcludeFromCodeCoverage] - internal string Format() - { - switch (this.drainStatus.Value) - { - case Idle: - return "Idle"; - case Required: - return "Required"; - case ProcessingToIdle: - return "ProcessingToIdle"; - case ProcessingToRequired: - return "ProcessingToRequired"; ; - } - - return "Invalid state"; - } - } - - [DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Upd = {Updated}, Evict = {Evicted}")] - internal class CacheMetrics : ICacheMetrics - { - public long requestHitCount; - public Counter requestMissCount = new(); - public long updatedCount; - public long evictedCount; - - public double HitRatio => (double)requestHitCount / (double)Total; - - public long Total => requestHitCount + requestMissCount.Count(); - - public long Hits => requestHitCount; - - public long Misses => requestMissCount.Count(); - - public long Updated => updatedCount; - - public long Evicted => evictedCount; - } - -#if DEBUG - /// - /// Format the LFU as a string by converting all the keys to strings. - /// - /// The LFU formatted as a string. - public string FormatLfuString() - { - var sb = new StringBuilder(); - - sb.Append("W ["); - sb.Append(string.Join(",", this.windowLru.Select(n => n.Key.ToString()))); - sb.Append("] Protected ["); - sb.Append(string.Join(",", this.protectedLru.Select(n => n.Key.ToString()))); - sb.Append("] Probation ["); - sb.Append(string.Join(",", this.probationLru.Select(n => n.Key.ToString()))); - sb.Append("]"); - - return sb.ToString(); - } -#endif - - [ExcludeFromCodeCoverage] - internal class LfuDebugView - { - private readonly ConcurrentLfu lfu; - - public LfuDebugView(ConcurrentLfu lfu) - { - this.lfu = lfu; - } - - public string Maintenance => lfu.drainStatus.Format(); - - public ICacheMetrics Metrics => lfu.metrics; - - public StripedMpscBuffer> ReadBuffer => this.lfu.readBuffer; - - public MpscBoundedBuffer> WriteBuffer => this.lfu.writeBuffer; - - public KeyValuePair[] Items - { - get - { - var items = new KeyValuePair[lfu.Count]; - - int index = 0; - foreach (var kvp in lfu) - { - items[index++] = kvp; - } - return items; - } - } - } - } - - // Explicit layout cannot be a generic class member - [StructLayout(LayoutKind.Explicit, Size = 2 * Padding.CACHE_LINE_SIZE)] - internal struct PaddedInt - { - [FieldOffset(1 * Padding.CACHE_LINE_SIZE)] public int Value; - } -} + public bool Cas(int oldStatus, int newStatus) + { + return Interlocked.CompareExchange(ref this.drainStatus.Value, newStatus, oldStatus) == oldStatus; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int VolatileRead() + { + return Volatile.Read(ref this.drainStatus.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int NonVolatileRead() + { + return this.drainStatus.Value; + } + + [ExcludeFromCodeCoverage] + internal string Format() + { + switch (this.drainStatus.Value) + { + case Idle: + return "Idle"; + case Required: + return "Required"; + case ProcessingToIdle: + return "ProcessingToIdle"; + case ProcessingToRequired: + return "ProcessingToRequired"; ; + } + + return "Invalid state"; + } + } + + [DebuggerDisplay("Hit = {Hits}, Miss = {Misses}, Upd = {Updated}, Evict = {Evicted}")] + internal class CacheMetrics : ICacheMetrics + { + public long requestHitCount; + public Counter requestMissCount = new(); + public long updatedCount; + public long evictedCount; + + public double HitRatio => (double)requestHitCount / (double)Total; + + public long Total => requestHitCount + requestMissCount.Count(); + + public long Hits => requestHitCount; + + public long Misses => requestMissCount.Count(); + + public long Updated => updatedCount; + + public long Evicted => evictedCount; + } + +#if DEBUG + /// + /// Format the LFU as a string by converting all the keys to strings. + /// + /// The LFU formatted as a string. + public string FormatLfuString() + { + var sb = new StringBuilder(); + + sb.Append("W ["); + sb.Append(string.Join(",", this.windowLru.Select(n => n.Key.ToString()))); + sb.Append("] Protected ["); + sb.Append(string.Join(",", this.protectedLru.Select(n => n.Key.ToString()))); + sb.Append("] Probation ["); + sb.Append(string.Join(",", this.probationLru.Select(n => n.Key.ToString()))); + sb.Append("]"); + + return sb.ToString(); + } +#endif + + [ExcludeFromCodeCoverage] + internal class LfuDebugView + { + private readonly ConcurrentLfu lfu; + + public LfuDebugView(ConcurrentLfu lfu) + { + this.lfu = lfu; + } + + public string Maintenance => lfu.drainStatus.Format(); + + public ICacheMetrics Metrics => lfu.metrics; + + public StripedMpscBuffer> ReadBuffer => this.lfu.readBuffer; + + public MpscBoundedBuffer> WriteBuffer => this.lfu.writeBuffer; + + public KeyValuePair[] Items + { + get + { + var items = new KeyValuePair[lfu.Count]; + + int index = 0; + foreach (var kvp in lfu) + { + items[index++] = kvp; + } + return items; + } + } + } + } + + // Explicit layout cannot be a generic class member + [StructLayout(LayoutKind.Explicit, Size = 2 * Padding.CACHE_LINE_SIZE)] + internal struct PaddedInt + { + [FieldOffset(1 * Padding.CACHE_LINE_SIZE)] public int Value; + } +} diff --git a/BitFaster.Caching/Lru/ClassicLru.cs b/BitFaster.Caching/Lru/ClassicLru.cs index 1216f330..bb69b33a 100644 --- a/BitFaster.Caching/Lru/ClassicLru.cs +++ b/BitFaster.Caching/Lru/ClassicLru.cs @@ -1,250 +1,250 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace BitFaster.Caching.Lru -{ - /// - /// LRU implementation where Lookup operations are backed by a ConcurrentDictionary and the LRU list is protected - /// by a global lock. All list operations performed within the lock are fast O(1) operations. - /// - /// - /// Due to the lock protecting list operations, this class may suffer lock contention under heavy load. - /// - /// The type of the key - /// The type of the value - public sealed class ClassicLru : ICache, IAsyncCache, IBoundedPolicy, IEnumerable> - { - private readonly int capacity; - private readonly ConcurrentDictionary> dictionary; - private readonly LinkedList linkedList = new(); - - private readonly CacheMetrics metrics = new(); - private readonly CachePolicy policy; - - /// - /// Initializes a new instance of the ClassicLru class with the specified capacity. - /// - /// - public ClassicLru(int capacity) - : this(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default) - { - } - - /// - /// Initializes a new instance of the ClassicLru class with the specified concurrencyLevel, capacity and equality comparer. - /// - /// The concurrency level. - /// The capacity. - /// The key comparer - /// - /// - public ClassicLru(int concurrencyLevel, int capacity, IEqualityComparer comparer) - { - if (capacity < 3) - Throw.ArgOutOfRange(nameof(capacity), "Capacity must be greater than or equal to 3."); - - if (comparer == null) - Throw.ArgNull(ExceptionArgument.comparer); - - this.capacity = capacity; - int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); - this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer); - this.policy = new CachePolicy(new Optional(this), Optional.None()); - } - - /// - public int Count => this.linkedList.Count; - - /// - public int Capacity => this.capacity; - - /// - public Optional Metrics => new(this.metrics); - - /// - public Optional> Events => Optional>.None(); - - /// - public CachePolicy Policy => this.policy; - - /// - /// Gets a collection containing the keys in the cache. - /// - public ICollection Keys => this.dictionary.Keys; - - /// Returns an enumerator that iterates through the cache. - /// An enumerator for the cache. - /// - /// The enumerator returned from the cache is safe to use concurrently with - /// reads and writes, however it does not represent a moment-in-time snapshot. - /// The contents exposed through the enumerator may contain modifications - /// made after was called. - /// - public IEnumerator> GetEnumerator() - { - foreach (var kvp in this.dictionary) - { - yield return new KeyValuePair(kvp.Key, kvp.Value.Value.Value); - } - } - - /// - public bool TryGet(K key, out V value) - { - Interlocked.Increment(ref this.metrics.requestTotalCount); - - if (dictionary.TryGetValue(key, out var node)) - { - LockAndMoveToEnd(node); - Interlocked.Increment(ref this.metrics.requestHitCount); - value = node.Value.Value; - return true; - } - - value = default; - return false; - } - - private bool TryAdd(K key, V value) - { - var node = new LinkedListNode(new LruItem(key, value)); - - if (this.dictionary.TryAdd(key, node)) - { - LinkedListNode first = null; - - lock (this.linkedList) - { - if (linkedList.Count >= capacity) - { - first = linkedList.First; - linkedList.RemoveFirst(); - } - - linkedList.AddLast(node); - } - - // Remove from the dictionary outside the lock. This means that the dictionary at this moment - // contains an item that is not in the linked list. If another thread fetches this item, - // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an - // item just as it was about to move to the back of the LRU list and be preserved. The next request - // for the same key will be a miss. Dictionary and list are eventually consistent. - // However, all operations inside the lock are extremely fast, so contention is minimized. - if (first != null) - { - dictionary.TryRemove(first.Value.Key, out var removed); - - Interlocked.Increment(ref this.metrics.evictedCount); - Disposer.Dispose(removed.Value.Value); - } - - return true; - } - - return false; - } - - /// - public V GetOrAdd(K key, Func valueFactory) - { - if (this.TryGet(key, out var value)) - { - return value; - } - - value = valueFactory(key); - - if (TryAdd(key, value)) - { - return value; - } - - return this.GetOrAdd(key, valueFactory); - } - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to generate a value for the key. - /// An argument value to pass into valueFactory. - /// The value for the key. This will be either the existing value for the key if the key is already - /// in the cache, or the new value if the key was not in the cache. - public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) - { - if (this.TryGet(key, out var value)) - { - return value; - } - - value = valueFactory(key, factoryArgument); - - if (TryAdd(key, value)) - { - return value; - } - - return this.GetOrAdd(key, valueFactory, factoryArgument); - } - - /// - public async ValueTask GetOrAddAsync(K key, Func> valueFactory) - { - if (this.TryGet(key, out var value)) - { - return value; - } - - value = await valueFactory(key).ConfigureAwait(false); - - if (TryAdd(key, value)) - { - return value; - } - - return await this.GetOrAddAsync(key, valueFactory).ConfigureAwait(false); - } - - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The type of an argument to pass into valueFactory. - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// An argument value to pass into valueFactory. - /// A task that represents the asynchronous GetOrAdd operation. - public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) - { - if (this.TryGet(key, out var value)) - { - return value; - } - - value = await valueFactory(key, factoryArgument).ConfigureAwait(false); - - if (TryAdd(key, value)) - { - return value; - } - - return await this.GetOrAddAsync(key, valueFactory, factoryArgument).ConfigureAwait(false); +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru +{ + /// + /// LRU implementation where Lookup operations are backed by a ConcurrentDictionary and the LRU list is protected + /// by a global lock. All list operations performed within the lock are fast O(1) operations. + /// + /// + /// Due to the lock protecting list operations, this class may suffer lock contention under heavy load. + /// + /// The type of the key + /// The type of the value + public sealed class ClassicLru : ICache, IAsyncCache, IBoundedPolicy, IEnumerable> + { + private readonly int capacity; + private readonly ConcurrentDictionary> dictionary; + private readonly LinkedList linkedList = new(); + + private readonly CacheMetrics metrics = new(); + private readonly CachePolicy policy; + + /// + /// Initializes a new instance of the ClassicLru class with the specified capacity. + /// + /// + public ClassicLru(int capacity) + : this(Defaults.ConcurrencyLevel, capacity, EqualityComparer.Default) + { + } + + /// + /// Initializes a new instance of the ClassicLru class with the specified concurrencyLevel, capacity and equality comparer. + /// + /// The concurrency level. + /// The capacity. + /// The key comparer + /// + /// + public ClassicLru(int concurrencyLevel, int capacity, IEqualityComparer comparer) + { + if (capacity < 3) + Throw.ArgOutOfRange(nameof(capacity), "Capacity must be greater than or equal to 3."); + + if (comparer == null) + Throw.ArgNull(ExceptionArgument.comparer); + + this.capacity = capacity; + int dictionaryCapacity = ConcurrentDictionarySize.Estimate(capacity); + this.dictionary = new ConcurrentDictionary>(concurrencyLevel, dictionaryCapacity, comparer); + this.policy = new CachePolicy(new Optional(this), Optional.None()); + } + + /// + public int Count => this.linkedList.Count; + + /// + public int Capacity => this.capacity; + + /// + public Optional Metrics => new(this.metrics); + + /// + public Optional> Events => Optional>.None(); + + /// + public CachePolicy Policy => this.policy; + + /// + /// Gets a collection containing the keys in the cache. + /// + public ICollection Keys => this.dictionary.Keys; + + /// Returns an enumerator that iterates through the cache. + /// An enumerator for the cache. + /// + /// The enumerator returned from the cache is safe to use concurrently with + /// reads and writes, however it does not represent a moment-in-time snapshot. + /// The contents exposed through the enumerator may contain modifications + /// made after was called. + /// + public IEnumerator> GetEnumerator() + { + foreach (var kvp in this.dictionary) + { + yield return new KeyValuePair(kvp.Key, kvp.Value.Value.Value); + } + } + + /// + public bool TryGet(K key, out V value) + { + Interlocked.Increment(ref this.metrics.requestTotalCount); + + if (dictionary.TryGetValue(key, out var node)) + { + LockAndMoveToEnd(node); + Interlocked.Increment(ref this.metrics.requestHitCount); + value = node.Value.Value; + return true; + } + + value = default; + return false; + } + + private bool TryAdd(K key, V value) + { + var node = new LinkedListNode(new LruItem(key, value)); + + if (this.dictionary.TryAdd(key, node)) + { + LinkedListNode first = null; + + lock (this.linkedList) + { + if (linkedList.Count >= capacity) + { + first = linkedList.First; + linkedList.RemoveFirst(); + } + + linkedList.AddLast(node); + } + + // Remove from the dictionary outside the lock. This means that the dictionary at this moment + // contains an item that is not in the linked list. If another thread fetches this item, + // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an + // item just as it was about to move to the back of the LRU list and be preserved. The next request + // for the same key will be a miss. Dictionary and list are eventually consistent. + // However, all operations inside the lock are extremely fast, so contention is minimized. + if (first != null) + { + dictionary.TryRemove(first.Value.Key, out var removed); + + Interlocked.Increment(ref this.metrics.evictedCount); + Disposer.Dispose(removed.Value.Value); + } + + return true; + } + + return false; + } + + /// + public V GetOrAdd(K key, Func valueFactory) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + value = valueFactory(key); + + if (TryAdd(key, value)) + { + return value; + } + + return this.GetOrAdd(key, valueFactory); + } + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to generate a value for the key. + /// An argument value to pass into valueFactory. + /// The value for the key. This will be either the existing value for the key if the key is already + /// in the cache, or the new value if the key was not in the cache. + public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + value = valueFactory(key, factoryArgument); + + if (TryAdd(key, value)) + { + return value; + } + + return this.GetOrAdd(key, valueFactory, factoryArgument); + } + + /// + public async ValueTask GetOrAddAsync(K key, Func> valueFactory) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + value = await valueFactory(key).ConfigureAwait(false); + + if (TryAdd(key, value)) + { + return value; + } + + return await this.GetOrAddAsync(key, valueFactory).ConfigureAwait(false); + } + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// An argument value to pass into valueFactory. + /// A task that represents the asynchronous GetOrAdd operation. + public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + value = await valueFactory(key, factoryArgument).ConfigureAwait(false); + + if (TryAdd(key, value)) + { + return value; + } + + return await this.GetOrAddAsync(key, valueFactory, factoryArgument).ConfigureAwait(false); } /// /// Attempts to remove the specified key value pair. /// /// The item to remove. - /// true if the item was removed successfully; otherwise, false. - public bool TryRemove(KeyValuePair item) - { + /// true if the item was removed successfully; otherwise, false. + public bool TryRemove(KeyValuePair item) + { if (this.dictionary.TryGetValue(item.Key, out var node)) { if (EqualityComparer.Default.Equals(node.Value.Value, item.Value)) @@ -258,13 +258,13 @@ public bool TryRemove(KeyValuePair item) if (((ICollection>>)this.dictionary).Remove(kvp)) #endif { - OnRemove(node); - return true; + OnRemove(node); + return true; } } } - return false; + return false; } /// @@ -272,25 +272,25 @@ public bool TryRemove(KeyValuePair item) /// /// The key of the element to remove. /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. - /// true if the object was removed successfully; otherwise, false. - public bool TryRemove(K key, out V value) - { - if (dictionary.TryRemove(key, out var node)) - { - OnRemove(node); - value = node.Value.Value; - return true; + /// true if the object was removed successfully; otherwise, false. + public bool TryRemove(K key, out V value) + { + if (dictionary.TryRemove(key, out var node)) + { + OnRemove(node); + value = node.Value.Value; + return true; } - value = default; + value = default; return false; - } - - /// - public bool TryRemove(K key) - { - return TryRemove(key, out var _); + } + + /// + public bool TryRemove(K key) + { + return TryRemove(key, out var _); } private void OnRemove(LinkedListNode node) @@ -309,185 +309,185 @@ private void OnRemove(LinkedListNode node) } } - Disposer.Dispose(node.Value.Value); - } - - /// - ///Note: Calling this method does not affect LRU order. - public bool TryUpdate(K key, V value) - { - if (this.dictionary.TryGetValue(key, out var node)) - { - node.Value.Value = value; - Interlocked.Increment(ref this.metrics.updatedCount); - return true; - } - - return false; - } - - /// - ///Note: Updates to existing items do not affect LRU order. Added items are at the top of the LRU. - public void AddOrUpdate(K key, V value) - { - // first, try to update - if (this.dictionary.TryGetValue(key, out var existingNode)) - { - existingNode.Value.Value = value; - Interlocked.Increment(ref this.metrics.updatedCount); - return; - } - - // then try add - var newNode = new LinkedListNode(new LruItem(key, value)); - - if (this.dictionary.TryAdd(key, newNode)) - { - LinkedListNode first = null; - - lock (this.linkedList) - { - if (linkedList.Count >= capacity) - { - first = linkedList.First; - linkedList.RemoveFirst(); - } - - linkedList.AddLast(newNode); - } - - // Remove from the dictionary outside the lock. This means that the dictionary at this moment - // contains an item that is not in the linked list. If another thread fetches this item, - // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an - // item just as it was about to move to the back of the LRU list and be preserved. The next request - // for the same key will be a miss. Dictionary and list are eventually consistent. - // However, all operations inside the lock are extremely fast, so contention is minimized. - if (first != null) - { - dictionary.TryRemove(first.Value.Key, out var removed); - - Interlocked.Increment(ref this.metrics.evictedCount); - Disposer.Dispose(removed.Value.Value); - } - - return; - } - - // if both update and add failed there was a race, try again - AddOrUpdate(key, value); - } - - /// - public void Clear() - { - // take a key snapshot - var keys = this.dictionary.Keys.ToList(); - - // remove all keys in the snapshot - this correctly handles disposable values - foreach (var key in keys) - { - TryRemove(key); - } - } - - /// - /// is less than 0./ - /// is greater than capacity./ - public void Trim(int itemCount) - { - if (itemCount < 1 || itemCount > this.capacity) - { - Throw.ArgOutOfRange(nameof(itemCount), "itemCount must be greater than or equal to one, and less than the capacity of the cache."); - } - - for (int i = 0; i < itemCount; i++) - { - LinkedListNode first = null; - - lock (this.linkedList) - { - if (linkedList.Count > 0) - { - first = linkedList.First; - linkedList.RemoveFirst(); - } - } - - if (first != null) - { - dictionary.TryRemove(first.Value.Key, out var removed); - Disposer.Dispose(removed.Value.Value); - } - } - } - - // Thead A reads x from the dictionary. Thread B adds a new item. Thread A moves x to the end. Thread B now removes the new first Node (removal is atomic on both data structures). - private void LockAndMoveToEnd(LinkedListNode node) - { - // If the node has already been removed from the list, ignore. - // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from - // the List & Dictionary. Now thread A will try to move x to the end of the list. - if (node.List == null) - { - return; - } - - lock (this.linkedList) - { - if (node.List == null) - { - return; - } - - linkedList.Remove(node); - linkedList.AddLast(node); - } - } - - /// Returns an enumerator that iterates through the cache. - /// An enumerator for the cache. - /// - /// The enumerator returned from the cache is safe to use concurrently with - /// reads and writes, however it does not represent a moment-in-time snapshot. - /// The contents exposed through the enumerator may contain modifications - /// made after was called. - /// - IEnumerator IEnumerable.GetEnumerator() - { - return ((ClassicLru)this).GetEnumerator(); - } - - private class LruItem - { - public LruItem(K k, V v) - { - Key = k; - Value = v; - } - - public K Key { get; } - - public V Value { get; set; } - } - - private class CacheMetrics : ICacheMetrics - { - public long requestHitCount; - public long requestTotalCount; - public long updatedCount; - public long evictedCount; - - public double HitRatio => (double)requestHitCount / (double)requestTotalCount; - - public long Total => requestTotalCount; - - public long Hits => requestHitCount; - - public long Misses => requestTotalCount - requestHitCount; - - public long Evicted => evictedCount; - - public long Updated => updatedCount; - } - } -} + Disposer.Dispose(node.Value.Value); + } + + /// + ///Note: Calling this method does not affect LRU order. + public bool TryUpdate(K key, V value) + { + if (this.dictionary.TryGetValue(key, out var node)) + { + node.Value.Value = value; + Interlocked.Increment(ref this.metrics.updatedCount); + return true; + } + + return false; + } + + /// + ///Note: Updates to existing items do not affect LRU order. Added items are at the top of the LRU. + public void AddOrUpdate(K key, V value) + { + // first, try to update + if (this.dictionary.TryGetValue(key, out var existingNode)) + { + existingNode.Value.Value = value; + Interlocked.Increment(ref this.metrics.updatedCount); + return; + } + + // then try add + var newNode = new LinkedListNode(new LruItem(key, value)); + + if (this.dictionary.TryAdd(key, newNode)) + { + LinkedListNode first = null; + + lock (this.linkedList) + { + if (linkedList.Count >= capacity) + { + first = linkedList.First; + linkedList.RemoveFirst(); + } + + linkedList.AddLast(newNode); + } + + // Remove from the dictionary outside the lock. This means that the dictionary at this moment + // contains an item that is not in the linked list. If another thread fetches this item, + // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an + // item just as it was about to move to the back of the LRU list and be preserved. The next request + // for the same key will be a miss. Dictionary and list are eventually consistent. + // However, all operations inside the lock are extremely fast, so contention is minimized. + if (first != null) + { + dictionary.TryRemove(first.Value.Key, out var removed); + + Interlocked.Increment(ref this.metrics.evictedCount); + Disposer.Dispose(removed.Value.Value); + } + + return; + } + + // if both update and add failed there was a race, try again + AddOrUpdate(key, value); + } + + /// + public void Clear() + { + // take a key snapshot + var keys = this.dictionary.Keys.ToList(); + + // remove all keys in the snapshot - this correctly handles disposable values + foreach (var key in keys) + { + TryRemove(key); + } + } + + /// + /// is less than 0./ + /// is greater than capacity./ + public void Trim(int itemCount) + { + if (itemCount < 1 || itemCount > this.capacity) + { + Throw.ArgOutOfRange(nameof(itemCount), "itemCount must be greater than or equal to one, and less than the capacity of the cache."); + } + + for (int i = 0; i < itemCount; i++) + { + LinkedListNode first = null; + + lock (this.linkedList) + { + if (linkedList.Count > 0) + { + first = linkedList.First; + linkedList.RemoveFirst(); + } + } + + if (first != null) + { + dictionary.TryRemove(first.Value.Key, out var removed); + Disposer.Dispose(removed.Value.Value); + } + } + } + + // Thead A reads x from the dictionary. Thread B adds a new item. Thread A moves x to the end. Thread B now removes the new first Node (removal is atomic on both data structures). + private void LockAndMoveToEnd(LinkedListNode node) + { + // If the node has already been removed from the list, ignore. + // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from + // the List & Dictionary. Now thread A will try to move x to the end of the list. + if (node.List == null) + { + return; + } + + lock (this.linkedList) + { + if (node.List == null) + { + return; + } + + linkedList.Remove(node); + linkedList.AddLast(node); + } + } + + /// Returns an enumerator that iterates through the cache. + /// An enumerator for the cache. + /// + /// The enumerator returned from the cache is safe to use concurrently with + /// reads and writes, however it does not represent a moment-in-time snapshot. + /// The contents exposed through the enumerator may contain modifications + /// made after was called. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((ClassicLru)this).GetEnumerator(); + } + + private class LruItem + { + public LruItem(K k, V v) + { + Key = k; + Value = v; + } + + public K Key { get; } + + public V Value { get; set; } + } + + private class CacheMetrics : ICacheMetrics + { + public long requestHitCount; + public long requestTotalCount; + public long updatedCount; + public long evictedCount; + + public double HitRatio => (double)requestHitCount / (double)requestTotalCount; + + public long Total => requestTotalCount; + + public long Hits => requestHitCount; + + public long Misses => requestTotalCount - requestHitCount; + + public long Evicted => evictedCount; + + public long Updated => updatedCount; + } + } +} diff --git a/BitFaster.Caching/Throw.cs b/BitFaster.Caching/Throw.cs index 10fe2767..7f0aa070 100644 --- a/BitFaster.Caching/Throw.cs +++ b/BitFaster.Caching/Throw.cs @@ -1,85 +1,85 @@ -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace BitFaster.Caching -{ - internal static class Throw - { -#if NETCOREAPP3_0_OR_GREATER - [DoesNotReturn] -#endif +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace BitFaster.Caching +{ + internal static class Throw + { +#if NETCOREAPP3_0_OR_GREATER + [DoesNotReturn] +#endif public static void ArgNull(ExceptionArgument arg) => throw CreateArgumentNullException(arg); -#if NETCOREAPP3_0_OR_GREATER - [DoesNotReturn] -#endif +#if NETCOREAPP3_0_OR_GREATER + [DoesNotReturn] +#endif public static void ArgOutOfRange(string paramName) => throw CreateArgumentOutOfRangeException(paramName); -#if NETCOREAPP3_0_OR_GREATER - [DoesNotReturn] -#endif - public static void ArgOutOfRange(string paramName, string message) => throw CreateArgumentOutOfRangeException(paramName, message); - - [ExcludeFromCodeCoverage] -#if NETCOREAPP3_0_OR_GREATER - [DoesNotReturn] -#endif +#if NETCOREAPP3_0_OR_GREATER + [DoesNotReturn] +#endif + public static void ArgOutOfRange(string paramName, string message) => throw CreateArgumentOutOfRangeException(paramName, message); + + [ExcludeFromCodeCoverage] +#if NETCOREAPP3_0_OR_GREATER + [DoesNotReturn] +#endif public static void InvalidOp(string message) => throw CreateInvalidOperationException(message); -#if NETCOREAPP3_0_OR_GREATER - [DoesNotReturn] -#endif - public static void ScopedRetryFailure() => throw CreateScopedRetryFailure(); - -#if NETCOREAPP3_0_OR_GREATER - [DoesNotReturn] -#endif - public static void Disposed() => throw CreateObjectDisposedException(); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ArgumentNullException CreateArgumentNullException(ExceptionArgument arg) => new ArgumentNullException(GetArgumentString(arg)); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ArgumentOutOfRangeException CreateArgumentOutOfRangeException(string paramName) => new ArgumentOutOfRangeException(paramName); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ArgumentOutOfRangeException CreateArgumentOutOfRangeException(string paramName, string message) => new ArgumentOutOfRangeException(paramName, message); - - [ExcludeFromCodeCoverage] - [MethodImpl(MethodImplOptions.NoInlining)] - private static InvalidOperationException CreateInvalidOperationException(string message) => new InvalidOperationException(message); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static InvalidOperationException CreateScopedRetryFailure() => new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ObjectDisposedException CreateObjectDisposedException() => new ObjectDisposedException(typeof(T).Name); - - [ExcludeFromCodeCoverage] - private static string GetArgumentString(ExceptionArgument argument) - { - switch (argument) - { - case ExceptionArgument.cache: return nameof(ExceptionArgument.cache); - case ExceptionArgument.comparer: return nameof(ExceptionArgument.comparer); - case ExceptionArgument.scoped: return nameof(ExceptionArgument.scoped); - case ExceptionArgument.capacity: return nameof(ExceptionArgument.capacity); - case ExceptionArgument.node: return nameof(ExceptionArgument.node); - default: - Debug.Fail("The ExceptionArgument value is not defined."); - return string.Empty; - } - } - } - - internal enum ExceptionArgument - { - cache, - comparer, - scoped, - capacity, - node, - } -} +#if NETCOREAPP3_0_OR_GREATER + [DoesNotReturn] +#endif + public static void ScopedRetryFailure() => throw CreateScopedRetryFailure(); + +#if NETCOREAPP3_0_OR_GREATER + [DoesNotReturn] +#endif + public static void Disposed() => throw CreateObjectDisposedException(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentNullException CreateArgumentNullException(ExceptionArgument arg) => new ArgumentNullException(GetArgumentString(arg)); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException CreateArgumentOutOfRangeException(string paramName) => new ArgumentOutOfRangeException(paramName); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException CreateArgumentOutOfRangeException(string paramName, string message) => new ArgumentOutOfRangeException(paramName, message); + + [ExcludeFromCodeCoverage] + [MethodImpl(MethodImplOptions.NoInlining)] + private static InvalidOperationException CreateInvalidOperationException(string message) => new InvalidOperationException(message); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static InvalidOperationException CreateScopedRetryFailure() => new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ObjectDisposedException CreateObjectDisposedException() => new ObjectDisposedException(typeof(T).Name); + + [ExcludeFromCodeCoverage] + private static string GetArgumentString(ExceptionArgument argument) + { + switch (argument) + { + case ExceptionArgument.cache: return nameof(ExceptionArgument.cache); + case ExceptionArgument.comparer: return nameof(ExceptionArgument.comparer); + case ExceptionArgument.scoped: return nameof(ExceptionArgument.scoped); + case ExceptionArgument.capacity: return nameof(ExceptionArgument.capacity); + case ExceptionArgument.node: return nameof(ExceptionArgument.node); + default: + Debug.Fail("The ExceptionArgument value is not defined."); + return string.Empty; + } + } + } + + internal enum ExceptionArgument + { + cache, + comparer, + scoped, + capacity, + node, + } +}