diff --git a/BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs b/BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs new file mode 100644 index 00000000..6ac998c9 --- /dev/null +++ b/BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Synchronized; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests +{ + public class CacheEventProxyBaseTests + { + private TestCacheEvents testCacheEvents; + private EventProxy eventProxy; + + private List> removedItems = new(); + + public CacheEventProxyBaseTests() + { + this.testCacheEvents = new TestCacheEvents(); + this.eventProxy = new EventProxy(this.testCacheEvents); + } + + [Fact] + public void EventsAreEnabled() + { + this.testCacheEvents.IsEnabled = true; + + this.eventProxy.IsEnabled.Should().BeTrue(); + } + + [Fact] + public void WhenEventHandlerIsRegisteredItIsFired() + { + this.eventProxy.ItemRemoved += OnItemRemoved; + + this.testCacheEvents.Fire(1, new AtomicFactory(1), ItemRemovedReason.Removed); + + this.removedItems.First().Key.Should().Be(1); + } + + [Fact] + public void WhenEventHandlerIsAddedThenRemovedItIsNotFired() + { + this.eventProxy.ItemRemoved += OnItemRemoved; + this.eventProxy.ItemRemoved -= OnItemRemoved; + + this.testCacheEvents.Fire(1, new AtomicFactory(1), ItemRemovedReason.Removed); + + this.removedItems.Count.Should().Be(0); + } + + [Fact] + public void WhenTwoEventHandlersAddedThenOneRemovedEventIsFired() + { + this.eventProxy.ItemRemoved += OnItemRemoved; + this.eventProxy.ItemRemoved += OnItemRemovedThrow; + this.eventProxy.ItemRemoved -= OnItemRemovedThrow; + + this.testCacheEvents.Fire(1, new AtomicFactory(1), ItemRemovedReason.Removed); + + this.removedItems.First().Key.Should().Be(1); + } + + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) + { + this.removedItems.Add(e); + } + + private void OnItemRemovedThrow(object sender, ItemRemovedEventArgs e) + { + throw new Exception("Should never happen"); + } + + private class TestCacheEvents : ICacheEvents> + { + public bool IsEnabled { get; set; } + + public event EventHandler>> ItemRemoved; + + public void Fire(K key, AtomicFactory value, ItemRemovedReason reason) + { + ItemRemoved?.Invoke(this, new ItemRemovedEventArgs>(key, value, reason)); + } + } + + 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); + } + } + } +} diff --git a/BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs b/BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs new file mode 100644 index 00000000..d0c014bc --- /dev/null +++ b/BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests +{ + public abstract class ScopedCacheTestBase + { + protected const int capacity = 6; + protected readonly IScopedCache cache; + + protected List>> removedItems = new(); + + protected ScopedCacheTestBase(IScopedCache cache) + { + this.cache = cache; + } + + [Fact] + public void WhenCreatedCapacityPropertyWrapsInnerCache() + { + this.cache.Capacity.Should().Be(capacity); + } + + [Fact] + public void WhenItemIsAddedCountIsCorrect() + { + this.cache.Count.Should().Be(0); + + this.cache.AddOrUpdate(1, new Disposable()); + + this.cache.Count.Should().Be(1); + } + + [Fact] + public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() + { + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.ScopedTryGet(1, out var lifetime); + + this.cache.Metrics.Misses.Should().Be(0); + this.cache.Metrics.Hits.Should().Be(1); + } + + [Fact] + public void WhenEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.ItemRemoved += OnItemRemoved; + + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.TryRemove(1); + + this.removedItems.First().Key.Should().Be(1); + } + + [Fact] + public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() + { + var d = new Disposable(); + this.cache.AddOrUpdate(1, d); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(d); + } + + [Fact] + public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() + { + var d1 = new Disposable(); + var d2 = new Disposable(); + this.cache.AddOrUpdate(1, d1); + this.cache.AddOrUpdate(1, d2); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(d2); + } + + [Fact] + public void WhenItemUpdatedOldValueIsAliveUntilLifetimeCompletes() + { + var d1 = new Disposable(); + var d2 = new Disposable(); + + // start a lifetime on 1 + this.cache.AddOrUpdate(1, d1); + this.cache.ScopedTryGet(1, out var lifetime1).Should().BeTrue(); + + using (lifetime1) + { + // replace 1 + this.cache.AddOrUpdate(1, d2); + + // cache reflects replacement + this.cache.ScopedTryGet(1, out var lifetime2).Should().BeTrue(); + lifetime2.Value.Should().Be(d2); + + d1.IsDisposed.Should().BeFalse(); + } + + d1.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenClearedItemsAreDisposed() + { + var d = new Disposable(); + this.cache.AddOrUpdate(1, d); + + this.cache.Clear(); + + d.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenItemExistsTryGetReturnsLifetime() + { + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + + lifetime.Should().NotBeNull(); + } + + [Fact] + public void WhenItemDoesNotExistTryGetReturnsFalse() + { + this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); + } + + [Fact] + public void WhenCacheContainsValuesTrim1RemovesColdestValue() + { + this.cache.AddOrUpdate(0, new Disposable()); + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.AddOrUpdate(2, new Disposable()); + + this.cache.Trim(1); + + this.cache.ScopedTryGet(0, out var lifetime).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistTryRemoveReturnsFalse() + { + this.cache.TryRemove(1).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryRemoveReturnsTrue() + { + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.TryRemove(1).Should().BeTrue(); + } + + [Fact] + public void WhenKeyDoesNotExistTryUpdateReturnsFalse() + { + this.cache.TryUpdate(1, new Disposable()).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryUpdateReturnsTrue() + { + this.cache.AddOrUpdate(1, new Disposable()); + + this.cache.TryUpdate(1, new Disposable()).Should().BeTrue(); + } + + protected void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + { + this.removedItems.Add(e); + } + } +} diff --git a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs index 10e90379..0adf2c6a 100644 --- a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs @@ -9,12 +9,12 @@ namespace BitFaster.Caching.UnitTests { - public class ScopedCacheTests + public class ScopedCacheTests : ScopedCacheTestBase { - private const int capacity = 6; - private readonly ScopedCache cache = new (new ConcurrentLru>(capacity)); - - private List>> removedItems = new(); + public ScopedCacheTests() + : base(new ScopedCache(new ConcurrentLru>(capacity))) + { + } [Fact] public void WhenInnerCacheIsNullCtorThrows() @@ -24,121 +24,6 @@ public void WhenInnerCacheIsNullCtorThrows() constructor.Should().Throw(); } - [Fact] - public void WhenCreatedCapacityPropertyWrapsInnerCache() - { - this.cache.Capacity.Should().Be(capacity); - } - - [Fact] - public void WhenItemIsAddedCountIsCorrect() - { - this.cache.Count.Should().Be(0); - - this.cache.AddOrUpdate(1, new Disposable()); - - this.cache.Count.Should().Be(1); - } - - [Fact] - public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() - { - this.cache.AddOrUpdate(1, new Disposable()); - this.cache.ScopedGetOrAdd(1, k => new Scoped(new Disposable())); - - this.cache.Metrics.Misses.Should().Be(0); - this.cache.Metrics.Hits.Should().Be(1); - } - - [Fact] - public void WhenEventHandlerIsRegisteredItIsFired() - { - this.cache.Events.ItemRemoved += OnItemRemoved; - - this.cache.AddOrUpdate(1, new Disposable()); - this.cache.TryRemove(1); - - this.removedItems.First().Key.Should().Be(1); - } - - private void OnItemRemoved(object sender, ItemRemovedEventArgs> e) - { - this.removedItems.Add(e); - } - - [Fact] - public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() - { - var d = new Disposable(); - this.cache.AddOrUpdate(1, d); - - this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); - lifetime.Value.Should().Be(d); - } - - [Fact] - public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() - { - var d1 = new Disposable(); - var d2 = new Disposable(); - this.cache.AddOrUpdate(1, d1); - this.cache.AddOrUpdate(1, d2); - - this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); - lifetime.Value.Should().Be(d2); - } - - [Fact] - public void WhenItemUpdatedOldValueIsAliveUntilLifetimeCompletes() - { - var d1 = new Disposable(); - var d2 = new Disposable(); - - // start a lifetime on 1 - this.cache.AddOrUpdate(1, d1); - this.cache.ScopedTryGet(1, out var lifetime1).Should().BeTrue(); - - using (lifetime1) - { - // replace 1 - this.cache.AddOrUpdate(1, d2); - - // cache reflects replacement - this.cache.ScopedTryGet(1, out var lifetime2).Should().BeTrue(); - lifetime2.Value.Should().Be(d2); - - d1.IsDisposed.Should().BeFalse(); - } - - d1.IsDisposed.Should().BeTrue(); - } - - [Fact] - public void WhenClearedItemsAreDisposed() - { - var d = new Disposable(); - this.cache.AddOrUpdate(1, d); - - this.cache.Clear(); - - d.IsDisposed.Should().BeTrue(); - } - - [Fact] - public void WhenItemExistsTryGetReturnsLifetime() - { - this.cache.AddOrUpdate(1, new Disposable()); - this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); - - lifetime.Should().NotBeNull(); - } - - [Fact] - public void WhenItemDoesNotExistTryGetReturnsFalse() - { - this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); - } - [Fact] public void WhenScopeIsDisposedTryGetReturnsFalse() { @@ -173,60 +58,20 @@ public void GetOrAddDisposedScopeThrows() var scope = new Scoped(new Disposable()); scope.Dispose(); - Action getOrAdd = () => { this.cache.ScopedGetOrAdd(1, k => scope); }; getOrAdd.Should().Throw(); } [Fact] - public void GetOrAddAsyncDisposedScopeThrows() + public async Task GetOrAddAsyncDisposedScopeThrows() { var scope = new Scoped(new Disposable()); scope.Dispose(); Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; - getOrAdd.Should().ThrowAsync(); - } - - [Fact] - public void WhenCacheContainsValuesTrim1RemovesColdestValue() - { - this.cache.AddOrUpdate(0, new Disposable()); - this.cache.AddOrUpdate(1, new Disposable()); - this.cache.AddOrUpdate(2, new Disposable()); - - this.cache.Trim(1); - - this.cache.ScopedTryGet(0, out var lifetime).Should().BeFalse(); - } - - [Fact] - public void WhenKeyDoesNotExistTryRemoveReturnsFalse() - { - this.cache.TryRemove(1).Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryRemoveReturnsTrue() - { - this.cache.AddOrUpdate(1, new Disposable()); - this.cache.TryRemove(1).Should().BeTrue(); - } - - [Fact] - public void WhenKeyDoesNotExistTryUpdateReturnsFalse() - { - this.cache.TryUpdate(1, new Disposable()).Should().BeFalse(); - } - - [Fact] - public void WhenKeyExistsTryUpdateReturnsTrue() - { - this.cache.AddOrUpdate(1, new Disposable()); - - this.cache.TryUpdate(1, new Disposable()).Should().BeTrue(); + await getOrAdd.Should().ThrowAsync(); } } } diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs new file mode 100644 index 00000000..e1f132a9 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Synchronized; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Synchronized +{ + public class AtomicFactoryAsyncCacheTests + { + private const int capacity = 6; + private readonly AtomicFactoryAsyncCache cache = new(new ConcurrentLru>(capacity)); + + private List> removedItems = new(); + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryAsyncCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCreatedCapacityPropertyWrapsInnerCache() + { + this.cache.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.Misses.Should().Be(0); + this.cache.Metrics.Hits.Should().Be(1); + } + + [Fact] + public void WhenEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.ItemRemoved += OnItemRemoved; + + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1); + + this.removedItems.First().Key.Should().Be(1); + } + + [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 GetOrAddThrows() + { + Action getOrAdd = () => { this.cache.GetOrAdd(1, k => k); }; + + getOrAdd.Should().Throw(); + } + + [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.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); + } + + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) + { + this.removedItems.Add(e); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs new file mode 100644 index 00000000..55fa417e --- /dev/null +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Synchronized; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Synchronized +{ + public class AtomicFactoryCacheTests + { + private const int capacity = 6; + private readonly AtomicFactoryCache cache = new(new ConcurrentLru>(capacity)); + + private List> removedItems = new(); + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenCreatedCapacityPropertyWrapsInnerCache() + { + this.cache.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.Misses.Should().Be(0); + this.cache.Metrics.Hits.Should().Be(1); + } + + [Fact] + public void WhenEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.ItemRemoved += OnItemRemoved; + + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1); + + this.removedItems.First().Key.Should().Be(1); + } + + [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 async Task GetOrAddAsyncThrows() + { + Func getOrAdd = async () => { await this.cache.GetOrAddAsync(1, k => Task.FromResult(k)); }; + + await getOrAdd.Should().ThrowAsync(); + } + + [Fact] + public void WhenCacheContainsValuesTrim1RemovesColdestValue() + { + this.cache.AddOrUpdate(0, 0); + this.cache.AddOrUpdate(1, 1); + this.cache.AddOrUpdate(2, 2); + + this.cache.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); + } + + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) + { + this.removedItems.Add(e); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs new file mode 100644 index 00000000..8818d11e --- /dev/null +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Synchronized; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Synchronized +{ + public class AtomicFactoryScopedAsyncCacheTests : ScopedCacheTestBase + { + public AtomicFactoryScopedAsyncCacheTests() + : base(new AtomicFactoryScopedAsyncCache(new ConcurrentLru>(capacity))) + { + } + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryScopedAsyncCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public async Task WhenScopeIsDisposedTryGetReturnsFalse() + { + var scope = new Scoped(new Disposable()); + + await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); + + scope.Dispose(); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistGetOrAddThrows() + { + Action getOrAdd = () => { this.cache.ScopedGetOrAdd(1, k => new Scoped(new Disposable())); }; + + getOrAdd.Should().Throw(); + } + + [Fact] + public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() + { + await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + } + + [Fact] + public async Task GetOrAddAsyncDisposedScopeThrows() + { + var scope = new Scoped(new Disposable()); + scope.Dispose(); + + Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; + + await getOrAdd.Should().ThrowAsync(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs new file mode 100644 index 00000000..e0927bdc --- /dev/null +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using BitFaster.Caching.Synchronized; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Synchronized +{ + public class AtomicFactoryScopedCacheTests : ScopedCacheTestBase + { + public AtomicFactoryScopedCacheTests() + : base(new AtomicFactoryScopedCache(new ConcurrentLru>(capacity))) + { + } + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryScopedCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public void WhenScopeIsDisposedTryGetReturnsFalse() + { + var scope = new Scoped(new Disposable()); + + this.cache.ScopedGetOrAdd(1, k => scope); + + scope.Dispose(); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistGetOrAddAddsValue() + { + this.cache.ScopedGetOrAdd(1, k => new Scoped(new Disposable())); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + } + + [Fact] + public async Task WhenKeyDoesNotExistGetOrAddAsyncThrows() + { + Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); }; + + await getOrAdd.Should().ThrowAsync(); + } + + [Fact] + public void GetOrAddDisposedScopeThrows() + { + var scope = new Scoped(new Disposable()); + scope.Dispose(); + + Action getOrAdd = () => { this.cache.ScopedGetOrAdd(1, k => scope); }; + + getOrAdd.Should().Throw(); + } + + [Fact] + public void GetOrAddAsyncDisposedScopeThrows() + { + var scope = new Scoped(new Disposable()); + scope.Dispose(); + + Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; + + getOrAdd.Should().ThrowAsync(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs index 42c76f37..bd04a979 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs @@ -12,6 +12,42 @@ namespace BitFaster.Caching.UnitTests.Synchronized { public class ScopedAsyncAtomicFactoryTests { + [Fact] + public void WhenScopeIsNotCreatedScopeIfCreatedReturnsNull() + { + var atomicFactory = new ScopedAsyncAtomicFactory(); + + atomicFactory.ScopeIfCreated.Should().BeNull(); + } + + [Fact] + public void WhenScopeIsCreatedScopeIfCreatedReturnsScope() + { + var expectedDisposable = new Disposable(); + var atomicFactory = new ScopedAsyncAtomicFactory(expectedDisposable); + + atomicFactory.ScopeIfCreated.Should().NotBeNull(); + atomicFactory.ScopeIfCreated.TryCreateLifetime(out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(expectedDisposable); + } + + [Fact] + public void WhenNotInitTryCreateReturnsFalse() + { + var sa = new ScopedAsyncAtomicFactory(); + sa.TryCreateLifetime(out var l).Should().BeFalse(); + } + + [Fact] + public void WhenCreatedTryCreateLifetimeReturnsScope() + { + var expectedDisposable = new Disposable(); + var sa = new ScopedAsyncAtomicFactory(expectedDisposable); + + sa.TryCreateLifetime(out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(expectedDisposable); + } + [Fact] public async Task WhenCreateFromValueLifetimeContainsValue() { @@ -19,7 +55,7 @@ public async Task WhenCreateFromValueLifetimeContainsValue() (bool r, Lifetime l) result = await atomicFactory.TryCreateLifetimeAsync(1, k => { - return Task.FromResult(new IntHolder() { actualNumber = 2 }); + return Task.FromResult(new Scoped(new IntHolder() { actualNumber = 2 })); }); result.r.Should().BeTrue(); @@ -34,7 +70,7 @@ public async Task WhenScopeIsDisposedTryCreateReturnsFalse() (bool r, Lifetime l) result = await atomicFactory.TryCreateLifetimeAsync(1, k => { - return Task.FromResult(new IntHolder() { actualNumber = 2 }); + return Task.FromResult(new Scoped(new IntHolder() { actualNumber = 2 })); }); result.r.Should().BeFalse(); @@ -69,7 +105,7 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() winningNumber = 1; Interlocked.Increment(ref winnerCount); - return new IntHolder() { actualNumber = 1 }; + return new Scoped(new IntHolder() { actualNumber = 1 }); }); Task<(bool r, Lifetime l)> second = atomicFactory.TryCreateLifetimeAsync(1, async k => @@ -79,7 +115,7 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() winningNumber = 2; Interlocked.Increment(ref winnerCount); - return new IntHolder() { actualNumber = 2 }; + return new Scoped(new IntHolder() { actualNumber = 2 }); }); await enter.Task; @@ -111,7 +147,7 @@ public async Task WhenDisposedWhileInitResultIsDisposed() enter.SetResult(true); await resume.Task; - return holder; + return new Scoped(holder); }); await enter.Task; @@ -156,7 +192,7 @@ public async Task WhenDisposedWhileThrowingNextInitIsDisposed() (bool r, Lifetime l) result = await atomicFactory.TryCreateLifetimeAsync(1, k => { - return Task.FromResult(holder); + return Task.FromResult(new Scoped(holder)); }); result.r.Should().BeFalse(); diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs index b201171b..89f9a323 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs @@ -17,7 +17,7 @@ public void WhenInitializedWithValueTryCreateLifetimeCreatesLifetimeWithValue() var expectedDisposable = new Disposable(); var sa = new ScopedAtomicFactory(expectedDisposable); - sa.TryCreateLifetime(1, k => new Disposable(), out var lifetime).Should().BeTrue(); + sa.TryCreateLifetime(1, k => new Scoped(new Disposable()), out var lifetime).Should().BeTrue(); lifetime.Value.Should().Be(expectedDisposable); } @@ -28,7 +28,7 @@ public void WhenInitializedWithFactoryTryCreateLifetimeCreatesLifetimeWithValue( var expectedDisposable = new Disposable(); var sa = new ScopedAtomicFactory(); - sa.TryCreateLifetime(1, k => expectedDisposable, out var lifetime).Should().BeTrue(); + sa.TryCreateLifetime(1, k => new Scoped(expectedDisposable), out var lifetime).Should().BeTrue(); lifetime.Value.Should().Be(expectedDisposable); } @@ -39,19 +39,64 @@ public void WhenInitializedWithFactoryValueIsCached() var expectedDisposable = new Disposable(); var sa = new ScopedAtomicFactory(); - sa.TryCreateLifetime(1, k => expectedDisposable, out var lifetime1).Should().BeTrue(); - sa.TryCreateLifetime(1, k => new Disposable(), out var lifetime2).Should().BeTrue(); + sa.TryCreateLifetime(1, k => new Scoped(expectedDisposable), out var lifetime1).Should().BeTrue(); + sa.TryCreateLifetime(1, k => new Scoped(new Disposable()), out var lifetime2).Should().BeTrue(); lifetime2.Value.Should().Be(expectedDisposable); } + [Fact] + public void WhenScopeIsNotCreatedScopeIfCreatedReturnsNull() + { + var sa = new ScopedAtomicFactory(); + + sa.ScopeIfCreated.Should().BeNull(); + } + + [Fact] + public void WhenScopeIsCreatedScopeIfCreatedReturnsScope() + { + var expectedDisposable = new Disposable(); + var sa = new ScopedAtomicFactory(expectedDisposable); + + sa.ScopeIfCreated.Should().NotBeNull(); + sa.ScopeIfCreated.TryCreateLifetime(out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(expectedDisposable); + } + + [Fact] + public void WhenNotInitTryCreateReturnsFalse() + { + var sa = new ScopedAtomicFactory(); + sa.TryCreateLifetime(out var l).Should().BeFalse(); + } + + [Fact] + public void WhenCreatedTryCreateLifetimeReturnsScope() + { + var expectedDisposable = new Disposable(); + var sa = new ScopedAtomicFactory(expectedDisposable); + + sa.TryCreateLifetime(out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(expectedDisposable); + } + + [Fact] + public void WhenScopeDisposedTryCreateLifetimeReturnsFalse() + { + var sa = new ScopedAtomicFactory(); + sa.Dispose(); + + sa.TryCreateLifetime(out var lifetime).Should().BeFalse(); + } + [Fact] public void WhenInitializedWithValueThenDisposedCreateLifetimeIsFalse() { var sa = new ScopedAtomicFactory(new Disposable()); sa.Dispose(); - sa.TryCreateLifetime(1, k => new Disposable(), out var l).Should().BeFalse(); + sa.TryCreateLifetime(1, k => new Scoped(new Disposable()), out var l).Should().BeFalse(); } [Fact] @@ -60,7 +105,7 @@ public void WhenCreatedThenDisposedCreateLifetimeIsFalse() var sa = new ScopedAtomicFactory(); sa.Dispose(); - sa.TryCreateLifetime(1, k => new Disposable(), out var l).Should().BeFalse(); + sa.TryCreateLifetime(1, k => new Scoped(new Disposable()), out var l).Should().BeFalse(); } [Fact] @@ -69,7 +114,7 @@ public void WhenInitializedLifetimeKeepsValueAlive() var disposable = new Disposable(); var sa = new ScopedAtomicFactory(); - sa.TryCreateLifetime(1, k => disposable, out var lifetime1).Should().BeTrue(); + sa.TryCreateLifetime(1, k => new Scoped(disposable), out var lifetime1).Should().BeTrue(); sa.TryCreateLifetime(1, k => null, out var lifetime2).Should().BeTrue(); sa.Dispose(); diff --git a/BitFaster.Caching/CacheEventProxyBase.cs b/BitFaster.Caching/CacheEventProxyBase.cs new file mode 100644 index 00000000..f8114324 --- /dev/null +++ b/BitFaster.Caching/CacheEventProxyBase.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching +{ + public abstract class CacheEventProxyBase : ICacheEvents + { + private readonly ICacheEvents events; + private event EventHandler> itemRemovedProxy; + + public CacheEventProxyBase(ICacheEvents events) + { + this.events = events; + } + + public bool IsEnabled => this.events.IsEnabled; + + public event EventHandler> ItemRemoved + { + add { this.Register(value); } + remove { this.UnRegister(value); } + } + + private void Register(EventHandler> value) + { + itemRemovedProxy += value; + events.ItemRemoved += OnItemRemoved; + } + + private void UnRegister(EventHandler> value) + { + this.itemRemovedProxy -= value; + + if (this.itemRemovedProxy == null) + { + this.events.ItemRemoved -= OnItemRemoved; + } + } + + private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs args) + { + itemRemovedProxy(sender, TranslateOnRemoved(args)); + } + + protected abstract ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs inner); + } +} diff --git a/BitFaster.Caching/ScopedCache.cs b/BitFaster.Caching/ScopedCache.cs index 20bc5cf3..4dcb3797 100644 --- a/BitFaster.Caching/ScopedCache.cs +++ b/BitFaster.Caching/ScopedCache.cs @@ -18,9 +18,6 @@ public sealed class ScopedCache : IScopedCache where V : IDisposable { private readonly ICache> cache; - private const int MaxRetry = 5; - private static readonly string RetryFailureMessage = $"Exceeded {MaxRetry} attempts to create a lifetime."; - public ScopedCache(ICache> cache) { if (cache == null) @@ -71,9 +68,9 @@ public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) spinwait.SpinOnce(); - if (c++ > MaxRetry) + if (c++ > ScopedCacheDefaults.MaxRetry) { - throw new InvalidOperationException(RetryFailureMessage); + throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); } } } @@ -94,9 +91,9 @@ public async Task> ScopedGetOrAddAsync(K key, Func spinwait.SpinOnce(); - if (c++ > MaxRetry) + if (c++ > ScopedCacheDefaults.MaxRetry) { - throw new InvalidOperationException(RetryFailureMessage); + throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); } } } diff --git a/BitFaster.Caching/ScopedCacheDefaults.cs b/BitFaster.Caching/ScopedCacheDefaults.cs new file mode 100644 index 00000000..0e826c62 --- /dev/null +++ b/BitFaster.Caching/ScopedCacheDefaults.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + internal static class ScopedCacheDefaults + { + internal const int MaxRetry = 5; + internal static readonly string RetryFailureMessage = $"Exceeded {MaxRetry} attempts to create a lifetime."; + } +} diff --git a/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs index 80f10d12..c9f5f289 100644 --- a/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs @@ -9,7 +9,7 @@ namespace BitFaster.Caching.Synchronized { [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public class AsyncAtomicFactory + public sealed class AsyncAtomicFactory { private Initializer initializer; diff --git a/BitFaster.Caching/Synchronized/AtomicFactory.cs b/BitFaster.Caching/Synchronized/AtomicFactory.cs index 95ec6940..4cb22d6c 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactory.cs @@ -9,7 +9,7 @@ namespace BitFaster.Caching.Synchronized { [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public class AtomicFactory + public sealed class AtomicFactory { private Initializer initializer; diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs new file mode 100644 index 00000000..41c141c5 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Synchronized +{ + public sealed class AtomicFactoryAsyncCache : ICache + { + private readonly ICache> cache; + private readonly EventProxy eventProxy; + + public AtomicFactoryAsyncCache(ICache> cache) + { + if (cache == null) + { + throw new ArgumentNullException(nameof(cache)); + } + + this.cache = cache; + this.eventProxy = new EventProxy(cache.Events); + } + + public int Capacity => cache.Capacity; + + public int Count => cache.Count; + + public ICacheMetrics Metrics => cache.Metrics; + + public ICacheEvents Events => this.eventProxy; + + public void AddOrUpdate(K key, V value) + { + cache.AddOrUpdate(key, new AsyncAtomicFactory(value)); + } + + public void Clear() + { + cache.Clear(); + } + + public V GetOrAdd(K key, Func valueFactory) + { + throw new NotImplementedException(); + } + + public Task GetOrAddAsync(K key, Func> valueFactory) + { + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory()); + return synchronized.GetValueAsync(key, valueFactory); + } + + public void Trim(int itemCount) + { + cache.Trim(itemCount); + } + + 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); + } + + public bool TryUpdate(K key, V value) + { + return cache.TryUpdate(key, new AsyncAtomicFactory(value)); + } + + private class EventProxy : CacheEventProxyBase, V> + { + public EventProxy(ICacheEvents> inner) + : base(inner) + { + } + + protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs> inner) + { + return new Lru.ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); + } + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs new file mode 100644 index 00000000..df03b91f --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Synchronized +{ + public sealed class AtomicFactoryCache : ICache + { + private readonly ICache> cache; + private readonly EventProxy eventProxy; + + public AtomicFactoryCache(ICache> cache) + { + if (cache == null) + { + throw new ArgumentNullException(nameof(cache)); + } + + this.cache = cache; + this.eventProxy = new EventProxy(cache.Events); + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public ICacheMetrics Metrics => this.cache.Metrics; + + public ICacheEvents Events => this.eventProxy; + + 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); + } + + public Task GetOrAddAsync(K key, Func> valueFactory) + { + throw new NotImplementedException(); + } + + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + 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; + } + + public bool TryRemove(K key) + { + return cache.TryRemove(key); + } + + public bool TryUpdate(K key, V value) + { + return cache.TryUpdate(key, new AtomicFactory(value)); + } + + private class EventProxy : CacheEventProxyBase, V> + { + public EventProxy(ICacheEvents> inner) + : base(inner) + { + } + + protected override ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs> inner) + { + return new Lru.ItemRemovedEventArgs(inner.Key, inner.Value.ValueIfCreated, inner.Reason); + } + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs new file mode 100644 index 00000000..4f62e203 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Synchronized +{ + public sealed class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable + { + private readonly ICache> cache; + private readonly EventProxy eventProxy; + + public AtomicFactoryScopedAsyncCache(ICache> cache) + { + if (cache == null) + { + throw new ArgumentNullException(nameof(cache)); + } + + this.cache = cache; + this.eventProxy = new EventProxy(cache.Events); + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public ICacheMetrics Metrics => this.cache.Metrics; + + public ICacheEvents> Events => this.eventProxy; + + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new ScopedAsyncAtomicFactory(value)); + } + + public void Clear() + { + this.cache.Clear(); + } + + public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) + { + throw new NotImplementedException(); + } + + public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + { + int c = 0; + var spinwait = new SpinWait(); + while (true) + { + var scope = cache.GetOrAdd(key, _ => new ScopedAsyncAtomicFactory()); + + var result = await scope.TryCreateLifetimeAsync(key, valueFactory).ConfigureAwait(false); + + if (result.success) + { + return result.lifetime; + } + + spinwait.SpinOnce(); + + if (c++ > ScopedCacheDefaults.MaxRetry) + { + throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); + } + } + } + + public bool ScopedTryGet(K key, out Lifetime lifetime) + { + if (this.cache.TryGet(key, out var scope)) + { + if (scope.TryCreateLifetime(out lifetime)) + { + return true; + } + } + + lifetime = default; + return false; + } + + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + public bool TryRemove(K key) + { + return this.cache.TryRemove(key); + } + + public bool TryUpdate(K key, V value) + { + return this.cache.TryUpdate(key, new ScopedAsyncAtomicFactory(value)); + } + + private class EventProxy : CacheEventProxyBase, Scoped> + { + public EventProxy(ICacheEvents> inner) + : base(inner) + { + } + + protected override ItemRemovedEventArgs> TranslateOnRemoved(ItemRemovedEventArgs> inner) + { + return new Lru.ItemRemovedEventArgs>(inner.Key, inner.Value.ScopeIfCreated, inner.Reason); + } + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs new file mode 100644 index 00000000..3e317441 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Synchronized +{ + public sealed class AtomicFactoryScopedCache : IScopedCache where V : IDisposable + { + private readonly ICache> cache; + private readonly EventProxy eventProxy; + + public AtomicFactoryScopedCache(ICache> cache) + { + if (cache == null) + { + throw new ArgumentNullException(nameof(cache)); + } + + this.cache = cache; + this.eventProxy = new EventProxy(cache.Events); + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public ICacheMetrics Metrics => this.cache.Metrics; + + public ICacheEvents> Events => this.eventProxy; + + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new ScopedAtomicFactory(value)); + } + + public void Clear() + { + this.cache.Clear(); + } + + public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) + { + int c = 0; + var spinwait = new SpinWait(); + while (true) + { + var scope = cache.GetOrAdd(key, _ => new ScopedAtomicFactory()); + + if (scope.TryCreateLifetime(key, valueFactory, out var lifetime)) + { + return lifetime; + } + + spinwait.SpinOnce(); + + if (c++ > ScopedCacheDefaults.MaxRetry) + { + throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); + } + } + } + + public Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + { + throw new NotImplementedException(); + } + + public bool ScopedTryGet(K key, out Lifetime lifetime) + { + if (this.cache.TryGet(key, out var scope)) + { + if (scope.TryCreateLifetime(out lifetime)) + { + return true; + } + } + + lifetime = default; + return false; + } + + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + public bool TryRemove(K key) + { + return this.cache.TryRemove(key); + } + + public bool TryUpdate(K key, V value) + { + return this.cache.TryUpdate(key, new ScopedAtomicFactory(value)); + } + + private class EventProxy : CacheEventProxyBase, Scoped> + { + public EventProxy(ICacheEvents> inner) + : base(inner) + { + } + + protected override ItemRemovedEventArgs> TranslateOnRemoved(ItemRemovedEventArgs> inner) + { + return new Lru.ItemRemovedEventArgs>(inner.Key, inner.Value.ScopeIfCreated, inner.Reason); + } + } + } +} diff --git a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs index a44fe0d4..c14d89d5 100644 --- a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Synchronized { - public class ScopedAsyncAtomicFactory : IScoped, IDisposable where V : IDisposable + public sealed class ScopedAsyncAtomicFactory : IScoped, IDisposable where V : IDisposable { private Scoped scope; private Initializer initializer; @@ -22,7 +22,31 @@ public ScopedAsyncAtomicFactory(V value) scope = new Scoped(value); } - public async Task<(bool success, Lifetime lifetime)> TryCreateLifetimeAsync(K key, Func> valueFactory) + public Scoped ScopeIfCreated + { + get + { + if (initializer != null) + { + return default; + } + + return scope; + } + } + + public bool TryCreateLifetime(out Lifetime lifetime) + { + if (scope?.IsDisposed ?? false || initializer != null) + { + lifetime = default; + return false; + } + + return scope.TryCreateLifetime(out lifetime); + } + + public async Task<(bool success, Lifetime lifetime)> TryCreateLifetimeAsync(K key, Func>> valueFactory) { // if disposed, return if (scope?.IsDisposed ?? false) @@ -40,7 +64,7 @@ public ScopedAsyncAtomicFactory(V value) return (res, lifetime); } - private async Task InitializeScopeAsync(K key, Func> valueFactory) + private async Task InitializeScopeAsync(K key, Func>> valueFactory) { var init = initializer; @@ -73,7 +97,7 @@ private class Initializer private bool isDisposeRequested; private Task> task; - public async Task> CreateScopeAsync(K key, Func> valueFactory) + public async Task> CreateScopeAsync(K key, Func>> valueFactory) { var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); @@ -83,8 +107,7 @@ public async Task> CreateScopeAsync(K key, Func> valueFacto { try { - var value = await valueFactory(key).ConfigureAwait(false); - var scope = new Scoped(value); + var scope = await valueFactory(key).ConfigureAwait(false); tcs.SetResult(scope); Volatile.Write(ref isTaskCompleted, true); diff --git a/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs index 7c43068c..81cf8f91 100644 --- a/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs @@ -12,7 +12,7 @@ namespace BitFaster.Caching.Synchronized // 1. Exactly once disposal. // 2. Exactly once invocation of value factory (synchronized create). // 3. Resolve race between create dispose init, if disposed is called before value is created, scoped value is disposed for life. - public class ScopedAtomicFactory : IScoped, IDisposable where V : IDisposable + public sealed class ScopedAtomicFactory : IScoped, IDisposable where V : IDisposable { private Scoped scope; private Initializer initializer; @@ -27,7 +27,31 @@ public ScopedAtomicFactory(V value) scope = new Scoped(value); } - public bool TryCreateLifetime(K key, Func valueFactory, out Lifetime lifetime) + public Scoped ScopeIfCreated + { + get + { + if (initializer != null) + { + return default; + } + + return scope; + } + } + + public bool TryCreateLifetime(out Lifetime lifetime) + { + if (scope?.IsDisposed ?? false || initializer != null) + { + lifetime = default; + return false; + } + + return scope.TryCreateLifetime(out lifetime); + } + + public bool TryCreateLifetime(K key, Func> valueFactory, out Lifetime lifetime) { if(scope?.IsDisposed ?? false) { @@ -44,7 +68,7 @@ public bool TryCreateLifetime(K key, Func valueFactory, out Lifetime li return scope.TryCreateLifetime(out lifetime); } - private void InitializeScope(K key, Func valueFactory) + private void InitializeScope(K key, Func> valueFactory) { var init = initializer; @@ -72,7 +96,7 @@ private class Initializer private bool isInitialized; private Scoped value; - public Scoped CreateScope(K key, Func valueFactory) + public Scoped CreateScope(K key, Func> valueFactory) { if (Volatile.Read(ref isInitialized)) { @@ -86,7 +110,7 @@ public Scoped CreateScope(K key, Func valueFactory) return value; } - value = new Scoped(valueFactory(key)); + value = valueFactory(key); Volatile.Write(ref isInitialized, true); return value;