From 9e270b2d89b8a8ec40363e34729a0d8322df0d2e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 18 Apr 2026 19:11:15 -0700 Subject: [PATCH 1/3] fix conc bug --- BitFaster.Caching/Atomic/ScopedAtomicFactory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs b/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs index 55661e99..d9b90f80 100644 --- a/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; namespace BitFaster.Caching.Atomic @@ -134,7 +135,7 @@ public bool TryCreateLifetime(K key, TFactory valueFactory, [MaybeNull private void InitializeScope(K key, TFactory valueFactory) where TFactory : struct, IValueFactory> { - var init = initializer; + var init = Volatile.Read(ref initializer); if (init != null) { From cb18da1303a918ecb073670cdbba6221f385f416 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 18 Apr 2026 19:23:31 -0700 Subject: [PATCH 2/3] test --- .../AtomicFactoryScopedCacheSoakTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 BitFaster.Caching.UnitTests/Atomic/AtomicFactoryScopedCacheSoakTests.cs diff --git a/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryScopedCacheSoakTests.cs b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryScopedCacheSoakTests.cs new file mode 100644 index 00000000..1ffca0c7 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Atomic/AtomicFactoryScopedCacheSoakTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BitFaster.Caching.Atomic; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Moq; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Atomic +{ + [Collection("Soak")] + public class AtomicFactoryScopedCacheSoakTests + { + private const int capacity = 6; + private const int threadCount = 4; + private const int soakIterations = 10; + private const int loopIterations = 100_000; + + [Theory] + [Repeat(soakIterations)] + public async Task ScopedGetOrAddAlternateLifetimeIsAlwaysAlive(int _) + { + var cache = new AtomicFactoryScopedCache(new ConcurrentLru>(1, capacity, EqualityComparer.Default)); + + var run = Threaded.Run(threadCount, _ => + { + var key = new char[8]; + + for (int i = 0; i < loopIterations; i++) + { + using (var lifetime = cache.ScopedGetOrAdd(i, k => { return new Scoped(new Disposable(k)); })) + { + lifetime.Value.IsDisposed.Should().BeFalse($"ref count {lifetime.ReferenceCount}"); + } + } + }); + + await run; + } + } +} From e860ff6d6c8dd553bb48bdfd0974334a6bd16ec9 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 18 Apr 2026 19:25:59 -0700 Subject: [PATCH 3/3] fix dispose --- BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs | 2 +- BitFaster.Caching/Atomic/ScopedAtomicFactory.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs b/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs index 839b51a1..e82e13ba 100644 --- a/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/ScopedAsyncAtomicFactory.cs @@ -132,7 +132,7 @@ private async ValueTask InitializeScopeAsync(K key, TFactory valueFact /// public void Dispose() { - var init = initializer; + var init = Volatile.Read(ref initializer); if (init != null && init.TryGetScope(out var disposeScope)) { diff --git a/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs b/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs index d9b90f80..ef1765bb 100644 --- a/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs +++ b/BitFaster.Caching/Atomic/ScopedAtomicFactory.cs @@ -150,7 +150,7 @@ private void InitializeScope(K key, TFactory valueFactory) where TFact /// public void Dispose() { - var init = initializer; + var init = Volatile.Read(ref initializer); if (init != null) {