From 48d9b6a491a0fb37440ee0355b3c5002b11ed648 Mon Sep 17 00:00:00 2001 From: alexpeck Date: Fri, 19 Jun 2020 13:47:38 -0700 Subject: [PATCH 01/31] outline --- .../LazyExperiments/AsyncLazy.cs | 55 ++++++++++++ .../LazyExperiments/DesiredApi.cs | 72 ++++++++++++++++ .../LazyExperiments/ScopedAsyncLazy.cs | 86 +++++++++++++++++++ .../LazyExperiments/ScopedLazy.cs | 75 ++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 BitFaster.Caching/LazyExperiments/AsyncLazy.cs create mode 100644 BitFaster.Caching/LazyExperiments/DesiredApi.cs create mode 100644 BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs create mode 100644 BitFaster.Caching/LazyExperiments/ScopedLazy.cs diff --git a/BitFaster.Caching/LazyExperiments/AsyncLazy.cs b/BitFaster.Caching/LazyExperiments/AsyncLazy.cs new file mode 100644 index 00000000..15bf9be0 --- /dev/null +++ b/BitFaster.Caching/LazyExperiments/AsyncLazy.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.LazyExperiments +{ + public class AsyncLazy + { + private readonly object mutex; + private Lazy> lazy; + private readonly Func> valueFactory; + + public AsyncLazy(Func> valueFactory) + { + this.mutex = new object(); + this.valueFactory = RetryOnFailure(valueFactory); + this.lazy = new Lazy>(this.valueFactory); + } + + private Func> RetryOnFailure(Func> valueFactory) + { + return async () => + { + try + { + return await valueFactory().ConfigureAwait(false); + } + catch + { + // lock exists only because lazy is replaced on error + // better approach might be either: + // - value factory is responsible for retry, not lazy (single responsibility) + // - use TLRU, expire invalid items + lock (mutex) + { + this.lazy = new Lazy>(this.valueFactory); + } + throw; + } + }; + } + + public Task Task + { + get { lock (this.mutex) { return this.lazy.Value; } } + } + + public TaskAwaiter GetAwaiter() + { + return Task.GetAwaiter(); + } + } +} diff --git a/BitFaster.Caching/LazyExperiments/DesiredApi.cs b/BitFaster.Caching/LazyExperiments/DesiredApi.cs new file mode 100644 index 00000000..12da0fc6 --- /dev/null +++ b/BitFaster.Caching/LazyExperiments/DesiredApi.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.LazyExperiments +{ + class DesiredApi + { + public static void HowToCacheALazy() + { + var lru = new ConcurrentLru>(4); + var factory = new ScopedLazyFactory(); + + using (var lifetime = lru.GetOrAdd(1, factory.Create).CreateLifetime()) + { + var y = lifetime.Value; + } + } + + // Requirements: + // 1. lifetime/value create is async end to end (if async delegate is used to create value) + // 2. value is created lazily, guarantee single instance of object, single invocation of lazy + // 3. lazy value is disposed by scope + // 4. lifetime keeps scope alive + public static async Task HowToCacheAnAsyncLazy() + { + var lru = new ConcurrentLru>(4); + var factory = new ScopedAsyncLazyFactory(); + + using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) + { + var y = lifetime.Value; + } + } + } + + public class ScopedLazyFactory + { + public Task CreateAsync(int key) + { + return Task.FromResult(new SomeDisposable()); + } + + public ScopedLazy Create(int key) + { + return new ScopedLazy(() => new SomeDisposable()); + } + } + + public class ScopedAsyncLazyFactory + { + public Task CreateAsync(int key) + { + return Task.FromResult(new SomeDisposable()); + } + + public ScopedAsyncLazy Create(int key) + { + return new ScopedAsyncLazy(() => Task.FromResult(new SomeDisposable())); + } + } + + public class SomeDisposable : IDisposable + { + public void Dispose() + { + + } + } +} diff --git a/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs b/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs new file mode 100644 index 00000000..48ecdca3 --- /dev/null +++ b/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.LazyExperiments +{ + // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal + public class ScopedAsyncLazy : IDisposable + where TValue : IDisposable + { + private ReferenceCount> refCount; + private bool isDisposed; + + private readonly Func> valueFactory; + + private readonly AsyncLazy lazy; + + // should this even be allowed? + public ScopedAsyncLazy(Func valueFactory) + { + this.lazy = new AsyncLazy(() => Task.FromResult(valueFactory())); + } + + public ScopedAsyncLazy(Func> valueFactory) + { + this.lazy = new AsyncLazy(valueFactory); + } + + public async Task> CreateLifetimeAsync() + { + if (this.isDisposed) + { + throw new ObjectDisposedException($"{nameof(TValue)} is disposed."); + } + + while (true) + { + // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. + // This mitigates the race where the value is disposed after the above check is run. + var oldRefCount = this.refCount; + var newRefCount = oldRefCount.IncrementCopy(); + + // guarantee ref held before lazy evaluated + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + { + // When Lease is disposed, it calls DecrementReferenceCount + var value = await this.lazy; + return new Lifetime(value, this.DecrementReferenceCount); + } + } + } + + private void DecrementReferenceCount() + { + while (true) + { + var oldRefCount = this.refCount; + var newRefCount = oldRefCount.DecrementCopy(); + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + { + if (newRefCount.Count == 0) + { + if (newRefCount.Value.IsValueCreated) + { + newRefCount.Value.Value.Dispose(); + } + } + + break; + } + } + } + + public void Dispose() + { + if (!this.isDisposed) + { + this.DecrementReferenceCount(); + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/LazyExperiments/ScopedLazy.cs b/BitFaster.Caching/LazyExperiments/ScopedLazy.cs new file mode 100644 index 00000000..844c7d42 --- /dev/null +++ b/BitFaster.Caching/LazyExperiments/ScopedLazy.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace BitFaster.Caching +{ + // Enable caching a Lazy disposable object - guarantee single instance, safe disposal + public class ScopedLazy : IDisposable + where T : IDisposable + { + private ReferenceCount> refCount; + private bool isDisposed; + + public ScopedLazy(Func valueFactory) + { + // TODO: this will cache exceptions + var lazy = new Lazy(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); + this.refCount = new ReferenceCount>(lazy); + } + + public Lifetime CreateLifetime() + { + if (this.isDisposed) + { + throw new ObjectDisposedException($"{nameof(T)} is disposed."); + } + + while (true) + { + // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. + // This mitigates the race where the value is disposed after the above check is run. + var oldRefCount = this.refCount; + var newRefCount = oldRefCount.IncrementCopy(); + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + { + // When Lease is disposed, it calls DecrementReferenceCount + return new Lifetime(oldRefCount.Value.Value, this.DecrementReferenceCount); + } + } + } + + private void DecrementReferenceCount() + { + while (true) + { + var oldRefCount = this.refCount; + var newRefCount = oldRefCount.DecrementCopy(); + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + { + if (newRefCount.Count == 0) + { + if (newRefCount.Value.IsValueCreated) + { + newRefCount.Value.Value.Dispose(); + } + } + + break; + } + } + } + + public void Dispose() + { + if (!this.isDisposed) + { + this.DecrementReferenceCount(); + this.isDisposed = true; + } + } + } +} From b51cd6335c25aa966d96285e78c028ac6718d812 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 17:37:01 -0700 Subject: [PATCH 02/31] atomic lazy --- BitFaster.Caching/BitFaster.Caching.csproj | 2 +- .../LazyExperiments/AsyncLazy.cs | 3 + .../LazyExperiments/AtomicLazy.cs | 58 +++++++++++++++++++ .../LazyExperiments/ScopedAsyncLazy.cs | 9 +-- .../LazyExperiments/ScopedLazy.cs | 4 +- 5 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 BitFaster.Caching/LazyExperiments/AtomicLazy.cs diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj index 34d486f1..2a8bf10c 100644 --- a/BitFaster.Caching/BitFaster.Caching.csproj +++ b/BitFaster.Caching/BitFaster.Caching.csproj @@ -36,7 +36,7 @@ - + diff --git a/BitFaster.Caching/LazyExperiments/AsyncLazy.cs b/BitFaster.Caching/LazyExperiments/AsyncLazy.cs index 15bf9be0..202a2418 100644 --- a/BitFaster.Caching/LazyExperiments/AsyncLazy.cs +++ b/BitFaster.Caching/LazyExperiments/AsyncLazy.cs @@ -51,5 +51,8 @@ public TaskAwaiter GetAwaiter() { return Task.GetAwaiter(); } + + public bool IsValueCreated + { get;set;} } } diff --git a/BitFaster.Caching/LazyExperiments/AtomicLazy.cs b/BitFaster.Caching/LazyExperiments/AtomicLazy.cs new file mode 100644 index 00000000..966702d2 --- /dev/null +++ b/BitFaster.Caching/LazyExperiments/AtomicLazy.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.LazyExperiments +{ + // https://github.com/dotnet/runtime/issues/27421 + // https://github.com/alastairtree/LazyCache/issues/73 + public class AtomicLazy + { + private readonly Func _factory; + + private T _value; + + private bool _initialized; + + private object _lock; + + public AtomicLazy(Func factory) + { + _factory = factory; + } + + public T Value => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _factory); + } + + public class AtomicAsyncLazy + { + private readonly Func> _factory; + + private Task _task; + + private bool _initialized; + + private object _lock; + + public AtomicAsyncLazy(Func> factory) + { + _factory = factory; + } + + public async Task Value() + { + try + { + return await LazyInitializer.EnsureInitialized(ref _task, ref _initialized, ref _lock, _factory); + } + catch + { + Volatile.Write(ref _initialized, false); + throw; + } + } + } +} diff --git a/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs b/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs index 48ecdca3..ee4cae75 100644 --- a/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs +++ b/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs @@ -10,7 +10,7 @@ namespace BitFaster.Caching.LazyExperiments public class ScopedAsyncLazy : IDisposable where TValue : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; private readonly Func> valueFactory; @@ -28,7 +28,7 @@ public ScopedAsyncLazy(Func> valueFactory) this.lazy = new AsyncLazy(valueFactory); } - public async Task> CreateLifetimeAsync() + public async Task>> CreateLifetimeAsync() { if (this.isDisposed) { @@ -47,7 +47,7 @@ public async Task> CreateLifetimeAsync() { // When Lease is disposed, it calls DecrementReferenceCount var value = await this.lazy; - return new Lifetime(value, this.DecrementReferenceCount); + return new Lifetime>(newRefCount, this.DecrementReferenceCount); } } } @@ -65,7 +65,8 @@ private void DecrementReferenceCount() { if (newRefCount.Value.IsValueCreated) { - newRefCount.Value.Value.Dispose(); + // TODO: badness + newRefCount.Value.Task.GetAwaiter().GetResult().Dispose(); } } diff --git a/BitFaster.Caching/LazyExperiments/ScopedLazy.cs b/BitFaster.Caching/LazyExperiments/ScopedLazy.cs index 844c7d42..15751cb7 100644 --- a/BitFaster.Caching/LazyExperiments/ScopedLazy.cs +++ b/BitFaster.Caching/LazyExperiments/ScopedLazy.cs @@ -19,7 +19,7 @@ public ScopedLazy(Func valueFactory) this.refCount = new ReferenceCount>(lazy); } - public Lifetime CreateLifetime() + public Lifetime> CreateLifetime() { if (this.isDisposed) { @@ -36,7 +36,7 @@ public Lifetime CreateLifetime() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lease is disposed, it calls DecrementReferenceCount - return new Lifetime(oldRefCount.Value.Value, this.DecrementReferenceCount); + return new Lifetime>(newRefCount, this.DecrementReferenceCount); } } } From 7a12ef9f3a274ed5d081f224bf25038a0265a211 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 18:05:40 -0700 Subject: [PATCH 03/31] cleanup, use atomic --- .../LazyExperiments/AtomicLazy.cs | 14 +++++++++++-- .../LazyExperiments/DesiredApi.cs | 7 +++++-- .../LazyExperiments/ScopedAsyncLazy.cs | 17 ++++++++-------- .../LazyExperiments/ScopedLazy.cs | 20 +++++++++++++------ 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/BitFaster.Caching/LazyExperiments/AtomicLazy.cs b/BitFaster.Caching/LazyExperiments/AtomicLazy.cs index 966702d2..9f81112d 100644 --- a/BitFaster.Caching/LazyExperiments/AtomicLazy.cs +++ b/BitFaster.Caching/LazyExperiments/AtomicLazy.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching.LazyExperiments +namespace BitFaster.Caching { // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 @@ -25,6 +26,8 @@ public AtomicLazy(Func factory) } public T Value => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _factory); + + public bool IsValueCreated => _initialized; } public class AtomicAsyncLazy @@ -46,7 +49,7 @@ public async Task Value() { try { - return await LazyInitializer.EnsureInitialized(ref _task, ref _initialized, ref _lock, _factory); + return await LazyInitializer.EnsureInitialized(ref _task, ref _initialized, ref _lock, _factory).ConfigureAwait(false); } catch { @@ -54,5 +57,12 @@ public async Task Value() throw; } } + + public TaskAwaiter GetAwaiter() + { + return Value().GetAwaiter(); + } + + public bool IsValueCreated => _initialized; } } diff --git a/BitFaster.Caching/LazyExperiments/DesiredApi.cs b/BitFaster.Caching/LazyExperiments/DesiredApi.cs index 12da0fc6..e136a16e 100644 --- a/BitFaster.Caching/LazyExperiments/DesiredApi.cs +++ b/BitFaster.Caching/LazyExperiments/DesiredApi.cs @@ -15,7 +15,9 @@ public static void HowToCacheALazy() using (var lifetime = lru.GetOrAdd(1, factory.Create).CreateLifetime()) { - var y = lifetime.Value; + // this is a bit ugly - double value or extension method + // would it be better to have a dedicated lifetime class that wraps this? + SomeDisposable y = lifetime.LazyValue(); } } @@ -31,7 +33,8 @@ public static async Task HowToCacheAnAsyncLazy() using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) { - var y = lifetime.Value; + // This is cleaned up by the magic GetAwaiter method + SomeDisposable y = await lifetime.Value; } } } diff --git a/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs b/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs index ee4cae75..ea99f39c 100644 --- a/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs +++ b/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs @@ -10,25 +10,25 @@ namespace BitFaster.Caching.LazyExperiments public class ScopedAsyncLazy : IDisposable where TValue : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; - private readonly Func> valueFactory; + //private readonly Func> valueFactory; - private readonly AsyncLazy lazy; + private readonly AtomicAsyncLazy lazy; // should this even be allowed? public ScopedAsyncLazy(Func valueFactory) { - this.lazy = new AsyncLazy(() => Task.FromResult(valueFactory())); + this.lazy = new AtomicAsyncLazy(() => Task.FromResult(valueFactory())); } public ScopedAsyncLazy(Func> valueFactory) { - this.lazy = new AsyncLazy(valueFactory); + this.lazy = new AtomicAsyncLazy(valueFactory); } - public async Task>> CreateLifetimeAsync() + public async Task>> CreateLifetimeAsync() { if (this.isDisposed) { @@ -47,11 +47,12 @@ public async Task>> CreateLifetimeAsync() { // When Lease is disposed, it calls DecrementReferenceCount var value = await this.lazy; - return new Lifetime>(newRefCount, this.DecrementReferenceCount); + return new Lifetime>(newRefCount, this.DecrementReferenceCount); } } } + // TODO: Do we need an async lifetime? private void DecrementReferenceCount() { while (true) @@ -66,7 +67,7 @@ private void DecrementReferenceCount() if (newRefCount.Value.IsValueCreated) { // TODO: badness - newRefCount.Value.Task.GetAwaiter().GetResult().Dispose(); + newRefCount.Value.GetAwaiter().GetResult().Dispose(); } } diff --git a/BitFaster.Caching/LazyExperiments/ScopedLazy.cs b/BitFaster.Caching/LazyExperiments/ScopedLazy.cs index 15751cb7..0e3f1db0 100644 --- a/BitFaster.Caching/LazyExperiments/ScopedLazy.cs +++ b/BitFaster.Caching/LazyExperiments/ScopedLazy.cs @@ -9,17 +9,17 @@ namespace BitFaster.Caching public class ScopedLazy : IDisposable where T : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; public ScopedLazy(Func valueFactory) { - // TODO: this will cache exceptions - var lazy = new Lazy(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication); - this.refCount = new ReferenceCount>(lazy); + // AtomicLazy will not cache exceptions + var lazy = new AtomicLazy(valueFactory); + this.refCount = new ReferenceCount>(lazy); } - public Lifetime> CreateLifetime() + public Lifetime> CreateLifetime() { if (this.isDisposed) { @@ -36,7 +36,7 @@ public Lifetime> CreateLifetime() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lease is disposed, it calls DecrementReferenceCount - return new Lifetime>(newRefCount, this.DecrementReferenceCount); + return new Lifetime>(newRefCount, this.DecrementReferenceCount); } } } @@ -72,4 +72,12 @@ public void Dispose() } } } + + public static class ScopedLazyExtensions + { + public static T LazyValue(this Lifetime> lifetime) where T : IDisposable + { + return lifetime.Value.Value; + } + } } From 8da2a4de624631383c4ccbae3b93cab5ca33dc47 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 18:17:36 -0700 Subject: [PATCH 04/31] cleanup namespace --- .../{LazyExperiments => Lazy}/AsyncLazy.cs | 2 +- .../{LazyExperiments => Lazy}/AtomicLazy.cs | 2 +- .../{LazyExperiments => Lazy}/DesiredApi.cs | 10 ++-- BitFaster.Caching/Lazy/LazyLifetime.cs | 48 +++++++++++++++++++ .../ScopedAsyncLazy.cs | 4 +- .../{LazyExperiments => Lazy}/ScopedLazy.cs | 14 ++---- 6 files changed, 60 insertions(+), 20 deletions(-) rename BitFaster.Caching/{LazyExperiments => Lazy}/AsyncLazy.cs (97%) rename BitFaster.Caching/{LazyExperiments => Lazy}/AtomicLazy.cs (97%) rename BitFaster.Caching/{LazyExperiments => Lazy}/DesiredApi.cs (87%) create mode 100644 BitFaster.Caching/Lazy/LazyLifetime.cs rename BitFaster.Caching/{LazyExperiments => Lazy}/ScopedAsyncLazy.cs (96%) rename BitFaster.Caching/{LazyExperiments => Lazy}/ScopedLazy.cs (84%) diff --git a/BitFaster.Caching/LazyExperiments/AsyncLazy.cs b/BitFaster.Caching/Lazy/AsyncLazy.cs similarity index 97% rename from BitFaster.Caching/LazyExperiments/AsyncLazy.cs rename to BitFaster.Caching/Lazy/AsyncLazy.cs index 202a2418..e296a366 100644 --- a/BitFaster.Caching/LazyExperiments/AsyncLazy.cs +++ b/BitFaster.Caching/Lazy/AsyncLazy.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching.LazyExperiments +namespace BitFaster.Caching.Lazy { public class AsyncLazy { diff --git a/BitFaster.Caching/LazyExperiments/AtomicLazy.cs b/BitFaster.Caching/Lazy/AtomicLazy.cs similarity index 97% rename from BitFaster.Caching/LazyExperiments/AtomicLazy.cs rename to BitFaster.Caching/Lazy/AtomicLazy.cs index 9f81112d..69df817e 100644 --- a/BitFaster.Caching/LazyExperiments/AtomicLazy.cs +++ b/BitFaster.Caching/Lazy/AtomicLazy.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching +namespace BitFaster.Caching.Lazy { // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 diff --git a/BitFaster.Caching/LazyExperiments/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs similarity index 87% rename from BitFaster.Caching/LazyExperiments/DesiredApi.cs rename to BitFaster.Caching/Lazy/DesiredApi.cs index e136a16e..14b6cb94 100644 --- a/BitFaster.Caching/LazyExperiments/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using BitFaster.Caching.Lru; -namespace BitFaster.Caching.LazyExperiments +namespace BitFaster.Caching.Lazy { class DesiredApi { @@ -15,9 +15,11 @@ public static void HowToCacheALazy() using (var lifetime = lru.GetOrAdd(1, factory.Create).CreateLifetime()) { - // this is a bit ugly - double value or extension method - // would it be better to have a dedicated lifetime class that wraps this? - SomeDisposable y = lifetime.LazyValue(); + // options: + // lazy lifetime = dupe class, cleaner API + // extension method to avoid lifetime.value.value + // just call lifetime.value.value (ugly) + SomeDisposable y = lifetime.Value; } } diff --git a/BitFaster.Caching/Lazy/LazyLifetime.cs b/BitFaster.Caching/Lazy/LazyLifetime.cs new file mode 100644 index 00000000..fd62c799 --- /dev/null +++ b/BitFaster.Caching/Lazy/LazyLifetime.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + public class LazyLifetime : IDisposable + { + private readonly Action onDisposeAction; + private readonly ReferenceCount> refCount; + private bool isDisposed; + + /// + /// Initializes a new instance of the Lifetime class. + /// + /// The value to keep alive. + /// The action to perform when the lifetime is terminated. + public LazyLifetime(ReferenceCount> value, Action onDisposeAction) + { + this.refCount = value; + this.onDisposeAction = onDisposeAction; + } + + /// + /// Gets the value. + /// + public T Value => this.refCount.Value.Value; + + /// + /// Gets the count of Lifetime instances referencing the same value. + /// + public int ReferenceCount => this.refCount.Count; + + /// + /// Terminates the lifetime and performs any cleanup required to release the value. + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.onDisposeAction(); + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs b/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs similarity index 96% rename from BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs rename to BitFaster.Caching/Lazy/ScopedAsyncLazy.cs index ea99f39c..77519405 100644 --- a/BitFaster.Caching/LazyExperiments/ScopedAsyncLazy.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching.LazyExperiments +namespace BitFaster.Caching.Lazy { // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal public class ScopedAsyncLazy : IDisposable @@ -13,8 +13,6 @@ public class ScopedAsyncLazy : IDisposable private ReferenceCount> refCount; private bool isDisposed; - //private readonly Func> valueFactory; - private readonly AtomicAsyncLazy lazy; // should this even be allowed? diff --git a/BitFaster.Caching/LazyExperiments/ScopedLazy.cs b/BitFaster.Caching/Lazy/ScopedLazy.cs similarity index 84% rename from BitFaster.Caching/LazyExperiments/ScopedLazy.cs rename to BitFaster.Caching/Lazy/ScopedLazy.cs index 0e3f1db0..06363cb9 100644 --- a/BitFaster.Caching/LazyExperiments/ScopedLazy.cs +++ b/BitFaster.Caching/Lazy/ScopedLazy.cs @@ -3,7 +3,7 @@ using System.Text; using System.Threading; -namespace BitFaster.Caching +namespace BitFaster.Caching.Lazy { // Enable caching a Lazy disposable object - guarantee single instance, safe disposal public class ScopedLazy : IDisposable @@ -19,7 +19,7 @@ public ScopedLazy(Func valueFactory) this.refCount = new ReferenceCount>(lazy); } - public Lifetime> CreateLifetime() + public LazyLifetime CreateLifetime() { if (this.isDisposed) { @@ -36,7 +36,7 @@ public Lifetime> CreateLifetime() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lease is disposed, it calls DecrementReferenceCount - return new Lifetime>(newRefCount, this.DecrementReferenceCount); + return new LazyLifetime(newRefCount, this.DecrementReferenceCount); } } } @@ -72,12 +72,4 @@ public void Dispose() } } } - - public static class ScopedLazyExtensions - { - public static T LazyValue(this Lifetime> lifetime) where T : IDisposable - { - return lifetime.Value.Value; - } - } } From bd2bc4270c2976a2a347307244fb6a1c106b376e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 19:07:11 -0700 Subject: [PATCH 05/31] async stuff --- BitFaster.Caching/Lazy/AsyncLifetime.cs | 59 +++++++++++++++++++++++ BitFaster.Caching/Lazy/AtomicLazy.cs | 8 ++- BitFaster.Caching/Lazy/DesiredApi.cs | 4 +- BitFaster.Caching/Lazy/ScopedAsyncLazy.cs | 20 +++++--- BitFaster.Caching/Lazy/ScopedLazy.cs | 2 + 5 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 BitFaster.Caching/Lazy/AsyncLifetime.cs diff --git a/BitFaster.Caching/Lazy/AsyncLifetime.cs b/BitFaster.Caching/Lazy/AsyncLifetime.cs new file mode 100644 index 00000000..82809d3a --- /dev/null +++ b/BitFaster.Caching/Lazy/AsyncLifetime.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ +#if NETCOREAPP3_1_OR_GREATER + public class AsyncLazyLifetime : IAsyncDisposable + { + private readonly Func onDisposeAction; + private readonly ReferenceCount> refCount; + private bool isDisposed; + + /// + /// Initializes a new instance of the AsyncLazyLifetime class. + /// + /// The value to keep alive. + /// The action to perform when the lifetime is terminated. + public AsyncLazyLifetime(ReferenceCount> value, Func onDisposeAction) + { + this.refCount = value; + this.onDisposeAction = onDisposeAction; + } + + /// + /// Gets the value. + /// + public Task Task + { + get { return this.refCount.Value.Value(); } + } + + public TaskAwaiter GetAwaiter() + { + return Task.GetAwaiter(); + } + + /// + /// Gets the count of Lifetime instances referencing the same value. + /// + public int ReferenceCount => this.refCount.Count; + + /// + /// Terminates the lifetime and performs any cleanup required to release the value. + /// + public async ValueTask DisposeAsync() + { + if (!this.isDisposed) + { + await this.onDisposeAction(); + this.isDisposed = true; + } + } + } +#endif +} diff --git a/BitFaster.Caching/Lazy/AtomicLazy.cs b/BitFaster.Caching/Lazy/AtomicLazy.cs index 69df817e..967fb86f 100644 --- a/BitFaster.Caching/Lazy/AtomicLazy.cs +++ b/BitFaster.Caching/Lazy/AtomicLazy.cs @@ -8,6 +8,10 @@ namespace BitFaster.Caching.Lazy { + // Should this be called simply Atomic? + // Then we have Atomic and Scoped, and ScopedAtomic + // Then AtomicAsync, ScopedAsync, and ScopedAtomicAsync would follow, but there is no ScopedAsync equivalent at this point. That should be IAsyncDisposable (.net Core 3.1 onwards). That would imply that GetOrAdd async understands IAsyncDispose + // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 public class AtomicLazy @@ -27,7 +31,7 @@ public AtomicLazy(Func factory) public T Value => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _factory); - public bool IsValueCreated => _initialized; + public bool IsValueCreated => Volatile.Read(ref _initialized); } public class AtomicAsyncLazy @@ -63,6 +67,6 @@ public TaskAwaiter GetAwaiter() return Value().GetAwaiter(); } - public bool IsValueCreated => _initialized; + public bool IsValueCreated => Volatile.Read(ref _initialized); } } diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index 14b6cb94..8266f482 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -33,10 +33,10 @@ public static async Task HowToCacheAnAsyncLazy() var lru = new ConcurrentLru>(4); var factory = new ScopedAsyncLazyFactory(); - using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) + await using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) { // This is cleaned up by the magic GetAwaiter method - SomeDisposable y = await lifetime.Value; + SomeDisposable y = await lifetime.Task; } } } diff --git a/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs b/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs index 77519405..a2b85958 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs @@ -7,7 +7,8 @@ namespace BitFaster.Caching.Lazy { // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal - public class ScopedAsyncLazy : IDisposable +#if NETCOREAPP3_1_OR_GREATER + public class ScopedAsyncLazy : IAsyncDisposable where TValue : IDisposable { private ReferenceCount> refCount; @@ -26,8 +27,9 @@ public ScopedAsyncLazy(Func> valueFactory) this.lazy = new AtomicAsyncLazy(valueFactory); } - public async Task>> CreateLifetimeAsync() + public async Task> CreateLifetimeAsync() { + // TODO: inside the loop? if (this.isDisposed) { throw new ObjectDisposedException($"{nameof(TValue)} is disposed."); @@ -45,13 +47,13 @@ public async Task>> CreateLifetimeAsync() { // When Lease is disposed, it calls DecrementReferenceCount var value = await this.lazy; - return new Lifetime>(newRefCount, this.DecrementReferenceCount); + return new AsyncLazyLifetime(newRefCount, this.DecrementReferenceCountAsync); } } } // TODO: Do we need an async lifetime? - private void DecrementReferenceCount() + private async Task DecrementReferenceCountAsync() { while (true) { @@ -60,12 +62,13 @@ private void DecrementReferenceCount() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { + // TODO: how to prevent a race here? Need to use the lock inside the lazy? if (newRefCount.Count == 0) { if (newRefCount.Value.IsValueCreated) { - // TODO: badness - newRefCount.Value.GetAwaiter().GetResult().Dispose(); + var v = await newRefCount.Value; + v.Dispose(); } } @@ -74,13 +77,14 @@ private void DecrementReferenceCount() } } - public void Dispose() + public async ValueTask DisposeAsync() { if (!this.isDisposed) { - this.DecrementReferenceCount(); + await this.DecrementReferenceCountAsync(); this.isDisposed = true; } } } +#endif } diff --git a/BitFaster.Caching/Lazy/ScopedLazy.cs b/BitFaster.Caching/Lazy/ScopedLazy.cs index 06363cb9..190ef9f2 100644 --- a/BitFaster.Caching/Lazy/ScopedLazy.cs +++ b/BitFaster.Caching/Lazy/ScopedLazy.cs @@ -21,6 +21,7 @@ public ScopedLazy(Func valueFactory) public LazyLifetime CreateLifetime() { + // TODO: inside the loop? if (this.isDisposed) { throw new ObjectDisposedException($"{nameof(T)} is disposed."); @@ -50,6 +51,7 @@ private void DecrementReferenceCount() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { + // TODO: how to prevent a race here? Need to use the lock inside the lazy? if (newRefCount.Count == 0) { if (newRefCount.Value.IsValueCreated) From 360f16b8ba000e188de64284e7c7ba022f982b2f Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 19:09:35 -0700 Subject: [PATCH 06/31] notes --- BitFaster.Caching/Lazy/ScopedAsyncLazy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs b/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs index a2b85958..2135cf6f 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs @@ -63,6 +63,7 @@ private async Task DecrementReferenceCountAsync() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // TODO: how to prevent a race here? Need to use the lock inside the lazy? + // Do we need atomic disposable? if (newRefCount.Count == 0) { if (newRefCount.Value.IsValueCreated) From c0803da963d73af525776446a5db7a491294ac5e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 19:16:19 -0700 Subject: [PATCH 07/31] fix build --- BitFaster.Caching/Lazy/DesiredApi.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index 8266f482..a476ed7a 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -28,6 +28,7 @@ public static void HowToCacheALazy() // 2. value is created lazily, guarantee single instance of object, single invocation of lazy // 3. lazy value is disposed by scope // 4. lifetime keeps scope alive +#if NETCOREAPP3_1_OR_GREATER public static async Task HowToCacheAnAsyncLazy() { var lru = new ConcurrentLru>(4); @@ -39,6 +40,7 @@ public static async Task HowToCacheAnAsyncLazy() SomeDisposable y = await lifetime.Task; } } +#endif } public class ScopedLazyFactory From 0a12b421bb86f6f6b86a67f348559b54213ff588 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 19:22:20 -0700 Subject: [PATCH 08/31] fix build --- BitFaster.Caching/Lazy/DesiredApi.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index a476ed7a..6b4efefb 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -56,6 +56,7 @@ public ScopedLazy Create(int key) } } +#if NETCOREAPP3_1_OR_GREATER public class ScopedAsyncLazyFactory { public Task CreateAsync(int key) @@ -68,6 +69,7 @@ public ScopedAsyncLazy Create(int key) return new ScopedAsyncLazy(() => Task.FromResult(new SomeDisposable())); } } +#endif public class SomeDisposable : IDisposable { From 91d29250b051ac9307bacad46c09061304d653a4 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 19:55:53 -0700 Subject: [PATCH 09/31] rename --- BitFaster.Caching/Lazy/AsyncLazy.cs | 58 ------------------- .../Lazy/{AtomicLazy.cs => Atomic.cs} | 8 +-- ...syncLifetime.cs => AtomicAsyncLifetime.cs} | 6 +- .../{LazyLifetime.cs => AtomicLifetime.cs} | 6 +- BitFaster.Caching/Lazy/DesiredApi.cs | 49 ++++++++++++---- .../Lazy/{ScopedLazy.cs => ScopedAtomic.cs} | 14 ++--- ...copedAsyncLazy.cs => ScopedAtomicAsync.cs} | 18 +++--- 7 files changed, 63 insertions(+), 96 deletions(-) delete mode 100644 BitFaster.Caching/Lazy/AsyncLazy.cs rename BitFaster.Caching/Lazy/{AtomicLazy.cs => Atomic.cs} (91%) rename BitFaster.Caching/Lazy/{AsyncLifetime.cs => AtomicAsyncLifetime.cs} (86%) rename BitFaster.Caching/Lazy/{LazyLifetime.cs => AtomicLifetime.cs} (86%) rename BitFaster.Caching/Lazy/{ScopedLazy.cs => ScopedAtomic.cs} (83%) rename BitFaster.Caching/Lazy/{ScopedAsyncLazy.cs => ScopedAtomicAsync.cs} (79%) diff --git a/BitFaster.Caching/Lazy/AsyncLazy.cs b/BitFaster.Caching/Lazy/AsyncLazy.cs deleted file mode 100644 index e296a366..00000000 --- a/BitFaster.Caching/Lazy/AsyncLazy.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -namespace BitFaster.Caching.Lazy -{ - public class AsyncLazy - { - private readonly object mutex; - private Lazy> lazy; - private readonly Func> valueFactory; - - public AsyncLazy(Func> valueFactory) - { - this.mutex = new object(); - this.valueFactory = RetryOnFailure(valueFactory); - this.lazy = new Lazy>(this.valueFactory); - } - - private Func> RetryOnFailure(Func> valueFactory) - { - return async () => - { - try - { - return await valueFactory().ConfigureAwait(false); - } - catch - { - // lock exists only because lazy is replaced on error - // better approach might be either: - // - value factory is responsible for retry, not lazy (single responsibility) - // - use TLRU, expire invalid items - lock (mutex) - { - this.lazy = new Lazy>(this.valueFactory); - } - throw; - } - }; - } - - public Task Task - { - get { lock (this.mutex) { return this.lazy.Value; } } - } - - public TaskAwaiter GetAwaiter() - { - return Task.GetAwaiter(); - } - - public bool IsValueCreated - { get;set;} - } -} diff --git a/BitFaster.Caching/Lazy/AtomicLazy.cs b/BitFaster.Caching/Lazy/Atomic.cs similarity index 91% rename from BitFaster.Caching/Lazy/AtomicLazy.cs rename to BitFaster.Caching/Lazy/Atomic.cs index 967fb86f..0e9455d9 100644 --- a/BitFaster.Caching/Lazy/AtomicLazy.cs +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -14,7 +14,7 @@ namespace BitFaster.Caching.Lazy // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 - public class AtomicLazy + public class Atomic { private readonly Func _factory; @@ -24,7 +24,7 @@ public class AtomicLazy private object _lock; - public AtomicLazy(Func factory) + public Atomic(Func factory) { _factory = factory; } @@ -34,7 +34,7 @@ public AtomicLazy(Func factory) public bool IsValueCreated => Volatile.Read(ref _initialized); } - public class AtomicAsyncLazy + public class AtomicAsync { private readonly Func> _factory; @@ -44,7 +44,7 @@ public class AtomicAsyncLazy private object _lock; - public AtomicAsyncLazy(Func> factory) + public AtomicAsync(Func> factory) { _factory = factory; } diff --git a/BitFaster.Caching/Lazy/AsyncLifetime.cs b/BitFaster.Caching/Lazy/AtomicAsyncLifetime.cs similarity index 86% rename from BitFaster.Caching/Lazy/AsyncLifetime.cs rename to BitFaster.Caching/Lazy/AtomicAsyncLifetime.cs index 82809d3a..67db28a7 100644 --- a/BitFaster.Caching/Lazy/AsyncLifetime.cs +++ b/BitFaster.Caching/Lazy/AtomicAsyncLifetime.cs @@ -8,10 +8,10 @@ namespace BitFaster.Caching.Lazy { #if NETCOREAPP3_1_OR_GREATER - public class AsyncLazyLifetime : IAsyncDisposable + public class AtomicAsyncLifetime : IAsyncDisposable { private readonly Func onDisposeAction; - private readonly ReferenceCount> refCount; + private readonly ReferenceCount> refCount; private bool isDisposed; /// @@ -19,7 +19,7 @@ public class AsyncLazyLifetime : IAsyncDisposable /// /// The value to keep alive. /// The action to perform when the lifetime is terminated. - public AsyncLazyLifetime(ReferenceCount> value, Func onDisposeAction) + public AtomicAsyncLifetime(ReferenceCount> value, Func onDisposeAction) { this.refCount = value; this.onDisposeAction = onDisposeAction; diff --git a/BitFaster.Caching/Lazy/LazyLifetime.cs b/BitFaster.Caching/Lazy/AtomicLifetime.cs similarity index 86% rename from BitFaster.Caching/Lazy/LazyLifetime.cs rename to BitFaster.Caching/Lazy/AtomicLifetime.cs index fd62c799..0107b968 100644 --- a/BitFaster.Caching/Lazy/LazyLifetime.cs +++ b/BitFaster.Caching/Lazy/AtomicLifetime.cs @@ -6,10 +6,10 @@ namespace BitFaster.Caching.Lazy { - public class LazyLifetime : IDisposable + public class AtomicLifetime : IDisposable { private readonly Action onDisposeAction; - private readonly ReferenceCount> refCount; + private readonly ReferenceCount> refCount; private bool isDisposed; /// @@ -17,7 +17,7 @@ public class LazyLifetime : IDisposable /// /// The value to keep alive. /// The action to perform when the lifetime is terminated. - public LazyLifetime(ReferenceCount> value, Action onDisposeAction) + public AtomicLifetime(ReferenceCount> value, Action onDisposeAction) { this.refCount = value; this.onDisposeAction = onDisposeAction; diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index 6b4efefb..d24569d5 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -8,10 +8,21 @@ namespace BitFaster.Caching.Lazy { class DesiredApi { - public static void HowToCacheALazy() + public static void HowToCacheWithAtomicValueFactory() + { + var lru = new ConcurrentLru>(4); + + // raw, this is a bit of a mess + Atomic r = lru.GetOrAdd(1, i => new Atomic(() => i)); + + // extension cleanup can hide it + int rr = lru.GetOrAdd(1, i => i); + } + + public static void HowToCacheADisposableAtomicValueFactory() { - var lru = new ConcurrentLru>(4); - var factory = new ScopedLazyFactory(); + var lru = new ConcurrentLru>(4); + var factory = new ScopedAtomicFactory(); using (var lifetime = lru.GetOrAdd(1, factory.Create).CreateLifetime()) { @@ -29,10 +40,10 @@ public static void HowToCacheALazy() // 3. lazy value is disposed by scope // 4. lifetime keeps scope alive #if NETCOREAPP3_1_OR_GREATER - public static async Task HowToCacheAnAsyncLazy() + public static async Task HowToCacheADisposableAsyncLazy() { - var lru = new ConcurrentLru>(4); - var factory = new ScopedAsyncLazyFactory(); + var lru = new ConcurrentLru>(4); + var factory = new ScopedAtomicAsyncFactory(); await using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) { @@ -43,30 +54,30 @@ public static async Task HowToCacheAnAsyncLazy() #endif } - public class ScopedLazyFactory + public class ScopedAtomicFactory { public Task CreateAsync(int key) { return Task.FromResult(new SomeDisposable()); } - public ScopedLazy Create(int key) + public ScopedAtomic Create(int key) { - return new ScopedLazy(() => new SomeDisposable()); + return new ScopedAtomic(() => new SomeDisposable()); } } #if NETCOREAPP3_1_OR_GREATER - public class ScopedAsyncLazyFactory + public class ScopedAtomicAsyncFactory { public Task CreateAsync(int key) { return Task.FromResult(new SomeDisposable()); } - public ScopedAsyncLazy Create(int key) + public ScopedAtomicAsync Create(int key) { - return new ScopedAsyncLazy(() => Task.FromResult(new SomeDisposable())); + return new ScopedAtomicAsync(() => Task.FromResult(new SomeDisposable())); } } #endif @@ -78,4 +89,18 @@ public void Dispose() } } + + public static class AtomicCacheExtensions + { + public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) + { + return cache.GetOrAdd(key, k => new Atomic(() => valueFactory(k))).Value; + } + + public static async Task GetOrAddAsync(this ICache> cache, K key, Func valueFactory) + { + var atomic = await cache.GetOrAddAsync(key, k => Task.FromResult(new Atomic(() => valueFactory(k)))).ConfigureAwait(false); + return atomic.Value; + } + } } diff --git a/BitFaster.Caching/Lazy/ScopedLazy.cs b/BitFaster.Caching/Lazy/ScopedAtomic.cs similarity index 83% rename from BitFaster.Caching/Lazy/ScopedLazy.cs rename to BitFaster.Caching/Lazy/ScopedAtomic.cs index 190ef9f2..91cea892 100644 --- a/BitFaster.Caching/Lazy/ScopedLazy.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomic.cs @@ -6,20 +6,20 @@ namespace BitFaster.Caching.Lazy { // Enable caching a Lazy disposable object - guarantee single instance, safe disposal - public class ScopedLazy : IDisposable + public class ScopedAtomic : IDisposable where T : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; - public ScopedLazy(Func valueFactory) + public ScopedAtomic(Func valueFactory) { // AtomicLazy will not cache exceptions - var lazy = new AtomicLazy(valueFactory); - this.refCount = new ReferenceCount>(lazy); + var lazy = new Atomic(valueFactory); + this.refCount = new ReferenceCount>(lazy); } - public LazyLifetime CreateLifetime() + public AtomicLifetime CreateLifetime() { // TODO: inside the loop? if (this.isDisposed) @@ -37,7 +37,7 @@ public LazyLifetime CreateLifetime() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lease is disposed, it calls DecrementReferenceCount - return new LazyLifetime(newRefCount, this.DecrementReferenceCount); + return new AtomicLifetime(newRefCount, this.DecrementReferenceCount); } } } diff --git a/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs b/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs similarity index 79% rename from BitFaster.Caching/Lazy/ScopedAsyncLazy.cs rename to BitFaster.Caching/Lazy/ScopedAtomicAsync.cs index 2135cf6f..27e5190d 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncLazy.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs @@ -8,26 +8,26 @@ namespace BitFaster.Caching.Lazy { // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal #if NETCOREAPP3_1_OR_GREATER - public class ScopedAsyncLazy : IAsyncDisposable + public class ScopedAtomicAsync : IAsyncDisposable where TValue : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; - private readonly AtomicAsyncLazy lazy; + private readonly AtomicAsync lazy; // should this even be allowed? - public ScopedAsyncLazy(Func valueFactory) + public ScopedAtomicAsync(Func valueFactory) { - this.lazy = new AtomicAsyncLazy(() => Task.FromResult(valueFactory())); + this.lazy = new AtomicAsync(() => Task.FromResult(valueFactory())); } - public ScopedAsyncLazy(Func> valueFactory) + public ScopedAtomicAsync(Func> valueFactory) { - this.lazy = new AtomicAsyncLazy(valueFactory); + this.lazy = new AtomicAsync(valueFactory); } - public async Task> CreateLifetimeAsync() + public async Task> CreateLifetimeAsync() { // TODO: inside the loop? if (this.isDisposed) @@ -47,7 +47,7 @@ public async Task> CreateLifetimeAsync() { // When Lease is disposed, it calls DecrementReferenceCount var value = await this.lazy; - return new AsyncLazyLifetime(newRefCount, this.DecrementReferenceCountAsync); + return new AtomicAsyncLifetime(newRefCount, this.DecrementReferenceCountAsync); } } } From 516627a229bea14ca9c26b91a6f54b223968e8eb Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 6 Nov 2021 20:08:47 -0700 Subject: [PATCH 10/31] more examples --- BitFaster.Caching/Lazy/Atomic.cs | 30 ++++++++++++++-------------- BitFaster.Caching/Lazy/DesiredApi.cs | 4 ++++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/BitFaster.Caching/Lazy/Atomic.cs b/BitFaster.Caching/Lazy/Atomic.cs index 0e9455d9..da12c3e8 100644 --- a/BitFaster.Caching/Lazy/Atomic.cs +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -16,48 +16,48 @@ namespace BitFaster.Caching.Lazy // https://github.com/alastairtree/LazyCache/issues/73 public class Atomic { - private readonly Func _factory; + private readonly Func valueFactory; - private T _value; + private T value; - private bool _initialized; + private bool isInitialized; - private object _lock; + private object @lock; public Atomic(Func factory) { - _factory = factory; + valueFactory = factory; } - public T Value => LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _factory); + public T Value => LazyInitializer.EnsureInitialized(ref value, ref isInitialized, ref @lock, valueFactory); - public bool IsValueCreated => Volatile.Read(ref _initialized); + public bool IsValueCreated => Volatile.Read(ref isInitialized); } public class AtomicAsync { - private readonly Func> _factory; + private readonly Func> valueFactory; - private Task _task; + private Task task; - private bool _initialized; + private bool isInitialized; - private object _lock; + private object @lock; public AtomicAsync(Func> factory) { - _factory = factory; + valueFactory = factory; } public async Task Value() { try { - return await LazyInitializer.EnsureInitialized(ref _task, ref _initialized, ref _lock, _factory).ConfigureAwait(false); + return await LazyInitializer.EnsureInitialized(ref task, ref isInitialized, ref @lock, valueFactory).ConfigureAwait(false); } catch { - Volatile.Write(ref _initialized, false); + Volatile.Write(ref isInitialized, false); throw; } } @@ -67,6 +67,6 @@ public TaskAwaiter GetAwaiter() return Value().GetAwaiter(); } - public bool IsValueCreated => Volatile.Read(ref _initialized); + public bool IsValueCreated => Volatile.Read(ref isInitialized); } } diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index d24569d5..c941e26a 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -17,6 +17,10 @@ public static void HowToCacheWithAtomicValueFactory() // extension cleanup can hide it int rr = lru.GetOrAdd(1, i => i); + + lru.TryUpdate(2, new Atomic(() => 3)); + lru.TryGet(1, out Atomic v); + lru.AddOrUpdate(1, new Atomic(() => 2)); } public static void HowToCacheADisposableAtomicValueFactory() From 656c5b91cab2d4da58771781812ab236c6ffe987 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 8 Nov 2021 18:08:30 -0800 Subject: [PATCH 11/31] more atomic --- .../AsyncAtomicBench.cs | 44 +++++++ BitFaster.Caching.Benchmarks/AtomicBench.cs | 44 +++++++ BitFaster.Caching/Lazy/AsyncAtomic.cs | 99 ++++++++++++++ .../Lazy/AsyncAtomicCacheExtensions.cs | 52 ++++++++ ...syncLifetime.cs => AsyncAtomicLifetime.cs} | 34 ++--- BitFaster.Caching/Lazy/Atomic.cs | 82 +++++++----- .../Lazy/AtomicCacheExtensions.cs | 51 +++++++ BitFaster.Caching/Lazy/AtomicLifetime.cs | 70 +++++----- BitFaster.Caching/Lazy/DesiredApi.cs | 100 +++++++------- BitFaster.Caching/Lazy/DisposableAtomic.cs | 124 ++++++++++++++++++ BitFaster.Caching/Lazy/ScopedAtomic.cs | 70 +++++++--- BitFaster.Caching/Lazy/ScopedAtomicAsync.cs | 33 +++-- .../Lazy/ScopedAtomicExtensions.cs | 59 +++++++++ BitFaster.Caching/Lazy/Synchronized.cs | 52 ++++++++ BitFaster.Caching/Scoped.cs | 34 +++-- 15 files changed, 773 insertions(+), 175 deletions(-) create mode 100644 BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs create mode 100644 BitFaster.Caching.Benchmarks/AtomicBench.cs create mode 100644 BitFaster.Caching/Lazy/AsyncAtomic.cs create mode 100644 BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs rename BitFaster.Caching/Lazy/{AtomicAsyncLifetime.cs => AsyncAtomicLifetime.cs} (60%) create mode 100644 BitFaster.Caching/Lazy/AtomicCacheExtensions.cs create mode 100644 BitFaster.Caching/Lazy/DisposableAtomic.cs create mode 100644 BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs create mode 100644 BitFaster.Caching/Lazy/Synchronized.cs diff --git a/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs b/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs new file mode 100644 index 00000000..7efa3759 --- /dev/null +++ b/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BitFaster.Caching.Lazy; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Benchmarks +{ + [DisassemblyDiagnoser(printSource: true)] + [MemoryDiagnoser] + public class AsyncAtomicBench + { + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> atomicConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + [Benchmark()] + public void ConcurrentDictionary() + { + Func func = x => x; + dictionary.GetOrAdd(1, func); + } + + [Benchmark(Baseline = true)] + public async Task ConcurrentLruAsync() + { + Func> func = x => Task.FromResult(x); + await concurrentLru.GetOrAddAsync(1, func).ConfigureAwait(false); + } + + [Benchmark()] + public async Task AtomicConcurrentLruAsync() + { + Func> func = x => Task.FromResult(x); + await atomicConcurrentLru.GetOrAddAsync(1, func).ConfigureAwait(false); + } + } +} diff --git a/BitFaster.Caching.Benchmarks/AtomicBench.cs b/BitFaster.Caching.Benchmarks/AtomicBench.cs new file mode 100644 index 00000000..5bb6f5f1 --- /dev/null +++ b/BitFaster.Caching.Benchmarks/AtomicBench.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BitFaster.Caching.Lazy; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Benchmarks +{ + [DisassemblyDiagnoser(printSource: true)] + [MemoryDiagnoser] + public class AtomicBench + { + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> atomicConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + [Benchmark()] + public void ConcurrentDictionary() + { + Func func = x => x; + dictionary.GetOrAdd(1, func); + } + + [Benchmark(Baseline = true)] + public void ConcurrentLru() + { + Func func = x => x; + concurrentLru.GetOrAdd(1, func); + } + + [Benchmark()] + public void AtomicConcurrentLru() + { + Func func = x => x; + atomicConcurrentLru.GetOrAdd(1, func); + } + } +} diff --git a/BitFaster.Caching/Lazy/AsyncAtomic.cs b/BitFaster.Caching/Lazy/AsyncAtomic.cs new file mode 100644 index 00000000..e5ff632b --- /dev/null +++ b/BitFaster.Caching/Lazy/AsyncAtomic.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + public class AsyncAtomic + { + private volatile Initializer initializer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private V value; + + public AsyncAtomic() + { + this.initializer = new Initializer(); + } + + public AsyncAtomic(V value) + { + this.value = value; + } + + public async Task GetValueAsync(K key, Func> valueFactory) + { + if (this.initializer == null) + { + return this.value; + } + + return await CreateValueAsync(key, valueFactory).ConfigureAwait(false); + } + + public bool IsValueCreated => this.initializer == null; + + public V ValueIfCreated + { + get + { + if (!this.IsValueCreated) + { + return default; + } + + return this.value; + } + } + + private async Task CreateValueAsync(K key, Func> valueFactory) + { + Initializer init = this.initializer; + + if (init != null) + { + this.value = await init.CreateValue(key, valueFactory).ConfigureAwait(false); + this.initializer = null; + } + + return this.value; + } + + private class Initializer + { + private object syncLock = new object(); + private bool isInitialized; + private Task valueTask; + + public async Task CreateValue(K key, Func> valueFactory) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var synchronizedTask = Synchronized.Initialize(ref this.valueTask, ref isInitialized, ref syncLock, tcs.Task); + + if (ReferenceEquals(synchronizedTask, tcs.Task)) + { + try + { + var value = await valueFactory(key).ConfigureAwait(false); + tcs.SetResult(value); + return value; + } + catch (Exception ex) + { + Volatile.Write(ref isInitialized, false); + tcs.SetException(ex); + throw; + } + } + + return await synchronizedTask.ConfigureAwait(false); + } + } + } +} diff --git a/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs new file mode 100644 index 00000000..2fe9f6d0 --- /dev/null +++ b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + public static class AsyncAtomicCacheExtensions + { + //public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) + //{ + // return cache + // .GetOrAdd(key, k => new AsyncAtomic()) + // .GetValueAsync(() => Task.FromResult(valueFactory(key))) + // .GetAwaiter().GetResult(); + //} + + public static Task GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) + { + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic()); + return synchronized.GetValueAsync(key, valueFactory); + } + + public static void AddOrUpdate(this ICache> cache, K key, V value) + { + cache.AddOrUpdate(key, new AsyncAtomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) + { + return cache.TryUpdate(key, new AsyncAtomic(value)); + } + + public static bool TryGet(this ICache> cache, K key, out V value) + { + AsyncAtomic output; + bool ret = cache.TryGet(key, out output); + + if (ret) + { + value = output.ValueIfCreated; + } + else + { + value = default; + } + + return ret; + } + } +} diff --git a/BitFaster.Caching/Lazy/AtomicAsyncLifetime.cs b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs similarity index 60% rename from BitFaster.Caching/Lazy/AtomicAsyncLifetime.cs rename to BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs index 67db28a7..18788f8d 100644 --- a/BitFaster.Caching/Lazy/AtomicAsyncLifetime.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs @@ -8,10 +8,10 @@ namespace BitFaster.Caching.Lazy { #if NETCOREAPP3_1_OR_GREATER - public class AtomicAsyncLifetime : IAsyncDisposable + public class AsyncAtomicLifetime : IDisposable { - private readonly Func onDisposeAction; - private readonly ReferenceCount> refCount; + private readonly Action onDisposeAction; + private readonly ReferenceCount> refCount; private bool isDisposed; /// @@ -19,24 +19,26 @@ public class AtomicAsyncLifetime : IAsyncDisposable /// /// The value to keep alive. /// The action to perform when the lifetime is terminated. - public AtomicAsyncLifetime(ReferenceCount> value, Func onDisposeAction) + public AsyncAtomicLifetime(ReferenceCount> value, Action onDisposeAction) { this.refCount = value; this.onDisposeAction = onDisposeAction; } - /// - /// Gets the value. - /// - public Task Task - { - get { return this.refCount.Value.Value(); } + public Task GetValueAsync(K key, Func> valueFactory) + { + return this.refCount.Value.GetValueAsync(key, valueFactory); } - public TaskAwaiter GetAwaiter() - { - return Task.GetAwaiter(); - } + //public Task Task + //{ + // get { return this.refCount.Value..Value(); } + //} + + //public TaskAwaiter GetAwaiter() + //{ + // return Task.GetAwaiter(); + //} /// /// Gets the count of Lifetime instances referencing the same value. @@ -46,11 +48,11 @@ public TaskAwaiter GetAwaiter() /// /// Terminates the lifetime and performs any cleanup required to release the value. /// - public async ValueTask DisposeAsync() + public void Dispose() { if (!this.isDisposed) { - await this.onDisposeAction(); + this.onDisposeAction(); this.isDisposed = true; } } diff --git a/BitFaster.Caching/Lazy/Atomic.cs b/BitFaster.Caching/Lazy/Atomic.cs index da12c3e8..ce6f0185 100644 --- a/BitFaster.Caching/Lazy/Atomic.cs +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -14,59 +15,72 @@ namespace BitFaster.Caching.Lazy // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 - public class Atomic + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + public class Atomic { - private readonly Func valueFactory; + private volatile Initializer initializer; - private T value; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private V value; - private bool isInitialized; - - private object @lock; - - public Atomic(Func factory) + public Atomic() { - valueFactory = factory; + this.initializer = new Initializer(); } - public T Value => LazyInitializer.EnsureInitialized(ref value, ref isInitialized, ref @lock, valueFactory); - - public bool IsValueCreated => Volatile.Read(ref isInitialized); - } + public Atomic(V value) + { + this.value = value; + } - public class AtomicAsync - { - private readonly Func> valueFactory; - - private Task task; + public V GetValue(K key, Func valueFactory) + { + if (this.initializer == null) + { + return this.value; + } - private bool isInitialized; + return CreateValue(valueFactory, key); + } - private object @lock; + public bool IsValueCreated => this.initializer == null; - public AtomicAsync(Func> factory) + public V ValueIfCreated { - valueFactory = factory; + get + { + if (!this.IsValueCreated) + { + return default; + } + + return this.value; + } } - public async Task Value() + private V CreateValue(Func valueFactory, K key) { - try - { - return await LazyInitializer.EnsureInitialized(ref task, ref isInitialized, ref @lock, valueFactory).ConfigureAwait(false); - } - catch + Initializer init = this.initializer; + + if (init != null) { - Volatile.Write(ref isInitialized, false); - throw; + this.value = init.CreateValue(valueFactory, key); + this.initializer = null; } + + return this.value; } - public TaskAwaiter GetAwaiter() + private class Initializer { - return Value().GetAwaiter(); - } + private object syncLock = new object(); + private bool isInitialized; + private V value; - public bool IsValueCreated => Volatile.Read(ref isInitialized); + public V CreateValue(Func valueFactory, K key) + { + return Synchronized.Initialize(ref this.value, ref isInitialized, ref syncLock, valueFactory, key); + } + } } } diff --git a/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs b/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs new file mode 100644 index 00000000..0204d35f --- /dev/null +++ b/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + public static class AtomicCacheExtensions + { + public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) + { + return cache + .GetOrAdd(key, _ => new Atomic()) + .GetValue(key, valueFactory); + } + + //public static async Task GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) + //{ + // var synchronized = cache.GetOrAdd(key, _ => new Atomic()); + // return synchronized.GetValue(() => valueFactory(key).GetAwaiter().GetResult()); + //} + + public static void AddOrUpdate(this ICache> cache, K key, V value) + { + cache.AddOrUpdate(key, new Atomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) + { + return cache.TryUpdate(key, new Atomic(value)); + } + + public static bool TryGet(this ICache> cache, K key, out V value) + { + Atomic output; + bool ret = cache.TryGet(key, out output); + + if (ret) + { + value = output.ValueIfCreated; + } + else + { + value = default; + } + + return ret; + } + } +} diff --git a/BitFaster.Caching/Lazy/AtomicLifetime.cs b/BitFaster.Caching/Lazy/AtomicLifetime.cs index 0107b968..73147d9f 100644 --- a/BitFaster.Caching/Lazy/AtomicLifetime.cs +++ b/BitFaster.Caching/Lazy/AtomicLifetime.cs @@ -6,43 +6,43 @@ namespace BitFaster.Caching.Lazy { - public class AtomicLifetime : IDisposable - { - private readonly Action onDisposeAction; - private readonly ReferenceCount> refCount; - private bool isDisposed; + //public class AtomicLifetime : IDisposable + //{ + // private readonly Action onDisposeAction; + // private readonly ReferenceCount> refCount; + // private bool isDisposed; - /// - /// Initializes a new instance of the Lifetime class. - /// - /// The value to keep alive. - /// The action to perform when the lifetime is terminated. - public AtomicLifetime(ReferenceCount> value, Action onDisposeAction) - { - this.refCount = value; - this.onDisposeAction = onDisposeAction; - } + // /// + // /// Initializes a new instance of the Lifetime class. + // /// + // /// The value to keep alive. + // /// The action to perform when the lifetime is terminated. + // public AtomicLifetime(ReferenceCount> value, Action onDisposeAction) + // { + // this.refCount = value; + // this.onDisposeAction = onDisposeAction; + // } - /// - /// Gets the value. - /// - public T Value => this.refCount.Value.Value; + // /// + // /// Gets the value. + // /// + // public T Value => this.refCount.Value.Value; - /// - /// Gets the count of Lifetime instances referencing the same value. - /// - public int ReferenceCount => this.refCount.Count; + // /// + // /// Gets the count of Lifetime instances referencing the same value. + // /// + // public int ReferenceCount => this.refCount.Count; - /// - /// Terminates the lifetime and performs any cleanup required to release the value. - /// - public void Dispose() - { - if (!this.isDisposed) - { - this.onDisposeAction(); - this.isDisposed = true; - } - } - } + // /// + // /// Terminates the lifetime and performs any cleanup required to release the value. + // /// + // public void Dispose() + // { + // if (!this.isDisposed) + // { + // this.onDisposeAction(); + // this.isDisposed = true; + // } + // } + //} } diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index c941e26a..825cdef5 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -8,33 +8,39 @@ namespace BitFaster.Caching.Lazy { class DesiredApi { - public static void HowToCacheWithAtomicValueFactory() + public static void HowToCacheAtomic() { - var lru = new ConcurrentLru>(4); + var lru = new ConcurrentLru>(4); // raw, this is a bit of a mess - Atomic r = lru.GetOrAdd(1, i => new Atomic(() => i)); + int r = lru.GetOrAdd(1, i => new Atomic()).GetValue(1, x => x); // extension cleanup can hide it int rr = lru.GetOrAdd(1, i => i); - lru.TryUpdate(2, new Atomic(() => 3)); - lru.TryGet(1, out Atomic v); - lru.AddOrUpdate(1, new Atomic(() => 2)); + lru.TryUpdate(2, 3); + lru.TryGet(1, out int v); + lru.AddOrUpdate(1, 2); } - public static void HowToCacheADisposableAtomicValueFactory() + public async static Task HowToCacheAsyncAtomic() + { + var asyncAtomicLru = new ConcurrentLru>(5); + + int ar = await asyncAtomicLru.GetOrAddAsync(1, i => Task.FromResult(i)); + + asyncAtomicLru.TryUpdate(2, 3); + asyncAtomicLru.TryGet(1, out int v); + asyncAtomicLru.AddOrUpdate(1, 2); + } + + public static void HowToCacheDisposableAtomic() { - var lru = new ConcurrentLru>(4); - var factory = new ScopedAtomicFactory(); + var scopedAtomicLru2 = new ConcurrentLru>(5); - using (var lifetime = lru.GetOrAdd(1, factory.Create).CreateLifetime()) + using (var l = scopedAtomicLru2.GetOrAdd(1, k => new SomeDisposable())) { - // options: - // lazy lifetime = dupe class, cleaner API - // extension method to avoid lifetime.value.value - // just call lifetime.value.value (ugly) - SomeDisposable y = lifetime.Value; + SomeDisposable d = l.Value; } } @@ -46,30 +52,30 @@ public static void HowToCacheADisposableAtomicValueFactory() #if NETCOREAPP3_1_OR_GREATER public static async Task HowToCacheADisposableAsyncLazy() { - var lru = new ConcurrentLru>(4); + var lru = new ConcurrentLru>(4); var factory = new ScopedAtomicAsyncFactory(); - await using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) - { - // This is cleaned up by the magic GetAwaiter method - SomeDisposable y = await lifetime.Task; - } + //await using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) + //{ + // // This is cleaned up by the magic GetAwaiter method + // SomeDisposable y = await lifetime.Task; + //} } #endif } - public class ScopedAtomicFactory - { - public Task CreateAsync(int key) - { - return Task.FromResult(new SomeDisposable()); - } + //public class ScopedAtomicFactory + //{ + // public Task CreateAsync(int key) + // { + // return Task.FromResult(new SomeDisposable()); + // } - public ScopedAtomic Create(int key) - { - return new ScopedAtomic(() => new SomeDisposable()); - } - } + // public ScopedAtomic Create(int key) + // { + // return new ScopedAtomic(() => new SomeDisposable()); + // } + //} #if NETCOREAPP3_1_OR_GREATER public class ScopedAtomicAsyncFactory @@ -79,9 +85,9 @@ public Task CreateAsync(int key) return Task.FromResult(new SomeDisposable()); } - public ScopedAtomicAsync Create(int key) + public ScopedAtomicAsync Create(int key) { - return new ScopedAtomicAsync(() => Task.FromResult(new SomeDisposable())); + return new ScopedAtomicAsync(); } } #endif @@ -94,17 +100,17 @@ public void Dispose() } } - public static class AtomicCacheExtensions - { - public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) - { - return cache.GetOrAdd(key, k => new Atomic(() => valueFactory(k))).Value; - } - - public static async Task GetOrAddAsync(this ICache> cache, K key, Func valueFactory) - { - var atomic = await cache.GetOrAddAsync(key, k => Task.FromResult(new Atomic(() => valueFactory(k)))).ConfigureAwait(false); - return atomic.Value; - } - } + //public static class AtomicCacheExtensions + //{ + // public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) + // { + // return cache.GetOrAdd(key, k => new Atomic(() => valueFactory(k))).Value; + // } + + // public static async Task GetOrAddAsync(this ICache> cache, K key, Func valueFactory) + // { + // var atomic = await cache.GetOrAddAsync(key, k => Task.FromResult(new Atomic(() => valueFactory(k)))).ConfigureAwait(false); + // return atomic.Value; + // } + //} } diff --git a/BitFaster.Caching/Lazy/DisposableAtomic.cs b/BitFaster.Caching/Lazy/DisposableAtomic.cs new file mode 100644 index 00000000..79a3e671 --- /dev/null +++ b/BitFaster.Caching/Lazy/DisposableAtomic.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + // requirements for IDisposable atomic + // if value !created, no dispose, cannot create - throws object disposed exception + // if created, dispose value + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + public class DisposableAtomic : IDisposable where V : IDisposable + { + private volatile Initializer initializer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private V value; + + public DisposableAtomic() + { + this.initializer = new Initializer(); + } + + public DisposableAtomic(V value) + { + this.value = value; + } + + public V GetValue(K key, Func valueFactory) + { + if (this.initializer == null) + { + return this.value; + } + + return CreateValue(key, valueFactory); + } + + public bool IsValueCreated => this.initializer == null; + + public V ValueIfCreated + { + get + { + if (!this.IsValueCreated) + { + return default; + } + + return this.value; + } + } + + private V CreateValue(K key, Func valueFactory) + { + Initializer init = this.initializer; + + if (init != null) + { + this.value = init.CreateValue(key, valueFactory); + this.initializer = null; + } + + return this.value; + } + + public void Dispose() + { + Initializer init = this.initializer; + + if (init != null) + { + init.Dispose(); + } + else + { + this.value?.Dispose(); + } + } + + private class Initializer : IDisposable + { + private object syncLock = new object(); + private bool isInitialized; + private volatile bool isDisposed; + private V value; + + public V CreateValue(K key, Func valueFactory) + { + var r = Synchronized.Initialize(ref this.value, ref isInitialized, ref syncLock, valueFactory, key); + + // 2 possible orders + // Create value then Dispose + // Dispose then CreateValue + + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(value)); + } + + return r; + } + + public void Dispose() + { + lock (this.syncLock) + { + if (this.isInitialized) + { + value.Dispose(); + } + + // LazyInitializer will no longer attempt to init in CreateValue + Volatile.Write(ref this.isInitialized, true); + } + + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedAtomic.cs b/BitFaster.Caching/Lazy/ScopedAtomic.cs index 91cea892..dc3ab868 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomic.cs @@ -1,47 +1,59 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace BitFaster.Caching.Lazy { - // Enable caching a Lazy disposable object - guarantee single instance, safe disposal - public class ScopedAtomic : IDisposable - where T : IDisposable + public class ScopedAtomic : IDisposable where V : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; - public ScopedAtomic(Func valueFactory) + public ScopedAtomic() { - // AtomicLazy will not cache exceptions - var lazy = new Atomic(valueFactory); - this.refCount = new ReferenceCount>(lazy); + this.refCount = new ReferenceCount>(new DisposableAtomic()); } - public AtomicLifetime CreateLifetime() + public bool TryCreateLifetime(K key, Func valueFactory, out AtomicLifetime lifetime) { // TODO: inside the loop? if (this.isDisposed) { - throw new ObjectDisposedException($"{nameof(T)} is disposed."); + lifetime = default(AtomicLifetime); + return false; } + // initialize - factory can throw so do this before we start counting refs + this.refCount.Value.GetValue(key, valueFactory); + while (true) { // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. // This mitigates the race where the value is disposed after the above check is run. var oldRefCount = this.refCount; var newRefCount = oldRefCount.IncrementCopy(); - if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { - // When Lease is disposed, it calls DecrementReferenceCount - return new AtomicLifetime(newRefCount, this.DecrementReferenceCount); + // When Lifetime is disposed, it calls DecrementReferenceCount + lifetime = new AtomicLifetime(oldRefCount, this.DecrementReferenceCount); + return true; } } } + public AtomicLifetime CreateLifetime(K key, Func valueFactory) + { + if (!TryCreateLifetime(key, valueFactory, out var lifetime)) + { + throw new ObjectDisposedException($"{nameof(V)} is disposed."); + } + + return lifetime; + } + private void DecrementReferenceCount() { while (true) @@ -51,13 +63,9 @@ private void DecrementReferenceCount() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { - // TODO: how to prevent a race here? Need to use the lock inside the lazy? if (newRefCount.Count == 0) { - if (newRefCount.Value.IsValueCreated) - { - newRefCount.Value.Value.Dispose(); - } + newRefCount.Value.Dispose(); } break; @@ -74,4 +82,30 @@ public void Dispose() } } } + + public class AtomicLifetime : IDisposable where V : IDisposable + { + private readonly Action onDisposeAction; + private readonly ReferenceCount> refCount; + private bool isDisposed; + + public AtomicLifetime(ReferenceCount> refCount, Action onDisposeAction) + { + this.refCount = refCount; + this.onDisposeAction = onDisposeAction; + } + + public V Value => this.refCount.Value.ValueIfCreated; + + public int ReferenceCount => this.refCount.Count; + + public void Dispose() + { + if (!this.isDisposed) + { + this.onDisposeAction(); + this.isDisposed = true; + } + } + } } diff --git a/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs b/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs index 27e5190d..0efe3a23 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs @@ -8,26 +8,26 @@ namespace BitFaster.Caching.Lazy { // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal #if NETCOREAPP3_1_OR_GREATER - public class ScopedAtomicAsync : IAsyncDisposable + public class ScopedAtomicAsync : IDisposable where TValue : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; - private readonly AtomicAsync lazy; + private readonly AsyncAtomic lazy; // should this even be allowed? - public ScopedAtomicAsync(Func valueFactory) + public ScopedAtomicAsync() { - this.lazy = new AtomicAsync(() => Task.FromResult(valueFactory())); + this.lazy = new AsyncAtomic(); } - public ScopedAtomicAsync(Func> valueFactory) - { - this.lazy = new AtomicAsync(valueFactory); - } + //public ScopedAtomicAsync(Func> valueFactory) + //{ + // this.lazy = new AsyncAtomic(valueFactory); + //} - public async Task> CreateLifetimeAsync() + public async Task> CreateLifetimeAsync() { // TODO: inside the loop? if (this.isDisposed) @@ -46,14 +46,14 @@ public async Task> CreateLifetimeAsync() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lease is disposed, it calls DecrementReferenceCount - var value = await this.lazy; - return new AtomicAsyncLifetime(newRefCount, this.DecrementReferenceCountAsync); + //var value = await this.lazy; + return new AsyncAtomicLifetime(newRefCount, this.DecrementReferenceCount); } } } // TODO: Do we need an async lifetime? - private async Task DecrementReferenceCountAsync() + private void DecrementReferenceCount() { while (true) { @@ -68,8 +68,7 @@ private async Task DecrementReferenceCountAsync() { if (newRefCount.Value.IsValueCreated) { - var v = await newRefCount.Value; - v.Dispose(); + newRefCount.Value.ValueIfCreated?.Dispose(); } } @@ -78,11 +77,11 @@ private async Task DecrementReferenceCountAsync() } } - public async ValueTask DisposeAsync() + public void Dispose() { if (!this.isDisposed) { - await this.DecrementReferenceCountAsync(); + this.DecrementReferenceCount(); this.isDisposed = true; } } diff --git a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs new file mode 100644 index 00000000..b235fcb8 --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + public static class ScopedAtomicExtensions + { + public static AtomicLifetime GetOrAdd(this ICache> cache, K key, Func valueFactory) where V : IDisposable + { + while (true) + { + var scope = cache.GetOrAdd(key, _ => new ScopedAtomic()); + + if (scope.TryCreateLifetime(key, valueFactory, out var lifetime)) + { + return lifetime; + } + + // How to make atomic lifetime a single alloc? + //return scope.CreateLifetime(key, valueFactory); + } + } + + public static void AddOrUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + throw new NotImplementedException(); + //cache.AddOrUpdate(key, new ScopedAtomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + throw new NotImplementedException(); + //return cache.TryUpdate(key, new Atomic(value)); + } + + public static bool TryGet(this ICache> cache, K key, out AtomicLifetime value) where V : IDisposable + { + throw new NotImplementedException(); + + ScopedAtomic output; + bool ret = cache.TryGet(key, out output); + + if (ret) + { + // TODO: only create a lifetime if the value exists, + //value = output.CreateLifetime(; + } + else + { + value = default; + } + + //return ret; + } + } +} diff --git a/BitFaster.Caching/Lazy/Synchronized.cs b/BitFaster.Caching/Lazy/Synchronized.cs new file mode 100644 index 00000000..eb32c42a --- /dev/null +++ b/BitFaster.Caching/Lazy/Synchronized.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + internal static class Synchronized + { + public static V Initialize(ref V target, ref bool initialized, ref object syncLock, Func valueFactory, K key) + { + // Fast path + if (Volatile.Read(ref initialized)) + { + return target; + } + + lock (syncLock) + { + if (!Volatile.Read(ref initialized)) + { + target = valueFactory(key); + Volatile.Write(ref initialized, true); + } + } + + return target; + } + + public static V Initialize(ref V target, ref bool initialized, ref object syncLock, V value) + { + // Fast path + if (Volatile.Read(ref initialized)) + { + return target; + } + + lock (syncLock) + { + if (!Volatile.Read(ref initialized)) + { + target = value; + Volatile.Write(ref initialized, true); + } + } + + return target; + } + } +} diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index c66f1ca6..784c4e3f 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -26,16 +26,18 @@ public Scoped(T value) } /// - /// Creates a lifetime for the scoped value. The lifetime guarantees the value is alive until + /// Attempts to create a lifetime for the scoped value. The lifetime guarantees the value is alive until /// the lifetime is disposed. /// - /// A value lifetime. - /// The scope is disposed. - public Lifetime CreateLifetime() + /// When this method returns, contains the Lifetime that was created, or the default value of the type if the operation failed. + /// true if the Lifetime was created; otherwise false. + public bool TryCreateLifetime(out Lifetime lifetime) { + // TODO: inside the loop? if (this.isDisposed) { - throw new ObjectDisposedException($"{nameof(T)} is disposed."); + lifetime = default(Lifetime); + return false; } while (true) @@ -44,15 +46,31 @@ public Lifetime CreateLifetime() // This mitigates the race where the value is disposed after the above check is run. var oldRefCount = this.refCount; var newRefCount = oldRefCount.IncrementCopy(); - if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { - // When Lease is disposed, it calls DecrementReferenceCount - return new Lifetime(oldRefCount, this.DecrementReferenceCount); + // When Lifetime is disposed, it calls DecrementReferenceCount + lifetime = new Lifetime(oldRefCount, this.DecrementReferenceCount); + return true; } } } + /// + /// Creates a lifetime for the scoped value. The lifetime guarantees the value is alive until + /// the lifetime is disposed. + /// + /// A value lifetime. + /// The scope is disposed. + public Lifetime CreateLifetime() + { + if (!TryCreateLifetime(out var lifetime)) + { + throw new ObjectDisposedException($"{nameof(T)} is disposed."); + } + + return lifetime; + } + private void DecrementReferenceCount() { while (true) From ea1bd2cb531a6b1c11f88247be847dde07eea82f Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 8 Nov 2021 20:26:50 -0800 Subject: [PATCH 12/31] cleanup --- BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs | 15 +-- BitFaster.Caching/Lazy/Atomic.cs | 4 +- BitFaster.Caching/Lazy/AtomicLifetime.cs | 48 ---------- BitFaster.Caching/Lazy/DesiredApi.cs | 95 +++++++------------ BitFaster.Caching/Lazy/DisposableAtomic.cs | 1 + ...pedAtomicAsync.cs => ScopedAsyncAtomic.cs} | 23 ++--- .../Lazy/ScopedAsyncAtomicExtensions.cs | 27 ++++++ .../Lazy/ScopedAtomicExtensions.cs | 3 - 8 files changed, 73 insertions(+), 143 deletions(-) delete mode 100644 BitFaster.Caching/Lazy/AtomicLifetime.cs rename BitFaster.Caching/Lazy/{ScopedAtomicAsync.cs => ScopedAsyncAtomic.cs} (78%) create mode 100644 BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs diff --git a/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs index 18788f8d..0f16b844 100644 --- a/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs @@ -7,7 +7,6 @@ namespace BitFaster.Caching.Lazy { -#if NETCOREAPP3_1_OR_GREATER public class AsyncAtomicLifetime : IDisposable { private readonly Action onDisposeAction; @@ -30,15 +29,10 @@ public Task GetValueAsync(K key, Func> valueFactory) return this.refCount.Value.GetValueAsync(key, valueFactory); } - //public Task Task - //{ - // get { return this.refCount.Value..Value(); } - //} - - //public TaskAwaiter GetAwaiter() - //{ - // return Task.GetAwaiter(); - //} + /// + /// Gets the value. + /// + public V Value => this.refCount.Value.ValueIfCreated; /// /// Gets the count of Lifetime instances referencing the same value. @@ -57,5 +51,4 @@ public void Dispose() } } } -#endif } diff --git a/BitFaster.Caching/Lazy/Atomic.cs b/BitFaster.Caching/Lazy/Atomic.cs index ce6f0185..0de67b9e 100644 --- a/BitFaster.Caching/Lazy/Atomic.cs +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -9,9 +9,7 @@ namespace BitFaster.Caching.Lazy { - // Should this be called simply Atomic? - // Then we have Atomic and Scoped, and ScopedAtomic - // Then AtomicAsync, ScopedAsync, and ScopedAtomicAsync would follow, but there is no ScopedAsync equivalent at this point. That should be IAsyncDisposable (.net Core 3.1 onwards). That would imply that GetOrAdd async understands IAsyncDispose + // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 diff --git a/BitFaster.Caching/Lazy/AtomicLifetime.cs b/BitFaster.Caching/Lazy/AtomicLifetime.cs deleted file mode 100644 index 73147d9f..00000000 --- a/BitFaster.Caching/Lazy/AtomicLifetime.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace BitFaster.Caching.Lazy -{ - //public class AtomicLifetime : IDisposable - //{ - // private readonly Action onDisposeAction; - // private readonly ReferenceCount> refCount; - // private bool isDisposed; - - // /// - // /// Initializes a new instance of the Lifetime class. - // /// - // /// The value to keep alive. - // /// The action to perform when the lifetime is terminated. - // public AtomicLifetime(ReferenceCount> value, Action onDisposeAction) - // { - // this.refCount = value; - // this.onDisposeAction = onDisposeAction; - // } - - // /// - // /// Gets the value. - // /// - // public T Value => this.refCount.Value.Value; - - // /// - // /// Gets the count of Lifetime instances referencing the same value. - // /// - // public int ReferenceCount => this.refCount.Count; - - // /// - // /// Terminates the lifetime and performs any cleanup required to release the value. - // /// - // public void Dispose() - // { - // if (!this.isDisposed) - // { - // this.onDisposeAction(); - // this.isDisposed = true; - // } - // } - //} -} diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index 825cdef5..d27a221d 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -6,7 +6,14 @@ namespace BitFaster.Caching.Lazy { - class DesiredApi + // Wrappers needed: + // - Atomic + // - Scoped (already exists) + // - ScopedAtomic + // - AsyncAtomic + // - ScopedAsyncAtomic + // There is no ScopedAsync, since that is just Scoped - the task is not stored so we only need scoped values in the cache. + public class DesiredApi { public static void HowToCacheAtomic() { @@ -23,6 +30,20 @@ public static void HowToCacheAtomic() lru.AddOrUpdate(1, 2); } + public static void HowToCacheScopedAtomic() + { + var scopedAtomicLru = new ConcurrentLru>(5); + + using (var l = scopedAtomicLru.GetOrAdd(1, k => new SomeDisposable())) + { + SomeDisposable d = l.Value; + } + + //scopedAtomicLru.TryUpdate(2, 3); + //scopedAtomicLru.TryGet(1, out SomeDisposable v); + //scopedAtomicLru.AddOrUpdate(1, 2); + } + public async static Task HowToCacheAsyncAtomic() { var asyncAtomicLru = new ConcurrentLru>(5); @@ -34,63 +55,27 @@ public async static Task HowToCacheAsyncAtomic() asyncAtomicLru.AddOrUpdate(1, 2); } - public static void HowToCacheDisposableAtomic() - { - var scopedAtomicLru2 = new ConcurrentLru>(5); - - using (var l = scopedAtomicLru2.GetOrAdd(1, k => new SomeDisposable())) - { - SomeDisposable d = l.Value; - } - } - // Requirements: // 1. lifetime/value create is async end to end (if async delegate is used to create value) // 2. value is created lazily, guarantee single instance of object, single invocation of lazy // 3. lazy value is disposed by scope // 4. lifetime keeps scope alive -#if NETCOREAPP3_1_OR_GREATER - public static async Task HowToCacheADisposableAsyncLazy() - { - var lru = new ConcurrentLru>(4); - var factory = new ScopedAtomicAsyncFactory(); - - //await using (var lifetime = await lru.GetOrAdd(1, factory.Create).CreateLifetimeAsync()) - //{ - // // This is cleaned up by the magic GetAwaiter method - // SomeDisposable y = await lifetime.Task; - //} - } -#endif - } - //public class ScopedAtomicFactory - //{ - // public Task CreateAsync(int key) - // { - // return Task.FromResult(new SomeDisposable()); - // } - - // public ScopedAtomic Create(int key) - // { - // return new ScopedAtomic(() => new SomeDisposable()); - // } - //} - -#if NETCOREAPP3_1_OR_GREATER - public class ScopedAtomicAsyncFactory - { - public Task CreateAsync(int key) + public static async Task HowToCacheScopedAsyncAtomic() { - return Task.FromResult(new SomeDisposable()); - } + var scopedAsyncAtomicLru = new ConcurrentLru>(4); + Func> valueFactory = k => Task.FromResult(new SomeDisposable()); - public ScopedAtomicAsync Create(int key) - { - return new ScopedAtomicAsync(); + using (var lifetime = await scopedAsyncAtomicLru.GetOrAddAsync(1, valueFactory)) + { + SomeDisposable y = lifetime.Value; + } + + //scopedAsyncAtomicLru.TryUpdate(2, 3); + //scopedAsyncAtomicLru.TryGet(1, out int v); + //scopedAsyncAtomicLru.AddOrUpdate(1, 2); } } -#endif public class SomeDisposable : IDisposable { @@ -99,18 +84,4 @@ public void Dispose() } } - - //public static class AtomicCacheExtensions - //{ - // public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) - // { - // return cache.GetOrAdd(key, k => new Atomic(() => valueFactory(k))).Value; - // } - - // public static async Task GetOrAddAsync(this ICache> cache, K key, Func valueFactory) - // { - // var atomic = await cache.GetOrAddAsync(key, k => Task.FromResult(new Atomic(() => valueFactory(k)))).ConfigureAwait(false); - // return atomic.Value; - // } - //} } diff --git a/BitFaster.Caching/Lazy/DisposableAtomic.cs b/BitFaster.Caching/Lazy/DisposableAtomic.cs index 79a3e671..0b5217da 100644 --- a/BitFaster.Caching/Lazy/DisposableAtomic.cs +++ b/BitFaster.Caching/Lazy/DisposableAtomic.cs @@ -8,6 +8,7 @@ namespace BitFaster.Caching.Lazy { + // TODO: is this actually even needed? Or is the approach in ScopedAsyncAtomic sufficient? E.g. rely on IsValueCreated at dispose time, scoped owns tracking dispose and is already thread safe. // requirements for IDisposable atomic // if value !created, no dispose, cannot create - throws object disposed exception // if created, dispose value diff --git a/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs similarity index 78% rename from BitFaster.Caching/Lazy/ScopedAtomicAsync.cs rename to BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs index 0efe3a23..1dcade27 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomicAsync.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs @@ -7,27 +7,21 @@ namespace BitFaster.Caching.Lazy { // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal -#if NETCOREAPP3_1_OR_GREATER - public class ScopedAtomicAsync : IDisposable + public class ScopedAsyncAtomic : IDisposable where TValue : IDisposable { private ReferenceCount> refCount; private bool isDisposed; - private readonly AsyncAtomic lazy; + private readonly AsyncAtomic asyncAtomic; // should this even be allowed? - public ScopedAtomicAsync() + public ScopedAsyncAtomic() { - this.lazy = new AsyncAtomic(); + this.asyncAtomic = new AsyncAtomic(); } - //public ScopedAtomicAsync(Func> valueFactory) - //{ - // this.lazy = new AsyncAtomic(valueFactory); - //} - - public async Task> CreateLifetimeAsync() + public async Task> CreateLifetimeAsync(K key, Func> valueFactory) { // TODO: inside the loop? if (this.isDisposed) @@ -35,6 +29,8 @@ public async Task> CreateLifetimeAsync() throw new ObjectDisposedException($"{nameof(TValue)} is disposed."); } + await this.asyncAtomic.GetValueAsync(key, valueFactory).ConfigureAwait(false); + while (true) { // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. @@ -46,13 +42,11 @@ public async Task> CreateLifetimeAsync() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lease is disposed, it calls DecrementReferenceCount - //var value = await this.lazy; return new AsyncAtomicLifetime(newRefCount, this.DecrementReferenceCount); } } } - // TODO: Do we need an async lifetime? private void DecrementReferenceCount() { while (true) @@ -62,8 +56,6 @@ private void DecrementReferenceCount() if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { - // TODO: how to prevent a race here? Need to use the lock inside the lazy? - // Do we need atomic disposable? if (newRefCount.Count == 0) { if (newRefCount.Value.IsValueCreated) @@ -86,5 +78,4 @@ public void Dispose() } } } -#endif } diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs new file mode 100644 index 00000000..2d3a8ef8 --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + public static class ScopedAsyncAtomicExtensions + { + public static Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable + { + //return cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()) + // .CreateLifetimeAsync(key, valueFactory); + + while (true) + { + var scope = cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()); + + // TODO: try create lifetime async + var t = scope.CreateLifetimeAsync(key, valueFactory); + + return t; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs index b235fcb8..61d877d1 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs @@ -18,9 +18,6 @@ public static AtomicLifetime GetOrAdd(this ICache Date: Mon, 8 Nov 2021 21:07:09 -0800 Subject: [PATCH 13/31] cleanup and comments --- BitFaster.Caching/Lazy/AtomicLifetime.cs | 34 ++++++++++++++++++ BitFaster.Caching/Lazy/DesiredApi.cs | 7 ++-- BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs | 1 - BitFaster.Caching/Lazy/ScopedAtomic.cs | 35 +++++-------------- .../Lazy/ScopedAtomicExtensions.cs | 22 ++++++------ BitFaster.Caching/Scoped.cs | 2 +- 6 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 BitFaster.Caching/Lazy/AtomicLifetime.cs diff --git a/BitFaster.Caching/Lazy/AtomicLifetime.cs b/BitFaster.Caching/Lazy/AtomicLifetime.cs new file mode 100644 index 00000000..0d9c0a52 --- /dev/null +++ b/BitFaster.Caching/Lazy/AtomicLifetime.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lazy +{ + public class AtomicLifetime : IDisposable where V : IDisposable + { + private readonly Action onDisposeAction; + private readonly ReferenceCount> refCount; + private bool isDisposed; + + public AtomicLifetime(ReferenceCount> refCount, Action onDisposeAction) + { + this.refCount = refCount; + this.onDisposeAction = onDisposeAction; + } + + public V Value => this.refCount.Value.ValueIfCreated; + + public int ReferenceCount => this.refCount.Count; + + public void Dispose() + { + if (!this.isDisposed) + { + this.onDisposeAction(); + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index d27a221d..3fc6dbec 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -32,6 +32,7 @@ public static void HowToCacheAtomic() public static void HowToCacheScopedAtomic() { + // ICache> var scopedAtomicLru = new ConcurrentLru>(5); using (var l = scopedAtomicLru.GetOrAdd(1, k => new SomeDisposable())) @@ -39,9 +40,9 @@ public static void HowToCacheScopedAtomic() SomeDisposable d = l.Value; } - //scopedAtomicLru.TryUpdate(2, 3); - //scopedAtomicLru.TryGet(1, out SomeDisposable v); - //scopedAtomicLru.AddOrUpdate(1, 2); + scopedAtomicLru.TryUpdate(2, new SomeDisposable()); + scopedAtomicLru.TryGet(1, out AtomicLifetime v); + scopedAtomicLru.AddOrUpdate(1, new SomeDisposable()); } public async static Task HowToCacheAsyncAtomic() diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs index 1dcade27..328c7b18 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs @@ -15,7 +15,6 @@ public class ScopedAsyncAtomic : IDisposable private readonly AsyncAtomic asyncAtomic; - // should this even be allowed? public ScopedAsyncAtomic() { this.asyncAtomic = new AsyncAtomic(); diff --git a/BitFaster.Caching/Lazy/ScopedAtomic.cs b/BitFaster.Caching/Lazy/ScopedAtomic.cs index dc3ab868..80043481 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomic.cs @@ -17,12 +17,17 @@ public ScopedAtomic() this.refCount = new ReferenceCount>(new DisposableAtomic()); } + public ScopedAtomic(V value) + { + this.refCount = new ReferenceCount>(new DisposableAtomic(value)); + } + public bool TryCreateLifetime(K key, Func valueFactory, out AtomicLifetime lifetime) { // TODO: inside the loop? if (this.isDisposed) { - lifetime = default(AtomicLifetime); + lifetime = default; return false; } @@ -31,6 +36,8 @@ public bool TryCreateLifetime(K key, Func valueFactory, out AtomicLifetime while (true) { + // TODO: this increment copy logic was removed - verify how this is intended to work. + // Could we simply check the value of IncrementCopy == 1 (meaning it started at zero and was therefore disposed?) // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. // This mitigates the race where the value is disposed after the above check is run. var oldRefCount = this.refCount; @@ -82,30 +89,4 @@ public void Dispose() } } } - - public class AtomicLifetime : IDisposable where V : IDisposable - { - private readonly Action onDisposeAction; - private readonly ReferenceCount> refCount; - private bool isDisposed; - - public AtomicLifetime(ReferenceCount> refCount, Action onDisposeAction) - { - this.refCount = refCount; - this.onDisposeAction = onDisposeAction; - } - - public V Value => this.refCount.Value.ValueIfCreated; - - public int ReferenceCount => this.refCount.Count; - - public void Dispose() - { - if (!this.isDisposed) - { - this.onDisposeAction(); - this.isDisposed = true; - } - } - } } diff --git a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs index 61d877d1..1b89697f 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs @@ -8,6 +8,7 @@ namespace BitFaster.Caching.Lazy { public static class ScopedAtomicExtensions { + // TODO: GetOrAddLifetime? public static AtomicLifetime GetOrAdd(this ICache> cache, K key, Func valueFactory) where V : IDisposable { while (true) @@ -23,34 +24,33 @@ public static AtomicLifetime GetOrAdd(this ICache(this ICache> cache, K key, V value) where V : IDisposable { - throw new NotImplementedException(); - //cache.AddOrUpdate(key, new ScopedAtomic(value)); + cache.AddOrUpdate(key, new ScopedAtomic(value)); } public static bool TryUpdate(this ICache> cache, K key, V value) where V : IDisposable { - throw new NotImplementedException(); - //return cache.TryUpdate(key, new Atomic(value)); + return cache.TryUpdate(key, new ScopedAtomic(value)); } + // TODO: TryGetLifetime? public static bool TryGet(this ICache> cache, K key, out AtomicLifetime value) where V : IDisposable { - throw new NotImplementedException(); - - ScopedAtomic output; - bool ret = cache.TryGet(key, out output); + ScopedAtomic scoped; + bool ret = cache.TryGet(key, out scoped); if (ret) { - // TODO: only create a lifetime if the value exists, - //value = output.CreateLifetime(; + // TODO: only create a lifetime if the value exists + // if (valueExists) + // scoped.TryCreateLifetime(/*no factory, use ValueIfCreated*/) + value = default; } else { value = default; } - //return ret; + return ret; } } } diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index 784c4e3f..a927796a 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -36,7 +36,7 @@ public bool TryCreateLifetime(out Lifetime lifetime) // TODO: inside the loop? if (this.isDisposed) { - lifetime = default(Lifetime); + lifetime = default; return false; } From e738e43f39a0cc3e076fb2965bd603c410ff2494 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 9 Nov 2021 17:23:47 -0800 Subject: [PATCH 14/31] fix more ext mthds --- BitFaster.Caching/Lazy/DesiredApi.cs | 11 ++++- BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs | 47 +++++++++++------- .../Lazy/ScopedAsyncAtomicExtensions.cs | 14 +++--- BitFaster.Caching/Lazy/ScopedAtomic.cs | 48 +++++++++++++++---- .../Lazy/ScopedAtomicExtensions.cs | 27 +++++------ BitFaster.Caching/Scoped.cs | 18 +++---- 6 files changed, 104 insertions(+), 61 deletions(-) diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index 3fc6dbec..5a5ba745 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -41,8 +41,17 @@ public static void HowToCacheScopedAtomic() } scopedAtomicLru.TryUpdate(2, new SomeDisposable()); - scopedAtomicLru.TryGet(1, out AtomicLifetime v); + scopedAtomicLru.AddOrUpdate(1, new SomeDisposable()); + + // TODO: how to clean this up to 1 line? + if (scopedAtomicLru.TryGetLifetime(1, out var lifetime)) + { + using (lifetime) + { + var x = lifetime.Value; + } + } } public async static Task HowToCacheAsyncAtomic() diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs index 328c7b18..cfb5cae3 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs @@ -7,45 +7,56 @@ namespace BitFaster.Caching.Lazy { // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal - public class ScopedAsyncAtomic : IDisposable - where TValue : IDisposable + public class ScopedAsyncAtomic : IDisposable + where V : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; - private readonly AsyncAtomic asyncAtomic; + private readonly AsyncAtomic asyncAtomic; public ScopedAsyncAtomic() { - this.asyncAtomic = new AsyncAtomic(); + this.asyncAtomic = new AsyncAtomic(); } - public async Task> CreateLifetimeAsync(K key, Func> valueFactory) - { - // TODO: inside the loop? - if (this.isDisposed) - { - throw new ObjectDisposedException($"{nameof(TValue)} is disposed."); - } - + public async Task<(bool succeeded, AsyncAtomicLifetime lifetime)> TryCreateLifetimeAsync(K key, Func> valueFactory) + { + // initialize - factory can throw so do this before we start counting refs await this.asyncAtomic.GetValueAsync(key, valueFactory).ConfigureAwait(false); while (true) { - // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. - // This mitigates the race where the value is disposed after the above check is run. var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + return (false, default); + } + var newRefCount = oldRefCount.IncrementCopy(); - // guarantee ref held before lazy evaluated if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { - // When Lease is disposed, it calls DecrementReferenceCount - return new AsyncAtomicLifetime(newRefCount, this.DecrementReferenceCount); + // When Lifetime is disposed, it calls DecrementReferenceCount + return (true, new AsyncAtomicLifetime(oldRefCount, this.DecrementReferenceCount)); } } } + public async Task> CreateLifetimeAsync(K key, Func> valueFactory) + { + var result = await TryCreateLifetimeAsync(key, valueFactory).ConfigureAwait(false); + + if (!result.succeeded) + { + throw new ObjectDisposedException($"{nameof(V)} is disposed."); + } + + return result.lifetime; + } + private void DecrementReferenceCount() { while (true) diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs index 2d3a8ef8..c47acf09 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs @@ -8,19 +8,17 @@ namespace BitFaster.Caching.Lazy { public static class ScopedAsyncAtomicExtensions { - public static Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable + public static async Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable { - //return cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()) - // .CreateLifetimeAsync(key, valueFactory); - while (true) { var scope = cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()); + var result = await scope.TryCreateLifetimeAsync(key, valueFactory).ConfigureAwait(false); - // TODO: try create lifetime async - var t = scope.CreateLifetimeAsync(key, valueFactory); - - return t; + if (result.succeeded) + { + return result.lifetime; + } } } } diff --git a/BitFaster.Caching/Lazy/ScopedAtomic.cs b/BitFaster.Caching/Lazy/ScopedAtomic.cs index 80043481..917ed941 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomic.cs @@ -24,24 +24,54 @@ public ScopedAtomic(V value) public bool TryCreateLifetime(K key, Func valueFactory, out AtomicLifetime lifetime) { - // TODO: inside the loop? - if (this.isDisposed) + // initialize - factory can throw so do this before we start counting refs + this.refCount.Value.GetValue(key, valueFactory); + + // TODO: exact dupe + while (true) + { + var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + lifetime = default; + return false; + } + + var newRefCount = oldRefCount.IncrementCopy(); + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + { + // When Lifetime is disposed, it calls DecrementReferenceCount + lifetime = new AtomicLifetime(oldRefCount, this.DecrementReferenceCount); + return true; + } + } + } + + public bool TryCreateLifetime(out AtomicLifetime lifetime) + { + if (!this.refCount.Value.IsValueCreated) { lifetime = default; return false; } - // initialize - factory can throw so do this before we start counting refs - this.refCount.Value.GetValue(key, valueFactory); - + // TODO: exact dupe while (true) { - // TODO: this increment copy logic was removed - verify how this is intended to work. - // Could we simply check the value of IncrementCopy == 1 (meaning it started at zero and was therefore disposed?) - // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. - // This mitigates the race where the value is disposed after the above check is run. var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + lifetime = default; + return false; + } + var newRefCount = oldRefCount.IncrementCopy(); + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lifetime is disposed, it calls DecrementReferenceCount diff --git a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs index 1b89697f..67922d5d 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs @@ -33,24 +33,19 @@ public static bool TryUpdate(this ICache> cache, K k } // TODO: TryGetLifetime? - public static bool TryGet(this ICache> cache, K key, out AtomicLifetime value) where V : IDisposable + public static bool TryGetLifetime(this ICache> cache, K key, out AtomicLifetime value) where V : IDisposable { - ScopedAtomic scoped; - bool ret = cache.TryGet(key, out scoped); - - if (ret) - { - // TODO: only create a lifetime if the value exists - // if (valueExists) - // scoped.TryCreateLifetime(/*no factory, use ValueIfCreated*/) - value = default; - } - else + if (cache.TryGet(key, out var scoped)) { - value = default; - } - - return ret; + if (scoped.TryCreateLifetime(out var lifetime)) + { + value = lifetime; + return true; + } + } + + value = default; + return false; } } } diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index a927796a..5edf567c 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -33,19 +33,19 @@ public Scoped(T value) /// true if the Lifetime was created; otherwise false. public bool TryCreateLifetime(out Lifetime lifetime) { - // TODO: inside the loop? - if (this.isDisposed) - { - lifetime = default; - return false; - } - while (true) { - // IncrementCopy will throw ObjectDisposedException if the referenced object has no references. - // This mitigates the race where the value is disposed after the above check is run. var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + lifetime = default; + return false; + } + var newRefCount = oldRefCount.IncrementCopy(); + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) { // When Lifetime is disposed, it calls DecrementReferenceCount From abaa1d3710d6abd5cfcab33c501e857c510d5546 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 9 Nov 2021 18:03:39 -0800 Subject: [PATCH 15/31] dedupe merge --- BitFaster.Caching/Scoped.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index 481a9fee..6379e442 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -69,22 +69,6 @@ public Lifetime CreateLifetime() return lifetime; } - /// - /// Creates a lifetime for the scoped value. The lifetime guarantees the value is alive until - /// the lifetime is disposed. - /// - /// A value lifetime. - /// The scope is disposed. - public Lifetime CreateLifetime() - { - if (!TryCreateLifetime(out var lifetime)) - { - throw new ObjectDisposedException($"{nameof(T)} is disposed."); - } - - return lifetime; - } - private void DecrementReferenceCount() { while (true) From 62ec82696348e06e6f55862a9a3c5103f6292b0b Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 9 Nov 2021 18:32:29 -0800 Subject: [PATCH 16/31] all ext --- .../Lazy/AsyncAtomicCacheExtensions.cs | 2 + BitFaster.Caching/Lazy/DesiredApi.cs | 15 ++++-- BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs | 49 +++++++++++++++---- .../Lazy/ScopedAsyncAtomicExtensions.cs | 28 ++++++++++- BitFaster.Caching/Lazy/ScopedAtomic.cs | 15 ++---- 5 files changed, 85 insertions(+), 24 deletions(-) diff --git a/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs index 2fe9f6d0..b5c0dee9 100644 --- a/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs @@ -37,6 +37,8 @@ public static bool TryGet(this ICache> cache, K key, AsyncAtomic output; bool ret = cache.TryGet(key, out output); + // TOOD: should this return false if the value is not created but the key exists? + // that would indicate a race between GetOrAdd and TryGet, maybe it should return false? if (ret) { value = output.ValueIfCreated; diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching/Lazy/DesiredApi.cs index 5a5ba745..135f078c 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching/Lazy/DesiredApi.cs @@ -81,9 +81,18 @@ public static async Task HowToCacheScopedAsyncAtomic() SomeDisposable y = lifetime.Value; } - //scopedAsyncAtomicLru.TryUpdate(2, 3); - //scopedAsyncAtomicLru.TryGet(1, out int v); - //scopedAsyncAtomicLru.AddOrUpdate(1, 2); + scopedAsyncAtomicLru.TryUpdate(2, new SomeDisposable()); + + scopedAsyncAtomicLru.AddOrUpdate(1, new SomeDisposable()); + + // TODO: how to clean this up to 1 line? + if (scopedAsyncAtomicLru.TryGetLifetime(1, out var lifetime2)) + { + using (lifetime2) + { + var x = lifetime2.Value; + } + } } } diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs index cfb5cae3..79d9fb58 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs @@ -20,11 +20,46 @@ public ScopedAsyncAtomic() this.asyncAtomic = new AsyncAtomic(); } + public ScopedAsyncAtomic(V value) + { + this.asyncAtomic = new AsyncAtomic(value); + } + + public bool TryCreateLifetime(out AsyncAtomicLifetime lifetime) + { + if (!this.refCount.Value.IsValueCreated) + { + lifetime = default; + return false; + } + + // TODO: exact dupe + while (true) + { + var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + lifetime = default; + return false; + } + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) + { + // When Lifetime is disposed, it calls DecrementReferenceCount + lifetime = new AsyncAtomicLifetime(oldRefCount, this.DecrementReferenceCount); + return true; + } + } + } + public async Task<(bool succeeded, AsyncAtomicLifetime lifetime)> TryCreateLifetimeAsync(K key, Func> valueFactory) { // initialize - factory can throw so do this before we start counting refs await this.asyncAtomic.GetValueAsync(key, valueFactory).ConfigureAwait(false); + // TODO: exact dupe while (true) { var oldRefCount = this.refCount; @@ -35,9 +70,7 @@ public ScopedAsyncAtomic() return (false, default); } - var newRefCount = oldRefCount.IncrementCopy(); - - if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) { // When Lifetime is disposed, it calls DecrementReferenceCount return (true, new AsyncAtomicLifetime(oldRefCount, this.DecrementReferenceCount)); @@ -62,16 +95,12 @@ private void DecrementReferenceCount() while (true) { var oldRefCount = this.refCount; - var newRefCount = oldRefCount.DecrementCopy(); - if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.DecrementCopy(), oldRefCount)) { - if (newRefCount.Count == 0) + if (this.refCount.Count == 0 && this.refCount.Value.IsValueCreated) { - if (newRefCount.Value.IsValueCreated) - { - newRefCount.Value.ValueIfCreated?.Dispose(); - } + this.refCount.Value.ValueIfCreated?.Dispose(); } break; diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs index c47acf09..fd7f5c36 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs @@ -20,6 +20,32 @@ public static async Task> GetOrAddAsync(this ICa return result.lifetime; } } - } + } + + public static void AddOrUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + cache.AddOrUpdate(key, new ScopedAsyncAtomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + return cache.TryUpdate(key, new ScopedAsyncAtomic(value)); + } + + // TODO: TryGetLifetime? + public static bool TryGetLifetime(this ICache> cache, K key, out AsyncAtomicLifetime value) where V : IDisposable + { + if (cache.TryGet(key, out var scoped)) + { + if (scoped.TryCreateLifetime(out var lifetime)) + { + value = lifetime; + return true; + } + } + + value = default; + return false; + } } } diff --git a/BitFaster.Caching/Lazy/ScopedAtomic.cs b/BitFaster.Caching/Lazy/ScopedAtomic.cs index 917ed941..9a274fa0 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomic.cs @@ -39,9 +39,7 @@ public bool TryCreateLifetime(K key, Func valueFactory, out AtomicLifetime return false; } - var newRefCount = oldRefCount.IncrementCopy(); - - if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) { // When Lifetime is disposed, it calls DecrementReferenceCount lifetime = new AtomicLifetime(oldRefCount, this.DecrementReferenceCount); @@ -70,9 +68,7 @@ public bool TryCreateLifetime(out AtomicLifetime lifetime) return false; } - var newRefCount = oldRefCount.IncrementCopy(); - - if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) { // When Lifetime is disposed, it calls DecrementReferenceCount lifetime = new AtomicLifetime(oldRefCount, this.DecrementReferenceCount); @@ -96,13 +92,12 @@ private void DecrementReferenceCount() while (true) { var oldRefCount = this.refCount; - var newRefCount = oldRefCount.DecrementCopy(); - if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount)) + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.DecrementCopy(), oldRefCount)) { - if (newRefCount.Count == 0) + if (this.refCount.Count == 0) { - newRefCount.Value.Dispose(); + this.refCount.Value.Dispose(); } break; From 76a9b3251ee0800908c820ecda2e59d8cdbde952 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 9 Nov 2021 18:40:25 -0800 Subject: [PATCH 17/31] fix ns, rem disposable atomic --- .../AsyncAtomicBench.cs | 1 - BitFaster.Caching.Benchmarks/AtomicBench.cs | 1 - .../DesiredApi.cs | 20 +-- BitFaster.Caching/Lazy/AsyncAtomic.cs | 2 +- .../Lazy/AsyncAtomicCacheExtensions.cs | 2 +- BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs | 2 +- BitFaster.Caching/Lazy/Atomic.cs | 4 +- .../Lazy/AtomicCacheExtensions.cs | 2 +- BitFaster.Caching/Lazy/AtomicLifetime.cs | 6 +- BitFaster.Caching/Lazy/DisposableAtomic.cs | 125 ------------------ BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs | 2 +- .../Lazy/ScopedAsyncAtomicExtensions.cs | 2 +- BitFaster.Caching/Lazy/ScopedAtomic.cs | 10 +- .../Lazy/ScopedAtomicExtensions.cs | 2 +- BitFaster.Caching/Lazy/Synchronized.cs | 2 +- 15 files changed, 27 insertions(+), 156 deletions(-) rename {BitFaster.Caching/Lazy => BitFaster.Caching.UnitTests}/DesiredApi.cs (89%) delete mode 100644 BitFaster.Caching/Lazy/DisposableAtomic.cs diff --git a/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs b/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs index 7efa3759..0b9786a8 100644 --- a/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs +++ b/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs @@ -5,7 +5,6 @@ using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; -using BitFaster.Caching.Lazy; using BitFaster.Caching.Lru; namespace BitFaster.Caching.Benchmarks diff --git a/BitFaster.Caching.Benchmarks/AtomicBench.cs b/BitFaster.Caching.Benchmarks/AtomicBench.cs index 5bb6f5f1..a87e69e5 100644 --- a/BitFaster.Caching.Benchmarks/AtomicBench.cs +++ b/BitFaster.Caching.Benchmarks/AtomicBench.cs @@ -5,7 +5,6 @@ using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; -using BitFaster.Caching.Lazy; using BitFaster.Caching.Lru; namespace BitFaster.Caching.Benchmarks diff --git a/BitFaster.Caching/Lazy/DesiredApi.cs b/BitFaster.Caching.UnitTests/DesiredApi.cs similarity index 89% rename from BitFaster.Caching/Lazy/DesiredApi.cs rename to BitFaster.Caching.UnitTests/DesiredApi.cs index 135f078c..fc700213 100644 --- a/BitFaster.Caching/Lazy/DesiredApi.cs +++ b/BitFaster.Caching.UnitTests/DesiredApi.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using BitFaster.Caching.Lru; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching.UnitTests { // Wrappers needed: // - Atomic @@ -16,14 +16,14 @@ namespace BitFaster.Caching.Lazy public class DesiredApi { public static void HowToCacheAtomic() - { + { var lru = new ConcurrentLru>(4); // raw, this is a bit of a mess - int r = lru.GetOrAdd(1, i => new Atomic()).GetValue(1, x => x); + var r = lru.GetOrAdd(1, i => new Atomic()).GetValue(1, x => x); // extension cleanup can hide it - int rr = lru.GetOrAdd(1, i => i); + var rr = lru.GetOrAdd(1, i => i); lru.TryUpdate(2, 3); lru.TryGet(1, out int v); @@ -37,7 +37,7 @@ public static void HowToCacheScopedAtomic() using (var l = scopedAtomicLru.GetOrAdd(1, k => new SomeDisposable())) { - SomeDisposable d = l.Value; + var d = l.Value; } scopedAtomicLru.TryUpdate(2, new SomeDisposable()); @@ -46,7 +46,7 @@ public static void HowToCacheScopedAtomic() // TODO: how to clean this up to 1 line? if (scopedAtomicLru.TryGetLifetime(1, out var lifetime)) - { + { using (lifetime) { var x = lifetime.Value; @@ -55,10 +55,10 @@ public static void HowToCacheScopedAtomic() } public async static Task HowToCacheAsyncAtomic() - { + { var asyncAtomicLru = new ConcurrentLru>(5); - int ar = await asyncAtomicLru.GetOrAddAsync(1, i => Task.FromResult(i)); + var ar = await asyncAtomicLru.GetOrAddAsync(1, i => Task.FromResult(i)); asyncAtomicLru.TryUpdate(2, 3); asyncAtomicLru.TryGet(1, out int v); @@ -78,11 +78,11 @@ public static async Task HowToCacheScopedAsyncAtomic() using (var lifetime = await scopedAsyncAtomicLru.GetOrAddAsync(1, valueFactory)) { - SomeDisposable y = lifetime.Value; + var y = lifetime.Value; } scopedAsyncAtomicLru.TryUpdate(2, new SomeDisposable()); - + scopedAsyncAtomicLru.AddOrUpdate(1, new SomeDisposable()); // TODO: how to clean this up to 1 line? diff --git a/BitFaster.Caching/Lazy/AsyncAtomic.cs b/BitFaster.Caching/Lazy/AsyncAtomic.cs index e5ff632b..61fa6621 100644 --- a/BitFaster.Caching/Lazy/AsyncAtomic.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomic.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] public class AsyncAtomic diff --git a/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs index b5c0dee9..665fa1b0 100644 --- a/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { public static class AsyncAtomicCacheExtensions { diff --git a/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs index 0f16b844..5356c477 100644 --- a/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { public class AsyncAtomicLifetime : IDisposable { diff --git a/BitFaster.Caching/Lazy/Atomic.cs b/BitFaster.Caching/Lazy/Atomic.cs index 0de67b9e..be9ecd7a 100644 --- a/BitFaster.Caching/Lazy/Atomic.cs +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -7,10 +7,8 @@ using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { - - // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] diff --git a/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs b/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs index 0204d35f..a921d257 100644 --- a/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs +++ b/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { public static class AtomicCacheExtensions { diff --git a/BitFaster.Caching/Lazy/AtomicLifetime.cs b/BitFaster.Caching/Lazy/AtomicLifetime.cs index 0d9c0a52..3acbc9b3 100644 --- a/BitFaster.Caching/Lazy/AtomicLifetime.cs +++ b/BitFaster.Caching/Lazy/AtomicLifetime.cs @@ -4,15 +4,15 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { public class AtomicLifetime : IDisposable where V : IDisposable { private readonly Action onDisposeAction; - private readonly ReferenceCount> refCount; + private readonly ReferenceCount> refCount; private bool isDisposed; - public AtomicLifetime(ReferenceCount> refCount, Action onDisposeAction) + public AtomicLifetime(ReferenceCount> refCount, Action onDisposeAction) { this.refCount = refCount; this.onDisposeAction = onDisposeAction; diff --git a/BitFaster.Caching/Lazy/DisposableAtomic.cs b/BitFaster.Caching/Lazy/DisposableAtomic.cs deleted file mode 100644 index 0b5217da..00000000 --- a/BitFaster.Caching/Lazy/DisposableAtomic.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace BitFaster.Caching.Lazy -{ - // TODO: is this actually even needed? Or is the approach in ScopedAsyncAtomic sufficient? E.g. rely on IsValueCreated at dispose time, scoped owns tracking dispose and is already thread safe. - // requirements for IDisposable atomic - // if value !created, no dispose, cannot create - throws object disposed exception - // if created, dispose value - [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public class DisposableAtomic : IDisposable where V : IDisposable - { - private volatile Initializer initializer; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private V value; - - public DisposableAtomic() - { - this.initializer = new Initializer(); - } - - public DisposableAtomic(V value) - { - this.value = value; - } - - public V GetValue(K key, Func valueFactory) - { - if (this.initializer == null) - { - return this.value; - } - - return CreateValue(key, valueFactory); - } - - public bool IsValueCreated => this.initializer == null; - - public V ValueIfCreated - { - get - { - if (!this.IsValueCreated) - { - return default; - } - - return this.value; - } - } - - private V CreateValue(K key, Func valueFactory) - { - Initializer init = this.initializer; - - if (init != null) - { - this.value = init.CreateValue(key, valueFactory); - this.initializer = null; - } - - return this.value; - } - - public void Dispose() - { - Initializer init = this.initializer; - - if (init != null) - { - init.Dispose(); - } - else - { - this.value?.Dispose(); - } - } - - private class Initializer : IDisposable - { - private object syncLock = new object(); - private bool isInitialized; - private volatile bool isDisposed; - private V value; - - public V CreateValue(K key, Func valueFactory) - { - var r = Synchronized.Initialize(ref this.value, ref isInitialized, ref syncLock, valueFactory, key); - - // 2 possible orders - // Create value then Dispose - // Dispose then CreateValue - - if (this.isDisposed) - { - throw new ObjectDisposedException(nameof(value)); - } - - return r; - } - - public void Dispose() - { - lock (this.syncLock) - { - if (this.isInitialized) - { - value.Dispose(); - } - - // LazyInitializer will no longer attempt to init in CreateValue - Volatile.Write(ref this.isInitialized, true); - } - - this.isDisposed = true; - } - } - } -} diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs index 79d9fb58..d627b210 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal public class ScopedAsyncAtomic : IDisposable diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs index fd7f5c36..2e18b0b8 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { public static class ScopedAsyncAtomicExtensions { diff --git a/BitFaster.Caching/Lazy/ScopedAtomic.cs b/BitFaster.Caching/Lazy/ScopedAtomic.cs index 9a274fa0..58ba25da 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomic.cs @@ -5,21 +5,21 @@ using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { public class ScopedAtomic : IDisposable where V : IDisposable { - private ReferenceCount> refCount; + private ReferenceCount> refCount; private bool isDisposed; public ScopedAtomic() { - this.refCount = new ReferenceCount>(new DisposableAtomic()); + this.refCount = new ReferenceCount>(new Atomic()); } public ScopedAtomic(V value) { - this.refCount = new ReferenceCount>(new DisposableAtomic(value)); + this.refCount = new ReferenceCount>(new Atomic(value)); } public bool TryCreateLifetime(K key, Func valueFactory, out AtomicLifetime lifetime) @@ -97,7 +97,7 @@ private void DecrementReferenceCount() { if (this.refCount.Count == 0) { - this.refCount.Value.Dispose(); + this.refCount.Value.ValueIfCreated?.Dispose(); } break; diff --git a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs index 67922d5d..589961de 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { public static class ScopedAtomicExtensions { diff --git a/BitFaster.Caching/Lazy/Synchronized.cs b/BitFaster.Caching/Lazy/Synchronized.cs index eb32c42a..aac16d2a 100644 --- a/BitFaster.Caching/Lazy/Synchronized.cs +++ b/BitFaster.Caching/Lazy/Synchronized.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; -namespace BitFaster.Caching.Lazy +namespace BitFaster.Caching { internal static class Synchronized { From 1b74354383cbbaffa7d4c954189125c1a071f16d Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 9 Nov 2021 19:38:01 -0800 Subject: [PATCH 18/31] add scoped ext --- BitFaster.Caching.Benchmarks/AtomicBench.cs | 9 +++++ BitFaster.Caching.UnitTests/DesiredApi.cs | 12 ++++++ BitFaster.Caching/Lazy/AsyncAtomic.cs | 2 +- BitFaster.Caching/Lazy/Atomic.cs | 2 +- .../Lazy/ScopedAsyncAtomicExtensions.cs | 3 +- .../Lazy/ScopedAtomicExtensions.cs | 2 + .../Lazy/ScopedCacheExtensions.cs | 39 +++++++++++++++++++ 7 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 BitFaster.Caching/Lazy/ScopedCacheExtensions.cs diff --git a/BitFaster.Caching.Benchmarks/AtomicBench.cs b/BitFaster.Caching.Benchmarks/AtomicBench.cs index a87e69e5..81ea610b 100644 --- a/BitFaster.Caching.Benchmarks/AtomicBench.cs +++ b/BitFaster.Caching.Benchmarks/AtomicBench.cs @@ -19,6 +19,8 @@ public class AtomicBench private static readonly ConcurrentLru> atomicConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + private static readonly ConcurrentLru> lazyConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + [Benchmark()] public void ConcurrentDictionary() { @@ -39,5 +41,12 @@ public void AtomicConcurrentLru() Func func = x => x; atomicConcurrentLru.GetOrAdd(1, func); } + + [Benchmark()] + public void LazyConcurrentLru() + { + Func> func = x => new Lazy(x); + lazyConcurrentLru.GetOrAdd(1, func); + } } } diff --git a/BitFaster.Caching.UnitTests/DesiredApi.cs b/BitFaster.Caching.UnitTests/DesiredApi.cs index fc700213..747e920b 100644 --- a/BitFaster.Caching.UnitTests/DesiredApi.cs +++ b/BitFaster.Caching.UnitTests/DesiredApi.cs @@ -30,6 +30,18 @@ public static void HowToCacheAtomic() lru.AddOrUpdate(1, 2); } + public static void HowToCacheScoped() + { + var lru = new ConcurrentLru>(4); + + // this is not so clean, because the lambda has to input the scoped object + // if we wrap it, would need a closure inside the extension method + using (var l = lru.ScopedGetOrAdd(1, x => new Scoped(new SomeDisposable()))) + { + var d = l.Value; + } + } + public static void HowToCacheScopedAtomic() { // ICache> diff --git a/BitFaster.Caching/Lazy/AsyncAtomic.cs b/BitFaster.Caching/Lazy/AsyncAtomic.cs index 61fa6621..80062f1a 100644 --- a/BitFaster.Caching/Lazy/AsyncAtomic.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomic.cs @@ -11,7 +11,7 @@ namespace BitFaster.Caching [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] public class AsyncAtomic { - private volatile Initializer initializer; + private Initializer initializer; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private V value; diff --git a/BitFaster.Caching/Lazy/Atomic.cs b/BitFaster.Caching/Lazy/Atomic.cs index be9ecd7a..b49dee8a 100644 --- a/BitFaster.Caching/Lazy/Atomic.cs +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -14,7 +14,7 @@ namespace BitFaster.Caching [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] public class Atomic { - private volatile Initializer initializer; + private Initializer initializer; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private V value; diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs index 2e18b0b8..11addf1b 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs @@ -7,7 +7,8 @@ namespace BitFaster.Caching { public static class ScopedAsyncAtomicExtensions - { + { + // If a disposed ScopedAsyncAtomic is added to the cache, this method will get stuck in an infinite loop public static async Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable { while (true) diff --git a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs index 589961de..dd6e9e3b 100644 --- a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs @@ -9,6 +9,8 @@ namespace BitFaster.Caching public static class ScopedAtomicExtensions { // TODO: GetOrAddLifetime? + // If a disposed ScopedAtomic is added to the cache, this method will get stuck in an infinite loop. + // Can this be prevented by making the ScopedAtomic ctor internal so that it can only be created via the ext methods? public static AtomicLifetime GetOrAdd(this ICache> cache, K key, Func valueFactory) where V : IDisposable { while (true) diff --git a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs new file mode 100644 index 00000000..ff05e709 --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public static class ScopedCacheExtensions + { + public static Lifetime ScopedGetOrAdd(this ICache> cache, K key, Func> valueFactory) + where T : IDisposable + { + while (true) + { + var scope = cache.GetOrAdd(key, valueFactory); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + + public static async Task> ScopedGetOrAdd(this ICache> cache, K key, Func>> valueFactory) + where T : IDisposable + { + while (true) + { + var scope = await cache.GetOrAddAsync(key, valueFactory); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + } +} From 202b136ae440795a4b67089976b01db3c01d19c5 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 9 Nov 2021 19:51:01 -0800 Subject: [PATCH 19/31] benchmark scoped ext --- .../ScopedExtBench.cs | 72 +++++++++++++++++++ BitFaster.Caching.UnitTests/DesiredApi.cs | 2 +- .../Lazy/ScopedCacheExtensions.cs | 14 ++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 BitFaster.Caching.Benchmarks/ScopedExtBench.cs diff --git a/BitFaster.Caching.Benchmarks/ScopedExtBench.cs b/BitFaster.Caching.Benchmarks/ScopedExtBench.cs new file mode 100644 index 00000000..af28adac --- /dev/null +++ b/BitFaster.Caching.Benchmarks/ScopedExtBench.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Benchmarks +{ + //| Method | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Gen 0 | Allocated | + //|--------------------- |-----------:|----------:|----------:|------:|--------:|----------:|-------:|----------:| + //| ConcurrentDictionary | 8.453 ns | 0.0445 ns | 0.0394 ns | 0.46 | 0.00 | 396 B | - | - | + //| ConcurrentLru | 18.405 ns | 0.1529 ns | 0.1277 ns | 1.00 | 0.00 | 701 B | - | - | + //| ScopedConcurrentLru | 115.748 ns | 0.5271 ns | 0.4673 ns | 6.29 | 0.04 | 662 B | 0.0389 | 168 B | + //| ScopedConcurrentLru2 | 134.296 ns | 0.9543 ns | 0.8927 ns | 7.30 | 0.07 | 565 B | 0.0610 | 264 B | + [DisassemblyDiagnoser(printSource: true)] + [MemoryDiagnoser] + public class ScopedExtBench + { + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> scopedConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> scopedConcurrentLru2 = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + [Benchmark()] + public SomeDisposable ConcurrentDictionary() + { + Func func = x => new SomeDisposable(); + return dictionary.GetOrAdd(1, func); + } + + [Benchmark(Baseline = true)] + public SomeDisposable ConcurrentLru() + { + Func func = x => new SomeDisposable(); + return concurrentLru.GetOrAdd(1, func); + } + + [Benchmark()] + public SomeDisposable ScopedConcurrentLru() + { + Func> func = x => new Scoped(new SomeDisposable()); + using (var l = scopedConcurrentLru.ScopedGetOrAdd(1, func)) + { + return l.Value; + } + } + + [Benchmark()] + public SomeDisposable ScopedConcurrentLru2() + { + Func func = x => new SomeDisposable(); + using (var l = scopedConcurrentLru.ScopedGetOrAdd2(1, func)) + { + return l.Value; + } + } + } + + public class SomeDisposable : IDisposable + { + public void Dispose() + { + + } + } +} diff --git a/BitFaster.Caching.UnitTests/DesiredApi.cs b/BitFaster.Caching.UnitTests/DesiredApi.cs index 747e920b..60cfd01a 100644 --- a/BitFaster.Caching.UnitTests/DesiredApi.cs +++ b/BitFaster.Caching.UnitTests/DesiredApi.cs @@ -35,7 +35,7 @@ public static void HowToCacheScoped() var lru = new ConcurrentLru>(4); // this is not so clean, because the lambda has to input the scoped object - // if we wrap it, would need a closure inside the extension method + // if we wrap it, would need a closure inside the extension method. How bad is that? using (var l = lru.ScopedGetOrAdd(1, x => new Scoped(new SomeDisposable()))) { var d = l.Value; diff --git a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs index ff05e709..543bd60d 100644 --- a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs @@ -22,6 +22,20 @@ public static Lifetime ScopedGetOrAdd(this ICache> cache, } } + public static Lifetime ScopedGetOrAdd2(this ICache> cache, K key, Func valueFactory) + where T : IDisposable + { + while (true) + { + var scope = cache.GetOrAdd(key, k => new Scoped(valueFactory(k))); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + public static async Task> ScopedGetOrAdd(this ICache> cache, K key, Func>> valueFactory) where T : IDisposable { From 69c8e4ad05e9d8e77a84a21d6e773d54144b3237 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Wed, 10 Nov 2021 14:24:05 -0800 Subject: [PATCH 20/31] bench scoped --- .../ScopedExtBench.cs | 34 +++++++++++++------ .../Lazy/ScopedCacheExtensions.cs | 26 ++++++++++++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/BitFaster.Caching.Benchmarks/ScopedExtBench.cs b/BitFaster.Caching.Benchmarks/ScopedExtBench.cs index af28adac..e46b19d5 100644 --- a/BitFaster.Caching.Benchmarks/ScopedExtBench.cs +++ b/BitFaster.Caching.Benchmarks/ScopedExtBench.cs @@ -9,12 +9,13 @@ namespace BitFaster.Caching.Benchmarks { - //| Method | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Gen 0 | Allocated | - //|--------------------- |-----------:|----------:|----------:|------:|--------:|----------:|-------:|----------:| - //| ConcurrentDictionary | 8.453 ns | 0.0445 ns | 0.0394 ns | 0.46 | 0.00 | 396 B | - | - | - //| ConcurrentLru | 18.405 ns | 0.1529 ns | 0.1277 ns | 1.00 | 0.00 | 701 B | - | - | - //| ScopedConcurrentLru | 115.748 ns | 0.5271 ns | 0.4673 ns | 6.29 | 0.04 | 662 B | 0.0389 | 168 B | - //| ScopedConcurrentLru2 | 134.296 ns | 0.9543 ns | 0.8927 ns | 7.30 | 0.07 | 565 B | 0.0610 | 264 B | + //| Method | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Gen 0 | Allocated | + //|---------------------------------------- |-----------:|----------:|----------:|------:|--------:|----------:|-------:|----------:| + //| ConcurrentDictionary | 8.791 ns | 0.0537 ns | 0.0476 ns | 0.48 | 0.00 | 396 B | - | - | + //| ConcurrentLru | 18.429 ns | 0.1539 ns | 0.1440 ns | 1.00 | 0.00 | 701 B | - | - | + //| ScopedConcurrentLruNativeFunc | 117.665 ns | 1.4390 ns | 1.3461 ns | 6.39 | 0.10 | 662 B | 0.0389 | 168 B | + //| ScopedConcurrentLruWrappedFunc | 132.697 ns | 0.6867 ns | 0.5734 ns | 7.19 | 0.08 | 565 B | 0.0610 | 264 B | + //| ScopedConcurrentLruWrappedFuncProtected | 133.997 ns | 0.5089 ns | 0.4249 ns | 7.26 | 0.05 | 621 B | 0.0610 | 264 B | [DisassemblyDiagnoser(printSource: true)] [MemoryDiagnoser] public class ScopedExtBench @@ -25,8 +26,6 @@ public class ScopedExtBench private static readonly ConcurrentLru> scopedConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); - private static readonly ConcurrentLru> scopedConcurrentLru2 = new ConcurrentLru>(8, 9, EqualityComparer.Default); - [Benchmark()] public SomeDisposable ConcurrentDictionary() { @@ -42,8 +41,9 @@ public SomeDisposable ConcurrentLru() } [Benchmark()] - public SomeDisposable ScopedConcurrentLru() + public SomeDisposable ScopedConcurrentLruNativeFunc() { + // function generates actual cached object (scoped wrapping item) Func> func = x => new Scoped(new SomeDisposable()); using (var l = scopedConcurrentLru.ScopedGetOrAdd(1, func)) { @@ -52,10 +52,22 @@ public SomeDisposable ScopedConcurrentLru() } [Benchmark()] - public SomeDisposable ScopedConcurrentLru2() + public SomeDisposable ScopedConcurrentLruWrappedFunc() + { + // function generates item, extension method allocates a closure to create scoped + Func func = x => new SomeDisposable(); + using (var l = scopedConcurrentLru.ScopedGetOrAdd(1, func)) + { + return l.Value; + } + } + + [Benchmark()] + public SomeDisposable ScopedConcurrentLruWrappedFuncProtected() { + // function generates item, extension method allocates a closure to create scoped Func func = x => new SomeDisposable(); - using (var l = scopedConcurrentLru.ScopedGetOrAdd2(1, func)) + using (var l = scopedConcurrentLru.ScopedGetOrAddProtected(1, func)) { return l.Value; } diff --git a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs index 543bd60d..78b29403 100644 --- a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs @@ -22,11 +22,12 @@ public static Lifetime ScopedGetOrAdd(this ICache> cache, } } - public static Lifetime ScopedGetOrAdd2(this ICache> cache, K key, Func valueFactory) + public static Lifetime ScopedGetOrAdd(this ICache> cache, K key, Func valueFactory) where T : IDisposable { while (true) { + // Note: allocates a closure on every call var scope = cache.GetOrAdd(key, k => new Scoped(valueFactory(k))); if (scope.TryCreateLifetime(out var lifetime)) @@ -36,7 +37,28 @@ public static Lifetime ScopedGetOrAdd2(this ICache> cache, } } - public static async Task> ScopedGetOrAdd(this ICache> cache, K key, Func>> valueFactory) + public static Lifetime ScopedGetOrAddProtected(this ICache> cache, K key, Func valueFactory) + where T : IDisposable + { + int c = 0; + while (true) + { + // Note: allocates a closure on every call + var scope = cache.GetOrAdd(key, k => new Scoped(valueFactory(k))); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + + if (c++ > 5) + { + throw new InvalidOperationException(); + } + } + } + + public static async Task> ScopedGetOrAddAsync(this ICache> cache, K key, Func>> valueFactory) where T : IDisposable { while (true) From f1887f2846082630605f3c56d4e5b1490d6d936a Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 14 Nov 2021 19:51:14 -0800 Subject: [PATCH 21/31] fix lazy --- BitFaster.Caching.Benchmarks/AtomicBench.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching.Benchmarks/AtomicBench.cs b/BitFaster.Caching.Benchmarks/AtomicBench.cs index 81ea610b..4c58d54c 100644 --- a/BitFaster.Caching.Benchmarks/AtomicBench.cs +++ b/BitFaster.Caching.Benchmarks/AtomicBench.cs @@ -45,7 +45,7 @@ public void AtomicConcurrentLru() [Benchmark()] public void LazyConcurrentLru() { - Func> func = x => new Lazy(x); + Func> func = x => new Lazy(() => x); lazyConcurrentLru.GetOrAdd(1, func); } } From 3e406f67186979e82056c82a4d9272407abf6812 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 15 Nov 2021 16:43:12 -0800 Subject: [PATCH 22/31] atomictests --- .../Lazy/AtomicTests.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 BitFaster.Caching.UnitTests/Lazy/AtomicTests.cs diff --git a/BitFaster.Caching.UnitTests/Lazy/AtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AtomicTests.cs new file mode 100644 index 00000000..72c4ea72 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AtomicTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class AtomicTests + { + [Fact] + public void WhenNotInitializedIsValueCreatedReturnsFalse() + { + Atomic a = new(); + + a.IsValueCreated.Should().Be(false); + } + + [Fact] + public void WhenNotInitializedValueIfCreatedReturnsDefault() + { + Atomic a = new(); + + a.ValueIfCreated.Should().Be(0); + } + + [Fact] + public void WhenInitializedByValueIsValueCreatedReturnsTrue() + { + Atomic a = new(1); + + a.IsValueCreated.Should().Be(true); + } + + [Fact] + public void WhenInitializedByValueValueIfCreatedReturnsValue() + { + Atomic a = new(1); + + a.ValueIfCreated.Should().Be(1); + } + + [Fact] + public void WhenNotInitGetValueReturnsValueFromFactory() + { + Atomic a = new(); + + a.GetValue(1, k => k + 1).Should().Be(2); + } + + [Fact] + public void WhenInitGetValueReturnsInitialValue() + { + Atomic a = new(); + + a.GetValue(1, k => k + 1); + a.GetValue(1, k => k + 2).Should().Be(2); + } + } +} From 826ad7147f5e34f08f4d468cb0945a1fa8a298f8 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 15 Nov 2021 17:42:41 -0800 Subject: [PATCH 23/31] AsyncAtomicTests --- .../Lazy/AsyncAtomicTests.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs new file mode 100644 index 00000000..eba7f7cb --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class AsyncAtomicTests + { + [Fact] + public void WhenNotInitializedIsValueCreatedReturnsFalse() + { + AsyncAtomic a = new(); + + a.IsValueCreated.Should().Be(false); + } + + [Fact] + public void WhenNotInitializedValueIfCreatedReturnsDefault() + { + AsyncAtomic a = new(); + + a.ValueIfCreated.Should().Be(0); + } + + [Fact] + public void WhenInitializedByValueIsValueCreatedReturnsTrue() + { + AsyncAtomic a = new(1); + + a.IsValueCreated.Should().Be(true); + } + + [Fact] + public void WhenInitializedByValueValueIfCreatedReturnsValue() + { + AsyncAtomic a = new(1); + + a.ValueIfCreated.Should().Be(1); + } + + [Fact] + public async Task WhenNotInitGetValueReturnsValueFromFactory() + { + AsyncAtomic a = new(); + + int r = await a.GetValueAsync(1, k => Task.FromResult(k + 1)); + r.Should().Be(2); + } + + [Fact] + public async Task WhenInitGetValueReturnsInitialValue() + { + AsyncAtomic a = new(); + + int r1 = await a.GetValueAsync(1, k => Task.FromResult(k + 1)); + int r2 = await a.GetValueAsync(1, k => Task.FromResult(k + 12)); + r2.Should().Be(2); + } + } +} From 2500c74c2e8f15840328fb4232106f0d2d531200 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 15 Nov 2021 18:07:10 -0800 Subject: [PATCH 24/31] more async tests --- .../Lazy/AsyncAtomicTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs index eba7f7cb..1a24116b 100644 --- a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs @@ -60,5 +60,70 @@ public async Task WhenInitGetValueReturnsInitialValue() int r2 = await a.GetValueAsync(1, k => Task.FromResult(k + 12)); r2.Should().Be(2); } + + [Fact] + public async Task WhenGetValueThrowsExceptionIsNotCached() + { + AsyncAtomic a = new(); + + try + { + int r1 = await a.GetValueAsync(1, k => throw new Exception()); + + throw new Exception("Expected GetValueAsync to throw"); + } + catch + { + } + + int r2 = await a.GetValueAsync(1, k => Task.FromResult(k + 2)); + r2.Should().Be(3); + } + + // TODO: this signal method is not reliable + [Fact] + public async Task WhenTaskIsCachedAllWaitersRecieveResult() + { + AsyncAtomic a = new(); + + TaskCompletionSource valueFactory = new TaskCompletionSource(); + TaskCompletionSource signal = new TaskCompletionSource(); + + // Cache the task, don't wait + var t1 = Task.Run(async () => await a.GetValueAsync(1, k => { signal.SetResult(); return valueFactory.Task; })); + + await signal.Task; + + var t2 = Task.Run(async () => await a.GetValueAsync(1, k => Task.FromResult(k + 2))); + + valueFactory.TrySetResult(666); + + int r2 = await t2; + r2.Should().Be(666); + } + + [Fact] + public async Task WhenTaskIsCachedAndThrowsAllWaitersRecieveException() + { + AsyncAtomic a = new(); + + TaskCompletionSource valueFactory = new TaskCompletionSource(); + TaskCompletionSource signal = new TaskCompletionSource(); + + // Cache the task, don't wait + var t1 = Task.Run(async () => await a.GetValueAsync(1, k => { signal.SetResult(); return valueFactory.Task; })); + + await signal.Task; + + var t2 = Task.Run(async () => await a.GetValueAsync(1, k => Task.FromResult(k + 2))); + + valueFactory.SetException(new InvalidOperationException()); + + Func r1 = async () => { await t1; }; + Func r2 = async () => { await t2; }; + + r1.Should().Throw(); + r2.Should().Throw(); + } } } From bc25feaba9d6e16e880f5e9c5f8df2d7d23ab031 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 16 Nov 2021 10:57:46 -0800 Subject: [PATCH 25/31] fix test --- .../Lazy/AsyncAtomicTests.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs index 1a24116b..e12c97eb 100644 --- a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs @@ -80,26 +80,25 @@ public async Task WhenGetValueThrowsExceptionIsNotCached() r2.Should().Be(3); } - // TODO: this signal method is not reliable [Fact] public async Task WhenTaskIsCachedAllWaitersRecieveResult() { AsyncAtomic a = new(); - TaskCompletionSource valueFactory = new TaskCompletionSource(); - TaskCompletionSource signal = new TaskCompletionSource(); + TaskCompletionSource enterFactory = new TaskCompletionSource(); + TaskCompletionSource exitFactory = new TaskCompletionSource(); // Cache the task, don't wait - var t1 = Task.Run(async () => await a.GetValueAsync(1, k => { signal.SetResult(); return valueFactory.Task; })); + var t1 = Task.Run(async () => await a.GetValueAsync(1, async k => { enterFactory.SetResult(); await exitFactory.Task; return 42; })); - await signal.Task; + await enterFactory.Task; var t2 = Task.Run(async () => await a.GetValueAsync(1, k => Task.FromResult(k + 2))); - valueFactory.TrySetResult(666); + exitFactory.SetResult(); int r2 = await t2; - r2.Should().Be(666); + r2.Should().Be(42); } [Fact] @@ -107,17 +106,17 @@ public async Task WhenTaskIsCachedAndThrowsAllWaitersRecieveException() { AsyncAtomic a = new(); - TaskCompletionSource valueFactory = new TaskCompletionSource(); - TaskCompletionSource signal = new TaskCompletionSource(); + TaskCompletionSource enterFactory = new TaskCompletionSource(); + TaskCompletionSource exitFactory = new TaskCompletionSource(); // Cache the task, don't wait - var t1 = Task.Run(async () => await a.GetValueAsync(1, k => { signal.SetResult(); return valueFactory.Task; })); + var t1 = Task.Run(async () => await a.GetValueAsync(1, async k => { enterFactory.SetResult(); await exitFactory.Task; throw new InvalidOperationException(); })); - await signal.Task; + await enterFactory.Task; var t2 = Task.Run(async () => await a.GetValueAsync(1, k => Task.FromResult(k + 2))); - valueFactory.SetException(new InvalidOperationException()); + exitFactory.SetResult(); Func r1 = async () => { await t1; }; Func r2 = async () => { await t2; }; From 350050faa4d8406c05109f975a41da1704cbdf14 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 16 Nov 2021 11:33:29 -0800 Subject: [PATCH 26/31] sync tets --- .../Lazy/AsyncAtomicTests.cs | 1 + .../Lazy/SynchronizedTests.cs | 30 +++++++++++++++++++ BitFaster.Caching/Metadata.cs | 8 +++++ 3 files changed, 39 insertions(+) create mode 100644 BitFaster.Caching.UnitTests/Lazy/SynchronizedTests.cs create mode 100644 BitFaster.Caching/Metadata.cs diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs index e12c97eb..bc795f58 100644 --- a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs @@ -101,6 +101,7 @@ public async Task WhenTaskIsCachedAllWaitersRecieveResult() r2.Should().Be(42); } + // TODO: this is flaky, why? [Fact] public async Task WhenTaskIsCachedAndThrowsAllWaitersRecieveException() { diff --git a/BitFaster.Caching.UnitTests/Lazy/SynchronizedTests.cs b/BitFaster.Caching.UnitTests/Lazy/SynchronizedTests.cs new file mode 100644 index 00000000..19e262a7 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/SynchronizedTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using Xunit; +using FluentAssertions; + +namespace BitFaster.Caching.UnitTests +{ + public class SynchronizedTests + { + private int target = 42; + private bool initialized = true; + private object syncLock = new object(); + + [Fact] + public void WhenIsIntializedValueParamIsNotUsed() + { + Synchronized.Initialize(ref target, ref initialized, ref syncLock, 666).Should().Be(42); + } + + [Fact] + public void WhenIsIntializedValueFactoryIsNotUsed() + { + Synchronized.Initialize(ref target, ref initialized, ref syncLock, k => 666, 2).Should().Be(42); + } + } +} diff --git a/BitFaster.Caching/Metadata.cs b/BitFaster.Caching/Metadata.cs new file mode 100644 index 00000000..0b99e83f --- /dev/null +++ b/BitFaster.Caching/Metadata.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo(@"BitFaster.Caching.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f55849315b02d525d40701eee5d8eba39e6a517644e8af3fa15141eab7058e76be808e36cfee8d7e071b5aac37bd5e45c67971602680f7bfc26d8c9ebca95dd33b4e3f17a4c28b01268ee6b110ad7e2106ab8ffd1c7be3143192527ce5f639395e46ab086518e881706c6ee9eb96f0263aa34e5152cf5aecf657d463fecf62ca")] From 59c90085eac6d8cfb7cc2a2491a2f7a60f16a15c Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 16 Nov 2021 11:51:39 -0800 Subject: [PATCH 27/31] wait --- .../Lazy/AsyncAtomicTests.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs index bc795f58..0cc4537d 100644 --- a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs @@ -92,8 +92,14 @@ public async Task WhenTaskIsCachedAllWaitersRecieveResult() var t1 = Task.Run(async () => await a.GetValueAsync(1, async k => { enterFactory.SetResult(); await exitFactory.Task; return 42; })); await enterFactory.Task; + TaskCompletionSource enter2nd = new TaskCompletionSource(); - var t2 = Task.Run(async () => await a.GetValueAsync(1, k => Task.FromResult(k + 2))); + var t2 = Task.Run(async () => { enter2nd.SetResult(); return await a.GetValueAsync(1, k => Task.FromResult(k + 2)); }); + + // there is no good way to synchronize here such that GetValueAsync has definately started running. + // Best we can do is wait for the task to run, then wait 10ms + await enter2nd.Task; + await Task.Delay(TimeSpan.FromMilliseconds(10)); exitFactory.SetResult(); @@ -101,7 +107,6 @@ public async Task WhenTaskIsCachedAllWaitersRecieveResult() r2.Should().Be(42); } - // TODO: this is flaky, why? [Fact] public async Task WhenTaskIsCachedAndThrowsAllWaitersRecieveException() { @@ -115,7 +120,14 @@ public async Task WhenTaskIsCachedAndThrowsAllWaitersRecieveException() await enterFactory.Task; - var t2 = Task.Run(async () => await a.GetValueAsync(1, k => Task.FromResult(k + 2))); + TaskCompletionSource enter2nd = new TaskCompletionSource(); + + var t2 = Task.Run(async () => { enter2nd.SetResult(); return await a.GetValueAsync(1, k => Task.FromResult(k + 2)); }); + + // there is no good way to synchronize here such that GetValueAsync has definately started running. + // Best we can do is wait for the task to run, then wait 10ms + await enter2nd.Task; + await Task.Delay(TimeSpan.FromMilliseconds(10)); exitFactory.SetResult(); From 4093e6953edba1eda13615a5c7d3d7434f2d29d5 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 16 Nov 2021 16:12:38 -0800 Subject: [PATCH 28/31] scoped atomic tests --- ...cAtomicBench.cs => AsyncLruAtomicBench.cs} | 7 +- .../{AtomicBench.cs => AtomicLruBench.cs} | 7 +- ...ScopedExtBench.cs => ScopedLruExtBench.cs} | 7 +- .../Lazy/ScopedAtomicTests.cs | 125 ++++++++++++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) rename BitFaster.Caching.Benchmarks/{AsyncAtomicBench.cs => AsyncLruAtomicBench.cs} (87%) rename BitFaster.Caching.Benchmarks/{AtomicBench.cs => AtomicLruBench.cs} (89%) rename BitFaster.Caching.Benchmarks/{ScopedExtBench.cs => ScopedLruExtBench.cs} (94%) create mode 100644 BitFaster.Caching.UnitTests/Lazy/ScopedAtomicTests.cs diff --git a/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs b/BitFaster.Caching.Benchmarks/AsyncLruAtomicBench.cs similarity index 87% rename from BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs rename to BitFaster.Caching.Benchmarks/AsyncLruAtomicBench.cs index 0b9786a8..ef980f48 100644 --- a/BitFaster.Caching.Benchmarks/AsyncAtomicBench.cs +++ b/BitFaster.Caching.Benchmarks/AsyncLruAtomicBench.cs @@ -5,13 +5,16 @@ using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; using BitFaster.Caching.Lru; namespace BitFaster.Caching.Benchmarks { - [DisassemblyDiagnoser(printSource: true)] + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net60)] + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] [MemoryDiagnoser] - public class AsyncAtomicBench + public class AsyncLruAtomicBench { private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); diff --git a/BitFaster.Caching.Benchmarks/AtomicBench.cs b/BitFaster.Caching.Benchmarks/AtomicLruBench.cs similarity index 89% rename from BitFaster.Caching.Benchmarks/AtomicBench.cs rename to BitFaster.Caching.Benchmarks/AtomicLruBench.cs index 4c58d54c..bd19be61 100644 --- a/BitFaster.Caching.Benchmarks/AtomicBench.cs +++ b/BitFaster.Caching.Benchmarks/AtomicLruBench.cs @@ -5,13 +5,16 @@ using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; using BitFaster.Caching.Lru; namespace BitFaster.Caching.Benchmarks { - [DisassemblyDiagnoser(printSource: true)] + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net60)] + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] [MemoryDiagnoser] - public class AtomicBench + public class AtomicLruBench { private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); diff --git a/BitFaster.Caching.Benchmarks/ScopedExtBench.cs b/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs similarity index 94% rename from BitFaster.Caching.Benchmarks/ScopedExtBench.cs rename to BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs index e46b19d5..8e95a8db 100644 --- a/BitFaster.Caching.Benchmarks/ScopedExtBench.cs +++ b/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; using BitFaster.Caching.Lru; namespace BitFaster.Caching.Benchmarks @@ -16,9 +17,11 @@ namespace BitFaster.Caching.Benchmarks //| ScopedConcurrentLruNativeFunc | 117.665 ns | 1.4390 ns | 1.3461 ns | 6.39 | 0.10 | 662 B | 0.0389 | 168 B | //| ScopedConcurrentLruWrappedFunc | 132.697 ns | 0.6867 ns | 0.5734 ns | 7.19 | 0.08 | 565 B | 0.0610 | 264 B | //| ScopedConcurrentLruWrappedFuncProtected | 133.997 ns | 0.5089 ns | 0.4249 ns | 7.26 | 0.05 | 621 B | 0.0610 | 264 B | - [DisassemblyDiagnoser(printSource: true)] + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net60)] + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] [MemoryDiagnoser] - public class ScopedExtBench + public class ScopedLruExtBench { private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicTests.cs new file mode 100644 index 00000000..79ceca56 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicTests.cs @@ -0,0 +1,125 @@ +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.Lazy +{ + public class ScopedAtomicTests + { + [Fact] + public void WhenScopeIsCreatedThenScopeDisposedLifetimeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + + scope.Dispose(); + scope.Dispose(); // validate double dispose is still single ref count + disposable.IsDisposed.Should().BeFalse(); + + lifetime.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenScopeIsCreatedThenLifetimeDisposedScopeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + + lifetime.Dispose(); + lifetime.Dispose(); // validate double dispose is still single ref count + + disposable.IsDisposed.Should().BeFalse(); + + scope.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenScopeIsDisposedCreateScopeThrows() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.Dispose(); + + scope.Invoking(s => s.CreateLifetime(1, k => new Disposable())).Should().Throw(); + } + + [Fact] + public void WhenScopeIsNotDisposedCreateScopeReturnsLifetime() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + + using (var l = scope.CreateLifetime(1, k => new Disposable())) + { + l.ReferenceCount.Should().Be(1); + } + } + + [Fact] + public void WhenScopeIsDisposedTryCreateScopeReturnsFalse() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.Dispose(); + + scope.TryCreateLifetime(out var l).Should().BeFalse(); + } + + + [Fact] + public void WhenAtomicIsNotCreatedTryCreateScopeReturnsFalse() + { + var scope = new ScopedAtomic(); + + scope.TryCreateLifetime(out var l).Should().BeFalse(); + } + + [Fact] + public void WhenScopedIsCreatedFromCacheItemHasExpectedLifetime() + { + var lru = new ConcurrentLru>(2, 9, EqualityComparer.Default); + var valueFactory = new DisposableValueFactory(); + + using (var lifetime = lru.GetOrAdd(1, valueFactory.Create)) + { + lifetime.Value.IsDisposed.Should().BeFalse(); + } + + valueFactory.Disposable.IsDisposed.Should().BeFalse(); + + lru.TryRemove(1); + + valueFactory.Disposable.IsDisposed.Should().BeTrue(); + } + + private class DisposableValueFactory + { + public Disposable Disposable { get; } = new Disposable(); + + public Disposable Create(int key) + { + return this.Disposable; + } + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} From e31cf5fd3a09574b58f3321a44addcad6decf0c1 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 16 Nov 2021 21:03:04 -0800 Subject: [PATCH 29/31] sa ext tests --- .../Lazy/ScopedAtomicExtensionsTests.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 BitFaster.Caching.UnitTests/Lazy/ScopedAtomicExtensionsTests.cs diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicExtensionsTests.cs new file mode 100644 index 00000000..dc00c670 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicExtensionsTests.cs @@ -0,0 +1,96 @@ +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.Lazy +{ + public class ScopedAtomicExtensionsTests + { + private ConcurrentLru> lru = new(2, 9, EqualityComparer.Default); + + [Fact] + public void GetOrAddRetrunsValidLifetime() + { + var valueFactory = new DisposableValueFactory(); + + using (var lifetime = lru.GetOrAdd(1, valueFactory.Create)) + { + lifetime.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void AddOrUpdateUpdatesValue() + { + var d = new Disposable(); + + lru.AddOrUpdate(1, d); + + lru.TryGetLifetime(1, out var lifetime).Should().BeTrue(); + using (lifetime) + { + lifetime.Value.Should().Be(d); + } + } + + [Fact] + public void TryUpdateWhenKeyDoesNotExistReturnsFalse() + { + var d = new Disposable(); + + lru.TryUpdate(1, d).Should().BeFalse(); + } + + [Fact] + public void TryUpdateWhenKeyExistsUpdatesValue() + { + var d1 = new Disposable(); + lru.AddOrUpdate(1, d1); + + var d2 = new Disposable(); + + lru.TryUpdate(1, d2).Should().BeTrue(); + + lru.TryGetLifetime(1, out var lifetime).Should().BeTrue(); + using (lifetime) + { + lifetime.Value.Should().Be(d2); + } + } + + [Fact] + public void TryGetLifetimeDuringRaceReturnsFalse() + { + // directly add an uninitialized ScopedAtomic, simulating catching GetOrAdd before value is created + lru.AddOrUpdate(1, new ScopedAtomic()); + + lru.TryGetLifetime(1, out var lifetime).Should().BeFalse(); + } + + private class DisposableValueFactory + { + public Disposable Disposable { get; } = new Disposable(); + + public Disposable Create(int key) + { + return this.Disposable; + } + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} From 3ac8bc945c4797a8ca73a23eee45eb98514eff4b Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Wed, 17 Nov 2021 20:06:58 -0800 Subject: [PATCH 30/31] +tests --- .../ScopedLruExtBench.cs | 4 +- .../Lazy/AtomicExtensionsTests.cs | 54 ++++++++++++ .../Lazy/ScopedExtensionsTests.cs | 82 +++++++++++++++++++ .../Lazy/ScopedCacheExtensions.cs | 5 +- 4 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs create mode 100644 BitFaster.Caching.UnitTests/Lazy/ScopedExtensionsTests.cs diff --git a/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs b/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs index 8e95a8db..91d57eb2 100644 --- a/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs +++ b/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs @@ -68,8 +68,8 @@ public SomeDisposable ScopedConcurrentLruWrappedFunc() [Benchmark()] public SomeDisposable ScopedConcurrentLruWrappedFuncProtected() { - // function generates item, extension method allocates a closure to create scoped - Func func = x => new SomeDisposable(); + // function generates actual cached object (scoped wrapping item) + Func> func = x => new Scoped(new SomeDisposable()); using (var l = scopedConcurrentLru.ScopedGetOrAddProtected(1, func)) { return l.Value; diff --git a/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs new file mode 100644 index 00000000..fd07afa0 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs @@ -0,0 +1,54 @@ +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.Lazy +{ + public class AtomicExtensionsTests + { + private ConcurrentLru> lru = new(2, 9, EqualityComparer.Default); + + [Fact] + public void GetOrAdd() + { + var rr = lru.GetOrAdd(1, i => i); + } + + [Fact] + public void TryUpdateWhenKeyDoesNotExistReturnsFalse() + { + lru.TryUpdate(2, 3).Should().BeFalse(); + } + + [Fact] + public void TryUpdateWhenKeyExistsUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryUpdate(1, 42).Should().BeTrue(); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(42); + } + + [Fact] + public void TryGetWhenKeyDoesNotExistReturnsFalse() + { + lru.TryGet(1, out int v).Should().BeFalse(); + } + + [Fact] + public void AddOrUpdateUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(2); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedExtensionsTests.cs new file mode 100644 index 00000000..dc5b77c4 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedExtensionsTests.cs @@ -0,0 +1,82 @@ +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.Lazy +{ + public class ScopedExtensionsTests + { + private ConcurrentLru> lru = new(4); + + [Fact] + public void GetOrAddRawRetrunsValidLifetime() + { + using (var l = lru.ScopedGetOrAdd(1, x => new Scoped(new Disposable()))) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void GetOrAddWrappedRetrunsValidLifetime() + { + using (var l = lru.ScopedGetOrAdd(1, x => new Disposable())) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void GetOrAddWrappedProtectedRetrunsValidLifetime() + { + using (var l = lru.ScopedGetOrAddProtected(1, x => new Scoped(new Disposable()))) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void GetOrAddWrappedProtectedRejectsDisposedObject() + { + var sd = new Scoped(new Disposable()); + sd.Dispose(); + + lru.Invoking(l => l.ScopedGetOrAddProtected(1, x => sd)).Should().Throw(); + } + + [Fact] + public async Task ScopedGetOrAddAsyncRetrunsValidLifetime() + { + using (var l = await lru.ScopedGetOrAddAsync(1, x => Task.FromResult(new Scoped(new Disposable())))) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + private class DisposableValueFactory + { + public Disposable Disposable { get; } = new Disposable(); + + public Disposable Create(int key) + { + return this.Disposable; + } + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs index 78b29403..acd2f2b3 100644 --- a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs @@ -37,14 +37,13 @@ public static Lifetime ScopedGetOrAdd(this ICache> cache, } } - public static Lifetime ScopedGetOrAddProtected(this ICache> cache, K key, Func valueFactory) + public static Lifetime ScopedGetOrAddProtected(this ICache> cache, K key, Func> valueFactory) where T : IDisposable { int c = 0; while (true) { - // Note: allocates a closure on every call - var scope = cache.GetOrAdd(key, k => new Scoped(valueFactory(k))); + var scope = cache.GetOrAdd(key, k => valueFactory(k)); if (scope.TryCreateLifetime(out var lifetime)) { From f45a10a5432e3f6faa8bfda3d11b38e463adac2e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 16 Dec 2021 13:06:15 -0800 Subject: [PATCH 31/31] tests --- .../Lazy/AsyncAtomicExtensionsTests.cs | 59 ++++++++ .../Lazy/AtomicExtensionsTests.cs | 2 +- .../Lazy/ScopedAsyncAtomicTests.cs | 128 ++++++++++++++++++ BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs | 2 + BitFaster.Caching/Lazy/Atomic.cs | 6 + BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs | 29 ++-- .../Lazy/ScopedAsyncAtomicExtensions.cs | 36 ++++- 7 files changed, 245 insertions(+), 17 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Lazy/AsyncAtomicExtensionsTests.cs create mode 100644 BitFaster.Caching.UnitTests/Lazy/ScopedAsyncAtomicTests.cs diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicExtensionsTests.cs new file mode 100644 index 00000000..c971819b --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicExtensionsTests.cs @@ -0,0 +1,59 @@ +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.Lazy +{ + public class AsyncAtomicExtensionsTests + { + private ConcurrentLru> lru = new(2, 9, EqualityComparer.Default); + + [Fact] + public async Task GetOrAddAsync() + { + var ar = await lru.GetOrAddAsync(1, i => Task.FromResult(i)); + + ar.Should().Be(1); + + lru.TryGet(1, out int v); + lru.AddOrUpdate(1, 2); + } + + [Fact] + public void TryUpdateWhenKeyDoesNotExistReturnsFalse() + { + lru.TryUpdate(2, 3).Should().BeFalse(); + } + + [Fact] + public void TryUpdateWhenKeyExistsUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryUpdate(1, 42).Should().BeTrue(); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(42); + } + + [Fact] + public void TryGetWhenKeyDoesNotExistReturnsFalse() + { + lru.TryGet(1, out int v).Should().BeFalse(); + } + + [Fact] + public void AddOrUpdateUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(2); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs index fd07afa0..8cb4890d 100644 --- a/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs +++ b/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs @@ -16,7 +16,7 @@ public class AtomicExtensionsTests [Fact] public void GetOrAdd() { - var rr = lru.GetOrAdd(1, i => i); + var rr = lru.GetOrAdd(1, i => i).Should().Be(1); } [Fact] diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedAsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedAsyncAtomicTests.cs new file mode 100644 index 00000000..aef0ac65 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedAsyncAtomicTests.cs @@ -0,0 +1,128 @@ +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.Lazy +{ + public class ScopedAsyncAtomicTests + { + private readonly ConcurrentLru> lru = new(4); + + [Fact] + public async Task API() + { + var scopedAsyncAtomicLru = new ConcurrentLru>(4); + Func> valueFactory = k => Task.FromResult(new SomeDisposable()); + + using (var lifetime = await scopedAsyncAtomicLru.GetOrAddAsync(1, valueFactory)) + { + var y = lifetime.Value; + } + + scopedAsyncAtomicLru.TryUpdate(2, new SomeDisposable()); + + scopedAsyncAtomicLru.AddOrUpdate(1, new SomeDisposable()); + + // TODO: how to clean this up to 1 line? + if (scopedAsyncAtomicLru.TryGetLifetime(1, out var lifetime2)) + { + using (lifetime2) + { + var x = lifetime2.Value; + } + } + } + + [Fact] + public void WhenScopeIsCreatedThenScopeDisposedLifetimeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + + scope.Dispose(); + scope.Dispose(); // validate double dispose is still single ref count + disposable.IsDisposed.Should().BeFalse(); + + lifetime.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public async Task WhenScopeIsCreatedAsyncThenScopeDisposedLifetimeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + + var r = await scope.TryCreateLifetimeAsync(1, k => Task.FromResult(disposable)); + + r.succeeded.Should().BeTrue(); + + scope.Dispose(); + scope.Dispose(); // validate double dispose is still single ref count + disposable.IsDisposed.Should().BeFalse(); + + r.lifetime.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenScopeIsDisposedCreateScopeAsyncThrows() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + scope.Dispose(); + + scope.Invoking(async s => await s.CreateLifetimeAsync(1, k => Task.FromResult(new Disposable()))).Should().Throw(); + } + + [Fact] + public void WhenScopeIsDisposedTryCreateScopeReturnsFalse() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + scope.Dispose(); + + scope.TryCreateLifetime(out var lifetime).Should().BeFalse(); + } + + [Fact] + public async Task WhenScopeIsDisposedTryCreateScopeAsyncReturnsFalse() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + scope.Dispose(); + + var r = await scope.TryCreateLifetimeAsync(1, k => Task.FromResult(new Disposable())); + r.succeeded.Should().Be(false); + } + + // TODO: this doesn't work without guard on TryCreate. + // where should value be initialized? + // how does scoped atomic handled value factory throw? + [Fact] + public void WhenValueIsNotCreatedTryCreateScopeReturnsFalse() + { + var scope = new ScopedAsyncAtomic(); + + scope.TryCreateLifetime(out var l).Should().BeFalse(); + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs index 5356c477..8204830c 100644 --- a/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs +++ b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs @@ -29,6 +29,8 @@ public Task GetValueAsync(K key, Func> valueFactory) return this.refCount.Value.GetValueAsync(key, valueFactory); } + public bool IsValueCreated => this.refCount.Value.IsValueCreated; + /// /// Gets the value. /// diff --git a/BitFaster.Caching/Lazy/Atomic.cs b/BitFaster.Caching/Lazy/Atomic.cs index b49dee8a..d382b3bd 100644 --- a/BitFaster.Caching/Lazy/Atomic.cs +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -9,6 +9,12 @@ namespace BitFaster.Caching { + // SyncedAsync + // SyncedScoped + // SyncedAsyncScoped + //public class Synced + //{ } + // https://github.com/dotnet/runtime/issues/27421 // https://github.com/alastairtree/LazyCache/issues/73 [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs index d627b210..44c7430d 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs @@ -13,25 +13,23 @@ public class ScopedAsyncAtomic : IDisposable private ReferenceCount> refCount; private bool isDisposed; - private readonly AsyncAtomic asyncAtomic; - public ScopedAsyncAtomic() { - this.asyncAtomic = new AsyncAtomic(); + this.refCount = new ReferenceCount>(new AsyncAtomic()); } public ScopedAsyncAtomic(V value) { - this.asyncAtomic = new AsyncAtomic(value); + this.refCount = new ReferenceCount>(new AsyncAtomic(value)); } public bool TryCreateLifetime(out AsyncAtomicLifetime lifetime) { - if (!this.refCount.Value.IsValueCreated) - { - lifetime = default; - return false; - } + //if (!this.refCount.Value.IsValueCreated) + //{ + // lifetime = default; + // return false; + //} // TODO: exact dupe while (true) @@ -55,11 +53,12 @@ public bool TryCreateLifetime(out AsyncAtomicLifetime lifetime) } public async Task<(bool succeeded, AsyncAtomicLifetime lifetime)> TryCreateLifetimeAsync(K key, Func> valueFactory) - { - // initialize - factory can throw so do this before we start counting refs - await this.asyncAtomic.GetValueAsync(key, valueFactory).ConfigureAwait(false); + { + if (!this.refCount.Value.IsValueCreated) + { + return (false, default); + } - // TODO: exact dupe while (true) { var oldRefCount = this.refCount; @@ -72,6 +71,10 @@ public bool TryCreateLifetime(out AsyncAtomicLifetime lifetime) if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) { + // initialize - + // TOOD: factory can throw so do this before we start counting refs + await oldRefCount.Value.GetValueAsync(key, valueFactory).ConfigureAwait(false); + // When Lifetime is disposed, it calls DecrementReferenceCount return (true, new AsyncAtomicLifetime(oldRefCount, this.DecrementReferenceCount)); } diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs index 11addf1b..5111c386 100644 --- a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs @@ -9,16 +9,46 @@ namespace BitFaster.Caching public static class ScopedAsyncAtomicExtensions { // If a disposed ScopedAsyncAtomic is added to the cache, this method will get stuck in an infinite loop + //public static async Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable + //{ + // while (true) + // { + // var scope = cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()); + // var result = await scope.TryCreateLifetimeAsync(key, valueFactory).ConfigureAwait(false); + + // if (result.succeeded) + // { + // return result.lifetime; + // } + // } + //} + public static async Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable { while (true) { var scope = cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()); - var result = await scope.TryCreateLifetimeAsync(key, valueFactory).ConfigureAwait(false); - if (result.succeeded) + if (scope.TryCreateLifetime(out var lifetime)) { - return result.lifetime; + // fast path + if (lifetime.IsValueCreated) + { + return lifetime; + } + + // create value, must handle factory method throwing + // TODO: should lifetime have an initilize method? return value never used + try + { + await lifetime.GetValueAsync(key, valueFactory).ConfigureAwait(false); + return lifetime; + } + catch + { + lifetime.Dispose(); + throw; + } } } }