From 452a64698aa35a777e4aecfc83d913e4c6fb60e3 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 12:39:17 -0700 Subject: [PATCH 01/11] outline --- .../Synchronized/ScopedAtomicFactoryTests.cs | 14 +- BitFaster.Caching/ScopedCache.cs | 11 +- BitFaster.Caching/ScopedCacheDefaults.cs | 14 ++ .../Synchronized/AtomicFactoryAsyncCache.cs | 78 ++++++++++ .../Synchronized/AtomicFactoryCache.cs | 78 ++++++++++ .../AtomicFactoryScopedAsyncCache.cs | 59 ++++++++ .../Synchronized/AtomicFactoryScopedCache.cs | 136 ++++++++++++++++++ .../Synchronized/ScopedAtomicFactory.cs | 32 ++++- 8 files changed, 404 insertions(+), 18 deletions(-) create mode 100644 BitFaster.Caching/ScopedCacheDefaults.cs create mode 100644 BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs create mode 100644 BitFaster.Caching/Synchronized/AtomicFactoryCache.cs create mode 100644 BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs create mode 100644 BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs index b201171b..efcbd256 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,8 +39,8 @@ 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); } @@ -51,7 +51,7 @@ 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 +60,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 +69,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/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/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs new file mode 100644 index 00000000..b1d3b224 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Synchronized +{ + public class AtomicFactoryAsyncCache : ICache + { + private readonly ICache> cache; + + public AtomicFactoryAsyncCache(ICache> cache) + { + this.cache = cache; + } + + public int Capacity => cache.Capacity; + + public int Count => cache.Count; + + public ICacheMetrics Metrics => cache.Metrics; + + // need to dispatch different events for this + public ICacheEvents Events => throw new Exception(); + + 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)); + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs new file mode 100644 index 00000000..df63eb02 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Synchronized +{ + public class AtomicFactoryCache : ICache + { + private readonly ICache> cache; + + public AtomicFactoryCache(ICache> cache) + { + this.cache = cache; + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public ICacheMetrics Metrics => this.cache.Metrics; + + // TODO: wrapper + public ICacheEvents Events => throw new NotImplementedException(); // this.cache.Events; + + 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)); + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs new file mode 100644 index 00000000..da7ac4ea --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Synchronized +{ + public class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable + { + public int Capacity => throw new NotImplementedException(); + + public int Count => throw new NotImplementedException(); + + public ICacheMetrics Metrics => throw new NotImplementedException(); + + public ICacheEvents> Events => throw new NotImplementedException(); + + public void AddOrUpdate(K key, V value) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) + { + throw new NotImplementedException(); + } + + public Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + { + throw new NotImplementedException(); + } + + public bool ScopedTryGet(K key, out Lifetime lifetime) + { + throw new NotImplementedException(); + } + + public void Trim(int itemCount) + { + throw new NotImplementedException(); + } + + public bool TryRemove(K key) + { + throw new NotImplementedException(); + } + + public bool TryUpdate(K key, V value) + { + throw new NotImplementedException(); + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs new file mode 100644 index 00000000..57461572 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Synchronized +{ + public class AtomicFactoryScopedCache : IScopedCache where V : IDisposable + { + private readonly ICache> cache; + private readonly EventsProxy eventsProxy; + + public AtomicFactoryScopedCache(ICache> cache) + { + this.cache = cache; + this.eventsProxy = new EventsProxy(cache.Events); + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public ICacheMetrics Metrics => this.cache.Metrics; + + public ICacheEvents> Events => this.eventsProxy; + + 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 EventsProxy : ICacheEvents> + { + private readonly ICacheEvents> inner; + private event EventHandler>> itemRemovedProxy; + + public EventsProxy(ICacheEvents> inner) + { + this.inner = inner; + } + + public bool IsEnabled => this.inner.IsEnabled; + + public event EventHandler>> ItemRemoved + { + add { this.Register(value); } + remove { this.UnRegister(value); } + } + + private void Register(EventHandler>> value) + { + itemRemovedProxy += value; + inner.ItemRemoved += OnItemRemoved; + } + + private void UnRegister(EventHandler>> value) + { + this.itemRemovedProxy -= value; + + if (this.itemRemovedProxy.GetInvocationList().Length == 0) + { + this.inner.ItemRemoved -= OnItemRemoved; + } + } + + private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs> e) + { + // forward from inner to outer + itemRemovedProxy.Invoke(sender, new Lru.ItemRemovedEventArgs>(e.Key, e.Value.ScopeIfCreated, e.Reason)); + } + } + } +} diff --git a/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs index 7c43068c..82c18176 100644 --- a/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs @@ -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; From 4b98a83754da9ba461806ba2a6630ab1070998b1 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 14:15:33 -0700 Subject: [PATCH 02/11] factory tests --- .../Synchronized/ScopedAtomicFactoryTests.cs | 48 +++++++++++++++++ .../Synchronized/AtomicFactoryScopedCache.cs | 51 +++++++++++++++++++ .../Synchronized/ScopedAtomicFactory.cs | 2 +- 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs index efcbd256..3407d91e 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs @@ -45,6 +45,54 @@ public void WhenInitializedWithFactoryValueIsCached() 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); + } + + + // when scope disposed try create returns false + + [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() { diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs index 57461572..76a552e0 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using BitFaster.Caching.Lru; namespace BitFaster.Caching.Synchronized { @@ -132,5 +133,55 @@ private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs>(e.Key, e.Value.ScopeIfCreated, e.Reason)); } } + + private class EventProxy : EventProxyBase, Scoped> + { + public EventProxy(ICacheEvents> inner) + : base(inner) + { + } + + protected override void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + { + //itemRemovedProxy.Invoke(sender, new Lru.ItemRemovedEventArgs>(e.Key, e.Value.ScopeIfCreated, e.Reason)); + } + } + } + + public abstract class EventProxyBase : ICacheEvents + { + private readonly ICacheEvents inner; + protected event EventHandler> itemRemovedProxy; + + public EventProxyBase(ICacheEvents inner) + { + this.inner = inner; + } + + public bool IsEnabled => this.inner.IsEnabled; + + public event EventHandler> ItemRemoved + { + add { this.Register(value); } + remove { this.UnRegister(value); } + } + + private void Register(EventHandler> value) + { + itemRemovedProxy += value; + inner.ItemRemoved += OnItemRemoved; + } + + private void UnRegister(EventHandler> value) + { + this.itemRemovedProxy -= value; + + if (this.itemRemovedProxy.GetInvocationList().Length == 0) + { + this.inner.ItemRemoved -= OnItemRemoved; + } + } + + protected abstract void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs e); } } diff --git a/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs index 82c18176..82e4047e 100644 --- a/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs @@ -42,7 +42,7 @@ public Scoped ScopeIfCreated public bool TryCreateLifetime(out Lifetime lifetime) { - if (scope?.IsDisposed ?? false || initializer == null) + if (scope?.IsDisposed ?? false || initializer != null) { lifetime = default; return false; From cb26b94c53e19f9694e1186f5ad6bfdb56e7f359 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 17:00:08 -0700 Subject: [PATCH 03/11] afs tests --- .../AtomicFactoryScopedCacheTests.cs | 265 ++++++++++++++++++ .../Synchronized/AtomicFactoryScopedCache.cs | 104 +++---- 2 files changed, 322 insertions(+), 47 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs new file mode 100644 index 00000000..3b8389e3 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -0,0 +1,265 @@ +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 + { + private const int capacity = 6; + private readonly AtomicFactoryScopedCache cache = new(new ConcurrentLru>(capacity)); + + private List>> removedItems = new(); + + // TODO: this is almost identical to ScopedCacheTests + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryScopedCache(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(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); + } + + [Fact] + public void WhenEventHandlerIsAddedThenRemovedItIsNotFired() + { + this.cache.Events.ItemRemoved += OnItemRemoved; + this.cache.Events.ItemRemoved -= OnItemRemoved; + + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.TryRemove(1); + + this.removedItems.Count.Should().Be(0); + } + + [Fact] + public void WhenTwoEventHandlersAddedThenOneRemovedEventIsFired() + { + this.cache.Events.ItemRemoved += OnItemRemoved; + this.cache.Events.ItemRemoved += OnItemRemovedThrow; + this.cache.Events.ItemRemoved -= OnItemRemovedThrow; + + 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 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 WhenKeyDoesNotExistGetOrAddAsyncAddsValue() + { + 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(); + } + + [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(); + } + + private void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + { + this.removedItems.Add(e); + } + + private void OnItemRemovedThrow(object sender, ItemRemovedEventArgs> e) + { + throw new Exception("Should never happen"); + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs index 76a552e0..85663474 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -11,12 +11,17 @@ namespace BitFaster.Caching.Synchronized public class AtomicFactoryScopedCache : IScopedCache where V : IDisposable { private readonly ICache> cache; - private readonly EventsProxy eventsProxy; + private readonly EventProxy eventProxy; public AtomicFactoryScopedCache(ICache> cache) { + if (cache == null) + { + throw new ArgumentNullException(nameof(cache)); + } + this.cache = cache; - this.eventsProxy = new EventsProxy(cache.Events); + this.eventProxy = new EventProxy(cache.Events); } public int Capacity => this.cache.Capacity; @@ -25,7 +30,7 @@ public AtomicFactoryScopedCache(ICache> cache) public ICacheMetrics Metrics => this.cache.Metrics; - public ICacheEvents> Events => this.eventsProxy; + public ICacheEvents> Events => this.eventProxy; public void AddOrUpdate(K key, V value) { @@ -93,46 +98,46 @@ public bool TryUpdate(K key, V value) return this.cache.TryUpdate(key, new ScopedAtomicFactory(value)); } - private class EventsProxy : ICacheEvents> - { - private readonly ICacheEvents> inner; - private event EventHandler>> itemRemovedProxy; - - public EventsProxy(ICacheEvents> inner) - { - this.inner = inner; - } - - public bool IsEnabled => this.inner.IsEnabled; - - public event EventHandler>> ItemRemoved - { - add { this.Register(value); } - remove { this.UnRegister(value); } - } - - private void Register(EventHandler>> value) - { - itemRemovedProxy += value; - inner.ItemRemoved += OnItemRemoved; - } - - private void UnRegister(EventHandler>> value) - { - this.itemRemovedProxy -= value; - - if (this.itemRemovedProxy.GetInvocationList().Length == 0) - { - this.inner.ItemRemoved -= OnItemRemoved; - } - } - - private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs> e) - { - // forward from inner to outer - itemRemovedProxy.Invoke(sender, new Lru.ItemRemovedEventArgs>(e.Key, e.Value.ScopeIfCreated, e.Reason)); - } - } + //private class EventsProxy : ICacheEvents> + //{ + // private readonly ICacheEvents> inner; + // private event EventHandler>> itemRemovedProxy; + + // public EventsProxy(ICacheEvents> inner) + // { + // this.inner = inner; + // } + + // public bool IsEnabled => this.inner.IsEnabled; + + // public event EventHandler>> ItemRemoved + // { + // add { this.Register(value); } + // remove { this.UnRegister(value); } + // } + + // private void Register(EventHandler>> value) + // { + // itemRemovedProxy += value; + // inner.ItemRemoved += OnItemRemoved; + // } + + // private void UnRegister(EventHandler>> value) + // { + // this.itemRemovedProxy -= value; + + // if (this.itemRemovedProxy.GetInvocationList().Length == 0) + // { + // this.inner.ItemRemoved -= OnItemRemoved; + // } + // } + + // private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs> e) + // { + // // forward from inner to outer + // itemRemovedProxy.Invoke(sender, new Lru.ItemRemovedEventArgs>(e.Key, e.Value.ScopeIfCreated, e.Reason)); + // } + //} private class EventProxy : EventProxyBase, Scoped> { @@ -141,9 +146,9 @@ public EventProxy(ICacheEvents> inner) { } - protected override void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + protected override ItemRemovedEventArgs> TranslateOnRemoved(ItemRemovedEventArgs> inner) { - //itemRemovedProxy.Invoke(sender, new Lru.ItemRemovedEventArgs>(e.Key, e.Value.ScopeIfCreated, e.Reason)); + return new Lru.ItemRemovedEventArgs>(inner.Key, inner.Value.ScopeIfCreated, inner.Reason); } } } @@ -176,12 +181,17 @@ private void UnRegister(EventHandler> value) { this.itemRemovedProxy -= value; - if (this.itemRemovedProxy.GetInvocationList().Length == 0) + if (this.itemRemovedProxy == null) { this.inner.ItemRemoved -= OnItemRemoved; } } - protected abstract void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs e); + private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs e) + { + itemRemovedProxy(sender, TranslateOnRemoved(e)); + } + + protected abstract ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs inner); } } From bedeca6b856d432835a556910bb6d3f2e6258c23 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 17:24:54 -0700 Subject: [PATCH 04/11] factor out eventbase --- .../AtomicFactoryScopedCacheTests.cs | 6 ++ .../ScopedAsyncAtomicFactoryTests.cs | 12 +-- BitFaster.Caching/CacheEventProxyBase.cs | 51 +++++++++++ .../AtomicFactoryScopedAsyncCache.cs | 64 ++++++++++++-- .../Synchronized/AtomicFactoryScopedCache.cs | 85 +------------------ .../Synchronized/ScopedAsyncAtomicFactory.cs | 22 +++-- 6 files changed, 137 insertions(+), 103 deletions(-) create mode 100644 BitFaster.Caching/CacheEventProxyBase.cs diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs index 3b8389e3..887162f1 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -53,6 +53,12 @@ public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() this.cache.Metrics.Hits.Should().Be(1); } + [Fact] + public void EventsAreEnabled() + { + this.cache.Events.IsEnabled.Should().BeTrue(); + } + [Fact] public void WhenEventHandlerIsRegisteredItIsFired() { diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs index 42c76f37..e096a708 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs @@ -19,7 +19,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 +34,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 +69,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 +79,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 +111,7 @@ public async Task WhenDisposedWhileInitResultIsDisposed() enter.SetResult(true); await resume.Task; - return holder; + return new Scoped(holder); }); await enter.Task; @@ -156,7 +156,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/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/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs index da7ac4ea..f5d0e4f0 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -2,28 +2,44 @@ 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 class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable { - public int Capacity => throw new NotImplementedException(); + private readonly ICache> cache; + private readonly EventProxy eventProxy; - public int Count => throw new NotImplementedException(); + 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 ICacheMetrics Metrics => throw new NotImplementedException(); + public int Count => this.cache.Count; - public ICacheEvents> Events => throw new NotImplementedException(); + public ICacheMetrics Metrics => this.cache.Metrics; + + public ICacheEvents> Events => this.eventProxy; public void AddOrUpdate(K key, V value) { - throw new NotImplementedException(); + this.cache.AddOrUpdate(key, new ScopedAsyncAtomicFactory(value)); } public void Clear() { - throw new NotImplementedException(); + this.cache.Clear(); } public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) @@ -31,9 +47,28 @@ public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) throw new NotImplementedException(); } - public Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) { - throw new NotImplementedException(); + 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) @@ -55,5 +90,18 @@ public bool TryUpdate(K key, V value) { throw new NotImplementedException(); } + + 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 index 85663474..0fc3f60b 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -98,48 +98,7 @@ public bool TryUpdate(K key, V value) return this.cache.TryUpdate(key, new ScopedAtomicFactory(value)); } - //private class EventsProxy : ICacheEvents> - //{ - // private readonly ICacheEvents> inner; - // private event EventHandler>> itemRemovedProxy; - - // public EventsProxy(ICacheEvents> inner) - // { - // this.inner = inner; - // } - - // public bool IsEnabled => this.inner.IsEnabled; - - // public event EventHandler>> ItemRemoved - // { - // add { this.Register(value); } - // remove { this.UnRegister(value); } - // } - - // private void Register(EventHandler>> value) - // { - // itemRemovedProxy += value; - // inner.ItemRemoved += OnItemRemoved; - // } - - // private void UnRegister(EventHandler>> value) - // { - // this.itemRemovedProxy -= value; - - // if (this.itemRemovedProxy.GetInvocationList().Length == 0) - // { - // this.inner.ItemRemoved -= OnItemRemoved; - // } - // } - - // private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs> e) - // { - // // forward from inner to outer - // itemRemovedProxy.Invoke(sender, new Lru.ItemRemovedEventArgs>(e.Key, e.Value.ScopeIfCreated, e.Reason)); - // } - //} - - private class EventProxy : EventProxyBase, Scoped> + private class EventProxy : CacheEventProxyBase, Scoped> { public EventProxy(ICacheEvents> inner) : base(inner) @@ -152,46 +111,4 @@ protected override ItemRemovedEventArgs> TranslateOnRemoved(ItemRem } } } - - public abstract class EventProxyBase : ICacheEvents - { - private readonly ICacheEvents inner; - protected event EventHandler> itemRemovedProxy; - - public EventProxyBase(ICacheEvents inner) - { - this.inner = inner; - } - - public bool IsEnabled => this.inner.IsEnabled; - - public event EventHandler> ItemRemoved - { - add { this.Register(value); } - remove { this.UnRegister(value); } - } - - private void Register(EventHandler> value) - { - itemRemovedProxy += value; - inner.ItemRemoved += OnItemRemoved; - } - - private void UnRegister(EventHandler> value) - { - this.itemRemovedProxy -= value; - - if (this.itemRemovedProxy == null) - { - this.inner.ItemRemoved -= OnItemRemoved; - } - } - - private void OnItemRemoved(object sender, Lru.ItemRemovedEventArgs e) - { - itemRemovedProxy(sender, TranslateOnRemoved(e)); - } - - protected abstract ItemRemovedEventArgs TranslateOnRemoved(ItemRemovedEventArgs inner); - } } diff --git a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs index a44fe0d4..a606e1af 100644 --- a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs @@ -22,7 +22,20 @@ 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 async Task<(bool success, Lifetime lifetime)> TryCreateLifetimeAsync(K key, Func>> valueFactory) { // if disposed, return if (scope?.IsDisposed ?? false) @@ -40,7 +53,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 +86,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 +96,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); From 22642430e623d6e6a30f1f89440e8a41b60a2a62 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 18:01:28 -0700 Subject: [PATCH 05/11] test afc --- .../ScopedCacheTests.cs | 4 +- .../Synchronized/AtomicFactoryCacheTests.cs | 163 ++++++++++++++++++ .../ScopedAsyncAtomicFactoryTests.cs | 19 ++ .../Synchronized/ScopedAtomicFactoryTests.cs | 3 - .../Synchronized/AtomicFactoryAsyncCache.cs | 24 ++- .../Synchronized/AtomicFactoryCache.cs | 24 ++- 6 files changed, 228 insertions(+), 9 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryCacheTests.cs diff --git a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs index 10e90379..b55155cc 100644 --- a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs @@ -180,14 +180,14 @@ public void GetOrAddDisposedScopeThrows() } [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(); + await getOrAdd.Should().ThrowAsync(); } [Fact] 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/ScopedAsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs index e096a708..e0ae65f7 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs @@ -12,6 +12,25 @@ 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 async Task WhenCreateFromValueLifetimeContainsValue() { diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs index 3407d91e..89f9a323 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAtomicFactoryTests.cs @@ -64,9 +64,6 @@ public void WhenScopeIsCreatedScopeIfCreatedReturnsScope() lifetime.Value.Should().Be(expectedDisposable); } - - // when scope disposed try create returns false - [Fact] public void WhenNotInitTryCreateReturnsFalse() { diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs index b1d3b224..4b5c87c1 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -3,16 +3,24 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using BitFaster.Caching.Lru; namespace BitFaster.Caching.Synchronized { public 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; @@ -21,8 +29,7 @@ public AtomicFactoryAsyncCache(ICache> cache) public ICacheMetrics Metrics => cache.Metrics; - // need to dispatch different events for this - public ICacheEvents Events => throw new Exception(); + public ICacheEvents Events => this.eventProxy; public void AddOrUpdate(K key, V value) { @@ -74,5 +81,18 @@ 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 index df63eb02..d903d076 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs @@ -3,16 +3,24 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using BitFaster.Caching.Lru; namespace BitFaster.Caching.Synchronized { public 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; @@ -21,8 +29,7 @@ public AtomicFactoryCache(ICache> cache) public ICacheMetrics Metrics => this.cache.Metrics; - // TODO: wrapper - public ICacheEvents Events => throw new NotImplementedException(); // this.cache.Events; + public ICacheEvents Events => this.eventProxy; public void AddOrUpdate(K key, V value) { @@ -74,5 +81,18 @@ 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); + } + } } } From ca75e5265a8df74146b1c28879895b977b08877a Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 18:54:40 -0700 Subject: [PATCH 06/11] test afa --- .../AtomicFactoryAsyncCacheTests.cs | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs 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); + } + } +} From 25fcf5632a09f7c474602a98c8595c0bd422167b Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 19:12:44 -0700 Subject: [PATCH 07/11] test afsa --- .../AtomicFactoryScopedAsyncCacheTests.cs | 221 ++++++++++++++++++ .../AtomicFactoryScopedCacheTests.cs | 4 +- .../AtomicFactoryScopedAsyncCache.cs | 17 +- .../Synchronized/ScopedAsyncAtomicFactory.cs | 12 + 4 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs new file mode 100644 index 00000000..fe700f36 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs @@ -0,0 +1,221 @@ +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 + { + private const int capacity = 6; + private readonly AtomicFactoryScopedAsyncCache cache = new(new ConcurrentLru>(capacity)); + + private List>> removedItems = new(); + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new AtomicFactoryScopedAsyncCache(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(1, new Disposable()); + + this.cache.Count.Should().Be(1); + } + + [Fact] + public async Task WhenItemIsAddedThenLookedUpMetricsAreCorrect() + { + this.cache.AddOrUpdate(1, new Disposable()); + await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(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); + } + + [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 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(); + } + + [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(); + } + + private void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + { + this.removedItems.Add(e); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs index 887162f1..c1c14837 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -53,6 +53,7 @@ public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() this.cache.Metrics.Hits.Should().Be(1); } + // TODO: test via base [Fact] public void EventsAreEnabled() { @@ -189,7 +190,7 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue() } [Fact] - public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() + public async Task WhenKeyDoesNotExistGetOrAddAsyncThrows() { Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); }; @@ -202,7 +203,6 @@ public void GetOrAddDisposedScopeThrows() var scope = new Scoped(new Disposable()); scope.Dispose(); - Action getOrAdd = () => { this.cache.ScopedGetOrAdd(1, k => scope); }; getOrAdd.Should().Throw(); diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs index f5d0e4f0..06b45613 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -73,22 +73,31 @@ public async Task> ScopedGetOrAddAsync(K key, Func public bool ScopedTryGet(K key, out Lifetime lifetime) { - throw new NotImplementedException(); + if (this.cache.TryGet(key, out var scope)) + { + if (scope.TryCreateLifetime(out lifetime)) + { + return true; + } + } + + lifetime = default; + return false; } public void Trim(int itemCount) { - throw new NotImplementedException(); + this.cache.Trim(itemCount); } public bool TryRemove(K key) { - throw new NotImplementedException(); + return this.cache.TryRemove(key); } public bool TryUpdate(K key, V value) { - throw new NotImplementedException(); + return this.cache.TryUpdate(key, new ScopedAsyncAtomicFactory(value)); } private class EventProxy : CacheEventProxyBase, Scoped> diff --git a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs index a606e1af..7dc43bf2 100644 --- a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs @@ -35,6 +35,18 @@ public Scoped ScopeIfCreated } } + // TODO: unit tests + 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 From c8a72c6bf1605465e548e1173698b9a8b13b3077 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 19:21:29 -0700 Subject: [PATCH 08/11] saaf tests --- .../ScopedAsyncAtomicFactoryTests.cs | 17 +++++++++++++++++ .../Synchronized/ScopedAsyncAtomicFactory.cs | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs index e0ae65f7..bd04a979 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/ScopedAsyncAtomicFactoryTests.cs @@ -31,6 +31,23 @@ public void WhenScopeIsCreatedScopeIfCreatedReturnsScope() 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() { diff --git a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs index 7dc43bf2..5bbc19f4 100644 --- a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs +++ b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs @@ -35,7 +35,6 @@ public Scoped ScopeIfCreated } } - // TODO: unit tests public bool TryCreateLifetime(out Lifetime lifetime) { if (scope?.IsDisposed ?? false || initializer != null) From fa52cd88f2f7d51b6fdc6eb84c1c6ed9aad275f5 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 19:40:30 -0700 Subject: [PATCH 09/11] event proxy tests --- .../CacheEventProxyBaseTests.cs | 102 ++++++++++++++++++ .../AtomicFactoryScopedCacheTests.cs | 37 ------- 2 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/CacheEventProxyBaseTests.cs 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/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs index c1c14837..448c4481 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -53,13 +53,6 @@ public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() this.cache.Metrics.Hits.Should().Be(1); } - // TODO: test via base - [Fact] - public void EventsAreEnabled() - { - this.cache.Events.IsEnabled.Should().BeTrue(); - } - [Fact] public void WhenEventHandlerIsRegisteredItIsFired() { @@ -71,31 +64,6 @@ public void WhenEventHandlerIsRegisteredItIsFired() this.removedItems.First().Key.Should().Be(1); } - [Fact] - public void WhenEventHandlerIsAddedThenRemovedItIsNotFired() - { - this.cache.Events.ItemRemoved += OnItemRemoved; - this.cache.Events.ItemRemoved -= OnItemRemoved; - - this.cache.AddOrUpdate(1, new Disposable()); - this.cache.TryRemove(1); - - this.removedItems.Count.Should().Be(0); - } - - [Fact] - public void WhenTwoEventHandlersAddedThenOneRemovedEventIsFired() - { - this.cache.Events.ItemRemoved += OnItemRemoved; - this.cache.Events.ItemRemoved += OnItemRemovedThrow; - this.cache.Events.ItemRemoved -= OnItemRemovedThrow; - - this.cache.AddOrUpdate(1, new Disposable()); - this.cache.TryRemove(1); - - this.removedItems.First().Key.Should().Be(1); - } - [Fact] public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() { @@ -262,10 +230,5 @@ private void OnItemRemoved(object sender, ItemRemovedEventArgs> e) - { - throw new Exception("Should never happen"); - } } } From e318372bed018286c122402bb3f2f04794217c53 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 13:15:30 -0700 Subject: [PATCH 10/11] dedupe --- .../ScopedCacheTestBase.cs | 178 ++++++++++++++++++ .../ScopedCacheTests.cs | 165 +--------------- .../AtomicFactoryScopedAsyncCacheTests.cs | 164 +--------------- .../AtomicFactoryScopedCacheTests.cs | 166 +--------------- 4 files changed, 193 insertions(+), 480 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/ScopedCacheTestBase.cs 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 b55155cc..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,7 +58,6 @@ public void GetOrAddDisposedScopeThrows() var scope = new Scoped(new Disposable()); scope.Dispose(); - Action getOrAdd = () => { this.cache.ScopedGetOrAdd(1, k => scope); }; getOrAdd.Should().Throw(); @@ -189,44 +73,5 @@ public async Task GetOrAddAsyncDisposedScopeThrows() await 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(); - } } } diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs index fe700f36..8818d11e 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs @@ -10,12 +10,12 @@ namespace BitFaster.Caching.UnitTests.Synchronized { - public class AtomicFactoryScopedAsyncCacheTests + public class AtomicFactoryScopedAsyncCacheTests : ScopedCacheTestBase { - private const int capacity = 6; - private readonly AtomicFactoryScopedAsyncCache cache = new(new ConcurrentLru>(capacity)); - - private List>> removedItems = new(); + public AtomicFactoryScopedAsyncCacheTests() + : base(new AtomicFactoryScopedAsyncCache(new ConcurrentLru>(capacity))) + { + } [Fact] public void WhenInnerCacheIsNullCtorThrows() @@ -25,116 +25,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 async Task WhenItemIsAddedThenLookedUpMetricsAreCorrect() - { - this.cache.AddOrUpdate(1, new Disposable()); - await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(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); - } - - [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 async Task WhenScopeIsDisposedTryGetReturnsFalse() { @@ -173,49 +63,5 @@ public async Task GetOrAddAsyncDisposedScopeThrows() await 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(); - } - - private void OnItemRemoved(object sender, ItemRemovedEventArgs> e) - { - this.removedItems.Add(e); - } } } diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs index 448c4481..e0927bdc 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -10,14 +10,12 @@ namespace BitFaster.Caching.UnitTests.Synchronized { - public class AtomicFactoryScopedCacheTests + public class AtomicFactoryScopedCacheTests : ScopedCacheTestBase { - private const int capacity = 6; - private readonly AtomicFactoryScopedCache cache = new(new ConcurrentLru>(capacity)); - - private List>> removedItems = new(); - - // TODO: this is almost identical to ScopedCacheTests + public AtomicFactoryScopedCacheTests() + : base(new AtomicFactoryScopedCache(new ConcurrentLru>(capacity))) + { + } [Fact] public void WhenInnerCacheIsNullCtorThrows() @@ -27,116 +25,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); - } - - [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() { @@ -186,49 +74,5 @@ public void GetOrAddAsyncDisposedScopeThrows() 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(); - } - - private void OnItemRemoved(object sender, ItemRemovedEventArgs> e) - { - this.removedItems.Add(e); - } } } From 03f96c75478c20c3642935717ce095d16b77b5d3 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 13:19:21 -0700 Subject: [PATCH 11/11] seal --- BitFaster.Caching/Synchronized/AsyncAtomicFactory.cs | 2 +- BitFaster.Caching/Synchronized/AtomicFactory.cs | 2 +- BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs | 2 +- BitFaster.Caching/Synchronized/AtomicFactoryCache.cs | 2 +- BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs | 2 +- BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs | 2 +- BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs | 2 +- BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) 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 index 4b5c87c1..41c141c5 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Synchronized { - public class AtomicFactoryAsyncCache : ICache + public sealed class AtomicFactoryAsyncCache : ICache { private readonly ICache> cache; private readonly EventProxy eventProxy; diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs index d903d076..df03b91f 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Synchronized { - public class AtomicFactoryCache : ICache + public sealed class AtomicFactoryCache : ICache { private readonly ICache> cache; private readonly EventProxy eventProxy; diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs index 06b45613..4f62e203 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -8,7 +8,7 @@ namespace BitFaster.Caching.Synchronized { - public class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable + public sealed class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable { private readonly ICache> cache; private readonly EventProxy eventProxy; diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs index 0fc3f60b..3e317441 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -8,7 +8,7 @@ namespace BitFaster.Caching.Synchronized { - public class AtomicFactoryScopedCache : IScopedCache where V : IDisposable + public sealed class AtomicFactoryScopedCache : IScopedCache where V : IDisposable { private readonly ICache> cache; private readonly EventProxy eventProxy; diff --git a/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAsyncAtomicFactory.cs index 5bbc19f4..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; diff --git a/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs b/BitFaster.Caching/Synchronized/ScopedAtomicFactory.cs index 82e4047e..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;