From 7a3d9914399c7dae99687b4d28ff9fb6a6062826 Mon Sep 17 00:00:00 2001 From: jonas Date: Wed, 30 Sep 2026 23:07:52 +0200 Subject: [PATCH] * Bump Microsoft.SourceLink.GitHub to 10.0.401 * Make Lifetime.Dispose thread-safe * SingletonCache.Acquire: dispose any created value that didn't end up in the ConcurrentDictionary --- BitFaster.Caching.UnitTests/LifetimeTests.cs | 32 ++++++++++++++++++++ BitFaster.Caching/BitFaster.Caching.csproj | 2 +- BitFaster.Caching/Lifetime.cs | 8 ++--- BitFaster.Caching/SingletonCache.cs | 16 +++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/LifetimeTests.cs diff --git a/BitFaster.Caching.UnitTests/LifetimeTests.cs b/BitFaster.Caching.UnitTests/LifetimeTests.cs new file mode 100644 index 00000000..89e9c2e7 --- /dev/null +++ b/BitFaster.Caching.UnitTests/LifetimeTests.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests +{ + public class LifetimeTests + { + [Fact] + public void WhenLifetimeIsDisposedThenOnDisposeActionIsCalled() + { + var calls = 0; + var lifetime = new Lifetime(new ReferenceCount(new object()), () => calls++); + + lifetime.Dispose(); + + calls.Should().Be(1); + } + + [Fact] + public void WhenLifetimeIsDisposedMultipleTimesThenOnDisposeActionIsCalledOnce() + { + var calls = 0; + var lifetime = new Lifetime(new ReferenceCount(new object()), () => Interlocked.Increment(ref calls)); + + Parallel.For(0, 100, _ => lifetime.Dispose()); + + calls.Should().Be(1); + } + } +} diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj index b67d018a..e9a0be04 100644 --- a/BitFaster.Caching/BitFaster.Caching.csproj +++ b/BitFaster.Caching/BitFaster.Caching.csproj @@ -53,7 +53,7 @@ - + diff --git a/BitFaster.Caching/Lifetime.cs b/BitFaster.Caching/Lifetime.cs index 378d3eea..ed656be9 100644 --- a/BitFaster.Caching/Lifetime.cs +++ b/BitFaster.Caching/Lifetime.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; +using System.Threading; namespace BitFaster.Caching { @@ -13,7 +12,7 @@ public sealed class Lifetime : IDisposable { private readonly Action onDisposeAction; private readonly ReferenceCount refCount; - private bool isDisposed; + private int isDisposed; /// /// Initializes a new instance of the Lifetime class. @@ -41,10 +40,9 @@ public Lifetime(ReferenceCount value, Action onDisposeAction) /// public void Dispose() { - if (!this.isDisposed) + if (Interlocked.Exchange(ref this.isDisposed, 1) == 0) { this.onDisposeAction(); - this.isDisposed = true; } } } diff --git a/BitFaster.Caching/SingletonCache.cs b/BitFaster.Caching/SingletonCache.cs index 8b251bf7..aac1ccd6 100644 --- a/BitFaster.Caching/SingletonCache.cs +++ b/BitFaster.Caching/SingletonCache.cs @@ -46,10 +46,24 @@ public SingletonCache(int concurrencyLevel, int capacity, IEqualityComparerA value lifetime public Lifetime Acquire(TKey key, Func valueFactory) { + var created = new List(1); var refCount = this.cache.AddOrUpdate(key, - (k) => new ReferenceCount(valueFactory(k)), + (k) => + { + var value = new ReferenceCount(valueFactory(k)); + created.Add(value.Value); + return value; + }, (_, existingRefCount) => existingRefCount.IncrementCopy()); + foreach (var value in created) + { + if (!ReferenceEquals(value, refCount.Value) && value is IDisposable disposable) + { + disposable.Dispose(); + } + } + return new Lifetime(refCount, () => this.Release(key)); }