From 032a8327adc4615382e4457c92bf6ce4aaa87ca7 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 30 Jun 2022 17:23:29 -0700 Subject: [PATCH 01/27] bump --- BitFaster.Caching/BitFaster.Caching.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BitFaster.Caching/BitFaster.Caching.csproj b/BitFaster.Caching/BitFaster.Caching.csproj index 8c928cd8..a67d292a 100644 --- a/BitFaster.Caching/BitFaster.Caching.csproj +++ b/BitFaster.Caching/BitFaster.Caching.csproj @@ -8,7 +8,7 @@ High performance, thread-safe in-memory caching primitives for .NET. LICENSE true - 1.0.7 + 1.1.0 Copyright © Alex Peck $([System.DateTime]::Now.ToString(yyyy)) https://github.com/bitfaster/BitFaster.Caching @@ -20,8 +20,8 @@ True true snupkg - 1.0.7.0 - 1.0.7.0 + 1.1.0.0 + 1.1.0.0 From b587d363524dd10254543c7308137a2eab85ce08 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 8 Jul 2022 19:34:32 -0700 Subject: [PATCH 02/27] buidler --- BitFaster.Caching/AsyncAtomic.cs | 173 ++++++++++++++++++++++ BitFaster.Caching/AtomicCacheDecorator.cs | 74 +++++++++ BitFaster.Caching/ICache.cs | 7 + BitFaster.Caching/LruBuilder.cs | 116 +++++++++++++++ 4 files changed, 370 insertions(+) create mode 100644 BitFaster.Caching/AsyncAtomic.cs create mode 100644 BitFaster.Caching/AtomicCacheDecorator.cs create mode 100644 BitFaster.Caching/LruBuilder.cs diff --git a/BitFaster.Caching/AsyncAtomic.cs b/BitFaster.Caching/AsyncAtomic.cs new file mode 100644 index 00000000..7ca4e40e --- /dev/null +++ b/BitFaster.Caching/AsyncAtomic.cs @@ -0,0 +1,173 @@ +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 +{ + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + public class AsyncAtomic + { + private Initializer initializer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private V value; + + public AsyncAtomic() + { + this.initializer = new Initializer(); + } + + public AsyncAtomic(V value) + { + this.value = value; + } + + public V GetValue(K key, Func valueFactory) + { + if (this.initializer == null) + { + return this.value; + } + + return CreateValue(key, valueFactory); + } + + 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 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; + } + + private async Task CreateValueAsync(K key, Func> valueFactory) + { + Initializer init = this.initializer; + + if (init != null) + { + this.value = await init.CreateValueAsync(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 V 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 = valueFactory(key); + tcs.SetResult(value); + return value; + } + catch (Exception ex) + { + Volatile.Write(ref isInitialized, false); + tcs.SetException(ex); + throw; + } + } + + // TODO: how dangerous is this? + // it can block forever if value factory blocks + return synchronizedTask.GetAwaiter().GetResult(); + } + + public async Task CreateValueAsync(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); + } + } + } + + internal static class Synchronized + { + 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/AtomicCacheDecorator.cs b/BitFaster.Caching/AtomicCacheDecorator.cs new file mode 100644 index 00000000..1a9ba24e --- /dev/null +++ b/BitFaster.Caching/AtomicCacheDecorator.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public class AtomicCacheDecorator : ICache + { + private readonly ICache> cache; + + public AtomicCacheDecorator(ICache> cache) + { + this.cache = cache; + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public void AddOrUpdate(K key, V value) + { + cache.AddOrUpdate(key, new AsyncAtomic(value)); + } + + public void Clear() + { + this.cache.Clear(); + } + + public V GetOrAdd(K key, Func valueFactory) + { + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic()); + return synchronized.GetValue(key, valueFactory); + } + + public Task GetOrAddAsync(K key, Func> valueFactory) + { + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic()); + return synchronized.GetValueAsync(key, valueFactory); + } + + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + public bool TryGet(K key, out V value) + { + AsyncAtomic output; + bool ret = cache.TryGet(key, out output); + + if (ret && output.IsValueCreated) + { + value = output.ValueIfCreated; + return true; + } + + value = default; + return false; + } + + public bool TryRemove(K key) + { + return this.cache.TryRemove(key); + } + + public bool TryUpdate(K key, V value) + { + return cache.TryUpdate(key, new AsyncAtomic(value)); ; + } + } +} diff --git a/BitFaster.Caching/ICache.cs b/BitFaster.Caching/ICache.cs index 0ca8e792..69cefb22 100644 --- a/BitFaster.Caching/ICache.cs +++ b/BitFaster.Caching/ICache.cs @@ -6,6 +6,13 @@ namespace BitFaster.Caching { + public interface ICacheTtl : ICache + { + void TrimExpired(); + + TimeSpan Ttl { get; } + } + /// /// Represents a generic cache of key/value pairs. /// diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs new file mode 100644 index 00000000..c0f510ce --- /dev/null +++ b/BitFaster.Caching/LruBuilder.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching +{ + public class LruBuilder + { + protected readonly Spec spec; + + public LruBuilder() + { + this.spec = new Spec(); + } + + protected LruBuilder(Spec spec) + { + this.spec = spec; + } + + public LruBuilder WithCapacity(int capacity) + { + this.spec.capacity = capacity; + return this; + } + + public LruBuilder WithConcurrencyLevel(int concurrencyLevel) + { + this.spec.concurrencyLevel = concurrencyLevel; + return this; + } + + public LruBuilder WithExpiration(TimeSpan expiration) + { + this.spec.expiration = expiration; + return this; + } + + public LruBuilder WithInstrumentation() + { + this.spec.withInstrumentation = true; + return this; + } + + public AtomicLruBuilder WithAtomicCreate() + { + return new AtomicLruBuilder(this.spec); + } + + public virtual ICache Build() + { + if (this.spec.expiration.HasValue) + { + return spec.withInstrumentation ? + new ConcurrentTLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) + : new FastConcurrentTLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) as ICache; + } + + return spec.withInstrumentation ? + new ConcurrentLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) + : new FastConcurrentLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) as ICache; + } + + public class Spec + { + public int capacity = 128; + public int concurrencyLevel = Defaults.ConcurrencyLevel; + public TimeSpan? expiration = null; + public bool withInstrumentation = false; + public IEqualityComparer comparer = EqualityComparer.Default; + } + } + + public class AtomicLruBuilder : LruBuilder + { + public AtomicLruBuilder(Spec spec) + : base(spec) + { + } + + public override ICache Build() + { + ICache> ret = null; + + if (this.spec.expiration.HasValue) + { + ret = spec.withInstrumentation ? + new ConcurrentTLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) + : new FastConcurrentTLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) as ICache>; + } + else + { + ret = spec.withInstrumentation ? + new ConcurrentLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) + : new FastConcurrentLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) as ICache>; + } + + return new AtomicCacheDecorator(ret); + } + } + + public class Test + { + public void T() + { + var cache = new LruBuilder() + .WithAtomicCreate() + .WithInstrumentation() + .Build(); + + } + } +} From 844b3ee16517f9169cfa3a2b073973fe957abf4f Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 9 Jul 2022 17:37:47 -0700 Subject: [PATCH 03/27] scoped --- BitFaster.Caching/LruBuilder.cs | 27 ++++- BitFaster.Caching/ScopedCacheDecorator.cs | 114 ++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 BitFaster.Caching/ScopedCacheDecorator.cs diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs index c0f510ce..a9a0b618 100644 --- a/BitFaster.Caching/LruBuilder.cs +++ b/BitFaster.Caching/LruBuilder.cs @@ -50,6 +50,7 @@ public AtomicLruBuilder WithAtomicCreate() return new AtomicLruBuilder(this.spec); } + // pretty crappy implementation... public virtual ICache Build() { if (this.spec.expiration.HasValue) @@ -110,7 +111,31 @@ public void T() .WithAtomicCreate() .WithInstrumentation() .Build(); - + } + + public void ScopedPOC() + { + // layer 1: can choose ConcurrentLru/TLru, fast etc. + var c = new ConcurrentLru>>(3); + + // layer 2: optional atomic creation + var atomic = new AtomicCacheDecorator>(c); + + // layer 3: optional scoping + IScopedCache scoped = new ScopedCacheDecorator(atomic); + + using (var lifetime = scoped.GetOrAdd(1, k => new Disposable())) + { + var d = lifetime.Value; + } + } + + public class Disposable : IDisposable + { + public void Dispose() + { + throw new NotImplementedException(); + } } } } diff --git a/BitFaster.Caching/ScopedCacheDecorator.cs b/BitFaster.Caching/ScopedCacheDecorator.cs new file mode 100644 index 00000000..dd436621 --- /dev/null +++ b/BitFaster.Caching/ScopedCacheDecorator.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public interface IScopedCache where V : IDisposable + { + int Count { get; } + + bool TryGet(K key, out Lifetime value); + + Lifetime GetOrAdd(K key, Func valueFactory); + + Task> GetOrAddAsync(K key, Func> valueFactory); + + bool TryRemove(K key); + + bool TryUpdate(K key, V value); + + void AddOrUpdate(K key, V value); + + void Clear(); + + void Trim(int itemCount); + } + + // completely encapsulates all scope objects + public class ScopedCacheDecorator : IScopedCache where V : IDisposable + { + private readonly ICache> cache; + + public ScopedCacheDecorator(ICache> cache) + { + this.cache = cache; + } + + public int Count => cache.Count; + + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new Scoped(value)); + } + + public void Clear() + { + this.cache.Clear(); + } + + public Lifetime GetOrAdd(K key, Func valueFactory) + { + while (true) + { + // Note: allocates a closure on every call + // alternative is Func>> valueFactory input arg, but this lets the caller see the scoped object + var scope = cache.GetOrAdd(key, k => new Scoped(valueFactory(k))); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + + public async Task> GetOrAddAsync(K key, Func> valueFactory) + { + while (true) + { + // Note: allocates a closure on every call + var scope = await cache.GetOrAddAsync(key, async k => + { + var v = await valueFactory(k); + return new Scoped(v); + }).ConfigureAwait(false); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + public bool TryGet(K key, out Lifetime value) + { + if (this.cache.TryGet(key, out var scope)) + { + if (scope.TryCreateLifetime(out value)) + { + return true; + } + } + + value = default; + return false; + } + + public bool TryRemove(K key) + { + return this.cache.TryRemove(key); + } + + public bool TryUpdate(K key, V value) + { + return this.cache.TryUpdate(key, new Scoped(value)); + } + } +} From 29f1abff34241604766c5b48cd96dcfb9e8dbec3 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 9 Jul 2022 20:23:00 -0700 Subject: [PATCH 04/27] task sync --- BitFaster.Caching/AsyncAtomic.cs | 64 ++++++++++++++++++++++++++++++-- BitFaster.Caching/LruBuilder.cs | 2 + 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/BitFaster.Caching/AsyncAtomic.cs b/BitFaster.Caching/AsyncAtomic.cs index 7ca4e40e..68fd5ff4 100644 --- a/BitFaster.Caching/AsyncAtomic.cs +++ b/BitFaster.Caching/AsyncAtomic.cs @@ -115,9 +115,9 @@ public V CreateValue(K key, Func valueFactory) } } - // TODO: how dangerous is this? - // it can block forever if value factory blocks - return synchronizedTask.GetAwaiter().GetResult(); + // this isn't needed for .NET Core + // https://stackoverflow.com/questions/53265020/c-sharp-async-await-deadlock-problem-gone-in-netcore + return TaskSynchronization.GetResult(synchronizedTask); } public async Task CreateValueAsync(K key, Func> valueFactory) @@ -170,4 +170,62 @@ public static V Initialize(ref V target, ref bool initialized, ref object syn return target; } } + + public static class TaskSynchronization + { + private static ISynchronizationPolicy SynchronizationPolicy = new GetAwaiterPolicy(); + + public static T GetResult(Task task) + { + return SynchronizationPolicy.GetResult(task); + } + + public static void GetResult(Task task) + { + SynchronizationPolicy.GetResult(task); + } + + public static void UseTaskRun() + { + SynchronizationPolicy = new TaskRunPolicy(); + } + + public static void UseAwaiter() + { + SynchronizationPolicy = new GetAwaiterPolicy(); + } + } + + internal interface ISynchronizationPolicy + { + T GetResult(Task task); + + void GetResult(Task task); + } + + internal class GetAwaiterPolicy : ISynchronizationPolicy + { + public T GetResult(Task task) + { + return task.GetAwaiter().GetResult(); + } + + public void GetResult(Task task) + { + task.GetAwaiter().GetResult(); + } + } + + internal class TaskRunPolicy : ISynchronizationPolicy + { + public T GetResult(Task task) + { + return Task.Run(async () => await task).Result; + } + + public void GetResult(Task task) + { + Task.Run(async () => await task).Wait(); + } + } } diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs index a9a0b618..95a71012 100644 --- a/BitFaster.Caching/LruBuilder.cs +++ b/BitFaster.Caching/LruBuilder.cs @@ -115,6 +115,8 @@ public void T() public void ScopedPOC() { + // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped + // layer 1: can choose ConcurrentLru/TLru, fast etc. var c = new ConcurrentLru>>(3); From cbc12b46dd277529ed2491aa5bb2c6f8aad1f3f0 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sun, 10 Jul 2022 00:14:46 -0700 Subject: [PATCH 05/27] notes --- BitFaster.Caching/Atomic.cs | 155 ++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 BitFaster.Caching/Atomic.cs diff --git a/BitFaster.Caching/Atomic.cs b/BitFaster.Caching/Atomic.cs new file mode 100644 index 00000000..bf6b2d2f --- /dev/null +++ b/BitFaster.Caching/Atomic.cs @@ -0,0 +1,155 @@ +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 +{ + // make a version of Atomic that is baised towards sync usage. + // Caller can then choose between async or async optimized version that still works with both. + // SHould benchmark whether the AsyncAtomic version is meaninfully worse in terms of latency/allocs + // Looks like it would be very similar except the additional TaskCompletionSource alloc + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + public class Atomic + { + private Initializer initializer; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private V value; + + public Atomic() + { + this.initializer = new Initializer(); + } + + public Atomic(V value) + { + this.value = value; + } + + public V GetValue(K key, Func valueFactory) + { + if (this.initializer == null) + { + return this.value; + } + + return CreateValue(key, valueFactory); + } + + 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 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; + } + + private async Task CreateValueAsync(K key, Func> valueFactory) + { + Initializer init = this.initializer; + + if (init != null) + { + this.value = await init.CreateValueAsync(key, valueFactory).ConfigureAwait(false); + this.initializer = null; + } + + return this.value; + } + + private class Initializer + { + private object syncLock = new object(); + private bool isInitialized; + private V value; + + public V CreateValue(K key, Func valueFactory) + { + if (!Volatile.Read(ref isInitialized)) + { + return value; + } + + lock (syncLock) + { + if (!Volatile.Read(ref isInitialized)) + { + return value; + } + + value = valueFactory(key); + Volatile.Write(ref isInitialized, true); + return value; + } + } + + // This is terrifyingly bad on many levels. + public async Task CreateValueAsync(K key, Func> valueFactory) + { + if (!Volatile.Read(ref isInitialized)) + { + return value; + } + + // start another thread that holds the lock until a signal is sent. + ManualResetEvent manualResetEvent = new ManualResetEvent(false); + + var lockTask = Task.Run(() => { + lock (syncLock) + { + if (!Volatile.Read(ref isInitialized)) + { + // EXIT somehow and return value + } + + manualResetEvent.WaitOne(); + } + }); + + // Problems: + // 1. what if value factory throws? We need to release the lock + // 2. how to do double checked lock in the other thread + value = await valueFactory(key); + Volatile.Write(ref isInitialized, true); + manualResetEvent.Set(); + await lockTask; + + return value; + } + } + } +} From f374cb97361c2a9308e540fc308f884217b220e4 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 18 Jul 2022 19:55:01 -0700 Subject: [PATCH 06/27] cleanup --- BitFaster.Caching/LruBuilder.cs | 4 +- BitFaster.Caching/ScopedCacheDecorator.cs | 114 ---------------------- 2 files changed, 2 insertions(+), 116 deletions(-) delete mode 100644 BitFaster.Caching/ScopedCacheDecorator.cs diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs index 95a71012..ce3d8522 100644 --- a/BitFaster.Caching/LruBuilder.cs +++ b/BitFaster.Caching/LruBuilder.cs @@ -124,9 +124,9 @@ public void ScopedPOC() var atomic = new AtomicCacheDecorator>(c); // layer 3: optional scoping - IScopedCache scoped = new ScopedCacheDecorator(atomic); + IScopedCache scoped = new ScopedCache(atomic); - using (var lifetime = scoped.GetOrAdd(1, k => new Disposable())) + using (var lifetime = scoped.ScopedGetOrAdd(1, k => new Scoped(new Disposable()))) { var d = lifetime.Value; } diff --git a/BitFaster.Caching/ScopedCacheDecorator.cs b/BitFaster.Caching/ScopedCacheDecorator.cs deleted file mode 100644 index dd436621..00000000 --- a/BitFaster.Caching/ScopedCacheDecorator.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace BitFaster.Caching -{ - public interface IScopedCache where V : IDisposable - { - int Count { get; } - - bool TryGet(K key, out Lifetime value); - - Lifetime GetOrAdd(K key, Func valueFactory); - - Task> GetOrAddAsync(K key, Func> valueFactory); - - bool TryRemove(K key); - - bool TryUpdate(K key, V value); - - void AddOrUpdate(K key, V value); - - void Clear(); - - void Trim(int itemCount); - } - - // completely encapsulates all scope objects - public class ScopedCacheDecorator : IScopedCache where V : IDisposable - { - private readonly ICache> cache; - - public ScopedCacheDecorator(ICache> cache) - { - this.cache = cache; - } - - public int Count => cache.Count; - - public void AddOrUpdate(K key, V value) - { - this.cache.AddOrUpdate(key, new Scoped(value)); - } - - public void Clear() - { - this.cache.Clear(); - } - - public Lifetime GetOrAdd(K key, Func valueFactory) - { - while (true) - { - // Note: allocates a closure on every call - // alternative is Func>> valueFactory input arg, but this lets the caller see the scoped object - var scope = cache.GetOrAdd(key, k => new Scoped(valueFactory(k))); - - if (scope.TryCreateLifetime(out var lifetime)) - { - return lifetime; - } - } - } - - public async Task> GetOrAddAsync(K key, Func> valueFactory) - { - while (true) - { - // Note: allocates a closure on every call - var scope = await cache.GetOrAddAsync(key, async k => - { - var v = await valueFactory(k); - return new Scoped(v); - }).ConfigureAwait(false); - - if (scope.TryCreateLifetime(out var lifetime)) - { - return lifetime; - } - } - } - - public void Trim(int itemCount) - { - this.cache.Trim(itemCount); - } - - public bool TryGet(K key, out Lifetime value) - { - if (this.cache.TryGet(key, out var scope)) - { - if (scope.TryCreateLifetime(out value)) - { - return true; - } - } - - value = default; - return false; - } - - public bool TryRemove(K key) - { - return this.cache.TryRemove(key); - } - - public bool TryUpdate(K key, V value) - { - return this.cache.TryUpdate(key, new Scoped(value)); - } - } -} From adb9d1c47800ce3aa708b31e32ec869971ea079b Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Mon, 18 Jul 2022 19:59:42 -0700 Subject: [PATCH 07/27] missing bits --- BitFaster.Caching/AtomicCacheDecorator.cs | 5 +++++ BitFaster.Caching/LruBuilder.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/BitFaster.Caching/AtomicCacheDecorator.cs b/BitFaster.Caching/AtomicCacheDecorator.cs index 1a9ba24e..618becf8 100644 --- a/BitFaster.Caching/AtomicCacheDecorator.cs +++ b/BitFaster.Caching/AtomicCacheDecorator.cs @@ -19,6 +19,11 @@ public AtomicCacheDecorator(ICache> cache) public int Count => this.cache.Count; + public ICacheMetrics Metrics => this.cache.Metrics; + + // need to dispatch different events for this + public ICacheEvents Events => throw new Exception(); + public void AddOrUpdate(K key, V value) { cache.AddOrUpdate(key, new AsyncAtomic(value)); diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs index ce3d8522..d505d73f 100644 --- a/BitFaster.Caching/LruBuilder.cs +++ b/BitFaster.Caching/LruBuilder.cs @@ -117,7 +117,7 @@ public void ScopedPOC() { // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped - // layer 1: can choose ConcurrentLru/TLru, fast etc. + // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru var c = new ConcurrentLru>>(3); // layer 2: optional atomic creation From fcfda616eb2861747760ff7a0d6a46226bf00d4c Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 14:04:06 -0700 Subject: [PATCH 08/27] recurse --- BitFaster.Caching/LruBuilder.cs | 128 +++++++++++++++----------------- 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs index d505d73f..86de3ee6 100644 --- a/BitFaster.Caching/LruBuilder.cs +++ b/BitFaster.Caching/LruBuilder.cs @@ -7,112 +7,97 @@ namespace BitFaster.Caching { - public class LruBuilder + // recursive generic base class + public abstract class LruBuilderBase where TBuilder : LruBuilderBase { - protected readonly Spec spec; + internal readonly LruInfo info; - public LruBuilder() - { - this.spec = new Spec(); - } - - protected LruBuilder(Spec spec) + public LruBuilderBase(LruInfo info) { - this.spec = spec; - } + this.info = info; + } - public LruBuilder WithCapacity(int capacity) + public TBuilder WithCapacity(int capacity) { - this.spec.capacity = capacity; - return this; + this.info.capacity = capacity; + return this as TBuilder; } - public LruBuilder WithConcurrencyLevel(int concurrencyLevel) + public TBuilder WithConcurrencyLevel(int concurrencyLevel) { - this.spec.concurrencyLevel = concurrencyLevel; - return this; + this.info.concurrencyLevel = concurrencyLevel; + return this as TBuilder; } - public LruBuilder WithExpiration(TimeSpan expiration) + public TBuilder WithKeyComparer(IEqualityComparer comparer) { - this.spec.expiration = expiration; - return this; + this.info.comparer = comparer; + return this as TBuilder; } - public LruBuilder WithInstrumentation() + public TBuilder WithMetrics() { - this.spec.withInstrumentation = true; - return this; + this.info.withMetrics = true; + return this as TBuilder; } - public AtomicLruBuilder WithAtomicCreate() + public TBuilder WithAbosluteExpiry(TimeSpan expiration) { - return new AtomicLruBuilder(this.spec); + this.info.expiration = expiration; + return this as TBuilder; } - // pretty crappy implementation... public virtual ICache Build() { - if (this.spec.expiration.HasValue) + if (this.info.expiration.HasValue) { - return spec.withInstrumentation ? - new ConcurrentTLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) - : new FastConcurrentTLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) as ICache; + return info.withMetrics ? + new ConcurrentTLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer, this.info.expiration.Value) + : new FastConcurrentTLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer, this.info.expiration.Value) as ICache; } - return spec.withInstrumentation ? - new ConcurrentLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) - : new FastConcurrentLru(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) as ICache; + return info.withMetrics ? + new ConcurrentLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer) + : new FastConcurrentLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer) as ICache; + } + } + public class ConcurrentLruBuilder : LruBuilderBase> + { + public ConcurrentLruBuilder() + : base(new LruInfo()) + { } - public class Spec + internal ConcurrentLruBuilder(LruInfo info) + : base(info) { - public int capacity = 128; - public int concurrencyLevel = Defaults.ConcurrencyLevel; - public TimeSpan? expiration = null; - public bool withInstrumentation = false; - public IEqualityComparer comparer = EqualityComparer.Default; } } - public class AtomicLruBuilder : LruBuilder - { - public AtomicLruBuilder(Spec spec) - : base(spec) - { + public static class ConcurrentLruBuilderExtensions + { + public static ConcurrentLruBuilder> WithScopedValues(this ConcurrentLruBuilder b) where V : IDisposable + { + return new ConcurrentLruBuilder>(b.info); } - public override ICache Build() + public static ConcurrentLruBuilder> WithAtomicCreate(this ConcurrentLruBuilder b) { - ICache> ret = null; - - if (this.spec.expiration.HasValue) - { - ret = spec.withInstrumentation ? - new ConcurrentTLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) - : new FastConcurrentTLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer, this.spec.expiration.Value) as ICache>; - } - else - { - ret = spec.withInstrumentation ? - new ConcurrentLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) - : new FastConcurrentLru>(this.spec.concurrencyLevel, this.spec.capacity, this.spec.comparer) as ICache>; - } - - return new AtomicCacheDecorator(ret); + return new ConcurrentLruBuilder>(b.info); } } - public class Test + public class LruInfo { - public void T() - { - var cache = new LruBuilder() - .WithAtomicCreate() - .WithInstrumentation() - .Build(); - } + public int capacity = 128; + public int concurrencyLevel = Defaults.ConcurrencyLevel; + public TimeSpan? expiration = null; + public bool withMetrics = false; + public IEqualityComparer comparer = EqualityComparer.Default; + } + public class Test + { public void ScopedPOC() { // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped @@ -130,6 +115,13 @@ public void ScopedPOC() { var d = lifetime.Value; } + + // This builds the correct layer 1 type, but it has not been wrapped with AtomicCacheDecorator or ScopedCache + var lru = new ConcurrentLruBuilder() + .WithScopedValues() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); } public class Disposable : IDisposable From 96d6c7751f313442908012f6b9c39dd40ac86c98 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 15:53:48 -0700 Subject: [PATCH 09/27] generic --- .../LruBuilderTests.cs | 108 ++++++++++++++++++ BitFaster.Caching/LruBuilder.cs | 97 +++++++++++++--- BitFaster.Caching/Scoped.cs | 2 +- 3 files changed, 193 insertions(+), 14 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/LruBuilderTests.cs diff --git a/BitFaster.Caching.UnitTests/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/LruBuilderTests.cs new file mode 100644 index 00000000..38e05953 --- /dev/null +++ b/BitFaster.Caching.UnitTests/LruBuilderTests.cs @@ -0,0 +1,108 @@ +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 +{ + public class LruBuilderTests + { + [Fact] + public void TestFastLru() + { + var lru = new ConcurrentLruBuilder() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestMetricsLru() + { + var lru = new ConcurrentLruBuilder() + .WithMetrics() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestFastTLru() + { + var lru = new ConcurrentLruBuilder() + .WithAbosluteExpiry(TimeSpan.FromSeconds(1)) + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestMetricsTLru() + { + var lru = new ConcurrentLruBuilder() + .WithAbosluteExpiry(TimeSpan.FromSeconds(1)) + .WithMetrics() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestScopedOnly() + { + var lru = new ConcurrentLruBuilder() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestScopedAtomic() + { + var lru = new ConcurrentLruBuilder() + .WithScopedValues() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void TestAtomic() + { + var lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void ScopedPOC() + { + // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped + + // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru + var c = new ConcurrentLru>>(3); + + // layer 2: optional atomic creation + var atomic = new AtomicCacheDecorator>(c); + + // layer 3: optional scoping + IScopedCache scoped = new ScopedCache(atomic); + + using (var lifetime = scoped.ScopedGetOrAdd(1, k => new Scoped(new Disposable()))) + { + var d = lifetime.Value; + } + } + } +} diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs index 86de3ee6..651dd8ee 100644 --- a/BitFaster.Caching/LruBuilder.cs +++ b/BitFaster.Caching/LruBuilder.cs @@ -8,7 +8,7 @@ namespace BitFaster.Caching { // recursive generic base class - public abstract class LruBuilderBase where TBuilder : LruBuilderBase + public abstract class LruBuilderBase where TBuilder : LruBuilderBase { internal readonly LruInfo info; @@ -47,7 +47,22 @@ public TBuilder WithAbosluteExpiry(TimeSpan expiration) return this as TBuilder; } - public virtual ICache Build() + public abstract TCacheReturn Build(); + } + + public class ConcurrentLruBuilder : LruBuilderBase, ICache> + { + public ConcurrentLruBuilder() + : base(new LruInfo()) + { + } + + internal ConcurrentLruBuilder(LruInfo info) + : base(info) + { + } + + public override ICache Build() { if (this.info.expiration.HasValue) { @@ -61,29 +76,85 @@ public virtual ICache Build() : new FastConcurrentLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer) as ICache; } } - public class ConcurrentLruBuilder : LruBuilderBase> + + // marker interface enables type constraints + public interface IScoped where T : IDisposable + { } + + public class ScopedLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped { - public ConcurrentLruBuilder() - : base(new LruInfo()) + private readonly ConcurrentLruBuilder inner; + + internal ScopedLruBuilder(ConcurrentLruBuilder inner) + : base(inner.info) { + this.inner = inner; } - internal ConcurrentLruBuilder(LruInfo info) - : base(info) + public override IScopedCache Build() + { + // this is a legal type conversion due to the generic constraint on W + ICache> scopedInnerCache = inner.Build() as ICache>; + + return new ScopedCache(scopedInnerCache); + } + } + + public class AtomicLruBuilder : LruBuilderBase, ICache> + { + private readonly ConcurrentLruBuilder> inner; + + internal AtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override ICache Build() + { + ICache> innerCache = inner.Build(); + + return new AtomicCacheDecorator(innerCache); + } + } + + public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped + { + private readonly ConcurrentLruBuilder> inner; + + internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) { + this.inner = inner; + } + + public override IScopedCache Build() + { + ICache>> level1 = inner.Build() as ICache>>; + var level2 = new AtomicCacheDecorator>(level1); + return new ScopedCache(level2); } } public static class ConcurrentLruBuilderExtensions { - public static ConcurrentLruBuilder> WithScopedValues(this ConcurrentLruBuilder b) where V : IDisposable + public static ScopedLruBuilder> WithScopedValues(this ConcurrentLruBuilder b) where V : IDisposable { - return new ConcurrentLruBuilder>(b.info); + var scoped = new ConcurrentLruBuilder>(b.info); + return new ScopedLruBuilder>(scoped); } - public static ConcurrentLruBuilder> WithAtomicCreate(this ConcurrentLruBuilder b) + public static AtomicLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) { - return new ConcurrentLruBuilder>(b.info); + var a = new ConcurrentLruBuilder>(b.info); + return new AtomicLruBuilder(a); + } + + public static ScopedAtomicLruBuilder> WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped + { + var atomicScoped = new ConcurrentLruBuilder>>(b.info); + + return new ScopedAtomicLruBuilder>(atomicScoped); } } @@ -117,9 +188,9 @@ public void ScopedPOC() } // This builds the correct layer 1 type, but it has not been wrapped with AtomicCacheDecorator or ScopedCache - var lru = new ConcurrentLruBuilder() + var lru = new ConcurrentLruBuilder() .WithScopedValues() - .WithAtomicCreate() + //.WithAtomicCreate() .WithCapacity(3) .Build(); } diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index dad6ee46..9e2902e4 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -11,7 +11,7 @@ namespace BitFaster.Caching /// the wrapped object from being diposed until the calling code completes. /// /// The type of scoped value. - public sealed class Scoped : IDisposable where T : IDisposable + public sealed class Scoped : IScoped, IDisposable where T : IDisposable { private ReferenceCount refCount; private bool isDisposed; From 147928d9abf3416465b79f2d8d45ef323bd1ac65 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 18:21:03 -0700 Subject: [PATCH 10/27] cleanup --- .../LruBuilderTests.cs | 16 ++ BitFaster.Caching/IScoped.cs | 12 + .../Lru/Builder/AtomicLruBuilder.cs | 26 +++ .../Lru/Builder/LruBuilderBase.cs | 51 +++++ BitFaster.Caching/Lru/Builder/LruInfo.cs | 17 ++ .../Lru/Builder/ScopedAtomicLruBuilder.cs | 26 +++ .../Lru/Builder/ScopedLruBuilder.cs | 27 +++ BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 36 +++ .../Lru/ConcurrentLruBuilderExtensions.cs | 37 ++++ BitFaster.Caching/LruBuilder.cs | 206 ------------------ BitFaster.Caching/Scoped.cs | 1 + 11 files changed, 249 insertions(+), 206 deletions(-) create mode 100644 BitFaster.Caching/IScoped.cs create mode 100644 BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs create mode 100644 BitFaster.Caching/Lru/Builder/LruBuilderBase.cs create mode 100644 BitFaster.Caching/Lru/Builder/LruInfo.cs create mode 100644 BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs create mode 100644 BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs create mode 100644 BitFaster.Caching/Lru/ConcurrentLruBuilder.cs create mode 100644 BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs delete mode 100644 BitFaster.Caching/LruBuilder.cs diff --git a/BitFaster.Caching.UnitTests/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/LruBuilderTests.cs index 38e05953..cecc3bd8 100644 --- a/BitFaster.Caching.UnitTests/LruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/LruBuilderTests.cs @@ -49,6 +49,7 @@ public void TestMetricsTLru() .Build(); lru.Should().BeOfType>(); + lru.Capacity.Should().Be(128); } [Fact] @@ -60,6 +61,7 @@ public void TestScopedOnly() .Build(); lru.Should().BeOfType>(); + lru.Capacity.Should().Be(3); } [Fact] @@ -72,6 +74,20 @@ public void TestScopedAtomic() .Build(); lru.Should().BeOfType>(); + lru.Capacity.Should().Be(3); + } + + [Fact] + public void TestScopedAtomicReverse() + { + var lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(3); } [Fact] diff --git a/BitFaster.Caching/IScoped.cs b/BitFaster.Caching/IScoped.cs new file mode 100644 index 00000000..5421803d --- /dev/null +++ b/BitFaster.Caching/IScoped.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + // marker interface enables type constraints + public interface IScoped where T : IDisposable + { } +} diff --git a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs new file mode 100644 index 00000000..ece49ed6 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public class AtomicLruBuilder : LruBuilderBase, ICache> + { + private readonly ConcurrentLruBuilder> inner; + + internal AtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override ICache Build() + { + var innerCache = inner.Build(); + + return new AtomicCacheDecorator(innerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs new file mode 100644 index 00000000..3bba032b --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + // recursive generic base class + public abstract class LruBuilderBase where TBuilder : LruBuilderBase + { + internal readonly LruInfo info; + + public LruBuilderBase(LruInfo info) + { + this.info = info; + } + + public TBuilder WithCapacity(int capacity) + { + this.info.capacity = capacity; + return this as TBuilder; + } + + public TBuilder WithConcurrencyLevel(int concurrencyLevel) + { + this.info.concurrencyLevel = concurrencyLevel; + return this as TBuilder; + } + + public TBuilder WithKeyComparer(IEqualityComparer comparer) + { + this.info.comparer = comparer; + return this as TBuilder; + } + + public TBuilder WithMetrics() + { + this.info.withMetrics = true; + return this as TBuilder; + } + + public TBuilder WithAbosluteExpiry(TimeSpan expiration) + { + this.info.expiration = expiration; + return this as TBuilder; + } + + public abstract TCacheReturn Build(); + } +} diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs new file mode 100644 index 00000000..5132b172 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public class LruInfo + { + public int capacity = 128; + public int concurrencyLevel = Defaults.ConcurrencyLevel; + public TimeSpan? expiration = null; + public bool withMetrics = false; + public IEqualityComparer comparer = EqualityComparer.Default; + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs new file mode 100644 index 00000000..c96a6602 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped + { + private readonly ConcurrentLruBuilder> inner; + + internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override IScopedCache Build() + { + var level1 = inner.Build() as ICache>>; + var level2 = new AtomicCacheDecorator>(level1); + return new ScopedCache(level2); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs new file mode 100644 index 00000000..33ccd251 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public class ScopedLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped + { + private readonly ConcurrentLruBuilder inner; + + internal ScopedLruBuilder(ConcurrentLruBuilder inner) + : base(inner.info) + { + this.inner = inner; + } + + public override IScopedCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as ICache>; + + return new ScopedCache(scopedInnerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs new file mode 100644 index 00000000..ca31dfaa --- /dev/null +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru.Builder; + +namespace BitFaster.Caching.Lru +{ + public class ConcurrentLruBuilder : LruBuilderBase, ICache> + { + public ConcurrentLruBuilder() + : base(new LruInfo()) + { + } + + internal ConcurrentLruBuilder(LruInfo info) + : base(info) + { + } + + public override ICache Build() + { + if (info.expiration.HasValue) + { + return info.withMetrics ? + new ConcurrentTLru(info.concurrencyLevel, info.capacity, info.comparer, info.expiration.Value) + : new FastConcurrentTLru(info.concurrencyLevel, info.capacity, info.comparer, info.expiration.Value) as ICache; + } + + return info.withMetrics ? + new ConcurrentLru(info.concurrencyLevel, info.capacity, info.comparer) + : new FastConcurrentLru(info.concurrencyLevel, info.capacity, info.comparer) as ICache; + } + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs new file mode 100644 index 00000000..2cc0f2be --- /dev/null +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru.Builder; + +namespace BitFaster.Caching.Lru +{ + public static class ConcurrentLruBuilderExtensions + { + public static ScopedLruBuilder> WithScopedValues(this ConcurrentLruBuilder b) where V : IDisposable + { + var scoped = new ConcurrentLruBuilder>(b.info); + return new ScopedLruBuilder>(scoped); + } + + public static AtomicLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) + { + var a = new ConcurrentLruBuilder>(b.info); + return new AtomicLruBuilder(a); + } + + public static ScopedAtomicLruBuilder> WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped + { + var atomicScoped = new ConcurrentLruBuilder>>(b.info); + + return new ScopedAtomicLruBuilder>(atomicScoped); + } + + public static ScopedAtomicLruBuilder> WithScopedValues(this AtomicLruBuilder b) where V : IDisposable + { + var atomicScoped = new ConcurrentLruBuilder>>(b.info); + return new ScopedAtomicLruBuilder>(atomicScoped); + } + } +} diff --git a/BitFaster.Caching/LruBuilder.cs b/BitFaster.Caching/LruBuilder.cs deleted file mode 100644 index 651dd8ee..00000000 --- a/BitFaster.Caching/LruBuilder.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using BitFaster.Caching.Lru; - -namespace BitFaster.Caching -{ - // recursive generic base class - public abstract class LruBuilderBase where TBuilder : LruBuilderBase - { - internal readonly LruInfo info; - - public LruBuilderBase(LruInfo info) - { - this.info = info; - } - - public TBuilder WithCapacity(int capacity) - { - this.info.capacity = capacity; - return this as TBuilder; - } - - public TBuilder WithConcurrencyLevel(int concurrencyLevel) - { - this.info.concurrencyLevel = concurrencyLevel; - return this as TBuilder; - } - - public TBuilder WithKeyComparer(IEqualityComparer comparer) - { - this.info.comparer = comparer; - return this as TBuilder; - } - - public TBuilder WithMetrics() - { - this.info.withMetrics = true; - return this as TBuilder; - } - - public TBuilder WithAbosluteExpiry(TimeSpan expiration) - { - this.info.expiration = expiration; - return this as TBuilder; - } - - public abstract TCacheReturn Build(); - } - - public class ConcurrentLruBuilder : LruBuilderBase, ICache> - { - public ConcurrentLruBuilder() - : base(new LruInfo()) - { - } - - internal ConcurrentLruBuilder(LruInfo info) - : base(info) - { - } - - public override ICache Build() - { - if (this.info.expiration.HasValue) - { - return info.withMetrics ? - new ConcurrentTLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer, this.info.expiration.Value) - : new FastConcurrentTLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer, this.info.expiration.Value) as ICache; - } - - return info.withMetrics ? - new ConcurrentLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer) - : new FastConcurrentLru(this.info.concurrencyLevel, this.info.capacity, this.info.comparer) as ICache; - } - } - - // marker interface enables type constraints - public interface IScoped where T : IDisposable - { } - - public class ScopedLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped - { - private readonly ConcurrentLruBuilder inner; - - internal ScopedLruBuilder(ConcurrentLruBuilder inner) - : base(inner.info) - { - this.inner = inner; - } - - public override IScopedCache Build() - { - // this is a legal type conversion due to the generic constraint on W - ICache> scopedInnerCache = inner.Build() as ICache>; - - return new ScopedCache(scopedInnerCache); - } - } - - public class AtomicLruBuilder : LruBuilderBase, ICache> - { - private readonly ConcurrentLruBuilder> inner; - - internal AtomicLruBuilder(ConcurrentLruBuilder> inner) - : base(inner.info) - { - this.inner = inner; - } - - public override ICache Build() - { - ICache> innerCache = inner.Build(); - - return new AtomicCacheDecorator(innerCache); - } - } - - public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped - { - private readonly ConcurrentLruBuilder> inner; - - internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) - : base(inner.info) - { - this.inner = inner; - } - - public override IScopedCache Build() - { - ICache>> level1 = inner.Build() as ICache>>; - var level2 = new AtomicCacheDecorator>(level1); - return new ScopedCache(level2); - } - } - - public static class ConcurrentLruBuilderExtensions - { - public static ScopedLruBuilder> WithScopedValues(this ConcurrentLruBuilder b) where V : IDisposable - { - var scoped = new ConcurrentLruBuilder>(b.info); - return new ScopedLruBuilder>(scoped); - } - - public static AtomicLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) - { - var a = new ConcurrentLruBuilder>(b.info); - return new AtomicLruBuilder(a); - } - - public static ScopedAtomicLruBuilder> WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped - { - var atomicScoped = new ConcurrentLruBuilder>>(b.info); - - return new ScopedAtomicLruBuilder>(atomicScoped); - } - } - - public class LruInfo - { - public int capacity = 128; - public int concurrencyLevel = Defaults.ConcurrencyLevel; - public TimeSpan? expiration = null; - public bool withMetrics = false; - public IEqualityComparer comparer = EqualityComparer.Default; - } - - public class Test - { - public void ScopedPOC() - { - // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped - - // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru - var c = new ConcurrentLru>>(3); - - // layer 2: optional atomic creation - var atomic = new AtomicCacheDecorator>(c); - - // layer 3: optional scoping - IScopedCache scoped = new ScopedCache(atomic); - - using (var lifetime = scoped.ScopedGetOrAdd(1, k => new Scoped(new Disposable()))) - { - var d = lifetime.Value; - } - - // This builds the correct layer 1 type, but it has not been wrapped with AtomicCacheDecorator or ScopedCache - var lru = new ConcurrentLruBuilder() - .WithScopedValues() - //.WithAtomicCreate() - .WithCapacity(3) - .Build(); - } - - public class Disposable : IDisposable - { - public void Dispose() - { - throw new NotImplementedException(); - } - } - } -} diff --git a/BitFaster.Caching/Scoped.cs b/BitFaster.Caching/Scoped.cs index 9e2902e4..69b41236 100644 --- a/BitFaster.Caching/Scoped.cs +++ b/BitFaster.Caching/Scoped.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.Threading; +using BitFaster.Caching.Lru; namespace BitFaster.Caching { From 7941ed8319e673a27c0ab4d3325983cf3620bf47 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 20:10:21 -0700 Subject: [PATCH 11/27] tests --- .../{ => Lru}/LruBuilderTests.cs | 24 ++++++++++++++++++- .../Lru/Builder/LruBuilderBase.cs | 12 ++++++---- BitFaster.Caching/Lru/Builder/LruInfo.cs | 14 +++++++---- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 17 ++++++------- 4 files changed, 48 insertions(+), 19 deletions(-) rename BitFaster.Caching.UnitTests/{ => Lru}/LruBuilderTests.cs (83%) diff --git a/BitFaster.Caching.UnitTests/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs similarity index 83% rename from BitFaster.Caching.UnitTests/LruBuilderTests.cs rename to BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs index cecc3bd8..09e678be 100644 --- a/BitFaster.Caching.UnitTests/LruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs @@ -7,7 +7,7 @@ using FluentAssertions; using Xunit; -namespace BitFaster.Caching.UnitTests +namespace BitFaster.Caching.UnitTests.Lru { public class LruBuilderTests { @@ -101,6 +101,28 @@ public void TestAtomic() lru.Should().BeOfType>(); } + [Fact] + public void TestComparer() + { + var fastLru = new ConcurrentLruBuilder() + .WithKeyComparer(StringComparer.OrdinalIgnoreCase) + .Build(); + + fastLru.GetOrAdd("a", k => 1); + fastLru.TryGet("A", out var value).Should().BeTrue(); + } + + [Fact] + public void TestConcurrencyLevel() + { + var b = new ConcurrentLruBuilder() + .WithConcurrencyLevel(-1); + + Action constructor = () => { var x = b.Build(); }; + + constructor.Should().Throw(); + } + [Fact] public void ScopedPOC() { diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index 3bba032b..fdac3eb9 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -16,33 +16,35 @@ public LruBuilderBase(LruInfo info) this.info = info; } + public LruInfo Info { get; } + public TBuilder WithCapacity(int capacity) { - this.info.capacity = capacity; + this.info.Capacity = capacity; return this as TBuilder; } public TBuilder WithConcurrencyLevel(int concurrencyLevel) { - this.info.concurrencyLevel = concurrencyLevel; + this.info.ConcurrencyLevel = concurrencyLevel; return this as TBuilder; } public TBuilder WithKeyComparer(IEqualityComparer comparer) { - this.info.comparer = comparer; + this.info.KeyComparer = comparer; return this as TBuilder; } public TBuilder WithMetrics() { - this.info.withMetrics = true; + this.info.WithMetrics = true; return this as TBuilder; } public TBuilder WithAbosluteExpiry(TimeSpan expiration) { - this.info.expiration = expiration; + this.info.Expiration = expiration; return this as TBuilder; } diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index 5132b172..a1a80dcf 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -8,10 +8,14 @@ namespace BitFaster.Caching.Lru.Builder { public class LruInfo { - public int capacity = 128; - public int concurrencyLevel = Defaults.ConcurrencyLevel; - public TimeSpan? expiration = null; - public bool withMetrics = false; - public IEqualityComparer comparer = EqualityComparer.Default; + public int Capacity { get; set; } = 128; + + public int ConcurrencyLevel { get; set; } = Defaults.ConcurrencyLevel; + + public TimeSpan? Expiration { get; set; } = null; + + public bool WithMetrics { get; set; } = false; + + public IEqualityComparer KeyComparer { get; set; } = EqualityComparer.Default; } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index ca31dfaa..7112914c 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -21,16 +21,17 @@ internal ConcurrentLruBuilder(LruInfo info) public override ICache Build() { - if (info.expiration.HasValue) + switch (info) { - return info.withMetrics ? - new ConcurrentTLru(info.concurrencyLevel, info.capacity, info.comparer, info.expiration.Value) - : new FastConcurrentTLru(info.concurrencyLevel, info.capacity, info.comparer, info.expiration.Value) as ICache; + case LruInfo i when i.WithMetrics && !i.Expiration.HasValue: + return new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + case LruInfo i when i.WithMetrics && i.Expiration.HasValue: + return new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.Expiration.Value); + case LruInfo i when i.Expiration.HasValue: + return new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.Expiration.Value); + default: + return new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); } - - return info.withMetrics ? - new ConcurrentLru(info.concurrencyLevel, info.capacity, info.comparer) - : new FastConcurrentLru(info.concurrencyLevel, info.capacity, info.comparer) as ICache; } } } From e9bc1d9b15a64e951212c96668f530e859843e33 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 21:27:35 -0700 Subject: [PATCH 12/27] fix test --- BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs index 47617938..22ee6acc 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs @@ -34,7 +34,7 @@ public void TestMetricsLru() public void TestFastTLru() { var lru = new ConcurrentLruBuilder() - .WithAbosluteExpiry(TimeSpan.FromSeconds(1)) + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) .Build(); lru.Should().BeOfType>(); From bf9f452ba9fd960c928ec6cbd71ce66e1a5a31eb Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 21:42:32 -0700 Subject: [PATCH 13/27] cleanup icache --- BitFaster.Caching/ICache.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/BitFaster.Caching/ICache.cs b/BitFaster.Caching/ICache.cs index 0132c21d..8200bafe 100644 --- a/BitFaster.Caching/ICache.cs +++ b/BitFaster.Caching/ICache.cs @@ -6,13 +6,6 @@ namespace BitFaster.Caching { - public interface ICacheTtl : ICache - { - void TrimExpired(); - - TimeSpan Ttl { get; } - } - /// /// Represents a generic cache of key/value pairs. /// From 9c3144e49346c18d123ba72163623d63ad492b31 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 21 Jul 2022 20:52:39 -0700 Subject: [PATCH 14/27] merge --- .../Lru/ConcurrentLruBuilderTests.cs | 2 +- BitFaster.Caching/AsyncAtomic.cs | 231 ------------------ BitFaster.Caching/Atomic.cs | 155 ------------ .../IdempotentAsyncCache.cs} | 31 ++- 4 files changed, 16 insertions(+), 403 deletions(-) delete mode 100644 BitFaster.Caching/AsyncAtomic.cs delete mode 100644 BitFaster.Caching/Atomic.cs rename BitFaster.Caching/{AtomicCacheDecorator.cs => Synchronized/IdempotentAsyncCache.cs} (60%) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 57151ab9..fa8e0942 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -98,7 +98,7 @@ public void TestAtomic() .WithCapacity(3) .Build(); - lru.Should().BeOfType>(); + lru.Should().BeOfType>(); } [Fact] diff --git a/BitFaster.Caching/AsyncAtomic.cs b/BitFaster.Caching/AsyncAtomic.cs deleted file mode 100644 index 68fd5ff4..00000000 --- a/BitFaster.Caching/AsyncAtomic.cs +++ /dev/null @@ -1,231 +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 -{ - [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public class AsyncAtomic - { - private Initializer initializer; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private V value; - - public AsyncAtomic() - { - this.initializer = new Initializer(); - } - - public AsyncAtomic(V value) - { - this.value = value; - } - - public V GetValue(K key, Func valueFactory) - { - if (this.initializer == null) - { - return this.value; - } - - return CreateValue(key, valueFactory); - } - - 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 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; - } - - private async Task CreateValueAsync(K key, Func> valueFactory) - { - Initializer init = this.initializer; - - if (init != null) - { - this.value = await init.CreateValueAsync(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 V 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 = valueFactory(key); - tcs.SetResult(value); - return value; - } - catch (Exception ex) - { - Volatile.Write(ref isInitialized, false); - tcs.SetException(ex); - throw; - } - } - - // this isn't needed for .NET Core - // https://stackoverflow.com/questions/53265020/c-sharp-async-await-deadlock-problem-gone-in-netcore - return TaskSynchronization.GetResult(synchronizedTask); - } - - public async Task CreateValueAsync(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); - } - } - } - - internal static class Synchronized - { - 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; - } - } - - public static class TaskSynchronization - { - private static ISynchronizationPolicy SynchronizationPolicy = new GetAwaiterPolicy(); - - public static T GetResult(Task task) - { - return SynchronizationPolicy.GetResult(task); - } - - public static void GetResult(Task task) - { - SynchronizationPolicy.GetResult(task); - } - - public static void UseTaskRun() - { - SynchronizationPolicy = new TaskRunPolicy(); - } - - public static void UseAwaiter() - { - SynchronizationPolicy = new GetAwaiterPolicy(); - } - } - - internal interface ISynchronizationPolicy - { - T GetResult(Task task); - - void GetResult(Task task); - } - - internal class GetAwaiterPolicy : ISynchronizationPolicy - { - public T GetResult(Task task) - { - return task.GetAwaiter().GetResult(); - } - - public void GetResult(Task task) - { - task.GetAwaiter().GetResult(); - } - } - - internal class TaskRunPolicy : ISynchronizationPolicy - { - public T GetResult(Task task) - { - return Task.Run(async () => await task).Result; - } - - public void GetResult(Task task) - { - Task.Run(async () => await task).Wait(); - } - } -} diff --git a/BitFaster.Caching/Atomic.cs b/BitFaster.Caching/Atomic.cs deleted file mode 100644 index bf6b2d2f..00000000 --- a/BitFaster.Caching/Atomic.cs +++ /dev/null @@ -1,155 +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 -{ - // make a version of Atomic that is baised towards sync usage. - // Caller can then choose between async or async optimized version that still works with both. - // SHould benchmark whether the AsyncAtomic version is meaninfully worse in terms of latency/allocs - // Looks like it would be very similar except the additional TaskCompletionSource alloc - [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] - public class Atomic - { - private Initializer initializer; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private V value; - - public Atomic() - { - this.initializer = new Initializer(); - } - - public Atomic(V value) - { - this.value = value; - } - - public V GetValue(K key, Func valueFactory) - { - if (this.initializer == null) - { - return this.value; - } - - return CreateValue(key, valueFactory); - } - - 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 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; - } - - private async Task CreateValueAsync(K key, Func> valueFactory) - { - Initializer init = this.initializer; - - if (init != null) - { - this.value = await init.CreateValueAsync(key, valueFactory).ConfigureAwait(false); - this.initializer = null; - } - - return this.value; - } - - private class Initializer - { - private object syncLock = new object(); - private bool isInitialized; - private V value; - - public V CreateValue(K key, Func valueFactory) - { - if (!Volatile.Read(ref isInitialized)) - { - return value; - } - - lock (syncLock) - { - if (!Volatile.Read(ref isInitialized)) - { - return value; - } - - value = valueFactory(key); - Volatile.Write(ref isInitialized, true); - return value; - } - } - - // This is terrifyingly bad on many levels. - public async Task CreateValueAsync(K key, Func> valueFactory) - { - if (!Volatile.Read(ref isInitialized)) - { - return value; - } - - // start another thread that holds the lock until a signal is sent. - ManualResetEvent manualResetEvent = new ManualResetEvent(false); - - var lockTask = Task.Run(() => { - lock (syncLock) - { - if (!Volatile.Read(ref isInitialized)) - { - // EXIT somehow and return value - } - - manualResetEvent.WaitOne(); - } - }); - - // Problems: - // 1. what if value factory throws? We need to release the lock - // 2. how to do double checked lock in the other thread - value = await valueFactory(key); - Volatile.Write(ref isInitialized, true); - manualResetEvent.Set(); - await lockTask; - - return value; - } - } - } -} diff --git a/BitFaster.Caching/AtomicCacheDecorator.cs b/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs similarity index 60% rename from BitFaster.Caching/AtomicCacheDecorator.cs rename to BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs index 618becf8..563e5653 100644 --- a/BitFaster.Caching/AtomicCacheDecorator.cs +++ b/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs @@ -4,57 +4,56 @@ using System.Text; using System.Threading.Tasks; -namespace BitFaster.Caching +namespace BitFaster.Caching.Synchronized { public class AtomicCacheDecorator : ICache { - private readonly ICache> cache; + private readonly ICache> cache; - public AtomicCacheDecorator(ICache> cache) + public AtomicCacheDecorator(ICache> cache) { this.cache = cache; } - public int Capacity => this.cache.Capacity; + public int Capacity => cache.Capacity; - public int Count => this.cache.Count; + public int Count => cache.Count; - public ICacheMetrics Metrics => this.cache.Metrics; + public ICacheMetrics Metrics => cache.Metrics; // need to dispatch different events for this public ICacheEvents Events => throw new Exception(); public void AddOrUpdate(K key, V value) { - cache.AddOrUpdate(key, new AsyncAtomic(value)); + cache.AddOrUpdate(key, new AsyncIdempotent(value)); } public void Clear() { - this.cache.Clear(); + cache.Clear(); } public V GetOrAdd(K key, Func valueFactory) { - var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic()); - return synchronized.GetValue(key, valueFactory); + throw new NotImplementedException(); } public Task GetOrAddAsync(K key, Func> valueFactory) { - var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic()); + var synchronized = cache.GetOrAdd(key, _ => new AsyncIdempotent()); return synchronized.GetValueAsync(key, valueFactory); } public void Trim(int itemCount) { - this.cache.Trim(itemCount); + cache.Trim(itemCount); } public bool TryGet(K key, out V value) { - AsyncAtomic output; - bool ret = cache.TryGet(key, out output); + AsyncIdempotent output; + var ret = cache.TryGet(key, out output); if (ret && output.IsValueCreated) { @@ -68,12 +67,12 @@ public bool TryGet(K key, out V value) public bool TryRemove(K key) { - return this.cache.TryRemove(key); + return cache.TryRemove(key); } public bool TryUpdate(K key, V value) { - return cache.TryUpdate(key, new AsyncAtomic(value)); ; + return cache.TryUpdate(key, new AsyncIdempotent(value)); ; } } } From b9ab180ab7844d0631069818b30232c6c25a9e87 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 21 Jul 2022 20:59:40 -0700 Subject: [PATCH 15/27] fix build --- .../Lru/ConcurrentLruBuilderTests.cs | 5 ++-- .../Lru/Builder/AtomicLruBuilder.cs | 26 ------------------ .../Lru/Builder/IdempotentAsyncLruBuilder.cs | 27 +++++++++++++++++++ .../Lru/Builder/ScopedAtomicLruBuilder.cs | 10 ++++--- .../Lru/ConcurrentLruBuilderExtensions.cs | 13 ++++----- .../Synchronized/IdempotentAsyncCache.cs | 4 +-- 6 files changed, 45 insertions(+), 40 deletions(-) delete mode 100644 BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs create mode 100644 BitFaster.Caching/Lru/Builder/IdempotentAsyncLruBuilder.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index fa8e0942..7499b47f 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using BitFaster.Caching.Lru; +using BitFaster.Caching.Synchronized; using FluentAssertions; using Xunit; @@ -149,10 +150,10 @@ public void ScopedPOC() // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru - var c = new ConcurrentLru>>(3); + var c = new ConcurrentLru>>(3); // layer 2: optional atomic creation - var atomic = new AtomicCacheDecorator>(c); + var atomic = new IdempotentAsyncCache>(c); // layer 3: optional scoping IScopedCache scoped = new ScopedCache(atomic); diff --git a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs deleted file mode 100644 index ece49ed6..00000000 --- a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace BitFaster.Caching.Lru.Builder -{ - public class AtomicLruBuilder : LruBuilderBase, ICache> - { - private readonly ConcurrentLruBuilder> inner; - - internal AtomicLruBuilder(ConcurrentLruBuilder> inner) - : base(inner.info) - { - this.inner = inner; - } - - public override ICache Build() - { - var innerCache = inner.Build(); - - return new AtomicCacheDecorator(innerCache); - } - } -} diff --git a/BitFaster.Caching/Lru/Builder/IdempotentAsyncLruBuilder.cs b/BitFaster.Caching/Lru/Builder/IdempotentAsyncLruBuilder.cs new file mode 100644 index 00000000..86a5a8dc --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/IdempotentAsyncLruBuilder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; + +namespace BitFaster.Caching.Lru.Builder +{ + public class IdempotentAsyncLruBuilder : LruBuilderBase, ICache> + { + private readonly ConcurrentLruBuilder> inner; + + internal IdempotentAsyncLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override ICache Build() + { + var innerCache = inner.Build(); + + return new IdempotentAsyncCache(innerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs index c96a6602..9c581c2b 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs @@ -3,14 +3,15 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; namespace BitFaster.Caching.Lru.Builder { public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped { - private readonly ConcurrentLruBuilder> inner; + private readonly ConcurrentLruBuilder> inner; - internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) + internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) : base(inner.info) { this.inner = inner; @@ -18,8 +19,9 @@ internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner public override IScopedCache Build() { - var level1 = inner.Build() as ICache>>; - var level2 = new AtomicCacheDecorator>(level1); + // TODO: This is actually wrong + var level1 = inner.Build() as ICache>>; + var level2 = new IdempotentAsyncCache>(level1); return new ScopedCache(level2); } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index d6f54cd0..8e58f3a1 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using BitFaster.Caching.Lru.Builder; +using BitFaster.Caching.Synchronized; namespace BitFaster.Caching.Lru { @@ -23,22 +24,22 @@ public static ScopedLruBuilder> WithScopedValues(this Conc return new ScopedLruBuilder>(scoped); } - public static AtomicLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) + public static IdempotentAsyncLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) { - var a = new ConcurrentLruBuilder>(b.info); - return new AtomicLruBuilder(a); + var a = new ConcurrentLruBuilder>(b.info); + return new IdempotentAsyncLruBuilder(a); } public static ScopedAtomicLruBuilder> WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped { - var atomicScoped = new ConcurrentLruBuilder>>(b.info); + var atomicScoped = new ConcurrentLruBuilder>>(b.info); return new ScopedAtomicLruBuilder>(atomicScoped); } - public static ScopedAtomicLruBuilder> WithScopedValues(this AtomicLruBuilder b) where V : IDisposable + public static ScopedAtomicLruBuilder> WithScopedValues(this IdempotentAsyncLruBuilder b) where V : IDisposable { - var atomicScoped = new ConcurrentLruBuilder>>(b.info); + var atomicScoped = new ConcurrentLruBuilder>>(b.info); return new ScopedAtomicLruBuilder>(atomicScoped); } } diff --git a/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs b/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs index 563e5653..5c35a3c1 100644 --- a/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs @@ -6,11 +6,11 @@ namespace BitFaster.Caching.Synchronized { - public class AtomicCacheDecorator : ICache + public class IdempotentAsyncCache : ICache { private readonly ICache> cache; - public AtomicCacheDecorator(ICache> cache) + public IdempotentAsyncCache(ICache> cache) { this.cache = cache; } From 4d1f759c8353af69d05705deb875d1f254253022 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 21 Jul 2022 21:00:41 -0700 Subject: [PATCH 16/27] async test --- BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 7499b47f..87d09097 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -145,7 +145,7 @@ public void TestPartitionCapacity() } [Fact] - public void ScopedPOC() + public async Task ScopedPOC() { // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped @@ -158,7 +158,7 @@ public void ScopedPOC() // layer 3: optional scoping IScopedCache scoped = new ScopedCache(atomic); - using (var lifetime = scoped.ScopedGetOrAdd(1, k => new Scoped(new Disposable()))) + using (var lifetime = await scoped.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable())))) { var d = lifetime.Value; } From 8e8df82e5701f658d39d60d1d3a5527a0ac96a4f Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 10:57:49 -0700 Subject: [PATCH 17/27] rename --- .../Lru/ConcurrentLruBuilderTests.cs | 6 +++--- ...uBuilder.cs => AtomicFactoryAsyncLruBuilder.cs} | 8 ++++---- .../Lru/Builder/ScopedAtomicLruBuilder.cs | 8 ++++---- .../Lru/ConcurrentLruBuilderExtensions.cs | 12 ++++++------ ...entAsyncCache.cs => AtomicFactoryAsyncCache.cs} | 14 +++++++------- 5 files changed, 24 insertions(+), 24 deletions(-) rename BitFaster.Caching/Lru/Builder/{IdempotentAsyncLruBuilder.cs => AtomicFactoryAsyncLruBuilder.cs} (52%) rename BitFaster.Caching/Synchronized/{IdempotentAsyncCache.cs => AtomicFactoryAsyncCache.cs} (78%) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 87d09097..ee9ffece 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -99,7 +99,7 @@ public void TestAtomic() .WithCapacity(3) .Build(); - lru.Should().BeOfType>(); + lru.Should().BeOfType>(); } [Fact] @@ -150,10 +150,10 @@ public async Task ScopedPOC() // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru - var c = new ConcurrentLru>>(3); + var c = new ConcurrentLru>>(3); // layer 2: optional atomic creation - var atomic = new IdempotentAsyncCache>(c); + var atomic = new AtomicFactoryAsyncCache>(c); // layer 3: optional scoping IScopedCache scoped = new ScopedCache(atomic); diff --git a/BitFaster.Caching/Lru/Builder/IdempotentAsyncLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs similarity index 52% rename from BitFaster.Caching/Lru/Builder/IdempotentAsyncLruBuilder.cs rename to BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs index 86a5a8dc..c5ea8188 100644 --- a/BitFaster.Caching/Lru/Builder/IdempotentAsyncLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs @@ -7,11 +7,11 @@ namespace BitFaster.Caching.Lru.Builder { - public class IdempotentAsyncLruBuilder : LruBuilderBase, ICache> + public class AtomicFactoryAsyncLruBuilder : LruBuilderBase, ICache> { - private readonly ConcurrentLruBuilder> inner; + private readonly ConcurrentLruBuilder> inner; - internal IdempotentAsyncLruBuilder(ConcurrentLruBuilder> inner) + internal AtomicFactoryAsyncLruBuilder(ConcurrentLruBuilder> inner) : base(inner.info) { this.inner = inner; @@ -21,7 +21,7 @@ public override ICache Build() { var innerCache = inner.Build(); - return new IdempotentAsyncCache(innerCache); + return new AtomicFactoryAsyncCache(innerCache); } } } diff --git a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs index 9c581c2b..704b8ad2 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs @@ -9,9 +9,9 @@ namespace BitFaster.Caching.Lru.Builder { public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped { - private readonly ConcurrentLruBuilder> inner; + private readonly ConcurrentLruBuilder> inner; - internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) + internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) : base(inner.info) { this.inner = inner; @@ -20,8 +20,8 @@ internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> i public override IScopedCache Build() { // TODO: This is actually wrong - var level1 = inner.Build() as ICache>>; - var level2 = new IdempotentAsyncCache>(level1); + var level1 = inner.Build() as ICache>>; + var level2 = new AtomicFactoryAsyncCache>(level1); return new ScopedCache(level2); } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index 8e58f3a1..6194f034 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -24,22 +24,22 @@ public static ScopedLruBuilder> WithScopedValues(this Conc return new ScopedLruBuilder>(scoped); } - public static IdempotentAsyncLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) + public static AtomicFactoryAsyncLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) { - var a = new ConcurrentLruBuilder>(b.info); - return new IdempotentAsyncLruBuilder(a); + var a = new ConcurrentLruBuilder>(b.info); + return new AtomicFactoryAsyncLruBuilder(a); } public static ScopedAtomicLruBuilder> WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped { - var atomicScoped = new ConcurrentLruBuilder>>(b.info); + var atomicScoped = new ConcurrentLruBuilder>>(b.info); return new ScopedAtomicLruBuilder>(atomicScoped); } - public static ScopedAtomicLruBuilder> WithScopedValues(this IdempotentAsyncLruBuilder b) where V : IDisposable + public static ScopedAtomicLruBuilder> WithScopedValues(this AtomicFactoryAsyncLruBuilder b) where V : IDisposable { - var atomicScoped = new ConcurrentLruBuilder>>(b.info); + var atomicScoped = new ConcurrentLruBuilder>>(b.info); return new ScopedAtomicLruBuilder>(atomicScoped); } } diff --git a/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs similarity index 78% rename from BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs rename to BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs index 5c35a3c1..99d1a561 100644 --- a/BitFaster.Caching/Synchronized/IdempotentAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -6,11 +6,11 @@ namespace BitFaster.Caching.Synchronized { - public class IdempotentAsyncCache : ICache + public class AtomicFactoryAsyncCache : ICache { - private readonly ICache> cache; + private readonly ICache> cache; - public IdempotentAsyncCache(ICache> cache) + public AtomicFactoryAsyncCache(ICache> cache) { this.cache = cache; } @@ -26,7 +26,7 @@ public IdempotentAsyncCache(ICache> cache) public void AddOrUpdate(K key, V value) { - cache.AddOrUpdate(key, new AsyncIdempotent(value)); + cache.AddOrUpdate(key, new AsyncAtomicFactory(value)); } public void Clear() @@ -41,7 +41,7 @@ public V GetOrAdd(K key, Func valueFactory) public Task GetOrAddAsync(K key, Func> valueFactory) { - var synchronized = cache.GetOrAdd(key, _ => new AsyncIdempotent()); + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory()); return synchronized.GetValueAsync(key, valueFactory); } @@ -52,7 +52,7 @@ public void Trim(int itemCount) public bool TryGet(K key, out V value) { - AsyncIdempotent output; + AsyncAtomicFactory output; var ret = cache.TryGet(key, out output); if (ret && output.IsValueCreated) @@ -72,7 +72,7 @@ public bool TryRemove(K key) public bool TryUpdate(K key, V value) { - return cache.TryUpdate(key, new AsyncIdempotent(value)); ; + return cache.TryUpdate(key, new AsyncAtomicFactory(value)); ; } } } From 13450b8e6794bb00a16b95294e9d2b3be86b0eb5 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 22 Jul 2022 12:02:59 -0700 Subject: [PATCH 18/27] decorator --- .../Synchronized/AtomicFactoryAsyncCache.cs | 2 +- .../Synchronized/AtomicFactoryCache.cs | 78 ++++++++++++++++ .../AtomicFactoryScopedAsyncCache.cs | 59 +++++++++++++ .../Synchronized/AtomicFactoryScopedCache.cs | 88 +++++++++++++++++++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 BitFaster.Caching/Synchronized/AtomicFactoryCache.cs create mode 100644 BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs create mode 100644 BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs index 99d1a561..b1d3b224 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -72,7 +72,7 @@ public bool TryRemove(K key) public bool TryUpdate(K key, V value) { - return cache.TryUpdate(key, new AsyncAtomicFactory(value)); ; + return cache.TryUpdate(key, new AsyncAtomicFactory(value)); } } } diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs new file mode 100644 index 00000000..f052c465 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Synchronized +{ + public class AtomicFactoryCache : ICache + { + private readonly ICache> cache; + + public AtomicFactoryCache(ICache> cache) + { + this.cache = cache; + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public ICacheMetrics Metrics => this.cache.Metrics; + + // TODO: wrapper + public ICacheEvents Events => throw new NotImplementedException(); // this.cache.Events; + + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new AtomicFactory(value)); + } + + public void Clear() + { + this.cache.Clear(); + } + + public V GetOrAdd(K key, Func valueFactory) + { + var atomicFactory = cache.GetOrAdd(key, _ => new AtomicFactory()); + return atomicFactory.GetValue(key, valueFactory); + } + + public Task GetOrAddAsync(K key, Func> valueFactory) + { + throw new NotImplementedException(); + } + + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + public bool TryGet(K key, out V value) + { + AtomicFactory output; + var ret = cache.TryGet(key, out output); + + if (ret && output.IsValueCreated) + { + value = output.ValueIfCreated; + return true; + } + + value = default; + return false; + } + + public bool TryRemove(K key) + { + return cache.TryRemove(key); + } + + public bool TryUpdate(K key, V value) + { + return cache.TryUpdate(key, new AtomicFactory(value)); + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs new file mode 100644 index 00000000..da7ac4ea --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Synchronized +{ + public class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable + { + public int Capacity => throw new NotImplementedException(); + + public int Count => throw new NotImplementedException(); + + public ICacheMetrics Metrics => throw new NotImplementedException(); + + public ICacheEvents> Events => throw new NotImplementedException(); + + public void AddOrUpdate(K key, V value) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) + { + throw new NotImplementedException(); + } + + public Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + { + throw new NotImplementedException(); + } + + public bool ScopedTryGet(K key, out Lifetime lifetime) + { + throw new NotImplementedException(); + } + + public void Trim(int itemCount) + { + throw new NotImplementedException(); + } + + public bool TryRemove(K key) + { + throw new NotImplementedException(); + } + + public bool TryUpdate(K key, V value) + { + throw new NotImplementedException(); + } + } +} diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs new file mode 100644 index 00000000..ba4fc671 --- /dev/null +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Synchronized +{ + public class AtomicFactoryScopedCache : IScopedCache where V : IDisposable + { + private readonly ICache> cache; + + public AtomicFactoryScopedCache(ICache> cache) + { + this.cache = cache; + } + + public int Capacity => this.cache.Capacity; + + public int Count => this.cache.Count; + + public ICacheMetrics Metrics => this.cache.Metrics; + + public ICacheEvents> Events => throw new NotImplementedException(); + + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new ScopedAtomicFactory(value)); + } + + public void Clear() + { + this.cache.Clear(); + } + + // TODO: dedupe + private const int MaxRetry = 5; + private static readonly string RetryFailureMessage = $"Exceeded {MaxRetry} attempts to create a lifetime."; + + public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) + { + int c = 0; + var spinwait = new SpinWait(); + while (true) + { + var scope = cache.GetOrAdd(key, _ => new ScopedAtomicFactory()); + + if (scope.TryCreateLifetime(key, valueFactory, out var lifetime)) + { + return lifetime; + } + + spinwait.SpinOnce(); + + if (c++ > MaxRetry) + { + throw new InvalidOperationException(RetryFailureMessage); + } + } + } + + public Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + { + throw new NotImplementedException(); + } + + public bool ScopedTryGet(K key, out Lifetime lifetime) + { + throw new NotImplementedException(); + } + + public void Trim(int itemCount) + { + throw new NotImplementedException(); + } + + public bool TryRemove(K key) + { + throw new NotImplementedException(); + } + + public bool TryUpdate(K key, V value) + { + throw new NotImplementedException(); + } + } +} From 9357d60ec12bcc770f8d37d5c5762f5d003a1900 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 13:32:26 -0700 Subject: [PATCH 19/27] clean --- BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs | 2 +- BitFaster.Caching/Synchronized/AtomicFactoryCache.cs | 2 +- BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs index 4b5c87c1..41c141c5 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Synchronized { - public class AtomicFactoryAsyncCache : ICache + public sealed class AtomicFactoryAsyncCache : ICache { private readonly ICache> cache; private readonly EventProxy eventProxy; diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs index 1c570512..df03b91f 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryCache.cs @@ -79,7 +79,7 @@ public bool TryRemove(K key) public bool TryUpdate(K key, V value) { - return cache.TryUpdate(key, new AtomicFactory(value)); + return cache.TryUpdate(key, new AtomicFactory(value)); } private class EventProxy : CacheEventProxyBase, V> diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs index 39f3b7b0..3e317441 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -42,10 +42,6 @@ public void Clear() this.cache.Clear(); } - // TODO: dedupe - private const int MaxRetry = 5; - private static readonly string RetryFailureMessage = $"Exceeded {MaxRetry} attempts to create a lifetime."; - public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) { int c = 0; From 78e37a2a128fdbf0f60949fdca23be06c612c2c6 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 13:57:22 -0700 Subject: [PATCH 20/27] more builder --- .../Lru/ConcurrentLruBuilderTests.cs | 4 +- .../Lru/Builder/AtomicLruBuilder.cs | 43 +++++++++++++++++++ .../Lru/Builder/LruBuilderBase.cs | 2 - .../Lru/Builder/ScopedAtomicLruBuilder.cs | 29 ++++++++++--- .../Lru/ConcurrentLruBuilderExtensions.cs | 12 +++--- 5 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index ee9ffece..dd9b3846 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -74,7 +74,7 @@ public void TestScopedAtomic() .WithCapacity(3) .Build(); - lru.Should().BeOfType>(); + lru.Should().BeOfType>(); lru.Capacity.Should().Be(3); } @@ -87,7 +87,7 @@ public void TestScopedAtomicReverse() .WithCapacity(3) .Build(); - lru.Should().BeOfType>(); + lru.Should().BeOfType>(); lru.Capacity.Should().Be(3); } diff --git a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs new file mode 100644 index 00000000..e7786c4b --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; + +namespace BitFaster.Caching.Lru.Builder +{ + public class AtomicLruBuilder : LruBuilderBase, ICache> + { + private readonly ConcurrentLruBuilder> inner; + + internal AtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override ICache Build() + { + var level1 = inner.Build(); + return new AtomicFactoryCache(level1); + } + } + + public class AsyncAtomicLruBuilder : LruBuilderBase, ICache> + { + private readonly ConcurrentLruBuilder> inner; + + internal AsyncAtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override ICache Build() + { + var level1 = inner.Build(); + return new AtomicFactoryAsyncCache(level1); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index 8af76f2d..cf9d6b14 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -18,8 +18,6 @@ protected LruBuilderBase(LruInfo info) this.info = info; } - public LruInfo Info { get; } - /// /// Set the maximum number of values to keep in the cache. If more items than this are added, /// the cache eviction policy will determine which values to remove. diff --git a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs index 704b8ad2..d7d595d7 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs @@ -7,11 +7,11 @@ namespace BitFaster.Caching.Lru.Builder { - public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped + public class ScopedAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable { - private readonly ConcurrentLruBuilder> inner; + private readonly ConcurrentLruBuilder> inner; - internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) + internal ScopedAtomicLruBuilder(ConcurrentLruBuilder> inner) : base(inner.info) { this.inner = inner; @@ -19,10 +19,25 @@ internal ScopedAtomicLruBuilder(ConcurrentLruBuilder public override IScopedCache Build() { - // TODO: This is actually wrong - var level1 = inner.Build() as ICache>>; - var level2 = new AtomicFactoryAsyncCache>(level1); - return new ScopedCache(level2); + var level1 = inner.Build() as ICache>; + return new AtomicFactoryScopedCache(level1); + } + } + + public class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable + { + private readonly ConcurrentLruBuilder> inner; + + internal ScopedAsyncAtomicLruBuilder(ConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + public override IScopedCache Build() + { + var level1 = inner.Build() as ICache>; + return new AtomicFactoryScopedAsyncCache(level1); } } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index 6194f034..d3112fc1 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -30,17 +30,17 @@ public static AtomicFactoryAsyncLruBuilder WithAtomicCreate(this Con return new AtomicFactoryAsyncLruBuilder(a); } - public static ScopedAtomicLruBuilder> WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped + public static ScopedAtomicLruBuilder WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped { - var atomicScoped = new ConcurrentLruBuilder>>(b.info); + var atomicScoped = new ConcurrentLruBuilder>(b.info); - return new ScopedAtomicLruBuilder>(atomicScoped); + return new ScopedAtomicLruBuilder(atomicScoped); } - public static ScopedAtomicLruBuilder> WithScopedValues(this AtomicFactoryAsyncLruBuilder b) where V : IDisposable + public static ScopedAtomicLruBuilder WithScopedValues(this AtomicFactoryAsyncLruBuilder b) where V : IDisposable { - var atomicScoped = new ConcurrentLruBuilder>>(b.info); - return new ScopedAtomicLruBuilder>(atomicScoped); + var atomicScoped = new ConcurrentLruBuilder>(b.info); + return new ScopedAtomicLruBuilder(atomicScoped); } } } From 70a3e6be59896273f0fcec3ac3c4a8a0daa4dfab Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 14:05:25 -0700 Subject: [PATCH 21/27] rev --- .../Lru/ConcurrentLruBuilderTests.cs | 2 +- BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs | 4 ++-- BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index dd9b3846..aef483f9 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -99,7 +99,7 @@ public void TestAtomic() .WithCapacity(3) .Build(); - lru.Should().BeOfType>(); + lru.Should().BeOfType>(); } [Fact] diff --git a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs index e7786c4b..da9bff6e 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lru.Builder { - public class AtomicLruBuilder : LruBuilderBase, ICache> + public class AtomicLruBuilder : LruBuilderBase, ICache> { private readonly ConcurrentLruBuilder> inner; @@ -24,7 +24,7 @@ public override ICache Build() } } - public class AsyncAtomicLruBuilder : LruBuilderBase, ICache> + public class AsyncAtomicLruBuilder : LruBuilderBase, ICache> { private readonly ConcurrentLruBuilder> inner; diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index d3112fc1..eb36b56f 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -24,10 +24,10 @@ public static ScopedLruBuilder> WithScopedValues(this Conc return new ScopedLruBuilder>(scoped); } - public static AtomicFactoryAsyncLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) + public static AtomicLruBuilder WithAtomicCreate(this ConcurrentLruBuilder b) { - var a = new ConcurrentLruBuilder>(b.info); - return new AtomicFactoryAsyncLruBuilder(a); + var a = new ConcurrentLruBuilder>(b.info); + return new AtomicLruBuilder(a); } public static ScopedAtomicLruBuilder WithAtomicCreate(this ScopedLruBuilder b) where V : IDisposable where W : IScoped @@ -37,7 +37,7 @@ public static ScopedAtomicLruBuilder WithAtomicCreate(this Scoped return new ScopedAtomicLruBuilder(atomicScoped); } - public static ScopedAtomicLruBuilder WithScopedValues(this AtomicFactoryAsyncLruBuilder b) where V : IDisposable + public static ScopedAtomicLruBuilder WithScopedValues(this AtomicLruBuilder b) where V : IDisposable { var atomicScoped = new ConcurrentLruBuilder>(b.info); return new ScopedAtomicLruBuilder(atomicScoped); From 79637e6e9ac61ab343588ddafc0d5b97ac144ec9 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 16:40:42 -0700 Subject: [PATCH 22/27] split interfaces --- .../Lru/ConcurrentLruBuilderTests.cs | 30 +-- .../ScopedAsyncCacheTestBase.cs | 178 ++++++++++++++++++ .../ScopedAsyncCacheTests.cs | 17 ++ .../ScopedCacheTests.cs | 28 +-- .../AtomicFactoryAsyncCacheTests.cs | 8 - .../AtomicFactoryScopedAsyncCacheTests.cs | 12 +- .../AtomicFactoryScopedCacheTests.cs | 28 +-- BitFaster.Caching/IAsyncCache.cs | 87 +++++++++ BitFaster.Caching/ICache.cs | 9 - BitFaster.Caching/IScopedAsyncCache.cs | 92 +++++++++ BitFaster.Caching/IScopedCache.cs | 9 - .../Builder/AtomicFactoryAsyncLruBuilder.cs | 4 +- .../Lru/Builder/AtomicLruBuilder.cs | 4 +- .../Lru/Builder/ScopedAtomicLruBuilder.cs | 4 +- BitFaster.Caching/Lru/ClassicLru.cs | 2 +- .../Lru/TemplateConcurrentLru.cs | 2 +- BitFaster.Caching/ScopedAsyncCache.cs | 111 +++++++++++ BitFaster.Caching/ScopedCache.cs | 23 --- .../Synchronized/AtomicFactoryAsyncCache.cs | 7 +- .../AtomicFactoryScopedAsyncCache.cs | 7 +- 20 files changed, 541 insertions(+), 121 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs create mode 100644 BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs create mode 100644 BitFaster.Caching/IAsyncCache.cs create mode 100644 BitFaster.Caching/IScopedAsyncCache.cs create mode 100644 BitFaster.Caching/ScopedAsyncCache.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index aef483f9..c82a7361 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -144,24 +144,24 @@ public void TestPartitionCapacity() lru.Capacity.Should().Be(6); } - [Fact] - public async Task ScopedPOC() - { - // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped + //[Fact] + // public async Task ScopedPOC() + // { + // // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped - // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru - var c = new ConcurrentLru>>(3); + // // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru + // var c = new ConcurrentLru>>(3); - // layer 2: optional atomic creation - var atomic = new AtomicFactoryAsyncCache>(c); + // // layer 2: optional atomic creation + // var atomic = new AtomicFactoryCache>(c); - // layer 3: optional scoping - IScopedCache scoped = new ScopedCache(atomic); + // // layer 3: optional scoping + // IScopedCache scoped = new ScopedCache(atomic); - using (var lifetime = await scoped.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable())))) - { - var d = lifetime.Value; - } - } + // using (var lifetime = await scoped.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable())))) + // { + // var d = lifetime.Value; + // } + // } } } diff --git a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs new file mode 100644 index 00000000..556701c1 --- /dev/null +++ b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTestBase.cs @@ -0,0 +1,178 @@ +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 +{ + public abstract class ScopedAsyncCacheTestBase + { + protected const int capacity = 6; + protected readonly IScopedAsyncCache cache; + + protected List>> removedItems = new(); + + protected ScopedAsyncCacheTestBase(IScopedAsyncCache cache) + { + this.cache = cache; + } + + [Fact] + public void WhenCreatedCapacityPropertyWrapsInnerCache() + { + this.cache.Capacity.Should().Be(capacity); + } + + [Fact] + public void WhenItemIsAddedCountIsCorrect() + { + this.cache.Count.Should().Be(0); + + this.cache.AddOrUpdate(1, new Disposable()); + + this.cache.Count.Should().Be(1); + } + + [Fact] + public void WhenItemIsAddedThenLookedUpMetricsAreCorrect() + { + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.ScopedTryGet(1, out var lifetime); + + this.cache.Metrics.Misses.Should().Be(0); + this.cache.Metrics.Hits.Should().Be(1); + } + + [Fact] + public void WhenEventHandlerIsRegisteredItIsFired() + { + this.cache.Events.ItemRemoved += OnItemRemoved; + + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.TryRemove(1); + + this.removedItems.First().Key.Should().Be(1); + } + + [Fact] + public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem() + { + var d = new Disposable(); + this.cache.AddOrUpdate(1, d); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(d); + } + + [Fact] + public void WhenKeyExistsAddOrUpdateUpdatesExistingItem() + { + var d1 = new Disposable(); + var d2 = new Disposable(); + this.cache.AddOrUpdate(1, d1); + this.cache.AddOrUpdate(1, d2); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + lifetime.Value.Should().Be(d2); + } + + [Fact] + public void WhenItemUpdatedOldValueIsAliveUntilLifetimeCompletes() + { + var d1 = new Disposable(); + var d2 = new Disposable(); + + // start a lifetime on 1 + this.cache.AddOrUpdate(1, d1); + this.cache.ScopedTryGet(1, out var lifetime1).Should().BeTrue(); + + using (lifetime1) + { + // replace 1 + this.cache.AddOrUpdate(1, d2); + + // cache reflects replacement + this.cache.ScopedTryGet(1, out var lifetime2).Should().BeTrue(); + lifetime2.Value.Should().Be(d2); + + d1.IsDisposed.Should().BeFalse(); + } + + d1.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenClearedItemsAreDisposed() + { + var d = new Disposable(); + this.cache.AddOrUpdate(1, d); + + this.cache.Clear(); + + d.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenItemExistsTryGetReturnsLifetime() + { + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + + lifetime.Should().NotBeNull(); + } + + [Fact] + public void WhenItemDoesNotExistTryGetReturnsFalse() + { + this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); + } + + [Fact] + public void WhenCacheContainsValuesTrim1RemovesColdestValue() + { + this.cache.AddOrUpdate(0, new Disposable()); + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.AddOrUpdate(2, new Disposable()); + + this.cache.Trim(1); + + this.cache.ScopedTryGet(0, out var lifetime).Should().BeFalse(); + } + + [Fact] + public void WhenKeyDoesNotExistTryRemoveReturnsFalse() + { + this.cache.TryRemove(1).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryRemoveReturnsTrue() + { + this.cache.AddOrUpdate(1, new Disposable()); + this.cache.TryRemove(1).Should().BeTrue(); + } + + [Fact] + public void WhenKeyDoesNotExistTryUpdateReturnsFalse() + { + this.cache.TryUpdate(1, new Disposable()).Should().BeFalse(); + } + + [Fact] + public void WhenKeyExistsTryUpdateReturnsTrue() + { + this.cache.AddOrUpdate(1, new Disposable()); + + this.cache.TryUpdate(1, new Disposable()).Should().BeTrue(); + } + + protected void OnItemRemoved(object sender, ItemRemovedEventArgs> e) + { + this.removedItems.Add(e); + } + } +} diff --git a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs new file mode 100644 index 00000000..e9cb75cb --- /dev/null +++ b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.UnitTests +{ + public class ScopedAsyncCacheTests : ScopedAsyncCacheTestBase + { + public ScopedAsyncCacheTests() + : base(new ScopedAsyncCache(new ConcurrentLru>(capacity))) + { + } + } +} diff --git a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs index 0adf2c6a..7373c720 100644 --- a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs @@ -44,13 +44,13 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue() this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); } - [Fact] - public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() - { - await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); + //[Fact] + //public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() + //{ + // await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); - this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); - } + // this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + //} [Fact] public void GetOrAddDisposedScopeThrows() @@ -63,15 +63,15 @@ public void GetOrAddDisposedScopeThrows() getOrAdd.Should().Throw(); } - [Fact] - public async Task GetOrAddAsyncDisposedScopeThrows() - { - var scope = new Scoped(new Disposable()); - scope.Dispose(); + //[Fact] + //public async Task GetOrAddAsyncDisposedScopeThrows() + //{ + // var scope = new Scoped(new Disposable()); + // scope.Dispose(); - Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; + // Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; - await getOrAdd.Should().ThrowAsync(); - } + // await getOrAdd.Should().ThrowAsync(); + //} } } diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs index e1f132a9..c4793d1b 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryAsyncCacheTests.cs @@ -97,14 +97,6 @@ public void WhenItemDoesNotExistTryGetReturnsFalse() this.cache.TryGet(1, out var value).Should().BeFalse(); } - [Fact] - public void GetOrAddThrows() - { - Action getOrAdd = () => { this.cache.GetOrAdd(1, k => k); }; - - getOrAdd.Should().Throw(); - } - [Fact] public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() { diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs index 8818d11e..0f28af16 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedAsyncCacheTests.cs @@ -10,8 +10,10 @@ namespace BitFaster.Caching.UnitTests.Synchronized { - public class AtomicFactoryScopedAsyncCacheTests : ScopedCacheTestBase + public class AtomicFactoryScopedAsyncCacheTests : ScopedAsyncCacheTestBase { + + public AtomicFactoryScopedAsyncCacheTests() : base(new AtomicFactoryScopedAsyncCache(new ConcurrentLru>(capacity))) { @@ -37,14 +39,6 @@ public async Task WhenScopeIsDisposedTryGetReturnsFalse() this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); } - [Fact] - public void WhenKeyDoesNotExistGetOrAddThrows() - { - Action getOrAdd = () => { this.cache.ScopedGetOrAdd(1, k => new Scoped(new Disposable())); }; - - getOrAdd.Should().Throw(); - } - [Fact] public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() { diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs index e0927bdc..08eb8034 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -45,13 +45,13 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue() this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); } - [Fact] - public async Task WhenKeyDoesNotExistGetOrAddAsyncThrows() - { - Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); }; + //[Fact] + //public async Task WhenKeyDoesNotExistGetOrAddAsyncThrows() + //{ + // Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); }; - await getOrAdd.Should().ThrowAsync(); - } + // await getOrAdd.Should().ThrowAsync(); + //} [Fact] public void GetOrAddDisposedScopeThrows() @@ -64,15 +64,15 @@ public void GetOrAddDisposedScopeThrows() getOrAdd.Should().Throw(); } - [Fact] - public void GetOrAddAsyncDisposedScopeThrows() - { - var scope = new Scoped(new Disposable()); - scope.Dispose(); + //[Fact] + //public void GetOrAddAsyncDisposedScopeThrows() + //{ + // var scope = new Scoped(new Disposable()); + // scope.Dispose(); - Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; + // Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; - getOrAdd.Should().ThrowAsync(); - } + // getOrAdd.Should().ThrowAsync(); + //} } } diff --git a/BitFaster.Caching/IAsyncCache.cs b/BitFaster.Caching/IAsyncCache.cs new file mode 100644 index 00000000..7e547cb1 --- /dev/null +++ b/BitFaster.Caching/IAsyncCache.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// Represents a generic cache of key/value pairs. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public interface IAsyncCache + { + /// + /// Gets the total number of items that can be stored in the cache. + /// + int Capacity { get; } + + /// + /// Gets the number of items currently held in the cache. + /// + int Count { get; } + + /// + /// Gets the cache metrics. + /// + ICacheMetrics Metrics { get; } + + /// + /// Gets the cache events. + /// + ICacheEvents Events { get; } + + /// + /// Attempts to get the value associated with the specified key from the cache. + /// + /// The key of the value to get. + /// When this method returns, contains the object from the cache that has the specified key, or the default value of the type if the operation failed. + /// true if the key was found in the cache; otherwise, false. + bool TryGet(K key, out V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the + /// existing value if the key already exists. + /// + /// The key of the element to add. + /// The factory function used to asynchronously generate a value for the key. + /// A task that represents the asynchronous GetOrAdd operation. + Task GetOrAddAsync(K key, Func> valueFactory); + + /// + /// Attempts to remove the value that has the specified key. + /// + /// The key of the element to remove. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key); + + /// + /// Attempts to update the value that has the specified key. + /// + /// The key of the element to update. + /// The new value. + /// true if the object was updated successfully; otherwise, false. + bool TryUpdate(K key, V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the + /// key already exists. + /// + /// The key of the element to update. + /// The new value. + void AddOrUpdate(K key, V value); + + /// + /// Removes all keys and values from the cache. + /// + void Clear(); + + /// + /// Trim the specified number of items from the cache. + /// + /// The number of items to remove. + void Trim(int itemCount); + } +} diff --git a/BitFaster.Caching/ICache.cs b/BitFaster.Caching/ICache.cs index 8200bafe..dbb9e892 100644 --- a/BitFaster.Caching/ICache.cs +++ b/BitFaster.Caching/ICache.cs @@ -51,15 +51,6 @@ public interface ICache /// in the cache, or the new value if the key was not in the cache. V GetOrAdd(K key, Func valueFactory); - /// - /// Adds a key/value pair to the cache if the key does not already exist. Returns the new value, or the - /// existing value if the key already exists. - /// - /// The key of the element to add. - /// The factory function used to asynchronously generate a value for the key. - /// A task that represents the asynchronous GetOrAdd operation. - Task GetOrAddAsync(K key, Func> valueFactory); - /// /// Attempts to remove the value that has the specified key. /// diff --git a/BitFaster.Caching/IScopedAsyncCache.cs b/BitFaster.Caching/IScopedAsyncCache.cs new file mode 100644 index 00000000..4906d441 --- /dev/null +++ b/BitFaster.Caching/IScopedAsyncCache.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// Represents a generic cache of key/scoped IDisposable value pairs. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public interface IScopedAsyncCache where V : IDisposable + { + /// + /// Gets the total number of items that can be stored in the cache. + /// + int Capacity { get; } + + /// + /// Gets the number of items currently held in the cache. + /// + int Count { get; } + + /// + /// Gets the cache metrics. + /// + ICacheMetrics Metrics { get; } + + /// + /// Gets the cache events. + /// + /// + /// Events expose the Scoped instance wrapping each value. To keep the value alive (blocking Dispose), try to + /// create a Lifetime from the scope. + /// + ICacheEvents> Events { get; } + + /// + /// Attempts to create a lifetime for the value associated with the specified key from the cache + /// + /// The key of the value to get. + /// When this method returns, contains a lifetime for the object from the cache that + /// has the specified key, or the default value of the type if the operation failed. + /// true if the key was found in the cache; otherwise, false. + bool ScopedTryGet(K key, out Lifetime lifetime); + + /// + /// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either + /// the new value, or the existing value if the key already exists. + /// + /// The key of the element to add. + /// The factory function used to asynchronously generate a scoped value for the key. + /// A task that represents the asynchronous ScopedGetOrAdd operation. + Task> ScopedGetOrAddAsync(K key, Func>> valueFactory); + + /// + /// Attempts to remove the value that has the specified key. + /// + /// The key of the element to remove. + /// true if the object was removed successfully; otherwise, false. + bool TryRemove(K key); + + /// + /// Attempts to update the value that has the specified key. + /// + /// The key of the element to update. + /// The new value. + /// true if the object was updated successfully; otherwise, false. + bool TryUpdate(K key, V value); + + /// + /// Adds a key/value pair to the cache if the key does not already exist, or updates a key/value pair if the + /// key already exists. + /// + /// The key of the element to update. + /// The new value. + void AddOrUpdate(K key, V value); + + /// + /// Removes all keys and values from the cache. + /// + void Clear(); + + /// + /// Trim the specified number of items from the cache. + /// + /// The number of items to remove. + void Trim(int itemCount); + } +} diff --git a/BitFaster.Caching/IScopedCache.cs b/BitFaster.Caching/IScopedCache.cs index 4eca0f1f..e06f11c1 100644 --- a/BitFaster.Caching/IScopedCache.cs +++ b/BitFaster.Caching/IScopedCache.cs @@ -57,15 +57,6 @@ public interface IScopedCache where V : IDisposable /// the cache. Lifetime ScopedGetOrAdd(K key, Func> valueFactory); - /// - /// Adds a key/scoped value pair to the cache if the key does not already exist. Returns a lifetime for either - /// the new value, or the existing value if the key already exists. - /// - /// The key of the element to add. - /// The factory function used to asynchronously generate a scoped value for the key. - /// A task that represents the asynchronous ScopedGetOrAdd operation. - Task> ScopedGetOrAddAsync(K key, Func>> valueFactory); - /// /// Attempts to remove the value that has the specified key. /// diff --git a/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs index c5ea8188..b10c3776 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lru.Builder { - public class AtomicFactoryAsyncLruBuilder : LruBuilderBase, ICache> + public class AtomicFactoryAsyncLruBuilder : LruBuilderBase, IAsyncCache> { private readonly ConcurrentLruBuilder> inner; @@ -17,7 +17,7 @@ internal AtomicFactoryAsyncLruBuilder(ConcurrentLruBuilder Build() + public override IAsyncCache Build() { var innerCache = inner.Build(); diff --git a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs index da9bff6e..4c5e9115 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs @@ -24,7 +24,7 @@ public override ICache Build() } } - public class AsyncAtomicLruBuilder : LruBuilderBase, ICache> + public class AsyncAtomicLruBuilder : LruBuilderBase, IAsyncCache> { private readonly ConcurrentLruBuilder> inner; @@ -34,7 +34,7 @@ internal AsyncAtomicLruBuilder(ConcurrentLruBuilder> this.inner = inner; } - public override ICache Build() + public override IAsyncCache Build() { var level1 = inner.Build(); return new AtomicFactoryAsyncCache(level1); diff --git a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs index d7d595d7..141d41c2 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs @@ -24,7 +24,7 @@ public override IScopedCache Build() } } - public class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable + public class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable { private readonly ConcurrentLruBuilder> inner; @@ -34,7 +34,7 @@ internal ScopedAsyncAtomicLruBuilder(ConcurrentLruBuilder Build() + public override IScopedAsyncCache Build() { var level1 = inner.Build() as ICache>; return new AtomicFactoryScopedAsyncCache(level1); diff --git a/BitFaster.Caching/Lru/ClassicLru.cs b/BitFaster.Caching/Lru/ClassicLru.cs index 26fd3eb2..b787d37b 100644 --- a/BitFaster.Caching/Lru/ClassicLru.cs +++ b/BitFaster.Caching/Lru/ClassicLru.cs @@ -18,7 +18,7 @@ namespace BitFaster.Caching.Lru /// /// The type of the key /// The type of the value - public sealed class ClassicLru : ICache, IEnumerable> + public sealed class ClassicLru : ICache, IAsyncCache, IEnumerable> { private readonly int capacity; private readonly ConcurrentDictionary> dictionary; diff --git a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs index 0190072e..c332fa97 100644 --- a/BitFaster.Caching/Lru/TemplateConcurrentLru.cs +++ b/BitFaster.Caching/Lru/TemplateConcurrentLru.cs @@ -28,7 +28,7 @@ namespace BitFaster.Caching.Lru /// 5. When warm is full, warm tail is moved to warm head or cold depending on WasAccessed. /// 6. When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed. /// - public class TemplateConcurrentLru : ICache, IEnumerable> + public class TemplateConcurrentLru : ICache, IAsyncCache, IEnumerable> where I : LruItem where P : struct, IItemPolicy where T : struct, ITelemetryPolicy diff --git a/BitFaster.Caching/ScopedAsyncCache.cs b/BitFaster.Caching/ScopedAsyncCache.cs new file mode 100644 index 00000000..97195fa1 --- /dev/null +++ b/BitFaster.Caching/ScopedAsyncCache.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + /// + /// A cache decorator for working with Scoped IDisposable values. The Scoped methods (e.g. ScopedGetOrAdd) + /// are threadsafe and create lifetimes that guarantee the value will not be disposed until the + /// lifetime is disposed. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public sealed class ScopedAsyncCache : IScopedAsyncCache where V : IDisposable + { + private readonly IAsyncCache> cache; + + public ScopedAsyncCache(IAsyncCache> cache) + { + if (cache == null) + { + throw new ArgumentNullException(nameof(cache)); + } + + this.cache = cache; + } + + /// + public int Capacity => this.cache.Capacity; + + /// + public int Count => this.cache.Count; + + /// + public ICacheMetrics Metrics => this.cache.Metrics; + + /// + public ICacheEvents> Events => this.cache.Events; + + /// + public void AddOrUpdate(K key, V value) + { + this.cache.AddOrUpdate(key, new Scoped(value)); + } + + /// + public void Clear() + { + this.cache.Clear(); + } + + /// + public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) + { + int c = 0; + var spinwait = new SpinWait(); + while (true) + { + var scope = await cache.GetOrAddAsync(key, valueFactory); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + + spinwait.SpinOnce(); + + if (c++ > ScopedCacheDefaults.MaxRetry) + { + throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); + } + } + } + + /// + public void Trim(int itemCount) + { + this.cache.Trim(itemCount); + } + + /// + public bool ScopedTryGet(K key, out Lifetime lifetime) + { + if (this.cache.TryGet(key, out var scope)) + { + if (scope.TryCreateLifetime(out lifetime)) + { + return true; + } + } + + lifetime = default; + return false; + } + + /// + public bool TryRemove(K key) + { + return this.cache.TryRemove(key); + } + + /// + public bool TryUpdate(K key, V value) + { + return this.cache.TryUpdate(key, new Scoped(value)); + } + } +} diff --git a/BitFaster.Caching/ScopedCache.cs b/BitFaster.Caching/ScopedCache.cs index 4dcb3797..8d34e229 100644 --- a/BitFaster.Caching/ScopedCache.cs +++ b/BitFaster.Caching/ScopedCache.cs @@ -75,29 +75,6 @@ public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) } } - /// - public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) - { - int c = 0; - var spinwait = new SpinWait(); - while (true) - { - var scope = await cache.GetOrAddAsync(key, valueFactory); - - if (scope.TryCreateLifetime(out var lifetime)) - { - return lifetime; - } - - spinwait.SpinOnce(); - - if (c++ > ScopedCacheDefaults.MaxRetry) - { - throw new InvalidOperationException(ScopedCacheDefaults.RetryFailureMessage); - } - } - } - /// public void Trim(int itemCount) { diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs index 41c141c5..ae015686 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryAsyncCache.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Synchronized { - public sealed class AtomicFactoryAsyncCache : ICache + public sealed class AtomicFactoryAsyncCache : IAsyncCache { private readonly ICache> cache; private readonly EventProxy eventProxy; @@ -41,11 +41,6 @@ public void Clear() cache.Clear(); } - public V GetOrAdd(K key, Func valueFactory) - { - throw new NotImplementedException(); - } - public Task GetOrAddAsync(K key, Func> valueFactory) { var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomicFactory()); diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs index 4f62e203..c717c713 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedAsyncCache.cs @@ -8,7 +8,7 @@ namespace BitFaster.Caching.Synchronized { - public sealed class AtomicFactoryScopedAsyncCache : IScopedCache where V : IDisposable + public sealed class AtomicFactoryScopedAsyncCache : IScopedAsyncCache where V : IDisposable { private readonly ICache> cache; private readonly EventProxy eventProxy; @@ -42,11 +42,6 @@ public void Clear() this.cache.Clear(); } - public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) - { - throw new NotImplementedException(); - } - public async Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) { int c = 0; From d7110a73c7419dc28fba95749b2d42afb68eea14 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 18:37:18 -0700 Subject: [PATCH 23/27] all built --- .../Lru/ConcurrentLruBuilderTests.cs | 304 +++++++++++++++--- .../Lru/Builder/ScopedAtomicLruBuilder.cs | 17 - .../Lru/Builder/ScopedLruBuilder.cs | 41 +++ BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 32 ++ .../Lru/ConcurrentLruBuilderExtensions.cs | 47 +++ 5 files changed, 381 insertions(+), 60 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index c82a7361..6ea36953 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -53,8 +53,102 @@ public void TestMetricsTLru() lru.Capacity.Should().Be(128); } + + [Fact] + public void TestComparer() + { + var fastLru = new ConcurrentLruBuilder() + .WithKeyComparer(StringComparer.OrdinalIgnoreCase) + .Build(); + + fastLru.GetOrAdd("a", k => 1); + fastLru.TryGet("A", out var value).Should().BeTrue(); + } + + [Fact] + public void TestConcurrencyLevel() + { + var b = new ConcurrentLruBuilder() + .WithConcurrencyLevel(-1); + + Action constructor = () => { var x = b.Build(); }; + + constructor.Should().Throw(); + } + + [Fact] + public void TestIntCapacity() + { + var lru = new ConcurrentLruBuilder() + .WithCapacity(3) + .Build(); + + lru.Capacity.Should().Be(3); + } + [Fact] - public void TestScopedOnly() + public void TestPartitionCapacity() + { + var lru = new ConcurrentLruBuilder() + .WithCapacity(new FavorFrequencyPartition(6)) + .Build(); + + lru.Capacity.Should().Be(6); + } + + // There are 15 combinations to test: + // ----------------------------- + //1 WithAtomic + //2 WithScoped + //3 AsAsync + // + // ----------------------------- + //4 WithAtomic + // WithScoped + // + //5 WithScoped + // WithAtomic + // + //6 AsAsync + // WithScoped + // + //7 WithScoped + // AsAsync + // + //8 WithAtomic + // AsAsync + // + //9 AsAsync + // WithAtomic + // + // ----------------------------- + //10 WithAtomic + // WithScoped + // AsAsync + // + //11 WithAtomic + // AsAsync + // WithScoped + // + //12 WithScoped + // WithAtomic + // AsAsync + // + //13 WithScoped + // AsAsync + // WithAtomic + // + //14 AsAsync + // WithScoped + // WithAtomic + // + //15 AsAsync + // WithAtomic + // WithScoped + + // 1 + [Fact] + public void WithScopedValues() { var lru = new ConcurrentLruBuilder() .WithScopedValues() @@ -65,12 +159,37 @@ public void TestScopedOnly() lru.Capacity.Should().Be(3); } + // 2 + [Fact] + public void WithAtomicFactory() + { + var lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeOfType>(); + } + + // 3 [Fact] - public void TestScopedAtomic() + public void AsAsync() + { + var lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 4 + [Fact] + public void WithAtomicWithScope() { var lru = new ConcurrentLruBuilder() - .WithScopedValues() .WithAtomicCreate() + .WithScopedValues() .WithCapacity(3) .Build(); @@ -78,12 +197,13 @@ public void TestScopedAtomic() lru.Capacity.Should().Be(3); } + // 5 [Fact] - public void TestScopedAtomicReverse() + public void WithScopedWithAtomic() { var lru = new ConcurrentLruBuilder() - .WithAtomicCreate() .WithScopedValues() + .WithAtomicCreate() .WithCapacity(3) .Build(); @@ -91,77 +211,175 @@ public void TestScopedAtomicReverse() lru.Capacity.Should().Be(3); } + // 6 [Fact] - public void TestAtomic() + public void AsAsyncWithScoped() + { + var lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + + lru.Capacity.Should().Be(3); + } + + // 7 + [Fact] + public void WithScopedAsAsync() + { + var lru = new ConcurrentLruBuilder() + .WithScopedValues() + .AsAsyncCache() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + lru.Capacity.Should().Be(3); + } + + // 8 + [Fact] + public void WithAtomicAsAsync() { var lru = new ConcurrentLruBuilder() .WithAtomicCreate() + .AsAsyncCache() .WithCapacity(3) .Build(); - lru.Should().BeOfType>(); + lru.Should().BeAssignableTo>(); } + // 9 [Fact] - public void TestComparer() + public void AsAsyncWithAtomic() { - var fastLru = new ConcurrentLruBuilder() - .WithKeyComparer(StringComparer.OrdinalIgnoreCase) + var lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithAtomicCreate() + .WithCapacity(3) .Build(); - fastLru.GetOrAdd("a", k => 1); - fastLru.TryGet("A", out var value).Should().BeTrue(); + lru.Should().BeAssignableTo>(); } + // 10 [Fact] - public void TestConcurrencyLevel() + public void WithAtomicWithScopedAsAsync() { - var b = new ConcurrentLruBuilder() - .WithConcurrencyLevel(-1); + // TODO: this will not resolve a TLru - Action constructor = () => { var x = b.Build(); }; + var lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .WithScopedValues() + .AsAsyncCache() + .WithCapacity(3) + .Build(); - constructor.Should().Throw(); + lru.Should().BeAssignableTo>(); } + // 11 [Fact] - public void TestIntCapacity() + public void WithAtomicAsAsyncWithScoped() { + // TODO: this will not resolve a TLru + var lru = new ConcurrentLruBuilder() + .WithAtomicCreate() + .AsAsyncCache() + .WithScopedValues() .WithCapacity(3) .Build(); - lru.Capacity.Should().Be(3); + lru.Should().BeAssignableTo>(); } + // 12 [Fact] - public void TestPartitionCapacity() + public void WithScopedWithAtomicAsAsync() { + // TODO: this will not resolve a TLru + var lru = new ConcurrentLruBuilder() - .WithCapacity(new FavorFrequencyPartition(6)) + .WithScopedValues() + .WithAtomicCreate() + .AsAsyncCache() + .WithCapacity(3) .Build(); - lru.Capacity.Should().Be(6); - } - - //[Fact] - // public async Task ScopedPOC() - // { - // // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped - - // // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru - // var c = new ConcurrentLru>>(3); - - // // layer 2: optional atomic creation - // var atomic = new AtomicFactoryCache>(c); - - // // layer 3: optional scoping - // IScopedCache scoped = new ScopedCache(atomic); - - // using (var lifetime = await scoped.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable())))) - // { - // var d = lifetime.Value; - // } - // } + lru.Should().BeAssignableTo>(); + } + + // 13 + [Fact] + public void WithScopedAsAsyncWithAtomic() + { + // TODO: this will not resolve a TLru + + var lru = new ConcurrentLruBuilder() + .WithScopedValues() + .AsAsyncCache() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 14 + [Fact] + public void AsAsyncWithScopedWithAtomic() + { + // TODO: this will not resolve a TLru + + var lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithScopedValues() + .WithAtomicCreate() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + // 15 + [Fact] + public void AsAsyncWithAtomicWithScoped() + { + // TODO: this will not resolve a TLru + + var lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .WithAtomicCreate() + .WithScopedValues() + .WithCapacity(3) + .Build(); + + lru.Should().BeAssignableTo>(); + } + + //[Fact] + // public async Task ScopedPOC() + // { + // // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped + + // // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru + // var c = new ConcurrentLru>>(3); + + // // layer 2: optional atomic creation + // var atomic = new AtomicFactoryCache>(c); + + // // layer 3: optional scoping + // IScopedCache scoped = new ScopedCache(atomic); + + // using (var lifetime = await scoped.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable())))) + // { + // var d = lifetime.Value; + // } + // } } } diff --git a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs index 141d41c2..25b15daf 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs @@ -23,21 +23,4 @@ public override IScopedCache Build() return new AtomicFactoryScopedCache(level1); } } - - public class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable - { - private readonly ConcurrentLruBuilder> inner; - - internal ScopedAsyncAtomicLruBuilder(ConcurrentLruBuilder> inner) - : base(inner.info) - { - this.inner = inner; - } - - public override IScopedAsyncCache Build() - { - var level1 = inner.Build() as ICache>; - return new AtomicFactoryScopedAsyncCache(level1); - } - } } diff --git a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs index e445de71..b96b24e5 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; namespace BitFaster.Caching.Lru.Builder { @@ -25,4 +26,44 @@ public override IScopedCache Build() return new ScopedCache(scopedInnerCache); } } + + public sealed class ScopedAsyncLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable + { + private readonly AsyncConcurrentLruBuilder> inner; + + internal ScopedAsyncLruBuilder(AsyncConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + /// + public override IScopedAsyncCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as IAsyncCache>; + + return new ScopedAsyncCache(scopedInnerCache); + } + } + + public sealed class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable + { + private readonly AsyncConcurrentLruBuilder> inner; + + internal ScopedAsyncAtomicLruBuilder(AsyncConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + /// + public override IScopedAsyncCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as ICache>; + + return new AtomicFactoryScopedAsyncCache(scopedInnerCache); + } + } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 91b3431d..d8824b2e 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -52,4 +52,36 @@ public override ICache Build() } } } + + public sealed class AsyncConcurrentLruBuilder : LruBuilderBase, IAsyncCache> + { + /// + /// Creates a ConcurrentLruBuilder. + /// + public AsyncConcurrentLruBuilder() + : base(new LruInfo()) + { + } + + internal AsyncConcurrentLruBuilder(LruInfo info) + : base(info) + { + } + + /// + public override IAsyncCache Build() + { + switch (info) + { + case LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + case LruInfo i when i.WithMetrics && i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + case LruInfo i when i.TimeToExpireAfterWrite.HasValue: + return new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + default: + return new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + } + } + } } diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index eb36b56f..8805d849 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -42,5 +42,52 @@ public static ScopedAtomicLruBuilder WithScopedValues(this AtomicLru var atomicScoped = new ConcurrentLruBuilder>(b.info); return new ScopedAtomicLruBuilder(atomicScoped); } + + public static AsyncConcurrentLruBuilder AsAsyncCache(this ConcurrentLruBuilder builder) + { + return new AsyncConcurrentLruBuilder(builder.info); + } + + public static ScopedAsyncLruBuilder WithScopedValues(this AsyncConcurrentLruBuilder b) where V : IDisposable + { + var asyncScoped = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncLruBuilder(asyncScoped); + } + + public static ScopedAsyncLruBuilder AsAsyncCache(this ScopedLruBuilder> b) where V : IDisposable + { + var asyncScoped = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncLruBuilder(asyncScoped); + } + + public static AsyncAtomicLruBuilder AsAsyncCache(this AtomicLruBuilder b) + { + var a = new ConcurrentLruBuilder>(b.info); + return new AsyncAtomicLruBuilder(a); + } + + public static AsyncAtomicLruBuilder WithAtomicCreate(this AsyncConcurrentLruBuilder b) + { + var a = new ConcurrentLruBuilder>(b.info); + return new AsyncAtomicLruBuilder(a); + } + + public static ScopedAsyncAtomicLruBuilder AsAsyncCache(this ScopedAtomicLruBuilder b) where V : IDisposable + { + var a = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncAtomicLruBuilder(a); + } + + public static ScopedAsyncAtomicLruBuilder WithScopedValues(this AsyncAtomicLruBuilder b) where V : IDisposable + { + var a = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncAtomicLruBuilder(a); + } + + public static ScopedAsyncAtomicLruBuilder WithAtomicCreate(this ScopedAsyncLruBuilder b) where V : IDisposable + { + var a = new AsyncConcurrentLruBuilder>(b.info); + return new ScopedAsyncAtomicLruBuilder(a); + } } } From 92ab5785da004e59f6228ba008f8d8cc01985bd3 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 18:40:02 -0700 Subject: [PATCH 24/27] sac tests --- .../ScopedAsyncCacheTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs index e9cb75cb..1908eedd 100644 --- a/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs +++ b/BitFaster.Caching.UnitTests/ScopedAsyncCacheTests.cs @@ -4,6 +4,8 @@ using System.Text; using System.Threading.Tasks; using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; namespace BitFaster.Caching.UnitTests { @@ -13,5 +15,44 @@ public ScopedAsyncCacheTests() : base(new ScopedAsyncCache(new ConcurrentLru>(capacity))) { } + + [Fact] + public void WhenInnerCacheIsNullCtorThrows() + { + Action constructor = () => { var x = new ScopedAsyncCache(null); }; + + constructor.Should().Throw(); + } + + [Fact] + public async Task WhenScopeIsDisposedTryGetReturnsFalse() + { + var scope = new Scoped(new Disposable()); + + await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); + + scope.Dispose(); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeFalse(); + } + + [Fact] + public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() + { + await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); + + this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); + } + + [Fact] + public async Task GetOrAddAsyncDisposedScopeThrows() + { + var scope = new Scoped(new Disposable()); + scope.Dispose(); + + Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; + + await getOrAdd.Should().ThrowAsync(); + } } } From c8577ffb9dc43cc065aab611e5b3c239c550a074 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 18:59:18 -0700 Subject: [PATCH 25/27] cleanup --- .../Lru/ConcurrentLruBuilderTests.cs | 109 +++++++++++------- ...LruBuilder.cs => AsyncAtomicLruBuilder.cs} | 9 +- .../Lru/Builder/AtomicLruBuilder.cs | 17 --- .../Builder/ScopedAsyncAtomicLruBuilder.cs | 29 +++++ .../Lru/Builder/ScopedAsyncLruBuilder.cs | 28 +++++ .../Lru/Builder/ScopedLruBuilder.cs | 40 ------- .../Synchronized/AtomicFactoryScopedCache.cs | 5 - 7 files changed, 128 insertions(+), 109 deletions(-) rename BitFaster.Caching/Lru/Builder/{AtomicFactoryAsyncLruBuilder.cs => AsyncAtomicLruBuilder.cs} (56%) create mode 100644 BitFaster.Caching/Lru/Builder/ScopedAsyncAtomicLruBuilder.cs create mode 100644 BitFaster.Caching/Lru/Builder/ScopedAsyncLruBuilder.cs diff --git a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs index 6ea36953..7d0a651c 100644 --- a/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/ConcurrentLruBuilderTests.cs @@ -15,7 +15,7 @@ public class ConcurrentLruBuilderTests [Fact] public void TestFastLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .Build(); lru.Should().BeOfType>(); @@ -24,7 +24,7 @@ public void TestFastLru() [Fact] public void TestMetricsLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithMetrics() .Build(); @@ -34,7 +34,7 @@ public void TestMetricsLru() [Fact] public void TestFastTLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) .Build(); @@ -44,7 +44,7 @@ public void TestFastTLru() [Fact] public void TestMetricsTLru() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) .WithMetrics() .Build(); @@ -53,11 +53,56 @@ public void TestMetricsTLru() lru.Capacity.Should().Be(128); } + [Fact] + public void AsAsyncTestFastLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestMetricsLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithMetrics() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestFastTLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + } + + [Fact] + public void AsAsyncTestMetricsTLru() + { + IAsyncCache lru = new ConcurrentLruBuilder() + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) + .WithMetrics() + .AsAsyncCache() + .Build(); + + lru.Should().BeOfType>(); + lru.Capacity.Should().Be(128); + } + [Fact] public void TestComparer() { - var fastLru = new ConcurrentLruBuilder() + ICache fastLru = new ConcurrentLruBuilder() .WithKeyComparer(StringComparer.OrdinalIgnoreCase) .Build(); @@ -79,7 +124,7 @@ public void TestConcurrencyLevel() [Fact] public void TestIntCapacity() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithCapacity(3) .Build(); @@ -89,7 +134,7 @@ public void TestIntCapacity() [Fact] public void TestPartitionCapacity() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithCapacity(new FavorFrequencyPartition(6)) .Build(); @@ -150,7 +195,7 @@ public void TestPartitionCapacity() [Fact] public void WithScopedValues() { - var lru = new ConcurrentLruBuilder() + IScopedCache lru = new ConcurrentLruBuilder() .WithScopedValues() .WithCapacity(3) .Build(); @@ -163,7 +208,7 @@ public void WithScopedValues() [Fact] public void WithAtomicFactory() { - var lru = new ConcurrentLruBuilder() + ICache lru = new ConcurrentLruBuilder() .WithAtomicCreate() .WithCapacity(3) .Build(); @@ -175,7 +220,7 @@ public void WithAtomicFactory() [Fact] public void AsAsync() { - var lru = new ConcurrentLruBuilder() + IAsyncCache lru = new ConcurrentLruBuilder() .AsAsyncCache() .WithCapacity(3) .Build(); @@ -187,7 +232,7 @@ public void AsAsync() [Fact] public void WithAtomicWithScope() { - var lru = new ConcurrentLruBuilder() + IScopedCache lru = new ConcurrentLruBuilder() .WithAtomicCreate() .WithScopedValues() .WithCapacity(3) @@ -201,7 +246,7 @@ public void WithAtomicWithScope() [Fact] public void WithScopedWithAtomic() { - var lru = new ConcurrentLruBuilder() + IScopedCache lru = new ConcurrentLruBuilder() .WithScopedValues() .WithAtomicCreate() .WithCapacity(3) @@ -215,7 +260,7 @@ public void WithScopedWithAtomic() [Fact] public void AsAsyncWithScoped() { - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .AsAsyncCache() .WithScopedValues() .WithCapacity(3) @@ -230,7 +275,7 @@ public void AsAsyncWithScoped() [Fact] public void WithScopedAsAsync() { - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .WithScopedValues() .AsAsyncCache() .WithCapacity(3) @@ -244,7 +289,7 @@ public void WithScopedAsAsync() [Fact] public void WithAtomicAsAsync() { - var lru = new ConcurrentLruBuilder() + IAsyncCache lru = new ConcurrentLruBuilder() .WithAtomicCreate() .AsAsyncCache() .WithCapacity(3) @@ -257,7 +302,7 @@ public void WithAtomicAsAsync() [Fact] public void AsAsyncWithAtomic() { - var lru = new ConcurrentLruBuilder() + IAsyncCache lru = new ConcurrentLruBuilder() .AsAsyncCache() .WithAtomicCreate() .WithCapacity(3) @@ -272,7 +317,7 @@ public void WithAtomicWithScopedAsAsync() { // TODO: this will not resolve a TLru - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .WithAtomicCreate() .WithScopedValues() .AsAsyncCache() @@ -288,7 +333,7 @@ public void WithAtomicAsAsyncWithScoped() { // TODO: this will not resolve a TLru - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .WithAtomicCreate() .AsAsyncCache() .WithScopedValues() @@ -304,7 +349,7 @@ public void WithScopedWithAtomicAsAsync() { // TODO: this will not resolve a TLru - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .WithScopedValues() .WithAtomicCreate() .AsAsyncCache() @@ -320,7 +365,7 @@ public void WithScopedAsAsyncWithAtomic() { // TODO: this will not resolve a TLru - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .WithScopedValues() .AsAsyncCache() .WithAtomicCreate() @@ -336,7 +381,7 @@ public void AsAsyncWithScopedWithAtomic() { // TODO: this will not resolve a TLru - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .AsAsyncCache() .WithScopedValues() .WithAtomicCreate() @@ -352,7 +397,7 @@ public void AsAsyncWithAtomicWithScoped() { // TODO: this will not resolve a TLru - var lru = new ConcurrentLruBuilder() + IScopedAsyncCache lru = new ConcurrentLruBuilder() .AsAsyncCache() .WithAtomicCreate() .WithScopedValues() @@ -361,25 +406,5 @@ public void AsAsyncWithAtomicWithScoped() lru.Should().BeAssignableTo>(); } - - //[Fact] - // public async Task ScopedPOC() - // { - // // Choose from 16 combinations of Lru/TLru, Instrumented/NotInstrumented, Atomic create/not atomic create, scoped/not scoped - - // // layer 1: can choose ConcurrentLru/TLru, FastConcurrentLru/FastConcurrentTLru - // var c = new ConcurrentLru>>(3); - - // // layer 2: optional atomic creation - // var atomic = new AtomicFactoryCache>(c); - - // // layer 3: optional scoping - // IScopedCache scoped = new ScopedCache(atomic); - - // using (var lifetime = await scoped.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable())))) - // { - // var d = lifetime.Value; - // } - // } } } diff --git a/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncAtomicLruBuilder.cs similarity index 56% rename from BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs rename to BitFaster.Caching/Lru/Builder/AsyncAtomicLruBuilder.cs index b10c3776..f92c26c6 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicFactoryAsyncLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AsyncAtomicLruBuilder.cs @@ -7,11 +7,11 @@ namespace BitFaster.Caching.Lru.Builder { - public class AtomicFactoryAsyncLruBuilder : LruBuilderBase, IAsyncCache> + public class AsyncAtomicLruBuilder : LruBuilderBase, IAsyncCache> { private readonly ConcurrentLruBuilder> inner; - internal AtomicFactoryAsyncLruBuilder(ConcurrentLruBuilder> inner) + internal AsyncAtomicLruBuilder(ConcurrentLruBuilder> inner) : base(inner.info) { this.inner = inner; @@ -19,9 +19,8 @@ internal AtomicFactoryAsyncLruBuilder(ConcurrentLruBuilder Build() { - var innerCache = inner.Build(); - - return new AtomicFactoryAsyncCache(innerCache); + var level1 = inner.Build(); + return new AtomicFactoryAsyncCache(level1); } } } diff --git a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs index 4c5e9115..cc1940b5 100644 --- a/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs @@ -23,21 +23,4 @@ public override ICache Build() return new AtomicFactoryCache(level1); } } - - public class AsyncAtomicLruBuilder : LruBuilderBase, IAsyncCache> - { - private readonly ConcurrentLruBuilder> inner; - - internal AsyncAtomicLruBuilder(ConcurrentLruBuilder> inner) - : base(inner.info) - { - this.inner = inner; - } - - public override IAsyncCache Build() - { - var level1 = inner.Build(); - return new AtomicFactoryAsyncCache(level1); - } - } } diff --git a/BitFaster.Caching/Lru/Builder/ScopedAsyncAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAsyncAtomicLruBuilder.cs new file mode 100644 index 00000000..8553f91f --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedAsyncAtomicLruBuilder.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Synchronized; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable + { + private readonly AsyncConcurrentLruBuilder> inner; + + internal ScopedAsyncAtomicLruBuilder(AsyncConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + /// + public override IScopedAsyncCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as ICache>; + + return new AtomicFactoryScopedAsyncCache(scopedInnerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedAsyncLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAsyncLruBuilder.cs new file mode 100644 index 00000000..2b280728 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/ScopedAsyncLruBuilder.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class ScopedAsyncLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable + { + private readonly AsyncConcurrentLruBuilder> inner; + + internal ScopedAsyncLruBuilder(AsyncConcurrentLruBuilder> inner) + : base(inner.info) + { + this.inner = inner; + } + + /// + public override IScopedAsyncCache Build() + { + // this is a legal type conversion due to the generic constraint on W + var scopedInnerCache = inner.Build() as IAsyncCache>; + + return new ScopedAsyncCache(scopedInnerCache); + } + } +} diff --git a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs index b96b24e5..8007103a 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs @@ -26,44 +26,4 @@ public override IScopedCache Build() return new ScopedCache(scopedInnerCache); } } - - public sealed class ScopedAsyncLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable - { - private readonly AsyncConcurrentLruBuilder> inner; - - internal ScopedAsyncLruBuilder(AsyncConcurrentLruBuilder> inner) - : base(inner.info) - { - this.inner = inner; - } - - /// - public override IScopedAsyncCache Build() - { - // this is a legal type conversion due to the generic constraint on W - var scopedInnerCache = inner.Build() as IAsyncCache>; - - return new ScopedAsyncCache(scopedInnerCache); - } - } - - public sealed class ScopedAsyncAtomicLruBuilder : LruBuilderBase, IScopedAsyncCache> where V : IDisposable - { - private readonly AsyncConcurrentLruBuilder> inner; - - internal ScopedAsyncAtomicLruBuilder(AsyncConcurrentLruBuilder> inner) - : base(inner.info) - { - this.inner = inner; - } - - /// - public override IScopedAsyncCache Build() - { - // this is a legal type conversion due to the generic constraint on W - var scopedInnerCache = inner.Build() as ICache>; - - return new AtomicFactoryScopedAsyncCache(scopedInnerCache); - } - } } diff --git a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs index 3e317441..62f85883 100644 --- a/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs +++ b/BitFaster.Caching/Synchronized/AtomicFactoryScopedCache.cs @@ -64,11 +64,6 @@ public Lifetime ScopedGetOrAdd(K key, Func> valueFactory) } } - public Task> ScopedGetOrAddAsync(K key, Func>> valueFactory) - { - throw new NotImplementedException(); - } - public bool ScopedTryGet(K key, out Lifetime lifetime) { if (this.cache.TryGet(key, out var scope)) From 6f86695b0972b9fd3be44e9a15af677240574a3e Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 19:01:52 -0700 Subject: [PATCH 26/27] dead code --- .../ScopedCacheTests.cs | 19 ------------------- .../AtomicFactoryScopedCacheTests.cs | 19 ------------------- 2 files changed, 38 deletions(-) diff --git a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs index 7373c720..2345b48f 100644 --- a/BitFaster.Caching.UnitTests/ScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/ScopedCacheTests.cs @@ -44,14 +44,6 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue() this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); } - //[Fact] - //public async Task WhenKeyDoesNotExistGetOrAddAsyncAddsValue() - //{ - // await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); - - // this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); - //} - [Fact] public void GetOrAddDisposedScopeThrows() { @@ -62,16 +54,5 @@ public void GetOrAddDisposedScopeThrows() getOrAdd.Should().Throw(); } - - //[Fact] - //public async Task GetOrAddAsyncDisposedScopeThrows() - //{ - // var scope = new Scoped(new Disposable()); - // scope.Dispose(); - - // Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; - - // await getOrAdd.Should().ThrowAsync(); - //} } } diff --git a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs index 08eb8034..f56d5896 100644 --- a/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs +++ b/BitFaster.Caching.UnitTests/Synchronized/AtomicFactoryScopedCacheTests.cs @@ -45,14 +45,6 @@ public void WhenKeyDoesNotExistGetOrAddAddsValue() this.cache.ScopedTryGet(1, out var lifetime).Should().BeTrue(); } - //[Fact] - //public async Task WhenKeyDoesNotExistGetOrAddAsyncThrows() - //{ - // Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(new Scoped(new Disposable()))); }; - - // await getOrAdd.Should().ThrowAsync(); - //} - [Fact] public void GetOrAddDisposedScopeThrows() { @@ -63,16 +55,5 @@ public void GetOrAddDisposedScopeThrows() getOrAdd.Should().Throw(); } - - //[Fact] - //public void GetOrAddAsyncDisposedScopeThrows() - //{ - // var scope = new Scoped(new Disposable()); - // scope.Dispose(); - - // Func getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); }; - - // getOrAdd.Should().ThrowAsync(); - //} } } From c3e2323f6520c323c6698efacbc8bdd851478e67 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Sat, 23 Jul 2022 19:08:23 -0700 Subject: [PATCH 27/27] dead code --- .../Lru/Builder/AsyncConcurrentLruBuilder.cs | 32 +++++++++++++++++++ BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 32 ------------------- 2 files changed, 32 insertions(+), 32 deletions(-) create mode 100644 BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs diff --git a/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs new file mode 100644 index 00000000..c47223f9 --- /dev/null +++ b/BitFaster.Caching/Lru/Builder/AsyncConcurrentLruBuilder.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching.Lru.Builder +{ + public sealed class AsyncConcurrentLruBuilder : LruBuilderBase, IAsyncCache> + { + internal AsyncConcurrentLruBuilder(LruInfo info) + : base(info) + { + } + + /// + public override IAsyncCache Build() + { + switch (info) + { + case LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + case LruInfo i when i.WithMetrics && i.TimeToExpireAfterWrite.HasValue: + return new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + case LruInfo i when i.TimeToExpireAfterWrite.HasValue: + return new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); + default: + return new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); + } + } + } +} diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index d8824b2e..91b3431d 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -52,36 +52,4 @@ public override ICache Build() } } } - - public sealed class AsyncConcurrentLruBuilder : LruBuilderBase, IAsyncCache> - { - /// - /// Creates a ConcurrentLruBuilder. - /// - public AsyncConcurrentLruBuilder() - : base(new LruInfo()) - { - } - - internal AsyncConcurrentLruBuilder(LruInfo info) - : base(info) - { - } - - /// - public override IAsyncCache Build() - { - switch (info) - { - case LruInfo i when i.WithMetrics && !i.TimeToExpireAfterWrite.HasValue: - return new ConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); - case LruInfo i when i.WithMetrics && i.TimeToExpireAfterWrite.HasValue: - return new ConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); - case LruInfo i when i.TimeToExpireAfterWrite.HasValue: - return new FastConcurrentTLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer, info.TimeToExpireAfterWrite.Value); - default: - return new FastConcurrentLru(info.ConcurrencyLevel, info.Capacity, info.KeyComparer); - } - } - } }