From bf4a3439a69fd2f6e0bb9aef7dd36956650af025 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 17 Mar 2023 14:50:29 -0700 Subject: [PATCH 1/5] arg --- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 52 +++++++++++++++--- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 61 +++++++++++++++++++--- 2 files changed, 99 insertions(+), 14 deletions(-) diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index b102bcee..1a715ab5 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -194,6 +194,18 @@ public void Trim(int itemCount) } } + private bool TryAdd(K key, LfuNode node) + { + if (this.dictionary.TryAdd(key, node)) + { + AfterWrite(node); + return true; + } + + Disposer.Dispose(node.Value); + return false; + } + /// public V GetOrAdd(K key, Func valueFactory) { @@ -205,13 +217,27 @@ public V GetOrAdd(K key, Func valueFactory) } var node = new LfuNode(key, valueFactory(key)); - if (this.dictionary.TryAdd(key, node)) + if (this.TryAdd(key, node)) { - AfterWrite(node); return node.Value; } + } + } - Disposer.Dispose(node.Value); + public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) + { + while (true) + { + if (this.TryGet(key, out V value)) + { + return value; + } + + var node = new LfuNode(key, valueFactory(key, factoryArgument)); + if (this.TryAdd(key, node)) + { + return node.Value; + } } } @@ -226,13 +252,27 @@ public async ValueTask GetOrAddAsync(K key, Func> valueFactory) } var node = new LfuNode(key, await valueFactory(key).ConfigureAwait(false)); - if (this.dictionary.TryAdd(key, node)) + if (this.TryAdd(key, node)) { - AfterWrite(node); return node.Value; } + } + } - Disposer.Dispose(node.Value); + public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) + { + while (true) + { + if (this.TryGet(key, out V value)) + { + return value; + } + + var node = new LfuNode(key, await valueFactory(key, factoryArgument).ConfigureAwait(false)); + if (this.TryAdd(key, node)) + { + return node.Value; + } } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 2cfa1a4c..51f0776c 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -179,6 +179,19 @@ private bool GetOrDiscard(I item, out V value) return true; } + private bool TryAdd(K key, I newItem) + { + if (this.dictionary.TryAdd(key, newItem)) + { + this.hotQueue.Enqueue(newItem); + Cycle(Interlocked.Increment(ref counter.hot)); + return true; + } + + Disposer.Dispose(newItem.Value); + return false; + } + /// public V GetOrAdd(K key, Func valueFactory) { @@ -193,14 +206,30 @@ public V GetOrAdd(K key, Func valueFactory) // This is identical logic in ConcurrentDictionary.GetOrAdd method. var newItem = this.itemPolicy.CreateItem(key, valueFactory(key)); - if (this.dictionary.TryAdd(key, newItem)) + if (TryAdd(key, newItem)) { - this.hotQueue.Enqueue(newItem); - Cycle(Interlocked.Increment(ref counter.hot)); return newItem.Value; } + } + } + + public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) + { + while (true) + { + if (this.TryGet(key, out var value)) + { + return value; + } + + // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. + // This is identical logic in ConcurrentDictionary.GetOrAdd method. + var newItem = this.itemPolicy.CreateItem(key, valueFactory(key, factoryArgument)); - Disposer.Dispose(newItem.Value); + if (TryAdd(key, newItem)) + { + return newItem.Value; + } } } @@ -218,14 +247,30 @@ public async ValueTask GetOrAddAsync(K key, Func> valueFactory) // This is identical logic in ConcurrentDictionary.GetOrAdd method. var newItem = this.itemPolicy.CreateItem(key, await valueFactory(key).ConfigureAwait(false)); - if (this.dictionary.TryAdd(key, newItem)) + if (TryAdd(key, newItem)) { - this.hotQueue.Enqueue(newItem); - Cycle(Interlocked.Increment(ref counter.hot)); return newItem.Value; } + } + } + + public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) + { + while (true) + { + if (this.TryGet(key, out var value)) + { + return value; + } - Disposer.Dispose(newItem.Value); + // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. + // This is identical logic in ConcurrentDictionary.GetOrAdd method. + var newItem = this.itemPolicy.CreateItem(key, await valueFactory(key, factoryArgument).ConfigureAwait(false)); + + if (TryAdd(key, newItem)) + { + return newItem.Value; + } } } From 97d1d531b2d8af3e9a2536db5ba2f1006eff361c Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 17 Mar 2023 15:35:35 -0700 Subject: [PATCH 2/5] basic tests --- .../Lfu/ConcurrentLfuTests.cs | 32 +++++++++++++++++++ .../Lru/ConcurrentLruTests.cs | 22 ++++++++++++- .../Lru/ValueFactory.cs | 12 +++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs index 5a3c0048..ea538553 100644 --- a/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs +++ b/BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs @@ -43,6 +43,16 @@ public void WhenKeyIsRequestedItIsCreatedAndCached() result1.Should().Be(result2); } + [Fact] + public void WhenKeyIsRequestedWithArgItIsCreatedAndCached() + { + var result1 = cache.GetOrAdd(1, valueFactory.Create, 9); + var result2 = cache.GetOrAdd(1, valueFactory.Create, 17); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + [Fact] public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() { @@ -53,6 +63,16 @@ public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() result1.Should().Be(result2); } + [Fact] + public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync() + { + var result1 = await cache.GetOrAddAsync(1, valueFactory.CreateAsync, 9).ConfigureAwait(false); + var result2 = await cache.GetOrAddAsync(1, valueFactory.CreateAsync, 17).ConfigureAwait(false); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + [Fact] public void WhenItemsAddedExceedsCapacityItemsAreDiscarded() { @@ -849,11 +869,23 @@ public int Create(int key) return key; } + public int Create(int key, int arg) + { + timesCalled++; + return key + arg; + } + public Task CreateAsync(int key) { timesCalled++; return Task.FromResult(key); } + + public Task CreateAsync(int key, int arg) + { + timesCalled++; + return Task.FromResult(key + arg); + } } } } diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs index 6ae2a956..44548d28 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs @@ -246,7 +246,17 @@ public void WhenKeyIsRequestedItIsCreatedAndCached() } [Fact] - public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() + public void WhenKeyIsRequestedWithArgItIsCreatedAndCached() + { + var result1 = lru.GetOrAdd(1, valueFactory.Create, "x"); + var result2 = lru.GetOrAdd(1, valueFactory.Create, "y"); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + + [Fact] + public async Task WhenKeyIsRequestedItIsCreatedAndCachedAsync() { var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync).ConfigureAwait(false); @@ -255,6 +265,16 @@ public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() result1.Should().Be(result2); } + [Fact] + public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync() + { + var result1 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "x").ConfigureAwait(false); + var result2 = await lru.GetOrAddAsync(1, valueFactory.CreateAsync, "y").ConfigureAwait(false); + + valueFactory.timesCalled.Should().Be(1); + result1.Should().Be(result2); + } + [Fact] public void WhenDifferentKeysAreRequestedValueIsCreatedForEach() { diff --git a/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs b/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs index 1aa61229..18156cfd 100644 --- a/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs +++ b/BitFaster.Caching.UnitTests/Lru/ValueFactory.cs @@ -15,10 +15,22 @@ public string Create(int key) return key.ToString(); } + public string Create(int key, TArg arg) + { + timesCalled++; + return $"{key}{arg}"; + } + public Task CreateAsync(int key) { timesCalled++; return Task.FromResult(key.ToString()); } + + public Task CreateAsync(int key, TArg arg) + { + timesCalled++; + return Task.FromResult($"{key}{arg}"); + } } } From 1396689421a196dc114e8949a231665c035cd487 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 17 Mar 2023 15:44:02 -0700 Subject: [PATCH 3/5] docs --- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 19 +++++++++++++++++++ BitFaster.Caching/Lru/ConcurrentLruCore.cs | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 1a715ab5..1ac22775 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -224,6 +224,16 @@ public 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 type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to generate a value for the key. + /// An argument value to pass into valueFactory. + /// The value for the key. This will be either the existing value for the key if the key is already + /// in the cache, or the new value if the key was not in the cache. public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) { while (true) @@ -259,6 +269,15 @@ public async ValueTask GetOrAddAsync(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 type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// An argument value to pass into valueFactory. + /// A task that represents the asynchronous GetOrAdd operation. public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) { while (true) diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 51f0776c..c4f4c4e9 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -213,6 +213,16 @@ public 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 type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to generate a value for the key. + /// An argument value to pass into valueFactory. + /// The value for the key. This will be either the existing value for the key if the key is already + /// in the cache, or the new value if the key was not in the cache. public V GetOrAdd(K key, Func valueFactory, TArg factoryArgument) { while (true) @@ -254,6 +264,15 @@ public async ValueTask GetOrAddAsync(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 type of an argument to pass into valueFactory. + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// An argument value to pass into valueFactory. + /// A task that represents the asynchronous GetOrAdd operation. public async ValueTask GetOrAddAsync(K key, Func> valueFactory, TArg factoryArgument) { while (true) From be279f4cdd0b02e5ba8f3b2fe84c33fcd148524f Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 24 Apr 2023 18:52:33 -0700 Subject: [PATCH 4/5] simpler --- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index 989f740d..5c6fc95d 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -184,8 +184,10 @@ private bool GetOrDiscard(I item, out V value) return true; } - private bool TryAdd(K key, I newItem) + private bool TryAdd(K key, V value) { + var newItem = this.itemPolicy.CreateItem(key, value); + if (this.dictionary.TryAdd(key, newItem)) { this.hotQueue.Enqueue(newItem); @@ -208,12 +210,11 @@ public V GetOrAdd(K key, Func valueFactory) } // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. - // This is identical logic in ConcurrentDictionary.GetOrAdd method. - var newItem = this.itemPolicy.CreateItem(key, valueFactory(key)); + value = valueFactory(key); - if (TryAdd(key, newItem)) + if (TryAdd(key, value)) { - return newItem.Value; + return value; } } } @@ -238,12 +239,11 @@ public V GetOrAdd(K key, Func valueFactory, TArg factoryArgume } // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. - // This is identical logic in ConcurrentDictionary.GetOrAdd method. - var newItem = this.itemPolicy.CreateItem(key, valueFactory(key, factoryArgument)); + value = valueFactory(key, factoryArgument); - if (TryAdd(key, newItem)) + if (TryAdd(key, value)) { - return newItem.Value; + return value; } } } @@ -260,11 +260,11 @@ public async ValueTask GetOrAddAsync(K key, Func> valueFactory) // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. // This is identical logic in ConcurrentDictionary.GetOrAdd method. - var newItem = this.itemPolicy.CreateItem(key, await valueFactory(key).ConfigureAwait(false)); + value = await valueFactory(key).ConfigureAwait(false); - if (TryAdd(key, newItem)) + if (TryAdd(key, value)) { - return newItem.Value; + return value; } } } @@ -288,12 +288,11 @@ public async ValueTask GetOrAddAsync(K key, Func> valu } // The value factory may be called concurrently for the same key, but the first write to the dictionary wins. - // This is identical logic in ConcurrentDictionary.GetOrAdd method. - var newItem = this.itemPolicy.CreateItem(key, await valueFactory(key, factoryArgument).ConfigureAwait(false)); + value = await valueFactory(key, factoryArgument).ConfigureAwait(false); - if (TryAdd(key, newItem)) + if (TryAdd(key, value)) { - return newItem.Value; + return value; } } } From 3d2ebdb03d72aee2a58563343d414d71eff7a457 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 24 Apr 2023 19:20:28 -0700 Subject: [PATCH 5/5] same for LFU --- BitFaster.Caching/Lfu/ConcurrentLfu.cs | 28 ++++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 1ac22775..1ca2bce5 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -194,8 +194,10 @@ public void Trim(int itemCount) } } - private bool TryAdd(K key, LfuNode node) + private bool TryAdd(K key, V value) { + var node = new LfuNode(key, value); + if (this.dictionary.TryAdd(key, node)) { AfterWrite(node); @@ -216,10 +218,10 @@ public V GetOrAdd(K key, Func valueFactory) return value; } - var node = new LfuNode(key, valueFactory(key)); - if (this.TryAdd(key, node)) + value = valueFactory(key); + if (this.TryAdd(key, value)) { - return node.Value; + return value; } } } @@ -243,10 +245,10 @@ public V GetOrAdd(K key, Func valueFactory, TArg factoryArgume return value; } - var node = new LfuNode(key, valueFactory(key, factoryArgument)); - if (this.TryAdd(key, node)) + value = valueFactory(key, factoryArgument); + if (this.TryAdd(key, value)) { - return node.Value; + return value; } } } @@ -261,10 +263,10 @@ public async ValueTask GetOrAddAsync(K key, Func> valueFactory) return value; } - var node = new LfuNode(key, await valueFactory(key).ConfigureAwait(false)); - if (this.TryAdd(key, node)) + value = await valueFactory(key).ConfigureAwait(false); + if (this.TryAdd(key, value)) { - return node.Value; + return value; } } } @@ -287,10 +289,10 @@ public async ValueTask GetOrAddAsync(K key, Func> valu return value; } - var node = new LfuNode(key, await valueFactory(key, factoryArgument).ConfigureAwait(false)); - if (this.TryAdd(key, node)) + value = await valueFactory(key, factoryArgument).ConfigureAwait(false); + if (this.TryAdd(key, value)) { - return node.Value; + return value; } } }