diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 48d6eeb2..7d0a651c 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using BitFaster.Caching.Lru; +using BitFaster.Caching.Synchronized; using FluentAssertions; using Xunit; @@ -14,7 +15,7 @@ public class ConcurrentLruBuilderTests [Fact] public void TestFastLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .Build(); lru.Should().BeOfType>(); @@ -23,7 +24,7 @@ public void TestFastLru() [Fact] public void TestMetricsLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithMetrics() .Build(); @@ -33,7 +34,7 @@ public void TestMetricsLru() [Fact] public void TestFastTLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) .Build(); @@ -43,7 +44,7 @@ public void TestFastTLru() [Fact] public void TestMetricsTLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) .WithMetrics() .Build(); @@ -53,22 +54,55 @@ public void TestMetricsTLru() } [Fact] - public void TestScoped() + public void AsAsyncTestFastLru() { - var lru = new ConcurrentLruBuilder() - .WithScopedValues() - .WithCapacity(3) - .WithExpireAfterWrite(TimeSpan.FromMinutes(1)) + IAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() .Build(); - lru.Should().BeOfType>(); - lru.Capacity.Should().Be(3); + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestMetricsLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithMetrics() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestFastTLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); } + [Fact] + public void AsAsyncTestMetricsTLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithMetrics() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(128); + } + + [Fact] public void TestComparer() { - var fastLru = new ConcurrentLruBuilder() + ICache fastLru = new ConcurrentLruBuilder() .WithKeyComparer(StringComparer.OrdinalIgnoreCase) .Build(); @@ -90,7 +124,7 @@ public void TestConcurrencyLevel() [Fact] public void TestIntCapacity() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithCapacity(3) .Build(); @@ -100,11 +134,277 @@ public void TestIntCapacity() [Fact] public void TestPartitionCapacity() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithCapacity(new FavorFrequencyPartition(6)) .Build(); lru.Capacity.Should().Be(6); } + + // There are 15 combinations to test: + // ----------------------------- + //1 WithAtomic + //2 WithScoped + //3 AsAsync + // + // ----------------------------- + //4 WithAtomic + // WithScoped + // + //5 WithScoped + // WithAtomic + // + //6 AsAsync + // WithScoped + // + //7 WithScoped + // AsAsync + // + //8 WithAtomic + // AsAsync + // + //9 AsAsync + // WithAtomic + // + // ----------------------------- + //10 WithAtomic + // WithScoped + // AsAsync + // + //11 WithAtomic + // AsAsync + // WithScoped + // + //12 WithScoped + // WithAtomic + // AsAsync + // + //13 WithScoped + // AsAsync + // WithAtomic + // + //14 AsAsync + // WithScoped + // WithAtomic + // + //15 AsAsync + // WithAtomic + // WithScoped + + // 1 + [Fact] + public void WithScopedValues() + { + IScopedCache lru = new ConcurrentLruBuilder() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(3); + } + + // 2 + [Fact] + public void WithAtomicFactory() + { + ICache lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + } + + // 3 + [Fact] + public void AsAsync() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 4 + [Fact] + public void WithAtomicWithScope() + { + IScopedCache lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(3); + } + + // 5 + [Fact] + public void WithScopedWithAtomic() + { + IScopedCache lru = new ConcurrentLruBuilder() + .WithScopedValues() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(3); + } + + // 6 + [Fact] + public void AsAsyncWithScoped() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + + lru.Capacity.Should().Be(3); + } + + // 7 + [Fact] + public void WithScopedAsAsync() + { + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .WithScopedValues() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + lru.Capacity.Should().Be(3); + } + + // 8 + [Fact] + public void WithAtomicAsAsync() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 9 + [Fact] + public void AsAsyncWithAtomic() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 10 + [Fact] + public void WithAtomicWithScopedAsAsync() + { + // TODO: this will not resolve a TLru + + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .WithScopedValues() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 11 + [Fact] + public void WithAtomicAsAsyncWithScoped() + { + // TODO: this will not resolve a TLru + + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .AsAsyncCache() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 12 + [Fact] + public void WithScopedWithAtomicAsAsync() + { + // TODO: this will not resolve a TLru + + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .WithScopedValues() + .WithAtomicCreate() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 13 + [Fact] + public void WithScopedAsAsyncWithAtomic() + { + // TODO: this will not resolve a TLru + + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .WithScopedValues() + .AsAsyncCache() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 14 + [Fact] + public void AsAsyncWithScopedWithAtomic() + { + // TODO: this will not resolve a TLru + + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithScopedValues() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 15 + [Fact] + public void AsAsyncWithAtomicWithScoped() + { + // TODO: this will not resolve a TLru + + IScopedAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithAtomicCreate() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } } } diff --git a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs new file mode 100644 index 00000000..556701c1 --- /dev/null +++ b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.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 ScopedAsyncCacheTestBase + { + protected const int capacity = 6; + protected readonly IScopedAsyncCache cache; + + protected List>> removedItems = new(); + + protected ScopedAsyncCacheTestBase(IScopedAsyncCache 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/ScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs new file mode 100644 index 00000000..1908eedd --- /dev/null +++ b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs @@ -0,0 +1,58 @@ +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 class ScopedAsyncCacheTests : ScopedAsyncCacheTestBase + { + public ScopedAsyncCacheTests() + : base(new ScopedAsyncCache(new ConcurrentLru>(capacity))) + { + } + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new ScopedAsyncCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public async Task WhenScopeIsDisposedTryGetReturnsFalse() + { + var scope = new Scoped(new Disposable()); + + await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); + + scope.Dispose(); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); + } + + [Fact] + public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() + { + await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + } + + [Fact] + public async Task GetOrAddAsyncDisposedScopeThrows() + { + var scope = new Scoped(new Disposable()); + scope.Dispose(); + + Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; + + await getOrAdd.Should().ThrowAsync(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs index 0adf2c6a..2345b48f 100644 --- a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs @@ -44,14 +44,6 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue() this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); } - [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 void GetOrAddDisposedScopeThrows() { @@ -62,16 +54,5 @@ public void GetOrAddDisposedScopeThrows() getOrAdd.Should().Throw(); } - - [Fact] - public async Task GetOrAddAsyncDisposedScopeThrows() - { - var scope = new Scoped(new Disposable()); - scope.Dispose(); - - Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; - - await getOrAdd.Should().ThrowAsync(); - } } } diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs index e1f132a9..c4793d1b 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs @@ -97,14 +97,6 @@ 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() { diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs index 8818d11e..0f28af16 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs @@ -10,8 +10,10 @@ namespace BitFaster.Caching.UnitTests.Synchronized { - public class AtomicFactoryScopedAsyncCacheTests : ScopedCacheTestBase + public class AtomicFactoryScopedAsyncCacheTests : ScopedAsyncCacheTestBase { + + public AtomicFactoryScopedAsyncCacheTests() : base(new AtomicFactoryScopedAsyncCache(new ConcurrentLru>(capacity))) { @@ -37,14 +39,6 @@ public async Task WhenScopeIsDisposedTryGetReturnsFalse() 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() { diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs index e0927bdc..f56d5896 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -45,14 +45,6 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue() this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); } - [Fact] - public async Task WhenKeyDoesNotExistGetOrAddAsyncThrows() - { - Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); }; - - await getOrAdd.Should().ThrowAsync(); - } - [Fact] public void GetOrAddDisposedScopeThrows() { @@ -63,16 +55,5 @@ public void GetOrAddDisposedScopeThrows() getOrAdd.Should().Throw(); } - - [Fact] - public void GetOrAddAsyncDisposedScopeThrows() - { - var scope = new Scoped(new Disposable()); - scope.Dispose(); - - Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; - - getOrAdd.Should().ThrowAsync(); - } } } diff --git a/BitFaster.Caching/IAsyncCache.cs b/BitFaster.Caching/IAsyncCache.cs new file mode 100644 index 00000000..7e547cb1 --- /dev/null +++ b/BitFaster.Caching/IAsyncCache.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// Represents a generic cache of key/value pairs. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public interface IAsyncCache + { + /// + /// Gets the total number of items that can be stored in the cache. + /// + int Capacity { get; } + + /// + /// Gets the number of items currently held in the cache. + /// + int Count { get; } + + /// + /// Gets the cache metrics. + /// + ICacheMetrics Metrics { get; } + + /// + /// Gets the cache events. + /// + ICacheEvents Events { get; } + + /// + /// Attempts to get the value associated with the specified key from the cache. + /// + /// The key of the value to get. + /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed. + /// true if the key was found in the cache; otherwise, false. + bool TryGet(K key, out V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// A task that represents the asynchronous GetOrAdd operation. + Task GetOrAddAsync(K key, Func> valueFactory); + + /// + /// Attempts to remove the value that has the specified key. + /// + /// The key of the element to remove. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key); + + /// + /// Attempts to update the value that has the specified key. + /// + /// The key of the element to update. + /// The new value. + /// true if the object was updated successfully; otherwise, false. + bool TryUpdate(K key, V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the + /// key already exists. + /// + /// The key of the element to update. + /// The new value. + void AddOrUpdate(K key, V value); + + /// + /// Removes all keys and values from the cache. + /// + void Clear(); + + /// + /// Trim the specified number of items from the cache. + /// + /// The number of items to remove. + void Trim(int itemCount); + } +} diff --git a/BitFaster.Caching/ICache.cs b/BitFaster.Caching/ICache.cs index 8200bafe..dbb9e892 100644 --- a/BitFaster.Caching/ICache.cs +++ b/BitFaster.Caching/ICache.cs @@ -51,15 +51,6 @@ public interface ICache /// in the cache, or the new value if the key was not in the cache. V GetOrAdd(K key, Func valueFactory); - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// A task that represents the asynchronous GetOrAdd operation. - Task GetOrAddAsync(K key, Func> valueFactory); - /// /// Attempts to remove the value that has the specified key. /// diff --git a/BitFaster.Caching/IScopedAsyncCache.cs b/BitFaster.Caching/IScopedAsyncCache.cs new file mode 100644 index 00000000..4906d441 --- /dev/null +++ b/BitFaster.Caching/IScopedAsyncCache.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// Represents a generic cache of key/scoped IDisposable value pairs. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public interface IScopedAsyncCache where V : IDisposable + { + /// + /// Gets the total number of items that can be stored in the cache. + /// + int Capacity { get; } + + /// + /// Gets the number of items currently held in the cache. + /// + int Count { get; } + + /// + /// Gets the cache metrics. + /// + ICacheMetrics Metrics { get; } + + /// + /// Gets the cache events. + /// + /// + /// Events expose the Scoped instance wrapping each value. To keep the value alive (blocking Dispose), try to + /// create a Lifetime from the scope. + /// + ICacheEvents> Events { get; } + + /// + /// Attempts to create a lifetime for the value associated with the specified key from the cache + /// + /// The key of the value to get. + /// When this method returns, contains a lifetime for the object from the cache that + /// has the specified key, or the default value of the type if the operation failed. + /// true if the key was found in the cache; otherwise, false. + bool ScopedTryGet(K key, out Lifetime lifetime); + + /// + /// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either + /// the new value, or the existing value if the key already exists. + /// + /// The key of the element to add. + /// The factory function used to asynchronously generate a scoped value for the key. + /// A task that represents the asynchronous ScopedGetOrAdd operation. + Task> ScopedGetOrAddAsync(K key, Func>> valueFactory); + + /// + /// Attempts to remove the value that has the specified key. + /// + /// The key of the element to remove. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key); + + /// + /// Attempts to update the value that has the specified key. + /// + /// The key of the element to update. + /// The new value. + /// true if the object was updated successfully; otherwise, false. + bool TryUpdate(K key, V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the + /// key already exists. + /// + /// The key of the element to update. + /// The new value. + void AddOrUpdate(K key, V value); + + /// + /// Removes all keys and values from the cache. + /// + void Clear(); + + /// + /// Trim the specified number of items from the cache. + /// + /// The number of items to remove. + void Trim(int itemCount); + } +} diff --git a/BitFaster.Caching/IScopedCache.cs b/BitFaster.Caching/IScopedCache.cs index 4eca0f1f..e06f11c1 100644 --- a/BitFaster.Caching/IScopedCache.cs +++ b/BitFaster.Caching/IScopedCache.cs @@ -57,15 +57,6 @@ public interface IScopedCache where V : IDisposable /// the cache. Lifetime ScopedGetOrAdd(K key, Func> valueFactory); - /// - /// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either - /// the new value, or the existing value if the key already exists. - /// - /// The key of the element to add. - /// The factory function used to asynchronously generate a scoped value for the key. - /// A task that represents the asynchronous ScopedGetOrAdd operation. - Task> ScopedGetOrAddAsync(K key, Func>> valueFactory); - /// /// Attempts to remove the value that has the specified key. /// diff --git a/BitFaster.Caching/Lru/Builder/AsyncAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncAtomicLruBuilder.cs new file mode 100644 index 00000000..f92c26c6 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/AsyncAtomicLruBuilder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; + +namespace BitFaster.Caching.Lru.Builder +{ + public class AsyncAtomicLruBuilder : LruBuilderBase, IAsyncCache> + { + private readonly ConcurrentLruBuilder> inner; + + internal AsyncAtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override IAsyncCache Build() + { + var level1 = inner.Build(); + return new AtomicFactoryAsyncCache(level1); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs new file mode 100644 index 00000000..c47223f9 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class AsyncConcurrentLruBuilder : LruBuilderBase, IAsyncCache> + { + internal AsyncConcurrentLruBuilder(LruInfo info) + : base(info) + { + } + + /// + public override IAsyncCache Build() + { + switch (info) + { + case LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + case LruInfo i when i.WithMetrics && i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + case LruInfo i when i.TimeToExpireAfterWrite.HasValue: + return new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + default: + return new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + } + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs new file mode 100644 index 00000000..cc1940b5 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; + +namespace BitFaster.Caching.Lru.Builder +{ + public class AtomicLruBuilder : LruBuilderBase, ICache> + { + private readonly ConcurrentLruBuilder> inner; + + internal AtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override ICache Build() + { + var level1 = inner.Build(); + return new AtomicFactoryCache(level1); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedAsyncAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAsyncAtomicLruBuilder.cs new file mode 100644 index 00000000..8553f91f --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedAsyncAtomicLruBuilder.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable + { + private readonly AsyncConcurrentLruBuilder> inner; + + internal ScopedAsyncAtomicLruBuilder(AsyncConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + /// + public override IScopedAsyncCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as ICache>; + + return new AtomicFactoryScopedAsyncCache(scopedInnerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedAsyncLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAsyncLruBuilder.cs new file mode 100644 index 00000000..2b280728 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedAsyncLruBuilder.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class ScopedAsyncLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable + { + private readonly AsyncConcurrentLruBuilder> inner; + + internal ScopedAsyncLruBuilder(AsyncConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + /// + public override IScopedAsyncCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as IAsyncCache>; + + return new ScopedAsyncCache(scopedInnerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs new file mode 100644 index 00000000..25b15daf --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; + +namespace BitFaster.Caching.Lru.Builder +{ + public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable + { + private readonly ConcurrentLruBuilder> inner; + + internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override IScopedCache Build() + { + var level1 = inner.Build() as ICache>; + return new AtomicFactoryScopedCache(level1); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs index e445de71..8007103a 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; namespace BitFaster.Caching.Lru.Builder { diff --git a/BitFaster.Caching/Lru/ClassicLru.cs b/BitFaster.Caching/Lru/ClassicLru.cs index 26fd3eb2..b787d37b 100644 --- a/BitFaster.Caching/Lru/ClassicLru.cs +++ b/BitFaster.Caching/Lru/ClassicLru.cs @@ -18,7 +18,7 @@ namespace BitFaster.Caching.Lru /// /// The type of the key /// The type of the value - public sealed class ClassicLru : ICache, IEnumerable> + public sealed class ClassicLru : ICache, IAsyncCache, IEnumerable> { private readonly int capacity; private readonly ConcurrentDictionary> dictionary; diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index 0e3dd1c8..8805d849 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using BitFaster.Caching.Lru.Builder; +using BitFaster.Caching.Synchronized; namespace BitFaster.Caching.Lru { @@ -22,5 +23,71 @@ public static ScopedLruBuilder> WithScopedValues(this Conc var scoped = new ConcurrentLruBuilder>(builder.info); return new ScopedLruBuilder>(scoped); } + + public static AtomicLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) + { + var a = new ConcurrentLruBuilder>(b.info); + return new AtomicLruBuilder(a); + } + + public static ScopedAtomicLruBuilder WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped + { + var atomicScoped = new ConcurrentLruBuilder>(b.info); + + return new ScopedAtomicLruBuilder(atomicScoped); + } + + public static ScopedAtomicLruBuilder WithScopedValues(this AtomicLruBuilder b) where V : IDisposable + { + var atomicScoped = new ConcurrentLruBuilder>(b.info); + return new ScopedAtomicLruBuilder(atomicScoped); + } + + public static AsyncConcurrentLruBuilder AsAsyncCache(this ConcurrentLruBuilder builder) + { + return new AsyncConcurrentLruBuilder(builder.info); + } + + public static ScopedAsyncLruBuilder WithScopedValues(this AsyncConcurrentLruBuilder b) where V : IDisposable + { + var asyncScoped = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncLruBuilder(asyncScoped); + } + + public static ScopedAsyncLruBuilder AsAsyncCache(this ScopedLruBuilder> b) where V : IDisposable + { + var asyncScoped = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncLruBuilder(asyncScoped); + } + + public static AsyncAtomicLruBuilder AsAsyncCache(this AtomicLruBuilder b) + { + var a = new ConcurrentLruBuilder>(b.info); + return new AsyncAtomicLruBuilder(a); + } + + public static AsyncAtomicLruBuilder WithAtomicCreate(this AsyncConcurrentLruBuilder b) + { + var a = new ConcurrentLruBuilder>(b.info); + return new AsyncAtomicLruBuilder(a); + } + + public static ScopedAsyncAtomicLruBuilder AsAsyncCache(this ScopedAtomicLruBuilder b) where V : IDisposable + { + var a = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncAtomicLruBuilder(a); + } + + public static ScopedAsyncAtomicLruBuilder WithScopedValues(this AsyncAtomicLruBuilder b) where V : IDisposable + { + var a = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncAtomicLruBuilder(a); + } + + public static ScopedAsyncAtomicLruBuilder WithAtomicCreate(this ScopedAsyncLruBuilder b) where V : IDisposable + { + var a = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncAtomicLruBuilder(a); + } } } diff --git a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs index 0190072e..c332fa97 100644 --- a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs +++ b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs @@ -28,7 +28,7 @@ namespace BitFaster.Caching.Lru /// 5. When warm is full, warm tail is moved to warm head or cold depending on WasAccessed. /// 6. When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed. /// - public class TemplateConcurrentLru : ICache, IEnumerable> + public class TemplateConcurrentLru : ICache, IAsyncCache, IEnumerable> where I : LruItem where P : struct, IItemPolicy where T : struct, ITelemetryPolicy diff --git a/BitFaster.Caching/ScopedAsyncCache.cs b/BitFaster.Caching/ScopedAsyncCache.cs new file mode 100644 index 00000000..97195fa1 --- /dev/null +++ b/BitFaster.Caching/ScopedAsyncCache.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// A cache decorator for working with Scoped IDisposable values. The Scoped methods (e.g. ScopedGetOrAdd) + /// are threadsafe and create lifetimes that guarantee the value will not be disposed until the + /// lifetime is disposed. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public sealed class ScopedAsyncCache : IScopedAsyncCache where V : IDisposable + { + private readonly IAsyncCache> cache; + + public ScopedAsyncCache(IAsyncCache> cache) + { + if (cache == null) + { + throw new ArgumentNullException(nameof(cache)); + } + + this.cache = cache; + } + + /// + public int Capacity => this.cache.Capacity; + + /// + public int Count => this.cache.Count; + + /// + public ICacheMetrics Metrics => this.cache.Metrics; + + /// + public ICacheEvents> Events => this.cache.Events; + + /// + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new Scoped(value)); + } + + /// + public void Clear() + { + this.cache.Clear(); + } + + /// + public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + { + int c = 0; + var spinwait = new SpinWait(); + while (true) + { + var scope = await cache.GetOrAddAsync(key, valueFactory); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + + spinwait.SpinOnce(); + + if (c++ > ScopedCacheDefaults.MaxRetry) + { + throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); + } + } + } + + /// + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + /// + 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 bool TryRemove(K key) + { + return this.cache.TryRemove(key); + } + + /// + public bool TryUpdate(K key, V value) + { + return this.cache.TryUpdate(key, new Scoped(value)); + } + } +} diff --git a/BitFaster.Caching/ScopedCache.cs b/BitFaster.Caching/ScopedCache.cs index 4dcb3797..8d34e229 100644 --- a/BitFaster.Caching/ScopedCache.cs +++ b/BitFaster.Caching/ScopedCache.cs @@ -75,29 +75,6 @@ public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) } } - /// - public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) - { - int c = 0; - var spinwait = new SpinWait(); - while (true) - { - var scope = await cache.GetOrAddAsync(key, valueFactory); - - if (scope.TryCreateLifetime(out var lifetime)) - { - return lifetime; - } - - spinwait.SpinOnce(); - - if (c++ > ScopedCacheDefaults.MaxRetry) - { - throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); - } - } - } - /// public void Trim(int itemCount) { diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs index 41c141c5..ae015686 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Synchronized { - public sealed class AtomicFactoryAsyncCache : ICache + public sealed class AtomicFactoryAsyncCache : IAsyncCache { private readonly ICache> cache; private readonly EventProxy eventProxy; @@ -41,11 +41,6 @@ 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()); diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs index 4f62e203..c717c713 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -8,7 +8,7 @@ namespace BitFaster.Caching.Synchronized { - public sealed class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable + public sealed class AtomicFactoryScopedAsyncCache : IScopedAsyncCache where V : IDisposable { private readonly ICache> cache; private readonly EventProxy eventProxy; @@ -42,11 +42,6 @@ public void Clear() this.cache.Clear(); } - public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) - { - throw new NotImplementedException(); - } - public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) { int c = 0; diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs index 3e317441..62f85883 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -64,11 +64,6 @@ public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) } } - 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))