From 324a75cfec33e17b3ab4493dcc841460abdfa4f6 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 22:58:47 -0800 Subject: [PATCH 1/4] try rem --- .../Atomic/AsyncAtomicFactoryTests.cs | 52 +++++++++++++++++++ .../Atomic/AsyncAtomicFactory.cs | 29 +++++++++++ .../Atomic/AtomicFactoryAsyncCache.cs | 29 +++++++++++ BitFaster.Caching/IAsyncCache.cs | 17 +++++- 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs index 7da59a28..08f3c297 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AsyncAtomicFactoryTests.cs @@ -112,6 +112,58 @@ public async Task WhenCallersRunConcurrentlyResultIsFromWinner() (await second).Should().Be(result); winnerCount.Should().Be(1); + } + + [Fact] + public void WhenValueNotCreatedHashCodeIsZero() + { + new AsyncAtomicFactory() + .GetHashCode() + .Should().Be(0); + } + + [Fact] + public void WhenValueCreatedHashCodeIsValueHashCode() + { + new AsyncAtomicFactory(1) + .GetHashCode() + .Should().Be(1); + } + + [Fact] + public void WhenValueNotCreatedEqualsFalse() + { + var a = new AsyncAtomicFactory(); + var b = new AsyncAtomicFactory(); + + a.Equals(b).Should().BeFalse(); + } + + [Fact] + public void WhenOtherValueNotCreatedEqualsFalse() + { + var a = new AsyncAtomicFactory(1); + var b = new AsyncAtomicFactory(); + + a.Equals(b).Should().BeFalse(); + } + + [Fact] + public void WhenArgNullEqualsFalse() + { + new AsyncAtomicFactory(1) + .Equals(null) + .Should().BeFalse(); + } + + [Fact] + public void WhenArgObjectValuesAreSameEqualsTrue() + { + object other = new AsyncAtomicFactory(1); + + new AsyncAtomicFactory(1) + .Equals(other) + .Should().BeTrue(); } } } diff --git a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs index c8ff84a4..6fd7c27e 100644 --- a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -92,6 +93,34 @@ public V ValueIfCreated } } + /// + public override bool Equals(object obj) + { + return Equals(obj as AsyncAtomicFactory); + } + + /// + public bool Equals(AsyncAtomicFactory other) + { + if (other is null || !IsValueCreated || !other.IsValueCreated) + { + return false; + } + + return EqualityComparer.Default.Equals(ValueIfCreated, other.ValueIfCreated); + } + + /// + public override int GetHashCode() + { + if (!IsValueCreated) + { + return 0; + } + + return ValueIfCreated.GetHashCode(); + } + private async ValueTask CreateValueAsync(K key, TFactory valueFactory) where TFactory : struct, IAsyncValueFactory { var init = Volatile.Read(ref initializer); diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 38e2cb18..5d3e94ba 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -109,7 +109,36 @@ public bool TryGet(K key, out V value) public bool TryRemove(K key) { return cache.TryRemove(key); + } + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + /// + /// + ///If the value factory is still executing, returns false. + /// + public bool TryRemove(KeyValuePair item) + { + var kvp = new KeyValuePair>(item.Key, new AsyncAtomicFactory(item.Value)); + return cache.TryRemove(kvp); + } + + /// + /// + /// If the value factory is still executing, the default value will be returned. + /// + public bool TryRemove(K key, out V value) + { + if (cache.TryRemove(key, out var atomic)) + { + value = atomic.ValueIfCreated; + return true; + } + + value = default; + return false; } +#endif /// public bool TryUpdate(K key, V value) diff --git a/BitFaster.Caching/IAsyncCache.cs b/BitFaster.Caching/IAsyncCache.cs index 6f4210a6..59299d60 100644 --- a/BitFaster.Caching/IAsyncCache.cs +++ b/BitFaster.Caching/IAsyncCache.cs @@ -64,7 +64,22 @@ public interface IAsyncCache : IEnumerable> /// The factory function used to asynchronously generate a value for the key. /// An argument value to pass into valueFactory. /// A task that represents the asynchronous GetOrAdd operation. - ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) => this.GetOrAddAsync(key, k => valueFactory(k, factoryArgument)); + ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) => this.GetOrAddAsync(key, k => valueFactory(k, factoryArgument)); + + /// + /// Attempts to remove and return the value that has the specified key. + /// + /// The key of the element to remove. + /// When this method returns, contains the object removed, or the default value of the value type if key does not exist. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key, out V value) => throw new NotSupportedException(); + + /// + /// Attempts to remove the specified key value pair. + /// + /// The item to remove. + /// true if the item was removed successfully; otherwise, false. + bool TryRemove(KeyValuePair item) => throw new NotSupportedException(); #endif /// From 3d77c273367d7c00dc873c3eecdc5aec8c4b95bc Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 23:06:10 -0800 Subject: [PATCH 2/4] atomic cache tests --- .../Atomic/AtomicFactoryAsyncCacheTests.cs | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs index 8fd427e3..d2b58d96 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheTests.cs @@ -15,11 +15,17 @@ namespace BitFaster.Caching.UnitTests.Atomic public class AtomicFactoryAsyncCacheTests { private const int capacity = 6; - private readonly AtomicFactoryAsyncCache cache = new(new ConcurrentLru>(capacity)); + private readonly ConcurrentLru> innerCache = new(capacity); + private readonly AtomicFactoryAsyncCache cache; private List> removedItems = new(); private List> updatedItems = new(); + public AtomicFactoryAsyncCacheTests() + { + cache = new(innerCache); + } + [Fact] public void WhenInnerCacheIsNullCtorThrows() { @@ -250,8 +256,53 @@ public async Task WhenFactoryThrowsEmptyKeyIsNotEnumerable() catch { } cache.Keys.Count().Should().Be(0); + } + + // backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenRemovedValueIsReturned() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(1, out var value); + + value.Should().Be(1); } + [Fact] + public void WhenNotRemovedValueIsDefault() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(2, out var value); + + value.Should().Be(0); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesntMatchDontRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 2)).Should().BeFalse(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueDoesMatchThenRemove() + { + this.cache.AddOrUpdate(1, 1); + this.cache.TryRemove(new KeyValuePair(1, 1)).Should().BeTrue(); + } + + [Fact] + public void WhenRemoveKeyValueAndValueIsNotCreatedDoesNotRemove() + { + // seed the inner cache with an not yet created value + this.innerCache.AddOrUpdate(1, new AsyncAtomicFactory()); + + // try to remove with the default value (0) + this.cache.TryRemove(new KeyValuePair(1, 0)).Should().BeFalse(); + } +#endif + private void OnItemRemoved(object sender, ItemRemovedEventArgs e) { this.removedItems.Add(e); From 9a5c2705d3814466286ab0888e607429a6c473b4 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 23:10:16 -0800 Subject: [PATCH 3/4] IEquatable --- BitFaster.Caching/Atomic/AsyncAtomicFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs index 6fd7c27e..4feb3ee0 100644 --- a/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/AsyncAtomicFactory.cs @@ -13,7 +13,7 @@ namespace BitFaster.Caching.Atomic /// The type of the key. /// The type of the value. [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public sealed class AsyncAtomicFactory + public sealed class AsyncAtomicFactory : IEquatable> { private Initializer initializer; From ac1f834d06faee37ae9cde8598d707a9ab7bb371 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 12 Nov 2023 23:19:17 -0800 Subject: [PATCH 4/4] test default impl --- BitFaster.Caching.UnitTests/CacheTests.cs | 250 ++++++++++++---------- 1 file changed, 136 insertions(+), 114 deletions(-) diff --git a/BitFaster.Caching.UnitTests/CacheTests.cs b/BitFaster.Caching.UnitTests/CacheTests.cs index 1840e301..5cdeafa0 100644 --- a/BitFaster.Caching.UnitTests/CacheTests.cs +++ b/BitFaster.Caching.UnitTests/CacheTests.cs @@ -1,119 +1,141 @@ - -using System; + +using System; using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Moq; -using Xunit; - -namespace BitFaster.Caching.UnitTests -{ - // Tests for interface default implementations. - public class CacheTests - { -// backcompat: remove conditional compile -#if NETCOREAPP3_0_OR_GREATER - [Fact] - public void WhenCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func, int> evaluate = (k, f) => f(k); - cache.Setup(c => c.GetOrAdd(It.IsAny(), It.IsAny>())).Returns(evaluate); - - cache.Object.GetOrAdd( - 1, - (k, a) => k + a, - 2).Should().Be(3); +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Xunit; + +namespace BitFaster.Caching.UnitTests +{ + // Tests for interface default implementations. + public class CacheTests + { +// backcompat: remove conditional compile +#if NETCOREAPP3_0_OR_GREATER + [Fact] + public void WhenCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func, int> evaluate = (k, f) => f(k); + cache.Setup(c => c.GetOrAdd(It.IsAny(), It.IsAny>())).Returns(evaluate); + + cache.Object.GetOrAdd( + 1, + (k, a) => k + a, + 2).Should().Be(3); + } + + [Fact] + public void WhenCacheInterfaceDefaultTryRemoveKeyThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(1, out var value); }; + + tryRemove.Should().Throw(); + } + + [Fact] + public void WhenCacheInterfaceDefaultTryRemoveKeyValueThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(new KeyValuePair(1, 1)); }; + + tryRemove.Should().Throw(); + } + + [Fact] + public async Task WhenAsyncCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func>, ValueTask> evaluate = (k, f) => new ValueTask(f(k)); + cache.Setup(c => c.GetOrAddAsync(It.IsAny(), It.IsAny>>())).Returns(evaluate); + + var r = await cache.Object.GetOrAddAsync( + 1, + (k, a) => Task.FromResult(k + a), + 2); + + r.Should().Be(3); + } + + [Fact] + public void WhenAsyncCacheInterfaceDefaultTryRemoveKeyThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(1, out var value); }; + + tryRemove.Should().Throw(); + } + + [Fact] + public void WhenAsyncCacheInterfaceDefaultTryRemoveKeyValueThrows() + { + var cache = new Mock>(); + cache.CallBase = true; + + Action tryRemove = () => { cache.Object.TryRemove(new KeyValuePair(1, 1)); }; + + tryRemove.Should().Throw(); } - [Fact] - public void WhenCacheInterfaceDefaultTryRemoveKeyThrows() - { - var cache = new Mock>(); - cache.CallBase = true; - - Action tryRemove = () => { cache.Object.TryRemove(1, out var value); }; - - tryRemove.Should().Throw(); + [Fact] + public void WhenScopedCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func>, Lifetime> evaluate = (k, f) => + { + var scope = f(k); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + return lifetime; + }; + + cache.Setup(c => c.ScopedGetOrAdd(It.IsAny(), It.IsAny>>())).Returns(evaluate); + + var l = cache.Object.ScopedGetOrAdd( + 1, + (k, a) => new Scoped(new Disposable(k + a)), + 2); + + l.Value.State.Should().Be(3); } - [Fact] - public void WhenCacheInterfaceDefaultTryRemoveKeyValueThrows() - { - var cache = new Mock>(); - cache.CallBase = true; - - Action tryRemove = () => { cache.Object.TryRemove(new KeyValuePair(1, 1)); }; - - tryRemove.Should().Throw(); - } - - [Fact] - public async Task WhenAsyncCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func>, ValueTask> evaluate = (k, f) => new ValueTask(f(k)); - cache.Setup(c => c.GetOrAddAsync(It.IsAny(), It.IsAny>>())).Returns(evaluate); - - var r = await cache.Object.GetOrAddAsync( - 1, - (k, a) => Task.FromResult(k + a), - 2); - - r.Should().Be(3); - } - - [Fact] - public void WhenScopedCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func>, Lifetime> evaluate = (k, f) => - { - var scope = f(k); - scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); - return lifetime; - }; - - cache.Setup(c => c.ScopedGetOrAdd(It.IsAny(), It.IsAny>>())).Returns(evaluate); - - var l = cache.Object.ScopedGetOrAdd( - 1, - (k, a) => new Scoped(new Disposable(k + a)), - 2); - - l.Value.State.Should().Be(3); - } - - [Fact] - public async Task WhenScopedAsyncCacheInterfaceDefaultGetOrAddFallback() - { - var cache = new Mock>(); - cache.CallBase = true; - - Func>>, ValueTask>> evaluate = async (k, f) => - { - var scope = await f(k); - scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); - return lifetime; - }; - - cache - .Setup(c => c.ScopedGetOrAddAsync(It.IsAny(), It.IsAny>>>())) - .Returns(evaluate); - - var lifetime = await cache.Object.ScopedGetOrAddAsync( - 1, - (k, a) => Task.FromResult(new Scoped(new Disposable(k + a))), - 2); - - lifetime.Value.State.Should().Be(3); - } -#endif - } -} + [Fact] + public async Task WhenScopedAsyncCacheInterfaceDefaultGetOrAddFallback() + { + var cache = new Mock>(); + cache.CallBase = true; + + Func>>, ValueTask>> evaluate = async (k, f) => + { + var scope = await f(k); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + return lifetime; + }; + + cache + .Setup(c => c.ScopedGetOrAddAsync(It.IsAny(), It.IsAny>>>())) + .Returns(evaluate); + + var lifetime = await cache.Object.ScopedGetOrAddAsync( + 1, + (k, a) => Task.FromResult(new Scoped(new Disposable(k + a))), + 2); + + lifetime.Value.State.Should().Be(3); + } +#endif + } +}