From a2642ac09fc66051cde96581ffd3770b4c6d50e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:50:07 +0000 Subject: [PATCH 1/5] Add GetAsyncAlternateLookup and TryGetAsyncAlternateLookup to IAsyncCache interface Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/0a2b82c7-2685-4e3c-9574-b730373ab8d4 Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- .../Atomic/AtomicFactoryAsyncCache.cs | 16 ++++++++++++ BitFaster.Caching/IAsyncCache.cs | 25 +++++++++++++++++++ BitFaster.Caching/Lfu/ConcurrentLfu.cs | 14 ++--------- BitFaster.Caching/Lfu/ConcurrentLfuCore.cs | 14 ++--------- BitFaster.Caching/Lfu/ConcurrentTLfu.cs | 14 ++--------- BitFaster.Caching/Lru/ClassicLru.cs | 14 ++--------- BitFaster.Caching/Lru/ConcurrentLruCore.cs | 14 ++--------- 7 files changed, 51 insertions(+), 60 deletions(-) diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 36f47759..07f2de7d 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -147,6 +147,22 @@ public bool TryUpdate(K key, V value) return cache.TryUpdate(key, new AsyncAtomicFactory(value)); } +#if NET9_0_OR_GREATER + /// + public IAsyncAlternateLookup GetAsyncAlternateLookup() + where TAlternateKey : notnull, allows ref struct + { + throw new NotSupportedException(); + } + + /// + public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) + where TAlternateKey : notnull, allows ref struct + { + throw new NotSupportedException(); + } +#endif + /// public IEnumerator> GetEnumerator() { diff --git a/BitFaster.Caching/IAsyncCache.cs b/BitFaster.Caching/IAsyncCache.cs index 56a55e5f..1c3edc23 100644 --- a/BitFaster.Caching/IAsyncCache.cs +++ b/BitFaster.Caching/IAsyncCache.cs @@ -111,5 +111,30 @@ public interface IAsyncCache : IEnumerable> /// Removes all keys and values from the cache. /// void Clear(); + +#if NET9_0_OR_GREATER +// backcompat: add not null constraint to IAsyncCache (where K : notnull) +#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. + /// + /// Gets an async alternate lookup that can use an alternate key type with the configured comparer. + /// + /// The alternate key type. + /// An async alternate lookup. + /// The configured comparer does not support . + IAsyncAlternateLookup GetAsyncAlternateLookup() + where TAlternateKey : notnull, allows ref struct + => throw new NotSupportedException(); + + /// + /// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer. + /// + /// The alternate key type. + /// The async alternate lookup when available. + /// when the configured comparer supports ; otherwise, . + bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) + where TAlternateKey : notnull, allows ref struct + => throw new NotSupportedException(); +#pragma warning restore CS8714 +#endif } } diff --git a/BitFaster.Caching/Lfu/ConcurrentLfu.cs b/BitFaster.Caching/Lfu/ConcurrentLfu.cs index 450e1ae3..63bb488e 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfu.cs @@ -214,24 +214,14 @@ public bool TryGetAlternateLookup([MaybeNullWhen(false)] out IAlt return core.TryGetAlternateLookup(out lookup); } - /// - /// Gets an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// An async alternate lookup. - /// The configured comparer does not support . + /// public IAsyncAlternateLookup GetAsyncAlternateLookup() where TAlternateKey : notnull, allows ref struct { return core.GetAsyncAlternateLookup(); } - /// - /// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// The async alternate lookup when available. - /// when the configured comparer supports ; otherwise, . + /// public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) where TAlternateKey : notnull, allows ref struct { diff --git a/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs b/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs index 0e150fd6..d43a3340 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs @@ -995,12 +995,7 @@ public bool TryGetAlternateLookup([MaybeNullWhen(false)] out IAlt return false; } - /// - /// Gets an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// An async alternate lookup. - /// The configured comparer does not support . + /// public IAsyncAlternateLookup GetAsyncAlternateLookup() where TAlternateKey : notnull, allows ref struct { @@ -1012,12 +1007,7 @@ public IAsyncAlternateLookup GetAsyncAlternateLookup(this); } - /// - /// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// The async alternate lookup when available. - /// when the configured comparer supports ; otherwise, . + /// public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) where TAlternateKey : notnull, allows ref struct { diff --git a/BitFaster.Caching/Lfu/ConcurrentTLfu.cs b/BitFaster.Caching/Lfu/ConcurrentTLfu.cs index 8babc895..9023cea2 100644 --- a/BitFaster.Caching/Lfu/ConcurrentTLfu.cs +++ b/BitFaster.Caching/Lfu/ConcurrentTLfu.cs @@ -154,24 +154,14 @@ public bool TryGetAlternateLookup([MaybeNullWhen(false)] out IAlt return core.TryGetAlternateLookup(out lookup); } - /// - /// Gets an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// An async alternate lookup. - /// The configured comparer does not support . + /// public IAsyncAlternateLookup GetAsyncAlternateLookup() where TAlternateKey : notnull, allows ref struct { return core.GetAsyncAlternateLookup(); } - /// - /// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// The async alternate lookup when available. - /// when the configured comparer supports ; otherwise, . + /// public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) where TAlternateKey : notnull, allows ref struct { diff --git a/BitFaster.Caching/Lru/ClassicLru.cs b/BitFaster.Caching/Lru/ClassicLru.cs index f5eb86f9..41e2fac3 100644 --- a/BitFaster.Caching/Lru/ClassicLru.cs +++ b/BitFaster.Caching/Lru/ClassicLru.cs @@ -496,12 +496,7 @@ public bool TryGetAlternateLookup([MaybeNullWhen(false)] out IAlt return false; } - /// - /// Gets an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// An async alternate lookup. - /// The configured comparer does not support . + /// public IAsyncAlternateLookup GetAsyncAlternateLookup() where TAlternateKey : notnull, allows ref struct { @@ -513,12 +508,7 @@ public IAsyncAlternateLookup GetAsyncAlternateLookup(this); } - /// - /// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// The async alternate lookup when available. - /// when the configured comparer supports ; otherwise, . + /// public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) where TAlternateKey : notnull, allows ref struct { diff --git a/BitFaster.Caching/Lru/ConcurrentLruCore.cs b/BitFaster.Caching/Lru/ConcurrentLruCore.cs index be84fa9d..b1d50bbf 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruCore.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruCore.cs @@ -927,12 +927,7 @@ public bool TryGetAlternateLookup([MaybeNullWhen(false)] out IAlt return false; } - /// - /// Gets an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// An async alternate lookup. - /// The configured comparer does not support . + /// public IAsyncAlternateLookup GetAsyncAlternateLookup() where TAlternateKey : notnull, allows ref struct { @@ -944,12 +939,7 @@ public IAsyncAlternateLookup GetAsyncAlternateLookup(this); } - /// - /// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer. - /// - /// The alternate key type. - /// The async alternate lookup when available. - /// when the configured comparer supports ; otherwise, . + /// public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) where TAlternateKey : notnull, allows ref struct { From 7944598d27247f8a67aaaf41f665a6bb532cbc8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:23:05 +0000 Subject: [PATCH 2/5] Implement GetAsyncAlternateLookup/TryGetAsyncAlternateLookup in AtomicFactoryAsyncCache with unit tests Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/ecb8a7bd-bccd-4ac6-a5ac-05912949a409 Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- ...toryAsyncCacheAsyncAlternateLookupTests.cs | 162 ++++++++++++++++++ .../Atomic/AtomicFactoryAsyncCache.cs | 83 ++++++++- 2 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs new file mode 100644 index 00000000..d57d6211 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs @@ -0,0 +1,162 @@ +#if NET9_0_OR_GREATER +using System; +using System.Threading.Tasks; +using BitFaster.Caching.Atomic; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Atomic +{ + public class AtomicFactoryAsyncCacheAsyncAlternateLookupTests + { + private readonly AtomicFactoryAsyncCache cache; + + public AtomicFactoryAsyncCacheAsyncAlternateLookupTests() + { + var innerCache = new ConcurrentLru>(1, 9, StringComparer.Ordinal); + cache = new AtomicFactoryAsyncCache(innerCache); + } + + [Fact] + public void TryGetAsyncAlternateLookupReturnsLookupForCompatibleComparer() + { + cache.AddOrUpdate("42", "value"); + ReadOnlySpan key = "42"; + + cache.TryGetAsyncAlternateLookup>(out var alternate).Should().BeTrue(); + alternate.TryGet(key, out var value).Should().BeTrue(); + value.Should().Be("value"); + } + + [Fact] + public void GetAsyncAlternateLookupThrowsForIncompatibleComparer() + { + Action act = () => cache.GetAsyncAlternateLookup(); + + act.Should().Throw().WithMessage("Incompatible comparer"); + cache.TryGetAsyncAlternateLookup(out var alternate).Should().BeFalse(); + alternate.Should().BeNull(); + } + + [Fact] + public void AsyncAlternateLookupTryGetReturnsFalseForMissingKey() + { + var alternate = cache.GetAsyncAlternateLookup>(); + ReadOnlySpan key = "42"; + + alternate.TryGet(key, out _).Should().BeFalse(); + } + + [Fact] + public void AsyncAlternateLookupTryRemoveReturnsActualKeyAndValue() + { + cache.AddOrUpdate("42", "value"); + var alternate = cache.GetAsyncAlternateLookup>(); + ReadOnlySpan key = "42"; + + alternate.TryRemove(key, out var actualKey, out var value).Should().BeTrue(); + + actualKey.Should().Be("42"); + value.Should().Be("value"); + cache.TryGet("42", out _).Should().BeFalse(); + } + + [Fact] + public void AsyncAlternateLookupTryRemoveReturnsFalseForMissingKey() + { + var alternate = cache.GetAsyncAlternateLookup>(); + ReadOnlySpan key = "42"; + + alternate.TryRemove(key, out _, out _).Should().BeFalse(); + } + + [Fact] + public void AsyncAlternateLookupTryUpdateReturnsFalseForMissingKeyAndUpdatesExistingValue() + { + var alternate = cache.GetAsyncAlternateLookup>(); + ReadOnlySpan key = "42"; + + alternate.TryUpdate(key, "value-42").Should().BeFalse(); + cache.TryGet("42", out _).Should().BeFalse(); + + cache.AddOrUpdate("42", "value-42"); + alternate.TryUpdate(key, "updated").Should().BeTrue(); + + cache.TryGet("42", out var value).Should().BeTrue(); + value.Should().Be("updated"); + alternate.TryGet(key, out value).Should().BeTrue(); + value.Should().Be("updated"); + } + + [Fact] + public void AsyncAlternateLookupAddOrUpdateAddsMissingValueAndUpdatesExistingValue() + { + var alternate = cache.GetAsyncAlternateLookup>(); + ReadOnlySpan key = "42"; + + alternate.AddOrUpdate(key, "value-42"); + + cache.TryGet("42", out var value).Should().BeTrue(); + value.Should().Be("value-42"); + + alternate.AddOrUpdate(key, "updated"); + + cache.TryGet("42", out value).Should().BeTrue(); + value.Should().Be("updated"); + alternate.TryGet(key, out value).Should().BeTrue(); + value.Should().Be("updated"); + } + + [Fact] + public async Task AsyncAlternateLookupGetOrAddAsyncUsesAlternateKeyOnMissAndHit() + { + var alternate = cache.GetAsyncAlternateLookup>(); + var factoryCalls = 0; + + var result = await alternate.GetOrAddAsync("42".AsSpan(), key => + { + factoryCalls++; + return Task.FromResult($"value-{key.ToString()}"); + }); + result.Should().Be("value-42"); + + result = await alternate.GetOrAddAsync("42".AsSpan(), key => + { + factoryCalls++; + return Task.FromResult("unused"); + }); + result.Should().Be("value-42"); + + factoryCalls.Should().Be(1); + cache.TryGet("42", out var value).Should().BeTrue(); + value.Should().Be("value-42"); + } + + [Fact] + public async Task AsyncAlternateLookupGetOrAddAsyncWithArgUsesAlternateKeyOnMissAndHit() + { + var alternate = cache.GetAsyncAlternateLookup>(); + var factoryCalls = 0; + + var result = await alternate.GetOrAddAsync("42".AsSpan(), (key, prefix) => + { + factoryCalls++; + return Task.FromResult($"{prefix}{key.ToString()}"); + }, "value-"); + result.Should().Be("value-42"); + + result = await alternate.GetOrAddAsync("42".AsSpan(), (key, prefix) => + { + factoryCalls++; + return Task.FromResult("unused"); + }, "unused-"); + result.Should().Be("value-42"); + + factoryCalls.Should().Be(1); + cache.TryGet("42", out var value).Should().BeTrue(); + value.Should().Be("value-42"); + } + } +} +#endif diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 07f2de7d..56f7c11f 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -152,14 +152,93 @@ public bool TryUpdate(K key, V value) public IAsyncAlternateLookup GetAsyncAlternateLookup() where TAlternateKey : notnull, allows ref struct { - throw new NotSupportedException(); + var inner = cache.GetAlternateLookup(); + return new AlternateLookup(inner); } /// public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out IAsyncAlternateLookup lookup) where TAlternateKey : notnull, allows ref struct { - throw new NotSupportedException(); + if (cache.TryGetAlternateLookup(out var inner)) + { + lookup = new AlternateLookup(inner); + return true; + } + + lookup = default; + return false; + } + + internal readonly struct AlternateLookup : IAsyncAlternateLookup + where TAlternateKey : notnull, allows ref struct + { + private readonly IAlternateLookup> inner; + + internal AlternateLookup(IAlternateLookup> inner) + { + this.inner = inner; + } + + public bool TryGet(TAlternateKey key, [MaybeNullWhen(false)] out V value) + { + if (inner.TryGet(key, out var atomic) && atomic.IsValueCreated) + { + value = atomic.ValueIfCreated!; + return true; + } + + value = default; + return false; + } + + public bool TryRemove(TAlternateKey key, [MaybeNullWhen(false)] out K actualKey, [MaybeNullWhen(false)] out V value) + { + if (inner.TryRemove(key, out actualKey, out var atomic)) + { + value = atomic.ValueIfCreated!; + return true; + } + + value = default; + return false; + } + + public bool TryUpdate(TAlternateKey key, V value) + { + return inner.TryUpdate(key, new AsyncAtomicFactory(value)); + } + + public void AddOrUpdate(TAlternateKey key, V value) + { + inner.AddOrUpdate(key, new AsyncAtomicFactory(value)); + } + + public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory) + { + var factory = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); + + if (factory.IsValueCreated) + { + return new ValueTask(factory.ValueIfCreated!); + } + + Task task = valueFactory(key); + return factory.GetValueAsync(default(K)!, static (_, t) => t, task); + } + + public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory, TArg factoryArgument) + { + var factory = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); + + if (factory.IsValueCreated) + { + return new ValueTask(factory.ValueIfCreated!); + } + + Task task = valueFactory(key, factoryArgument); + return factory.GetValueAsync(default(K)!, static (_, t) => t, task); + } } #endif From 92725a6337971f68b5e161cc662a9037e6e5bc13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:44:43 +0000 Subject: [PATCH 3/5] Fix GetOrAddAsync to preserve exactly-once initialization via AsyncAtomicFactory double-check Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/172d9137-e999-4a1e-b572-963a2f919311 Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- ...toryAsyncCacheAsyncAlternateLookupTests.cs | 8 +-- .../Atomic/AtomicFactoryAsyncCache.cs | 57 +++++++++++++++---- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs index d57d6211..ba5524f2 100644 --- a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryAsyncCacheAsyncAlternateLookupTests.cs @@ -109,7 +109,7 @@ public void AsyncAlternateLookupAddOrUpdateAddsMissingValueAndUpdatesExistingVal } [Fact] - public async Task AsyncAlternateLookupGetOrAddAsyncUsesAlternateKeyOnMissAndHit() + public async Task AsyncAlternateLookupGetOrAddAsyncUsesActualKeyOnMissAndHit() { var alternate = cache.GetAsyncAlternateLookup>(); var factoryCalls = 0; @@ -117,7 +117,7 @@ public async Task AsyncAlternateLookupGetOrAddAsyncUsesAlternateKeyOnMissAndHit( var result = await alternate.GetOrAddAsync("42".AsSpan(), key => { factoryCalls++; - return Task.FromResult($"value-{key.ToString()}"); + return Task.FromResult($"value-{key}"); }); result.Should().Be("value-42"); @@ -134,7 +134,7 @@ public async Task AsyncAlternateLookupGetOrAddAsyncUsesAlternateKeyOnMissAndHit( } [Fact] - public async Task AsyncAlternateLookupGetOrAddAsyncWithArgUsesAlternateKeyOnMissAndHit() + public async Task AsyncAlternateLookupGetOrAddAsyncWithArgUsesActualKeyOnMissAndHit() { var alternate = cache.GetAsyncAlternateLookup>(); var factoryCalls = 0; @@ -142,7 +142,7 @@ public async Task AsyncAlternateLookupGetOrAddAsyncWithArgUsesAlternateKeyOnMiss var result = await alternate.GetOrAddAsync("42".AsSpan(), (key, prefix) => { factoryCalls++; - return Task.FromResult($"{prefix}{key.ToString()}"); + return Task.FromResult($"{prefix}{key}"); }, "value-"); result.Should().Be("value-42"); diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 56f7c11f..8f070560 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -214,32 +214,65 @@ public void AddOrUpdate(TAlternateKey key, V value) inner.AddOrUpdate(key, new AsyncAtomicFactory(value)); } - public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory) + public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory) { - var factory = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); + if (inner.TryGet(key, out var existing) && existing.IsValueCreated) + { + return new ValueTask(existing.ValueIfCreated!); + } - if (factory.IsValueCreated) + return GetOrAddAsyncSlow(key, valueFactory); + } + + private ValueTask GetOrAddAsyncSlow(TAlternateKey key, Func> valueFactory) + { + var box = new KeyBox(); + var synchronized = inner.GetOrAdd(key, static (k, state) => { - return new ValueTask(factory.ValueIfCreated!); + state.box.Key = k; + return new AsyncAtomicFactory(); + }, (box, valueFactory)); + + if (synchronized.IsValueCreated) + { + return new ValueTask(synchronized.ValueIfCreated!); } - Task task = valueFactory(key); - return factory.GetValueAsync(default(K)!, static (_, t) => t, task); + return synchronized.GetValueAsync(box.Key, valueFactory); } - public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory, TArg factoryArgument) + public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory, TArg factoryArgument) { - var factory = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); + if (inner.TryGet(key, out var existing) && existing.IsValueCreated) + { + return new ValueTask(existing.ValueIfCreated!); + } - if (factory.IsValueCreated) + return GetOrAddAsyncSlow(key, valueFactory, factoryArgument); + } + + private ValueTask GetOrAddAsyncSlow(TAlternateKey key, Func> valueFactory, TArg factoryArgument) + { + var box = new KeyBox(); + var synchronized = inner.GetOrAdd(key, static (k, state) => + { + state.box.Key = k; + return new AsyncAtomicFactory(); + }, (box, valueFactory, factoryArgument)); + + if (synchronized.IsValueCreated) { - return new ValueTask(factory.ValueIfCreated!); + return new ValueTask(synchronized.ValueIfCreated!); } - Task task = valueFactory(key, factoryArgument); - return factory.GetValueAsync(default(K)!, static (_, t) => t, task); + return synchronized.GetValueAsync(box.Key, valueFactory, factoryArgument); } } + + private class KeyBox + { + public TKey Key = default!; + } #endif /// From 88f9d4e456d35c413a67f4f08ef1a367e5fad009 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 07:09:19 +0000 Subject: [PATCH 4/5] Replace KeyBox key capture with comparer.Create() for alternate key to K conversion Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/c7c00ddc-3647-483f-bec1-2611fa2cc6e5 Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com> --- .../Atomic/AtomicFactoryAsyncCache.cs | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index 9e03de88..e8863c16 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -159,7 +159,8 @@ public IAsyncAlternateLookup GetAsyncAlternateLookup(); - return new AlternateLookup(inner); + var comparer = (IAlternateEqualityComparer)cache.Comparer; + return new AlternateLookup(inner, comparer); } /// @@ -168,7 +169,8 @@ public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out { if (cache.TryGetAlternateLookup(out var inner)) { - lookup = new AlternateLookup(inner); + var comparer = (IAlternateEqualityComparer)cache.Comparer; + lookup = new AlternateLookup(inner, comparer); return true; } @@ -180,10 +182,12 @@ public bool TryGetAsyncAlternateLookup([MaybeNullWhen(false)] out where TAlternateKey : notnull, allows ref struct { private readonly IAlternateLookup> inner; + private readonly IAlternateEqualityComparer comparer; - internal AlternateLookup(IAlternateLookup> inner) + internal AlternateLookup(IAlternateLookup> inner, IAlternateEqualityComparer comparer) { this.inner = inner; + this.comparer = comparer; } public bool TryGet(TAlternateKey key, [MaybeNullWhen(false)] out V value) @@ -232,19 +236,15 @@ public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFacto private ValueTask GetOrAddAsyncSlow(TAlternateKey key, Func> valueFactory) { - var box = new KeyBox(); - var synchronized = inner.GetOrAdd(key, static (k, state) => - { - state.box.Key = k; - return new AsyncAtomicFactory(); - }, (box, valueFactory)); + K actualKey = comparer.Create(key); + var synchronized = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); if (synchronized.IsValueCreated) { return new ValueTask(synchronized.ValueIfCreated!); } - return synchronized.GetValueAsync(box.Key, valueFactory); + return synchronized.GetValueAsync(actualKey, valueFactory); } public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory, TArg factoryArgument) @@ -259,26 +259,17 @@ public ValueTask GetOrAddAsync(TAlternateKey key, Func private ValueTask GetOrAddAsyncSlow(TAlternateKey key, Func> valueFactory, TArg factoryArgument) { - var box = new KeyBox(); - var synchronized = inner.GetOrAdd(key, static (k, state) => - { - state.box.Key = k; - return new AsyncAtomicFactory(); - }, (box, valueFactory, factoryArgument)); + K actualKey = comparer.Create(key); + var synchronized = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); if (synchronized.IsValueCreated) { return new ValueTask(synchronized.ValueIfCreated!); } - return synchronized.GetValueAsync(box.Key, valueFactory, factoryArgument); + return synchronized.GetValueAsync(actualKey, valueFactory, factoryArgument); } } - - private class KeyBox - { - public TKey Key = default!; - } #endif /// From afac1798f622fcc4396913769361a9b6ade340a2 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 13 Apr 2026 12:10:17 -0700 Subject: [PATCH 5/5] improve getoradd --- .../Atomic/AtomicFactoryAsyncCache.cs | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs index e8863c16..fcfe26c8 100644 --- a/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs @@ -226,47 +226,27 @@ public void AddOrUpdate(TAlternateKey key, V value) public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory) { - if (inner.TryGet(key, out var existing) && existing.IsValueCreated) - { - return new ValueTask(existing.ValueIfCreated!); - } - - return GetOrAddAsyncSlow(key, valueFactory); - } - - private ValueTask GetOrAddAsyncSlow(TAlternateKey key, Func> valueFactory) - { - K actualKey = comparer.Create(key); - var synchronized = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); + var synchronized = inner.GetOrAdd(key, _ => new AsyncAtomicFactory()); if (synchronized.IsValueCreated) { return new ValueTask(synchronized.ValueIfCreated!); } + K actualKey = comparer.Create(key); return synchronized.GetValueAsync(actualKey, valueFactory); } public ValueTask GetOrAddAsync(TAlternateKey key, Func> valueFactory, TArg factoryArgument) { - if (inner.TryGet(key, out var existing) && existing.IsValueCreated) - { - return new ValueTask(existing.ValueIfCreated!); - } - - return GetOrAddAsyncSlow(key, valueFactory, factoryArgument); - } - - private ValueTask GetOrAddAsyncSlow(TAlternateKey key, Func> valueFactory, TArg factoryArgument) - { - K actualKey = comparer.Create(key); - var synchronized = inner.GetOrAdd(key, static _ => new AsyncAtomicFactory()); + var synchronized = inner.GetOrAdd(key, _ => new AsyncAtomicFactory()); if (synchronized.IsValueCreated) { return new ValueTask(synchronized.ValueIfCreated!); } + K actualKey = comparer.Create(key); return synchronized.GetValueAsync(actualKey, valueFactory, factoryArgument); } }