diff --git a/BitFaster.Caching.Benchmarks/AsyncLruAtomicBench.cs b/BitFaster.Caching.Benchmarks/AsyncLruAtomicBench.cs new file mode 100644 index 00000000..ef980f48 --- /dev/null +++ b/BitFaster.Caching.Benchmarks/AsyncLruAtomicBench.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net60)] + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] + [MemoryDiagnoser] + public class AsyncLruAtomicBench + { + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> atomicConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + [Benchmark()] + public void ConcurrentDictionary() + { + Func func = x => x; + dictionary.GetOrAdd(1, func); + } + + [Benchmark(Baseline = true)] + public async Task ConcurrentLruAsync() + { + Func> func = x => Task.FromResult(x); + await concurrentLru.GetOrAddAsync(1, func).ConfigureAwait(false); + } + + [Benchmark()] + public async Task AtomicConcurrentLruAsync() + { + Func> func = x => Task.FromResult(x); + await atomicConcurrentLru.GetOrAddAsync(1, func).ConfigureAwait(false); + } + } +} diff --git a/BitFaster.Caching.Benchmarks/AtomicLruBench.cs b/BitFaster.Caching.Benchmarks/AtomicLruBench.cs new file mode 100644 index 00000000..bd19be61 --- /dev/null +++ b/BitFaster.Caching.Benchmarks/AtomicLruBench.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Benchmarks +{ + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net60)] + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] + [MemoryDiagnoser] + public class AtomicLruBench + { + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> atomicConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> lazyConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + [Benchmark()] + public void ConcurrentDictionary() + { + Func func = x => x; + dictionary.GetOrAdd(1, func); + } + + [Benchmark(Baseline = true)] + public void ConcurrentLru() + { + Func func = x => x; + concurrentLru.GetOrAdd(1, func); + } + + [Benchmark()] + public void AtomicConcurrentLru() + { + Func func = x => x; + atomicConcurrentLru.GetOrAdd(1, func); + } + + [Benchmark()] + public void LazyConcurrentLru() + { + Func> func = x => new Lazy(() => x); + lazyConcurrentLru.GetOrAdd(1, func); + } + } +} diff --git a/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs b/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs new file mode 100644 index 00000000..91d57eb2 --- /dev/null +++ b/BitFaster.Caching.Benchmarks/ScopedLruExtBench.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.Benchmarks +{ + //| Method | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Gen 0 | Allocated | + //|---------------------------------------- |-----------:|----------:|----------:|------:|--------:|----------:|-------:|----------:| + //| ConcurrentDictionary | 8.791 ns | 0.0537 ns | 0.0476 ns | 0.48 | 0.00 | 396 B | - | - | + //| ConcurrentLru | 18.429 ns | 0.1539 ns | 0.1440 ns | 1.00 | 0.00 | 701 B | - | - | + //| ScopedConcurrentLruNativeFunc | 117.665 ns | 1.4390 ns | 1.3461 ns | 6.39 | 0.10 | 662 B | 0.0389 | 168 B | + //| ScopedConcurrentLruWrappedFunc | 132.697 ns | 0.6867 ns | 0.5734 ns | 7.19 | 0.08 | 565 B | 0.0610 | 264 B | + //| ScopedConcurrentLruWrappedFuncProtected | 133.997 ns | 0.5089 ns | 0.4249 ns | 7.26 | 0.05 | 621 B | 0.0610 | 264 B | + [SimpleJob(RuntimeMoniker.Net48)] + [SimpleJob(RuntimeMoniker.Net60)] + [DisassemblyDiagnoser(printSource: true, maxDepth: 5)] + [MemoryDiagnoser] + public class ScopedLruExtBench + { + private static readonly ConcurrentDictionary dictionary = new ConcurrentDictionary(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru concurrentLru = new ConcurrentLru(8, 9, EqualityComparer.Default); + + private static readonly ConcurrentLru> scopedConcurrentLru = new ConcurrentLru>(8, 9, EqualityComparer.Default); + + [Benchmark()] + public SomeDisposable ConcurrentDictionary() + { + Func func = x => new SomeDisposable(); + return dictionary.GetOrAdd(1, func); + } + + [Benchmark(Baseline = true)] + public SomeDisposable ConcurrentLru() + { + Func func = x => new SomeDisposable(); + return concurrentLru.GetOrAdd(1, func); + } + + [Benchmark()] + public SomeDisposable ScopedConcurrentLruNativeFunc() + { + // function generates actual cached object (scoped wrapping item) + Func> func = x => new Scoped(new SomeDisposable()); + using (var l = scopedConcurrentLru.ScopedGetOrAdd(1, func)) + { + return l.Value; + } + } + + [Benchmark()] + public SomeDisposable ScopedConcurrentLruWrappedFunc() + { + // function generates item, extension method allocates a closure to create scoped + Func func = x => new SomeDisposable(); + using (var l = scopedConcurrentLru.ScopedGetOrAdd(1, func)) + { + return l.Value; + } + } + + [Benchmark()] + public SomeDisposable ScopedConcurrentLruWrappedFuncProtected() + { + // function generates actual cached object (scoped wrapping item) + Func> func = x => new Scoped(new SomeDisposable()); + using (var l = scopedConcurrentLru.ScopedGetOrAddProtected(1, func)) + { + return l.Value; + } + } + } + + public class SomeDisposable : IDisposable + { + public void Dispose() + { + + } + } +} diff --git a/BitFaster.Caching.UnitTests/DesiredApi.cs b/BitFaster.Caching.UnitTests/DesiredApi.cs new file mode 100644 index 00000000..60cfd01a --- /dev/null +++ b/BitFaster.Caching.UnitTests/DesiredApi.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; + +namespace BitFaster.Caching.UnitTests +{ + // Wrappers needed: + // - Atomic + // - Scoped (already exists) + // - ScopedAtomic + // - AsyncAtomic + // - ScopedAsyncAtomic + // There is no ScopedAsync, since that is just Scoped - the task is not stored so we only need scoped values in the cache. + public class DesiredApi + { + public static void HowToCacheAtomic() + { + var lru = new ConcurrentLru>(4); + + // raw, this is a bit of a mess + var r = lru.GetOrAdd(1, i => new Atomic()).GetValue(1, x => x); + + // extension cleanup can hide it + var rr = lru.GetOrAdd(1, i => i); + + lru.TryUpdate(2, 3); + lru.TryGet(1, out int v); + lru.AddOrUpdate(1, 2); + } + + public static void HowToCacheScoped() + { + var lru = new ConcurrentLru>(4); + + // this is not so clean, because the lambda has to input the scoped object + // if we wrap it, would need a closure inside the extension method. How bad is that? + using (var l = lru.ScopedGetOrAdd(1, x => new Scoped(new SomeDisposable()))) + { + var d = l.Value; + } + } + + public static void HowToCacheScopedAtomic() + { + // ICache> + var scopedAtomicLru = new ConcurrentLru>(5); + + using (var l = scopedAtomicLru.GetOrAdd(1, k => new SomeDisposable())) + { + var d = l.Value; + } + + scopedAtomicLru.TryUpdate(2, new SomeDisposable()); + + scopedAtomicLru.AddOrUpdate(1, new SomeDisposable()); + + // TODO: how to clean this up to 1 line? + if (scopedAtomicLru.TryGetLifetime(1, out var lifetime)) + { + using (lifetime) + { + var x = lifetime.Value; + } + } + } + + public async static Task HowToCacheAsyncAtomic() + { + var asyncAtomicLru = new ConcurrentLru>(5); + + var ar = await asyncAtomicLru.GetOrAddAsync(1, i => Task.FromResult(i)); + + asyncAtomicLru.TryUpdate(2, 3); + asyncAtomicLru.TryGet(1, out int v); + asyncAtomicLru.AddOrUpdate(1, 2); + } + + // Requirements: + // 1. lifetime/value create is async end to end (if async delegate is used to create value) + // 2. value is created lazily, guarantee single instance of object, single invocation of lazy + // 3. lazy value is disposed by scope + // 4. lifetime keeps scope alive + + public static async Task HowToCacheScopedAsyncAtomic() + { + var scopedAsyncAtomicLru = new ConcurrentLru>(4); + Func> valueFactory = k => Task.FromResult(new SomeDisposable()); + + using (var lifetime = await scopedAsyncAtomicLru.GetOrAddAsync(1, valueFactory)) + { + var y = lifetime.Value; + } + + scopedAsyncAtomicLru.TryUpdate(2, new SomeDisposable()); + + scopedAsyncAtomicLru.AddOrUpdate(1, new SomeDisposable()); + + // TODO: how to clean this up to 1 line? + if (scopedAsyncAtomicLru.TryGetLifetime(1, out var lifetime2)) + { + using (lifetime2) + { + var x = lifetime2.Value; + } + } + } + } + + public class SomeDisposable : IDisposable + { + public void Dispose() + { + + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicExtensionsTests.cs new file mode 100644 index 00000000..c971819b --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicExtensionsTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class AsyncAtomicExtensionsTests + { + private ConcurrentLru> lru = new(2, 9, EqualityComparer.Default); + + [Fact] + public async Task GetOrAddAsync() + { + var ar = await lru.GetOrAddAsync(1, i => Task.FromResult(i)); + + ar.Should().Be(1); + + lru.TryGet(1, out int v); + lru.AddOrUpdate(1, 2); + } + + [Fact] + public void TryUpdateWhenKeyDoesNotExistReturnsFalse() + { + lru.TryUpdate(2, 3).Should().BeFalse(); + } + + [Fact] + public void TryUpdateWhenKeyExistsUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryUpdate(1, 42).Should().BeTrue(); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(42); + } + + [Fact] + public void TryGetWhenKeyDoesNotExistReturnsFalse() + { + lru.TryGet(1, out int v).Should().BeFalse(); + } + + [Fact] + public void AddOrUpdateUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(2); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs new file mode 100644 index 00000000..0cc4537d --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AsyncAtomicTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class AsyncAtomicTests + { + [Fact] + public void WhenNotInitializedIsValueCreatedReturnsFalse() + { + AsyncAtomic a = new(); + + a.IsValueCreated.Should().Be(false); + } + + [Fact] + public void WhenNotInitializedValueIfCreatedReturnsDefault() + { + AsyncAtomic a = new(); + + a.ValueIfCreated.Should().Be(0); + } + + [Fact] + public void WhenInitializedByValueIsValueCreatedReturnsTrue() + { + AsyncAtomic a = new(1); + + a.IsValueCreated.Should().Be(true); + } + + [Fact] + public void WhenInitializedByValueValueIfCreatedReturnsValue() + { + AsyncAtomic a = new(1); + + a.ValueIfCreated.Should().Be(1); + } + + [Fact] + public async Task WhenNotInitGetValueReturnsValueFromFactory() + { + AsyncAtomic a = new(); + + int r = await a.GetValueAsync(1, k => Task.FromResult(k + 1)); + r.Should().Be(2); + } + + [Fact] + public async Task WhenInitGetValueReturnsInitialValue() + { + AsyncAtomic a = new(); + + int r1 = await a.GetValueAsync(1, k => Task.FromResult(k + 1)); + int r2 = await a.GetValueAsync(1, k => Task.FromResult(k + 12)); + r2.Should().Be(2); + } + + [Fact] + public async Task WhenGetValueThrowsExceptionIsNotCached() + { + AsyncAtomic a = new(); + + try + { + int r1 = await a.GetValueAsync(1, k => throw new Exception()); + + throw new Exception("Expected GetValueAsync to throw"); + } + catch + { + } + + int r2 = await a.GetValueAsync(1, k => Task.FromResult(k + 2)); + r2.Should().Be(3); + } + + [Fact] + public async Task WhenTaskIsCachedAllWaitersRecieveResult() + { + AsyncAtomic a = new(); + + TaskCompletionSource enterFactory = new TaskCompletionSource(); + TaskCompletionSource exitFactory = new TaskCompletionSource(); + + // Cache the task, don't wait + var t1 = Task.Run(async () => await a.GetValueAsync(1, async k => { enterFactory.SetResult(); await exitFactory.Task; return 42; })); + + await enterFactory.Task; + TaskCompletionSource enter2nd = new TaskCompletionSource(); + + var t2 = Task.Run(async () => { enter2nd.SetResult(); return await a.GetValueAsync(1, k => Task.FromResult(k + 2)); }); + + // there is no good way to synchronize here such that GetValueAsync has definately started running. + // Best we can do is wait for the task to run, then wait 10ms + await enter2nd.Task; + await Task.Delay(TimeSpan.FromMilliseconds(10)); + + exitFactory.SetResult(); + + int r2 = await t2; + r2.Should().Be(42); + } + + [Fact] + public async Task WhenTaskIsCachedAndThrowsAllWaitersRecieveException() + { + AsyncAtomic a = new(); + + TaskCompletionSource enterFactory = new TaskCompletionSource(); + TaskCompletionSource exitFactory = new TaskCompletionSource(); + + // Cache the task, don't wait + var t1 = Task.Run(async () => await a.GetValueAsync(1, async k => { enterFactory.SetResult(); await exitFactory.Task; throw new InvalidOperationException(); })); + + await enterFactory.Task; + + TaskCompletionSource enter2nd = new TaskCompletionSource(); + + var t2 = Task.Run(async () => { enter2nd.SetResult(); return await a.GetValueAsync(1, k => Task.FromResult(k + 2)); }); + + // there is no good way to synchronize here such that GetValueAsync has definately started running. + // Best we can do is wait for the task to run, then wait 10ms + await enter2nd.Task; + await Task.Delay(TimeSpan.FromMilliseconds(10)); + + exitFactory.SetResult(); + + Func r1 = async () => { await t1; }; + Func r2 = async () => { await t2; }; + + r1.Should().Throw(); + r2.Should().Throw(); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs new file mode 100644 index 00000000..8cb4890d --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AtomicExtensionsTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class AtomicExtensionsTests + { + private ConcurrentLru> lru = new(2, 9, EqualityComparer.Default); + + [Fact] + public void GetOrAdd() + { + var rr = lru.GetOrAdd(1, i => i).Should().Be(1); + } + + [Fact] + public void TryUpdateWhenKeyDoesNotExistReturnsFalse() + { + lru.TryUpdate(2, 3).Should().BeFalse(); + } + + [Fact] + public void TryUpdateWhenKeyExistsUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryUpdate(1, 42).Should().BeTrue(); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(42); + } + + [Fact] + public void TryGetWhenKeyDoesNotExistReturnsFalse() + { + lru.TryGet(1, out int v).Should().BeFalse(); + } + + [Fact] + public void AddOrUpdateUpdatesValue() + { + lru.AddOrUpdate(1, 2); + + lru.TryGet(1, out int v).Should().BeTrue(); + v.Should().Be(2); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/AtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/AtomicTests.cs new file mode 100644 index 00000000..72c4ea72 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/AtomicTests.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class AtomicTests + { + [Fact] + public void WhenNotInitializedIsValueCreatedReturnsFalse() + { + Atomic a = new(); + + a.IsValueCreated.Should().Be(false); + } + + [Fact] + public void WhenNotInitializedValueIfCreatedReturnsDefault() + { + Atomic a = new(); + + a.ValueIfCreated.Should().Be(0); + } + + [Fact] + public void WhenInitializedByValueIsValueCreatedReturnsTrue() + { + Atomic a = new(1); + + a.IsValueCreated.Should().Be(true); + } + + [Fact] + public void WhenInitializedByValueValueIfCreatedReturnsValue() + { + Atomic a = new(1); + + a.ValueIfCreated.Should().Be(1); + } + + [Fact] + public void WhenNotInitGetValueReturnsValueFromFactory() + { + Atomic a = new(); + + a.GetValue(1, k => k + 1).Should().Be(2); + } + + [Fact] + public void WhenInitGetValueReturnsInitialValue() + { + Atomic a = new(); + + a.GetValue(1, k => k + 1); + a.GetValue(1, k => k + 2).Should().Be(2); + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedAsyncAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedAsyncAtomicTests.cs new file mode 100644 index 00000000..aef0ac65 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedAsyncAtomicTests.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class ScopedAsyncAtomicTests + { + private readonly ConcurrentLru> lru = new(4); + + [Fact] + public async Task API() + { + var scopedAsyncAtomicLru = new ConcurrentLru>(4); + Func> valueFactory = k => Task.FromResult(new SomeDisposable()); + + using (var lifetime = await scopedAsyncAtomicLru.GetOrAddAsync(1, valueFactory)) + { + var y = lifetime.Value; + } + + scopedAsyncAtomicLru.TryUpdate(2, new SomeDisposable()); + + scopedAsyncAtomicLru.AddOrUpdate(1, new SomeDisposable()); + + // TODO: how to clean this up to 1 line? + if (scopedAsyncAtomicLru.TryGetLifetime(1, out var lifetime2)) + { + using (lifetime2) + { + var x = lifetime2.Value; + } + } + } + + [Fact] + public void WhenScopeIsCreatedThenScopeDisposedLifetimeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + + scope.Dispose(); + scope.Dispose(); // validate double dispose is still single ref count + disposable.IsDisposed.Should().BeFalse(); + + lifetime.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public async Task WhenScopeIsCreatedAsyncThenScopeDisposedLifetimeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + + var r = await scope.TryCreateLifetimeAsync(1, k => Task.FromResult(disposable)); + + r.succeeded.Should().BeTrue(); + + scope.Dispose(); + scope.Dispose(); // validate double dispose is still single ref count + disposable.IsDisposed.Should().BeFalse(); + + r.lifetime.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenScopeIsDisposedCreateScopeAsyncThrows() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + scope.Dispose(); + + scope.Invoking(async s => await s.CreateLifetimeAsync(1, k => Task.FromResult(new Disposable()))).Should().Throw(); + } + + [Fact] + public void WhenScopeIsDisposedTryCreateScopeReturnsFalse() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + scope.Dispose(); + + scope.TryCreateLifetime(out var lifetime).Should().BeFalse(); + } + + [Fact] + public async Task WhenScopeIsDisposedTryCreateScopeAsyncReturnsFalse() + { + var disposable = new Disposable(); + var scope = new ScopedAsyncAtomic(disposable); + scope.Dispose(); + + var r = await scope.TryCreateLifetimeAsync(1, k => Task.FromResult(new Disposable())); + r.succeeded.Should().Be(false); + } + + // TODO: this doesn't work without guard on TryCreate. + // where should value be initialized? + // how does scoped atomic handled value factory throw? + [Fact] + public void WhenValueIsNotCreatedTryCreateScopeReturnsFalse() + { + var scope = new ScopedAsyncAtomic(); + + scope.TryCreateLifetime(out var l).Should().BeFalse(); + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicExtensionsTests.cs new file mode 100644 index 00000000..dc00c670 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicExtensionsTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class ScopedAtomicExtensionsTests + { + private ConcurrentLru> lru = new(2, 9, EqualityComparer.Default); + + [Fact] + public void GetOrAddRetrunsValidLifetime() + { + var valueFactory = new DisposableValueFactory(); + + using (var lifetime = lru.GetOrAdd(1, valueFactory.Create)) + { + lifetime.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void AddOrUpdateUpdatesValue() + { + var d = new Disposable(); + + lru.AddOrUpdate(1, d); + + lru.TryGetLifetime(1, out var lifetime).Should().BeTrue(); + using (lifetime) + { + lifetime.Value.Should().Be(d); + } + } + + [Fact] + public void TryUpdateWhenKeyDoesNotExistReturnsFalse() + { + var d = new Disposable(); + + lru.TryUpdate(1, d).Should().BeFalse(); + } + + [Fact] + public void TryUpdateWhenKeyExistsUpdatesValue() + { + var d1 = new Disposable(); + lru.AddOrUpdate(1, d1); + + var d2 = new Disposable(); + + lru.TryUpdate(1, d2).Should().BeTrue(); + + lru.TryGetLifetime(1, out var lifetime).Should().BeTrue(); + using (lifetime) + { + lifetime.Value.Should().Be(d2); + } + } + + [Fact] + public void TryGetLifetimeDuringRaceReturnsFalse() + { + // directly add an uninitialized ScopedAtomic, simulating catching GetOrAdd before value is created + lru.AddOrUpdate(1, new ScopedAtomic()); + + lru.TryGetLifetime(1, out var lifetime).Should().BeFalse(); + } + + private class DisposableValueFactory + { + public Disposable Disposable { get; } = new Disposable(); + + public Disposable Create(int key) + { + return this.Disposable; + } + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicTests.cs new file mode 100644 index 00000000..79ceca56 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedAtomicTests.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class ScopedAtomicTests + { + [Fact] + public void WhenScopeIsCreatedThenScopeDisposedLifetimeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + + scope.Dispose(); + scope.Dispose(); // validate double dispose is still single ref count + disposable.IsDisposed.Should().BeFalse(); + + lifetime.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenScopeIsCreatedThenLifetimeDisposedScopeDisposesValue() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.TryCreateLifetime(out var lifetime).Should().BeTrue(); + + lifetime.Dispose(); + lifetime.Dispose(); // validate double dispose is still single ref count + + disposable.IsDisposed.Should().BeFalse(); + + scope.Dispose(); + disposable.IsDisposed.Should().BeTrue(); + } + + [Fact] + public void WhenScopeIsDisposedCreateScopeThrows() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.Dispose(); + + scope.Invoking(s => s.CreateLifetime(1, k => new Disposable())).Should().Throw(); + } + + [Fact] + public void WhenScopeIsNotDisposedCreateScopeReturnsLifetime() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + + using (var l = scope.CreateLifetime(1, k => new Disposable())) + { + l.ReferenceCount.Should().Be(1); + } + } + + [Fact] + public void WhenScopeIsDisposedTryCreateScopeReturnsFalse() + { + var disposable = new Disposable(); + var scope = new ScopedAtomic(disposable); + scope.Dispose(); + + scope.TryCreateLifetime(out var l).Should().BeFalse(); + } + + + [Fact] + public void WhenAtomicIsNotCreatedTryCreateScopeReturnsFalse() + { + var scope = new ScopedAtomic(); + + scope.TryCreateLifetime(out var l).Should().BeFalse(); + } + + [Fact] + public void WhenScopedIsCreatedFromCacheItemHasExpectedLifetime() + { + var lru = new ConcurrentLru>(2, 9, EqualityComparer.Default); + var valueFactory = new DisposableValueFactory(); + + using (var lifetime = lru.GetOrAdd(1, valueFactory.Create)) + { + lifetime.Value.IsDisposed.Should().BeFalse(); + } + + valueFactory.Disposable.IsDisposed.Should().BeFalse(); + + lru.TryRemove(1); + + valueFactory.Disposable.IsDisposed.Should().BeTrue(); + } + + private class DisposableValueFactory + { + public Disposable Disposable { get; } = new Disposable(); + + public Disposable Create(int key) + { + return this.Disposable; + } + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/ScopedExtensionsTests.cs b/BitFaster.Caching.UnitTests/Lazy/ScopedExtensionsTests.cs new file mode 100644 index 00000000..dc5b77c4 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/ScopedExtensionsTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitFaster.Caching.Lru; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lazy +{ + public class ScopedExtensionsTests + { + private ConcurrentLru> lru = new(4); + + [Fact] + public void GetOrAddRawRetrunsValidLifetime() + { + using (var l = lru.ScopedGetOrAdd(1, x => new Scoped(new Disposable()))) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void GetOrAddWrappedRetrunsValidLifetime() + { + using (var l = lru.ScopedGetOrAdd(1, x => new Disposable())) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void GetOrAddWrappedProtectedRetrunsValidLifetime() + { + using (var l = lru.ScopedGetOrAddProtected(1, x => new Scoped(new Disposable()))) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + [Fact] + public void GetOrAddWrappedProtectedRejectsDisposedObject() + { + var sd = new Scoped(new Disposable()); + sd.Dispose(); + + lru.Invoking(l => l.ScopedGetOrAddProtected(1, x => sd)).Should().Throw(); + } + + [Fact] + public async Task ScopedGetOrAddAsyncRetrunsValidLifetime() + { + using (var l = await lru.ScopedGetOrAddAsync(1, x => Task.FromResult(new Scoped(new Disposable())))) + { + var d = l.Value.IsDisposed.Should().BeFalse(); + } + } + + private class DisposableValueFactory + { + public Disposable Disposable { get; } = new Disposable(); + + public Disposable Create(int key) + { + return this.Disposable; + } + } + + private class Disposable : IDisposable + { + public bool IsDisposed { get; set; } + + public void Dispose() + { + this.IsDisposed.Should().BeFalse(); + IsDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching.UnitTests/Lazy/SynchronizedTests.cs b/BitFaster.Caching.UnitTests/Lazy/SynchronizedTests.cs new file mode 100644 index 00000000..19e262a7 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lazy/SynchronizedTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using Xunit; +using FluentAssertions; + +namespace BitFaster.Caching.UnitTests +{ + public class SynchronizedTests + { + private int target = 42; + private bool initialized = true; + private object syncLock = new object(); + + [Fact] + public void WhenIsIntializedValueParamIsNotUsed() + { + Synchronized.Initialize(ref target, ref initialized, ref syncLock, 666).Should().Be(42); + } + + [Fact] + public void WhenIsIntializedValueFactoryIsNotUsed() + { + Synchronized.Initialize(ref target, ref initialized, ref syncLock, k => 666, 2).Should().Be(42); + } + } +} diff --git a/BitFaster.Caching/Lazy/AsyncAtomic.cs b/BitFaster.Caching/Lazy/AsyncAtomic.cs new file mode 100644 index 00000000..80062f1a --- /dev/null +++ b/BitFaster.Caching/Lazy/AsyncAtomic.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + [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 async Task GetValueAsync(K key, Func> valueFactory) + { + if (this.initializer == null) + { + return this.value; + } + + return await CreateValueAsync(key, valueFactory).ConfigureAwait(false); + } + + public bool IsValueCreated => this.initializer == null; + + public V ValueIfCreated + { + get + { + if (!this.IsValueCreated) + { + return default; + } + + return this.value; + } + } + + private async Task CreateValueAsync(K key, Func> valueFactory) + { + Initializer init = this.initializer; + + if (init != null) + { + this.value = await init.CreateValue(key, valueFactory).ConfigureAwait(false); + this.initializer = null; + } + + return this.value; + } + + private class Initializer + { + private object syncLock = new object(); + private bool isInitialized; + private Task valueTask; + + public async Task CreateValue(K key, Func> valueFactory) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var synchronizedTask = Synchronized.Initialize(ref this.valueTask, ref isInitialized, ref syncLock, tcs.Task); + + if (ReferenceEquals(synchronizedTask, tcs.Task)) + { + try + { + var value = await valueFactory(key).ConfigureAwait(false); + tcs.SetResult(value); + return value; + } + catch (Exception ex) + { + Volatile.Write(ref isInitialized, false); + tcs.SetException(ex); + throw; + } + } + + return await synchronizedTask.ConfigureAwait(false); + } + } + } +} diff --git a/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs new file mode 100644 index 00000000..665fa1b0 --- /dev/null +++ b/BitFaster.Caching/Lazy/AsyncAtomicCacheExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public static class AsyncAtomicCacheExtensions + { + //public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) + //{ + // return cache + // .GetOrAdd(key, k => new AsyncAtomic()) + // .GetValueAsync(() => Task.FromResult(valueFactory(key))) + // .GetAwaiter().GetResult(); + //} + + public static Task GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) + { + var synchronized = cache.GetOrAdd(key, _ => new AsyncAtomic()); + return synchronized.GetValueAsync(key, valueFactory); + } + + public static void AddOrUpdate(this ICache> cache, K key, V value) + { + cache.AddOrUpdate(key, new AsyncAtomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) + { + return cache.TryUpdate(key, new AsyncAtomic(value)); + } + + public static bool TryGet(this ICache> cache, K key, out V value) + { + AsyncAtomic output; + bool ret = cache.TryGet(key, out output); + + // TOOD: should this return false if the value is not created but the key exists? + // that would indicate a race between GetOrAdd and TryGet, maybe it should return false? + if (ret) + { + value = output.ValueIfCreated; + } + else + { + value = default; + } + + return ret; + } + } +} diff --git a/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs new file mode 100644 index 00000000..8204830c --- /dev/null +++ b/BitFaster.Caching/Lazy/AsyncAtomicLifetime.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public class AsyncAtomicLifetime : IDisposable + { + private readonly Action onDisposeAction; + private readonly ReferenceCount> refCount; + private bool isDisposed; + + /// + /// Initializes a new instance of the AsyncLazyLifetime class. + /// + /// The value to keep alive. + /// The action to perform when the lifetime is terminated. + public AsyncAtomicLifetime(ReferenceCount> value, Action onDisposeAction) + { + this.refCount = value; + this.onDisposeAction = onDisposeAction; + } + + public Task GetValueAsync(K key, Func> valueFactory) + { + return this.refCount.Value.GetValueAsync(key, valueFactory); + } + + public bool IsValueCreated => this.refCount.Value.IsValueCreated; + + /// + /// Gets the value. + /// + public V Value => this.refCount.Value.ValueIfCreated; + + /// + /// Gets the count of Lifetime instances referencing the same value. + /// + public int ReferenceCount => this.refCount.Count; + + /// + /// Terminates the lifetime and performs any cleanup required to release the value. + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.onDisposeAction(); + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/Atomic.cs b/BitFaster.Caching/Lazy/Atomic.cs new file mode 100644 index 00000000..d382b3bd --- /dev/null +++ b/BitFaster.Caching/Lazy/Atomic.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + // SyncedAsync + // SyncedScoped + // SyncedAsyncScoped + //public class Synced + //{ } + + // https://github.com/dotnet/runtime/issues/27421 + // https://github.com/alastairtree/LazyCache/issues/73 + [DebuggerDisplay("IsValueCreated={IsValueCreated}, Value={ValueIfCreated}")] + 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(valueFactory, key); + } + + public bool IsValueCreated => this.initializer == null; + + public V ValueIfCreated + { + get + { + if (!this.IsValueCreated) + { + return default; + } + + return this.value; + } + } + + private V CreateValue(Func valueFactory, K key) + { + Initializer init = this.initializer; + + if (init != null) + { + this.value = init.CreateValue(valueFactory, key); + this.initializer = null; + } + + return this.value; + } + + private class Initializer + { + private object syncLock = new object(); + private bool isInitialized; + private V value; + + public V CreateValue(Func valueFactory, K key) + { + return Synchronized.Initialize(ref this.value, ref isInitialized, ref syncLock, valueFactory, key); + } + } + } +} diff --git a/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs b/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs new file mode 100644 index 00000000..a921d257 --- /dev/null +++ b/BitFaster.Caching/Lazy/AtomicCacheExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public static class AtomicCacheExtensions + { + public static V GetOrAdd(this ICache> cache, K key, Func valueFactory) + { + return cache + .GetOrAdd(key, _ => new Atomic()) + .GetValue(key, valueFactory); + } + + //public static async Task GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) + //{ + // var synchronized = cache.GetOrAdd(key, _ => new Atomic()); + // return synchronized.GetValue(() => valueFactory(key).GetAwaiter().GetResult()); + //} + + public static void AddOrUpdate(this ICache> cache, K key, V value) + { + cache.AddOrUpdate(key, new Atomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) + { + return cache.TryUpdate(key, new Atomic(value)); + } + + public static bool TryGet(this ICache> cache, K key, out V value) + { + Atomic output; + bool ret = cache.TryGet(key, out output); + + if (ret) + { + value = output.ValueIfCreated; + } + else + { + value = default; + } + + return ret; + } + } +} diff --git a/BitFaster.Caching/Lazy/AtomicLifetime.cs b/BitFaster.Caching/Lazy/AtomicLifetime.cs new file mode 100644 index 00000000..3acbc9b3 --- /dev/null +++ b/BitFaster.Caching/Lazy/AtomicLifetime.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public class AtomicLifetime : IDisposable where V : IDisposable + { + private readonly Action onDisposeAction; + private readonly ReferenceCount> refCount; + private bool isDisposed; + + public AtomicLifetime(ReferenceCount> refCount, Action onDisposeAction) + { + this.refCount = refCount; + this.onDisposeAction = onDisposeAction; + } + + public V Value => this.refCount.Value.ValueIfCreated; + + public int ReferenceCount => this.refCount.Count; + + public void Dispose() + { + if (!this.isDisposed) + { + this.onDisposeAction(); + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs new file mode 100644 index 00000000..44c7430d --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomic.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + // Enable caching an AsyncLazy disposable object - guarantee single instance, safe disposal + public class ScopedAsyncAtomic : IDisposable + where V : IDisposable + { + private ReferenceCount> refCount; + private bool isDisposed; + + public ScopedAsyncAtomic() + { + this.refCount = new ReferenceCount>(new AsyncAtomic()); + } + + public ScopedAsyncAtomic(V value) + { + this.refCount = new ReferenceCount>(new AsyncAtomic(value)); + } + + public bool TryCreateLifetime(out AsyncAtomicLifetime lifetime) + { + //if (!this.refCount.Value.IsValueCreated) + //{ + // lifetime = default; + // return false; + //} + + // TODO: exact dupe + while (true) + { + var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + lifetime = default; + return false; + } + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) + { + // When Lifetime is disposed, it calls DecrementReferenceCount + lifetime = new AsyncAtomicLifetime(oldRefCount, this.DecrementReferenceCount); + return true; + } + } + } + + public async Task<(bool succeeded, AsyncAtomicLifetime lifetime)> TryCreateLifetimeAsync(K key, Func> valueFactory) + { + if (!this.refCount.Value.IsValueCreated) + { + return (false, default); + } + + while (true) + { + var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + return (false, default); + } + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) + { + // initialize - + // TOOD: factory can throw so do this before we start counting refs + await oldRefCount.Value.GetValueAsync(key, valueFactory).ConfigureAwait(false); + + // When Lifetime is disposed, it calls DecrementReferenceCount + return (true, new AsyncAtomicLifetime(oldRefCount, this.DecrementReferenceCount)); + } + } + } + + public async Task> CreateLifetimeAsync(K key, Func> valueFactory) + { + var result = await TryCreateLifetimeAsync(key, valueFactory).ConfigureAwait(false); + + if (!result.succeeded) + { + throw new ObjectDisposedException($"{nameof(V)} is disposed."); + } + + return result.lifetime; + } + + private void DecrementReferenceCount() + { + while (true) + { + var oldRefCount = this.refCount; + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.DecrementCopy(), oldRefCount)) + { + if (this.refCount.Count == 0 && this.refCount.Value.IsValueCreated) + { + this.refCount.Value.ValueIfCreated?.Dispose(); + } + + break; + } + } + } + + public void Dispose() + { + if (!this.isDisposed) + { + this.DecrementReferenceCount(); + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs new file mode 100644 index 00000000..5111c386 --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedAsyncAtomicExtensions.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public static class ScopedAsyncAtomicExtensions + { + // If a disposed ScopedAsyncAtomic is added to the cache, this method will get stuck in an infinite loop + //public static async Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable + //{ + // while (true) + // { + // var scope = cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()); + // var result = await scope.TryCreateLifetimeAsync(key, valueFactory).ConfigureAwait(false); + + // if (result.succeeded) + // { + // return result.lifetime; + // } + // } + //} + + public static async Task> GetOrAddAsync(this ICache> cache, K key, Func> valueFactory) where V : IDisposable + { + while (true) + { + var scope = cache.GetOrAdd(key, _ => new ScopedAsyncAtomic()); + + if (scope.TryCreateLifetime(out var lifetime)) + { + // fast path + if (lifetime.IsValueCreated) + { + return lifetime; + } + + // create value, must handle factory method throwing + // TODO: should lifetime have an initilize method? return value never used + try + { + await lifetime.GetValueAsync(key, valueFactory).ConfigureAwait(false); + return lifetime; + } + catch + { + lifetime.Dispose(); + throw; + } + } + } + } + + public static void AddOrUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + cache.AddOrUpdate(key, new ScopedAsyncAtomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + return cache.TryUpdate(key, new ScopedAsyncAtomic(value)); + } + + // TODO: TryGetLifetime? + public static bool TryGetLifetime(this ICache> cache, K key, out AsyncAtomicLifetime value) where V : IDisposable + { + if (cache.TryGet(key, out var scoped)) + { + if (scoped.TryCreateLifetime(out var lifetime)) + { + value = lifetime; + return true; + } + } + + value = default; + return false; + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedAtomic.cs b/BitFaster.Caching/Lazy/ScopedAtomic.cs new file mode 100644 index 00000000..58ba25da --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedAtomic.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public class ScopedAtomic : IDisposable where V : IDisposable + { + private ReferenceCount> refCount; + private bool isDisposed; + + public ScopedAtomic() + { + this.refCount = new ReferenceCount>(new Atomic()); + } + + public ScopedAtomic(V value) + { + this.refCount = new ReferenceCount>(new Atomic(value)); + } + + public bool TryCreateLifetime(K key, Func valueFactory, out AtomicLifetime lifetime) + { + // initialize - factory can throw so do this before we start counting refs + this.refCount.Value.GetValue(key, valueFactory); + + // TODO: exact dupe + while (true) + { + var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + lifetime = default; + return false; + } + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) + { + // When Lifetime is disposed, it calls DecrementReferenceCount + lifetime = new AtomicLifetime(oldRefCount, this.DecrementReferenceCount); + return true; + } + } + } + + public bool TryCreateLifetime(out AtomicLifetime lifetime) + { + if (!this.refCount.Value.IsValueCreated) + { + lifetime = default; + return false; + } + + // TODO: exact dupe + while (true) + { + var oldRefCount = this.refCount; + + // If old ref count is 0, the scoped object has been disposed and there was a race. + if (this.isDisposed || oldRefCount.Count == 0) + { + lifetime = default; + return false; + } + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount)) + { + // When Lifetime is disposed, it calls DecrementReferenceCount + lifetime = new AtomicLifetime(oldRefCount, this.DecrementReferenceCount); + return true; + } + } + } + + public AtomicLifetime CreateLifetime(K key, Func valueFactory) + { + if (!TryCreateLifetime(key, valueFactory, out var lifetime)) + { + throw new ObjectDisposedException($"{nameof(V)} is disposed."); + } + + return lifetime; + } + + private void DecrementReferenceCount() + { + while (true) + { + var oldRefCount = this.refCount; + + if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.DecrementCopy(), oldRefCount)) + { + if (this.refCount.Count == 0) + { + this.refCount.Value.ValueIfCreated?.Dispose(); + } + + break; + } + } + } + + public void Dispose() + { + if (!this.isDisposed) + { + this.DecrementReferenceCount(); + this.isDisposed = true; + } + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs new file mode 100644 index 00000000..dd6e9e3b --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedAtomicExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + public static class ScopedAtomicExtensions + { + // TODO: GetOrAddLifetime? + // If a disposed ScopedAtomic is added to the cache, this method will get stuck in an infinite loop. + // Can this be prevented by making the ScopedAtomic ctor internal so that it can only be created via the ext methods? + public static AtomicLifetime GetOrAdd(this ICache> cache, K key, Func valueFactory) where V : IDisposable + { + while (true) + { + var scope = cache.GetOrAdd(key, _ => new ScopedAtomic()); + + if (scope.TryCreateLifetime(key, valueFactory, out var lifetime)) + { + return lifetime; + } + } + } + + public static void AddOrUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + cache.AddOrUpdate(key, new ScopedAtomic(value)); + } + + public static bool TryUpdate(this ICache> cache, K key, V value) where V : IDisposable + { + return cache.TryUpdate(key, new ScopedAtomic(value)); + } + + // TODO: TryGetLifetime? + public static bool TryGetLifetime(this ICache> cache, K key, out AtomicLifetime value) where V : IDisposable + { + if (cache.TryGet(key, out var scoped)) + { + if (scoped.TryCreateLifetime(out var lifetime)) + { + value = lifetime; + return true; + } + } + + value = default; + return false; + } + } +} diff --git a/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs b/BitFaster.Caching/Lazy/ScopedCacheExtensions.cs new file mode 100644 index 00000000..acd2f2b3 --- /dev/null +++ b/BitFaster.Caching/Lazy/ScopedCacheExtensions.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 static class ScopedCacheExtensions + { + public static Lifetime ScopedGetOrAdd(this ICache> cache, K key, Func> valueFactory) + where T : IDisposable + { + while (true) + { + var scope = cache.GetOrAdd(key, valueFactory); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + + public static Lifetime ScopedGetOrAdd(this ICache> cache, K key, Func valueFactory) + where T : IDisposable + { + while (true) + { + // Note: allocates a closure on every call + var scope = cache.GetOrAdd(key, k => new Scoped(valueFactory(k))); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + + public static Lifetime ScopedGetOrAddProtected(this ICache> cache, K key, Func> valueFactory) + where T : IDisposable + { + int c = 0; + while (true) + { + var scope = cache.GetOrAdd(key, k => valueFactory(k)); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + + if (c++ > 5) + { + throw new InvalidOperationException(); + } + } + } + + public static async Task> ScopedGetOrAddAsync(this ICache> cache, K key, Func>> valueFactory) + where T : IDisposable + { + while (true) + { + var scope = await cache.GetOrAddAsync(key, valueFactory); + + if (scope.TryCreateLifetime(out var lifetime)) + { + return lifetime; + } + } + } + } +} diff --git a/BitFaster.Caching/Lazy/Synchronized.cs b/BitFaster.Caching/Lazy/Synchronized.cs new file mode 100644 index 00000000..aac16d2a --- /dev/null +++ b/BitFaster.Caching/Lazy/Synchronized.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace BitFaster.Caching +{ + internal static class Synchronized + { + public static V Initialize(ref V target, ref bool initialized, ref object syncLock, Func valueFactory, K key) + { + // Fast path + if (Volatile.Read(ref initialized)) + { + return target; + } + + lock (syncLock) + { + if (!Volatile.Read(ref initialized)) + { + target = valueFactory(key); + Volatile.Write(ref initialized, true); + } + } + + return target; + } + + public static V Initialize(ref V target, ref bool initialized, ref object syncLock, V value) + { + // Fast path + if (Volatile.Read(ref initialized)) + { + return target; + } + + lock (syncLock) + { + if (!Volatile.Read(ref initialized)) + { + target = value; + Volatile.Write(ref initialized, true); + } + } + + return target; + } + } +} diff --git a/BitFaster.Caching/Metadata.cs b/BitFaster.Caching/Metadata.cs new file mode 100644 index 00000000..0b99e83f --- /dev/null +++ b/BitFaster.Caching/Metadata.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo(@"BitFaster.Caching.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f55849315b02d525d40701eee5d8eba39e6a517644e8af3fa15141eab7058e76be808e36cfee8d7e071b5aac37bd5e45c67971602680f7bfc26d8c9ebca95dd33b4e3f17a4c28b01268ee6b110ad7e2106ab8ffd1c7be3143192527ce5f639395e46ab086518e881706c6ee9eb96f0263aa34e5152cf5aecf657d463fecf62ca")]