From 032a8327adc4615382e4457c92bf6ce4aaa87ca7 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Thu, 30 Jun 2022 17:23:29 -0700 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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 64c358a1433346f499271e6b020bb0b62f0ba608 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 20:13:54 -0700 Subject: [PATCH 12/17] rem atomic --- .../Lru/LruBuilderTests.cs | 57 ----- BitFaster.Caching/AsyncAtomic.cs | 231 ------------------ BitFaster.Caching/Atomic.cs | 155 ------------ BitFaster.Caching/AtomicCacheDecorator.cs | 79 ------ .../Lru/Builder/AtomicLruBuilder.cs | 26 -- .../Lru/Builder/ScopedAtomicLruBuilder.cs | 26 -- .../Lru/ConcurrentLruBuilderExtensions.cs | 19 -- 7 files changed, 593 deletions(-) delete mode 100644 BitFaster.Caching/AsyncAtomic.cs delete mode 100644 BitFaster.Caching/Atomic.cs delete mode 100644 BitFaster.Caching/AtomicCacheDecorator.cs delete mode 100644 BitFaster.Caching/Lru/Builder/AtomicLruBuilder.cs delete mode 100644 BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs diff --git a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs index 09e678be..228e593e 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs @@ -64,43 +64,6 @@ public void TestScopedOnly() lru.Capacity.Should().Be(3); } - [Fact] - public void TestScopedAtomic() - { - var lru = new ConcurrentLruBuilder() - .WithScopedValues() - .WithAtomicCreate() - .WithCapacity(3) - .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] - public void TestAtomic() - { - var lru = new ConcurrentLruBuilder() - .WithAtomicCreate() - .WithCapacity(3) - .Build(); - - lru.Should().BeOfType>(); - } - [Fact] public void TestComparer() { @@ -122,25 +85,5 @@ public void TestConcurrencyLevel() constructor.Should().Throw(); } - - [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/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/AtomicCacheDecorator.cs deleted file mode 100644 index 618becf8..00000000 --- a/BitFaster.Caching/AtomicCacheDecorator.cs +++ /dev/null @@ -1,79 +0,0 @@ -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 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)); - } - - 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/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/ScopedAtomicLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.cs deleted file mode 100644 index c96a6602..00000000 --- a/BitFaster.Caching/Lru/Builder/ScopedAtomicLruBuilder.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 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/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index 2cc0f2be..25e9f202 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -14,24 +14,5 @@ public static ScopedLruBuilder> WithScopedValues(this Conc 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); - } } } From c4c81945ed46a34fc36d7ab2d64da1cd9f81c1f2 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 20:15:16 -0700 Subject: [PATCH 13/17] rem ttl interface --- 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 5b0d2b3907dce1e4b8d753c8a077f4c11fd0a625 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 20:24:10 -0700 Subject: [PATCH 14/17] rem prop --- BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs | 4 ++-- BitFaster.Caching/Lru/Builder/LruBuilderBase.cs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs index 228e593e..a74af8a9 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs @@ -44,7 +44,6 @@ public void TestFastTLru() public void TestMetricsTLru() { var lru = new ConcurrentLruBuilder() - .WithAbosluteExpiry(TimeSpan.FromSeconds(1)) .WithMetrics() .Build(); @@ -53,11 +52,12 @@ public void TestMetricsTLru() } [Fact] - public void TestScopedOnly() + public void TestScoped() { var lru = new ConcurrentLruBuilder() .WithScopedValues() .WithCapacity(3) + .WithAbosluteExpiry(TimeSpan.FromMinutes(1)) .Build(); lru.Should().BeOfType>(); diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index fdac3eb9..4a357e03 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -16,8 +16,6 @@ public LruBuilderBase(LruInfo info) this.info = info; } - public LruInfo Info { get; } - public TBuilder WithCapacity(int capacity) { this.info.Capacity = capacity; From 813630744911d29d2e52f5a188b93decc4e530ff Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 20:24:55 -0700 Subject: [PATCH 15/17] fix test --- BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs index a74af8a9..75c6bd01 100644 --- a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs +++ b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs @@ -44,6 +44,7 @@ public void TestFastTLru() public void TestMetricsTLru() { var lru = new ConcurrentLruBuilder() + .WithAbosluteExpiry(TimeSpan.FromSeconds(1)) .WithMetrics() .Build(); From 975320dfeff2ed51c0840886db8061311310f078 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 21:04:02 -0700 Subject: [PATCH 16/17] docs --- .../Lru/LruBuilderTests.cs | 6 +-- BitFaster.Caching/IScoped.cs | 5 ++- .../Lru/Builder/LruBuilderBase.cs | 37 +++++++++++++++++-- BitFaster.Caching/Lru/Builder/LruInfo.cs | 2 +- .../Lru/Builder/ScopedLruBuilder.cs | 3 +- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 20 +++++++++- .../Lru/ConcurrentLruBuilderExtensions.cs | 12 +++++- 7 files changed, 73 insertions(+), 12 deletions(-) diff --git a/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs b/BitFaster.Caching.UnitTests/Lru/LruBuilderTests.cs index 75c6bd01..95d7f601 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>(); @@ -44,7 +44,7 @@ public void TestFastTLru() public void TestMetricsTLru() { var lru = new ConcurrentLruBuilder() - .WithAbosluteExpiry(TimeSpan.FromSeconds(1)) + .WithExpireAfterWrite(TimeSpan.FromSeconds(1)) .WithMetrics() .Build(); @@ -58,7 +58,7 @@ public void TestScoped() var lru = new ConcurrentLruBuilder() .WithScopedValues() .WithCapacity(3) - .WithAbosluteExpiry(TimeSpan.FromMinutes(1)) + .WithExpireAfterWrite(TimeSpan.FromMinutes(1)) .Build(); lru.Should().BeOfType>(); diff --git a/BitFaster.Caching/IScoped.cs b/BitFaster.Caching/IScoped.cs index 5421803d..5a760055 100644 --- a/BitFaster.Caching/IScoped.cs +++ b/BitFaster.Caching/IScoped.cs @@ -6,7 +6,10 @@ namespace BitFaster.Caching { - // marker interface enables type constraints + /// + /// A marker interface for scopes to enable type constraints. + /// + /// public interface IScoped where T : IDisposable { } } diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index 4a357e03..7c5e42e0 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -6,46 +6,77 @@ namespace BitFaster.Caching.Lru.Builder { - // recursive generic base class + /// + /// Recursive generic base class enables builder inheritance. + /// public abstract class LruBuilderBase where TBuilder : LruBuilderBase { internal readonly LruInfo info; - public LruBuilderBase(LruInfo info) + protected LruBuilderBase(LruInfo info) { this.info = info; } + /// + /// 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. + /// + /// The maximum number of values to keep in the cache. + /// A ConcurrentLruBuilder public TBuilder WithCapacity(int capacity) { this.info.Capacity = capacity; return this as TBuilder; } + /// + /// Use the specified concurrency level. + /// + /// The estimated number of threads that will update the cache concurrently. + /// A ConcurrentLruBuilder public TBuilder WithConcurrencyLevel(int concurrencyLevel) { this.info.ConcurrencyLevel = concurrencyLevel; return this as TBuilder; } + /// + /// Use the specified equality comparison implementation to compare keys. + /// + /// The equality comparison implementation to use when comparing keys. + /// A ConcurrentLruBuilder public TBuilder WithKeyComparer(IEqualityComparer comparer) { this.info.KeyComparer = comparer; return this as TBuilder; } + /// + /// Collect cache metrics, such as Hit rate. Metrics have a small performance penalty. + /// + /// A ConcurrentLruBuilder public TBuilder WithMetrics() { this.info.WithMetrics = true; return this as TBuilder; } - public TBuilder WithAbosluteExpiry(TimeSpan expiration) + /// + /// Evict after a fixed duration since an entry's creation or most recent replacement. + /// + /// The length of time before an entry is automatically removed. + /// A ConcurrentLruBuilder + public TBuilder WithExpireAfterWrite(TimeSpan expiration) { this.info.Expiration = expiration; return this as TBuilder; } + /// + /// Builds a cache configured via the method calls invoked on the builder instance. + /// + /// A cache. public abstract TCacheReturn Build(); } } diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index a1a80dcf..fb36cc4a 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -6,7 +6,7 @@ namespace BitFaster.Caching.Lru.Builder { - public class LruInfo + public sealed class LruInfo { public int Capacity { get; set; } = 128; diff --git a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs index 33ccd251..e445de71 100644 --- a/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs +++ b/BitFaster.Caching/Lru/Builder/ScopedLruBuilder.cs @@ -6,7 +6,7 @@ namespace BitFaster.Caching.Lru.Builder { - public class ScopedLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped + public sealed class ScopedLruBuilder : LruBuilderBase, IScopedCache> where V : IDisposable where W : IScoped { private readonly ConcurrentLruBuilder inner; @@ -16,6 +16,7 @@ internal ScopedLruBuilder(ConcurrentLruBuilder inner) this.inner = inner; } + /// public override IScopedCache Build() { // this is a legal type conversion due to the generic constraint on W diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index 7112914c..ad9be961 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -7,8 +7,25 @@ namespace BitFaster.Caching.Lru { - public class ConcurrentLruBuilder : LruBuilderBase, ICache> + /// + /// A builder of ICache and IScopedCache instances with the following configuration + /// settings: + /// - The maximum size. + /// - The concurrency level. + /// - The key comparer. + /// + /// The following features can be selected which change the underlying cache implementation: + /// - Collect metrics (e.g. hit rate). Small perf penalty. + /// - Time based expiration, measured since write. + /// - Scoped IDisposable values. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + public sealed class ConcurrentLruBuilder : LruBuilderBase, ICache> { + /// + /// Creates a ConcurrentLruBuilder. + /// public ConcurrentLruBuilder() : base(new LruInfo()) { @@ -19,6 +36,7 @@ internal ConcurrentLruBuilder(LruInfo info) { } + /// public override ICache Build() { switch (info) diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs index 25e9f202..0e3dd1c8 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilderExtensions.cs @@ -9,9 +9,17 @@ namespace BitFaster.Caching.Lru { public static class ConcurrentLruBuilderExtensions { - public static ScopedLruBuilder> WithScopedValues(this ConcurrentLruBuilder b) where V : IDisposable + /// + /// Wrap IDisposable values in a lifetime scope. Scoped caches return lifetimes that prevent + /// values from being disposed until the calling code completes. + /// + /// The type of keys in the cache. + /// The type of values in the cache. + /// The ConcurrentLruBuilder to chain method calls onto. + /// A ScopedLruBuilder + public static ScopedLruBuilder> WithScopedValues(this ConcurrentLruBuilder builder) where V : IDisposable { - var scoped = new ConcurrentLruBuilder>(b.info); + var scoped = new ConcurrentLruBuilder>(builder.info); return new ScopedLruBuilder>(scoped); } } From c15806a623baf9be7d3257eaadbc6ba3f94b4751 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Tue, 19 Jul 2022 21:07:07 -0700 Subject: [PATCH 17/17] rename --- BitFaster.Caching/Lru/Builder/LruBuilderBase.cs | 2 +- BitFaster.Caching/Lru/Builder/LruInfo.cs | 2 +- BitFaster.Caching/Lru/ConcurrentLruBuilder.cs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs index 7c5e42e0..bfd8c253 100644 --- a/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs +++ b/BitFaster.Caching/Lru/Builder/LruBuilderBase.cs @@ -69,7 +69,7 @@ public TBuilder WithMetrics() /// A ConcurrentLruBuilder public TBuilder WithExpireAfterWrite(TimeSpan expiration) { - this.info.Expiration = expiration; + this.info.TimeToExpireAfterWrite = expiration; return this as TBuilder; } diff --git a/BitFaster.Caching/Lru/Builder/LruInfo.cs b/BitFaster.Caching/Lru/Builder/LruInfo.cs index fb36cc4a..42581963 100644 --- a/BitFaster.Caching/Lru/Builder/LruInfo.cs +++ b/BitFaster.Caching/Lru/Builder/LruInfo.cs @@ -12,7 +12,7 @@ public sealed class LruInfo public int ConcurrencyLevel { get; set; } = Defaults.ConcurrencyLevel; - public TimeSpan? Expiration { get; set; } = null; + public TimeSpan? TimeToExpireAfterWrite { get; set; } = null; public bool WithMetrics { get; set; } = false; diff --git a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs index ad9be961..91b3431d 100644 --- a/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs +++ b/BitFaster.Caching/Lru/ConcurrentLruBuilder.cs @@ -41,12 +41,12 @@ public override ICache Build() { switch (info) { - case LruInfo i when i.WithMetrics && !i.Expiration.HasValue: + 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.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); + 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); }