From 71f8b13b50d3b5ff2a181fee51f1e55858a8ef4d Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sat, 16 May 2020 17:55:35 +0930 Subject: [PATCH] Initial support for forward propagation after a threshold --- .../RedisLockExtension.cs | 4 +- src/CacheTower/CacheEntry.cs | 14 +- src/CacheTower/CacheEntryLifetime.cs | 24 +++ src/CacheTower/CacheSettings.cs | 19 +-- src/CacheTower/CacheStack.cs | 159 +++++++++++------- src/CacheTower/CacheStack{TContext}.cs | 4 +- .../Extensions/ExtensionContainer.cs | 2 +- src/CacheTower/ICacheExtension.cs | 2 +- src/CacheTower/ICacheStack.cs | 8 +- .../Providers/Memory/MemoryCacheLayer.cs | 1 + .../CacheAlternatives_File_Benchmark.cs | 4 +- .../CacheAlternatives_Memory_Benchmark.cs | 2 +- .../CacheAlternatives_Redis_Benchmark.cs | 2 +- .../CacheStackBenchmark.cs | 16 +- .../CacheStackRefreshWaitingBenchmark.cs | 4 +- .../BaseRefreshWrapperExtensionsBenchmark.cs | 2 +- .../RealCostOfCacheStackBenchmark.cs | 4 +- tests/CacheTower.Tests/CacheEntryTests.cs | 6 +- .../CacheStackContextTests.cs | 10 +- tests/CacheTower.Tests/CacheStackTests.cs | 24 +-- .../CacheTower.Tests/CacheTower.Tests.csproj | 2 +- .../Extensions/ExtensionContainerTests.cs | 4 +- .../Redis/RedisLockExtensionTests.cs | 12 +- tests/CacheTower.Tests/TestBase.cs | 2 +- 24 files changed, 201 insertions(+), 130 deletions(-) create mode 100644 src/CacheTower/CacheEntryLifetime.cs diff --git a/src/CacheTower.Extensions.Redis/RedisLockExtension.cs b/src/CacheTower.Extensions.Redis/RedisLockExtension.cs index 75405713..770f8e2c 100644 --- a/src/CacheTower.Extensions.Redis/RedisLockExtension.cs +++ b/src/CacheTower.Extensions.Redis/RedisLockExtension.cs @@ -59,7 +59,7 @@ public void Register(ICacheStack cacheStack) RegisteredStack = cacheStack; } - public async ValueTask> RefreshValueAsync(string cacheKey, Func>> valueProvider, CacheSettings settings) + public async ValueTask> RefreshValueAsync(string cacheKey, Func>> valueProvider, CacheEntryLifetime settings) { var hasLock = await Database.StringSetAsync(cacheKey, RedisValue.EmptyString, expiry: LockTimeout, when: When.NotExists); @@ -82,7 +82,7 @@ public async ValueTask> RefreshValueAsync(string cacheKey, Func } } - private async Task> WaitForResult(string cacheKey, CacheSettings settings) + private async Task> WaitForResult(string cacheKey, CacheEntryLifetime settings) { var delayedResultSource = new TaskCompletionSource(); var waitList = new[] { delayedResultSource }; diff --git a/src/CacheTower/CacheEntry.cs b/src/CacheTower/CacheEntry.cs index 8722cde9..ebb3d209 100644 --- a/src/CacheTower/CacheEntry.cs +++ b/src/CacheTower/CacheEntry.cs @@ -7,7 +7,17 @@ namespace CacheTower { public abstract class CacheEntry { + /// + /// The absolute expiry date of the . + /// public DateTime Expiry { get; } + /// + /// The number of in-memory cache hits the has had. + /// + public int CacheHitCount => _CacheHitCount; + + internal int _CacheHitCount; + internal bool _HasBeenForwardPropagated; protected CacheEntry(DateTime expiry) { @@ -18,9 +28,9 @@ protected CacheEntry(DateTime expiry) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public DateTime GetStaleDate(CacheSettings cacheSettings) + public DateTime GetStaleDate(CacheEntryLifetime entryLifetime) { - return Expiry - cacheSettings.TimeToLive + cacheSettings.StaleAfter; + return Expiry - entryLifetime.TimeToLive + entryLifetime.StaleAfter; } } diff --git a/src/CacheTower/CacheEntryLifetime.cs b/src/CacheTower/CacheEntryLifetime.cs new file mode 100644 index 00000000..840edd35 --- /dev/null +++ b/src/CacheTower/CacheEntryLifetime.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CacheTower +{ + public struct CacheEntryLifetime + { + public TimeSpan TimeToLive { get; } + public TimeSpan StaleAfter { get; } + + public CacheEntryLifetime(TimeSpan timeToLive) + { + TimeToLive = timeToLive; + StaleAfter = TimeSpan.Zero; + } + + public CacheEntryLifetime(TimeSpan timeToLive, TimeSpan staleAfter) + { + TimeToLive = timeToLive; + StaleAfter = staleAfter; + } + } +} diff --git a/src/CacheTower/CacheSettings.cs b/src/CacheTower/CacheSettings.cs index 7820e732..b2fc56f6 100644 --- a/src/CacheTower/CacheSettings.cs +++ b/src/CacheTower/CacheSettings.cs @@ -1,24 +1,15 @@ using System; using System.Collections.Generic; using System.Text; +using CacheTower.Providers.Memory; namespace CacheTower { public struct CacheSettings { - public TimeSpan TimeToLive { get; } - public TimeSpan StaleAfter { get; } - - public CacheSettings(TimeSpan timeToLive) - { - TimeToLive = timeToLive; - StaleAfter = TimeSpan.Zero; - } - - public CacheSettings(TimeSpan timeToLive, TimeSpan staleAfter) - { - TimeToLive = timeToLive; - StaleAfter = staleAfter; - } + /// + /// The number of cache hits before forward propagating from a to higher level caches. + /// + public uint ForwardPropagateAfterXCacheHits { get; set; } } } diff --git a/src/CacheTower/CacheStack.cs b/src/CacheTower/CacheStack.cs index f07b4670..12f2a83c 100644 --- a/src/CacheTower/CacheStack.cs +++ b/src/CacheTower/CacheStack.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using CacheTower.Extensions; +using CacheTower.Providers.Memory; using Microsoft.Extensions.DependencyInjection; namespace CacheTower @@ -89,17 +90,17 @@ public async ValueTask EvictAsync(string cacheKey) } } - public async ValueTask> SetAsync(string cacheKey, T value, TimeSpan timeToLive) + public async ValueTask> SetAsync(string cacheKey, T value, TimeSpan timeToLive, CacheSettings settings = default) { ThrowIfDisposed(); var expiry = DateTime.UtcNow + timeToLive; var cacheEntry = new CacheEntry(value, expiry); - await SetAsync(cacheKey, cacheEntry); + await SetAsync(cacheKey, cacheEntry, settings); return cacheEntry; } - public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry) + public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry, CacheSettings settings = default) { ThrowIfDisposed(); @@ -113,16 +114,23 @@ public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry) throw new ArgumentNullException(nameof(cacheEntry)); } - for (int i = 0, l = CacheLayers.Length; i < l; i++) + if (settings.ForwardPropagateAfterXCacheHits > 0 && CacheLayers[0] is MemoryCacheLayer memoryCacheLayer) { - var layer = CacheLayers[i]; - if (layer is ISyncCacheLayer syncLayerOne) - { - syncLayerOne.Set(cacheKey, cacheEntry); - } - else + memoryCacheLayer.Set(cacheKey, cacheEntry); + } + else + { + for (int i = 0, l = CacheLayers.Length; i < l; i++) { - await (layer as IAsyncCacheLayer).SetAsync(cacheKey, cacheEntry); + var layer = CacheLayers[i]; + if (layer is ISyncCacheLayer syncLayerOne) + { + syncLayerOne.Set(cacheKey, cacheEntry); + } + else + { + await (layer as IAsyncCacheLayer).SetAsync(cacheKey, cacheEntry); + } } } } @@ -201,7 +209,7 @@ public async ValueTask> GetAsync(string cacheKey) return default; } - public async ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheSettings settings) + public async ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheEntryLifetime entryLifetime, CacheSettings settings = default) { ThrowIfDisposed(); @@ -220,13 +228,14 @@ public async ValueTask GetOrSetAsync(string cacheKey, Func> get { var cacheEntry = cacheEntryPoint.CacheEntry; var currentTime = DateTime.UtcNow; - if (cacheEntry.GetStaleDate(settings) < currentTime) + var isStale = cacheEntry.GetStaleDate(entryLifetime) < currentTime; + if (isStale) { if (cacheEntry.Expiry < currentTime) { //Refresh the value in the current thread though short circuit if we're unable to establish a lock //If the lock isn't established, it will instead use the stale cache entry (even if past the allowed stale period) - var refreshedCacheEntry = await RefreshValueAsync(cacheKey, getter, settings, waitForRefresh: false); + var refreshedCacheEntry = await RefreshValueAsync(cacheKey, getter, entryLifetime, settings, waitForRefresh: false); if (refreshedCacheEntry != default) { cacheEntry = refreshedCacheEntry; @@ -235,13 +244,21 @@ public async ValueTask GetOrSetAsync(string cacheKey, Func> get else { //Refresh the value in the background - _ = RefreshValueAsync(cacheKey, getter, settings, waitForRefresh: false); + _ = RefreshValueAsync(cacheKey, getter, entryLifetime, settings, waitForRefresh: false); } } - else if (cacheEntryPoint.LayerIndex > 0) + else { - //If a lower-level cache is missing the latest data, attempt to set it in the background - _ = BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry); + if (cacheEntryPoint.LayerIndex > 0) + { + //If a lower-level cache (eg. a memory cache) is missing the latest data, attempt to set it in the background + _ = BackPropagateCacheEntryAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry); + } + else if (!cacheEntry._HasBeenForwardPropagated && settings.ForwardPropagateAfterXCacheHits > 0 && cacheEntry.CacheHitCount >= settings.ForwardPropagateAfterXCacheHits) + { + //If enabled, we push the local cache entry to higher-level caches, doing so in the background + _ = ForwardPropagateCacheEntryAsync(cacheEntryPoint.LayerIndex + 1, cacheKey, cacheEntry); + } } return cacheEntry.Value; @@ -249,48 +266,34 @@ public async ValueTask GetOrSetAsync(string cacheKey, Func> get else { //Refresh the value in the current thread though because we have no old cache value, we have to lock and wait - return (await RefreshValueAsync(cacheKey, getter, settings, waitForRefresh: true)).Value; + return (await RefreshValueAsync(cacheKey, getter, entryLifetime, settings, waitForRefresh: true)).Value; } } - private async ValueTask BackPopulateCacheAsync(int fromIndexExclusive, string cacheKey, CacheEntry cacheEntry) + private async ValueTask BackPropagateCacheEntryAsync(int fromIndexExclusive, string cacheKey, CacheEntry cacheEntry) { ThrowIfDisposed(); - var hasLock = false; - lock (WaitingKeyRefresh) - { -#if NETSTANDARD2_0 - hasLock = !WaitingKeyRefresh.ContainsKey(cacheKey); - if (hasLock) - { - WaitingKeyRefresh[cacheKey] = Array.Empty>(); - } -#elif NETSTANDARD2_1 - hasLock = WaitingKeyRefresh.TryAdd(cacheKey, Array.Empty>()); -#endif - } - - if (hasLock) + if (TryGetKeyRefreshLock(cacheKey)) { try { for (; --fromIndexExclusive >= 0;) { - var previousLayer = CacheLayers[fromIndexExclusive]; - if (previousLayer is ISyncCacheLayer prevSyncLayer) + var cacheLayer = CacheLayers[fromIndexExclusive]; + if (cacheLayer is ISyncCacheLayer syncLayer) { - if (prevSyncLayer.IsAvailable(cacheKey)) + if (syncLayer.IsAvailable(cacheKey)) { - prevSyncLayer.Set(cacheKey, cacheEntry); + syncLayer.Set(cacheKey, cacheEntry); } } else { - var prevAsyncLayer = previousLayer as IAsyncCacheLayer; - if (await prevAsyncLayer.IsAvailableAsync(cacheKey)) + var asyncCacheLayer = cacheLayer as IAsyncCacheLayer; + if (await asyncCacheLayer.IsAvailableAsync(cacheKey)) { - await prevAsyncLayer.SetAsync(cacheKey, cacheEntry); + await asyncCacheLayer.SetAsync(cacheKey, cacheEntry); } } } @@ -302,25 +305,48 @@ private async ValueTask BackPopulateCacheAsync(int fromIndexExclusive, string } } - private async ValueTask> RefreshValueAsync(string cacheKey, Func> getter, CacheSettings settings, bool waitForRefresh) + private async ValueTask ForwardPropagateCacheEntryAsync(int fromIndexExclusive, string cacheKey, CacheEntry cacheEntry) { ThrowIfDisposed(); - var hasLock = false; - lock (WaitingKeyRefresh) + if (TryGetKeyRefreshLock(cacheKey) && cacheEntry._HasBeenForwardPropagated) { -#if NETSTANDARD2_0 - hasLock = !WaitingKeyRefresh.ContainsKey(cacheKey); - if (hasLock) + try { - WaitingKeyRefresh[cacheKey] = Array.Empty>(); + for (; ++fromIndexExclusive < CacheLayers.Length;) + { + var cacheLayer = CacheLayers[fromIndexExclusive]; + if (cacheLayer is ISyncCacheLayer syncLayer) + { + if (syncLayer.IsAvailable(cacheKey)) + { + syncLayer.Set(cacheKey, cacheEntry); + } + } + else + { + var asyncCacheLayer = cacheLayer as IAsyncCacheLayer; + if (await asyncCacheLayer.IsAvailableAsync(cacheKey)) + { + await asyncCacheLayer.SetAsync(cacheKey, cacheEntry); + } + } + } + + cacheEntry._HasBeenForwardPropagated = true; + } + finally + { + UnlockWaitingTasks(cacheKey, cacheEntry); } -#elif NETSTANDARD2_1 - hasLock = WaitingKeyRefresh.TryAdd(cacheKey, Array.Empty>()); -#endif } + } - if (hasLock) + private async ValueTask> RefreshValueAsync(string cacheKey, Func> getter, CacheEntryLifetime entryLifetime, CacheSettings settings, bool waitForRefresh) + { + ThrowIfDisposed(); + + if (TryGetKeyRefreshLock(cacheKey)) { try { @@ -335,14 +361,14 @@ private async ValueTask> RefreshValueAsync(string cacheKey, Fun } var value = await getter(oldValue); - var refreshedEntry = await SetAsync(cacheKey, value, settings.TimeToLive); + var refreshedEntry = await SetAsync(cacheKey, value, entryLifetime.TimeToLive, settings); - _ = Extensions.OnValueRefreshAsync(cacheKey, settings.TimeToLive); + _ = Extensions.OnValueRefreshAsync(cacheKey, entryLifetime.TimeToLive); UnlockWaitingTasks(cacheKey, refreshedEntry); return refreshedEntry; - }, settings); + }, entryLifetime); } catch { @@ -369,7 +395,7 @@ private async ValueTask> RefreshValueAsync(string cacheKey, Fun //Last minute check to confirm whether waiting is required var currentEntry = await GetAsync(cacheKey); - if (currentEntry != null && currentEntry.GetStaleDate(settings) > DateTime.UtcNow) + if (currentEntry != null && currentEntry.GetStaleDate(entryLifetime) > DateTime.UtcNow) { UnlockWaitingTasks(cacheKey, currentEntry); return currentEntry; @@ -400,6 +426,25 @@ private void UnlockWaitingTasks(string cacheKey, CacheEntry cacheEntry) } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetKeyRefreshLock(string cacheKey) + { + var hasLock = false; + lock (WaitingKeyRefresh) + { +#if NETSTANDARD2_0 + hasLock = !WaitingKeyRefresh.ContainsKey(cacheKey); + if (hasLock) + { + WaitingKeyRefresh[cacheKey] = Array.Empty>(); + } +#elif NETSTANDARD2_1 + hasLock = WaitingKeyRefresh.TryAdd(cacheKey, Array.Empty>()); +#endif + } + return hasLock; + } + #if NETSTANDARD2_0 public void Dispose() { diff --git a/src/CacheTower/CacheStack{TContext}.cs b/src/CacheTower/CacheStack{TContext}.cs index 75af0ec6..e09b8af6 100644 --- a/src/CacheTower/CacheStack{TContext}.cs +++ b/src/CacheTower/CacheStack{TContext}.cs @@ -16,7 +16,7 @@ public CacheStack(Func contextFactory, ICacheLayer[] cacheLayers, ICac ContextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); } - public async ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheSettings settings) + public async ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheEntryLifetime settings, CacheSettings storageSettings = default) { ThrowIfDisposed(); @@ -34,7 +34,7 @@ public async ValueTask GetOrSetAsync(string cacheKey, Func> RefreshValueAsync(string cacheKey, Func>> valueProvider, CacheSettings settings) + public ValueTask> RefreshValueAsync(string cacheKey, Func>> valueProvider, CacheEntryLifetime settings) { if (!HasRefreshWrapperExtension) { diff --git a/src/CacheTower/ICacheExtension.cs b/src/CacheTower/ICacheExtension.cs index cfae51ea..7cc7137a 100644 --- a/src/CacheTower/ICacheExtension.cs +++ b/src/CacheTower/ICacheExtension.cs @@ -17,6 +17,6 @@ public interface IValueRefreshExtension : ICacheExtension public interface IRefreshWrapperExtension : ICacheExtension { - ValueTask> RefreshValueAsync(string cacheKey, Func>> valueProvider, CacheSettings settings); + ValueTask> RefreshValueAsync(string cacheKey, Func>> valueProvider, CacheEntryLifetime settings); } } diff --git a/src/CacheTower/ICacheStack.cs b/src/CacheTower/ICacheStack.cs index 3e077ea9..d7c24e46 100644 --- a/src/CacheTower/ICacheStack.cs +++ b/src/CacheTower/ICacheStack.cs @@ -9,14 +9,14 @@ public interface ICacheStack { ValueTask CleanupAsync(); ValueTask EvictAsync(string cacheKey); - ValueTask> SetAsync(string cacheKey, T value, TimeSpan timeToLive); - ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry); + ValueTask> SetAsync(string cacheKey, T value, TimeSpan timeToLive, CacheSettings storageSettings = default); + ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry, CacheSettings storageSettings = default); ValueTask> GetAsync(string cacheKey); - ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheSettings settings); + ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheEntryLifetime settings, CacheSettings storageSettings = default); } public interface ICacheStack : ICacheStack { - ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheSettings settings); + ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheEntryLifetime settings, CacheSettings storageSettings = default); } } diff --git a/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs b/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs index 66e2fb03..8c63bc12 100644 --- a/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs +++ b/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs @@ -35,6 +35,7 @@ public CacheEntry Get(string cacheKey) { if (Cache.TryGetValue(cacheKey, out var cacheEntry)) { + Interlocked.Increment(ref cacheEntry._CacheHitCount); return cacheEntry as CacheEntry; } diff --git a/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_File_Benchmark.cs b/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_File_Benchmark.cs index d6d9c21d..68ac3c87 100644 --- a/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_File_Benchmark.cs +++ b/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_File_Benchmark.cs @@ -69,7 +69,7 @@ await LoopActionAsync(Iterations, async () => await cacheStack.GetOrSetAsync("GetOrSet_TestKey", (old) => { return Task.FromResult("Hello World"); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); }); } } @@ -86,7 +86,7 @@ await LoopActionAsync(Iterations, async () => await cacheStack.GetOrSetAsync("GetOrSet_TestKey", (old) => { return Task.FromResult("Hello World"); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); }); } } diff --git a/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs b/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs index 375e1de7..07aed876 100644 --- a/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs +++ b/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs @@ -27,7 +27,7 @@ await LoopActionAsync(Iterations, async () => await cacheStack.GetOrSetAsync("GetOrSet_TestKey", (old) => { return Task.FromResult("Hello World"); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); }); } } diff --git a/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Benchmark.cs b/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Benchmark.cs index 8ce0112c..41c23042 100644 --- a/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Benchmark.cs +++ b/tests/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Benchmark.cs @@ -35,7 +35,7 @@ await LoopActionAsync(Iterations, async () => await cacheStack.GetOrSetAsync("GetOrSet_TestKey", (old) => { return Task.FromResult("Hello World"); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); }); } } diff --git a/tests/CacheTower.Benchmarks/CacheStackBenchmark.cs b/tests/CacheTower.Benchmarks/CacheStackBenchmark.cs index 1a06371d..8879a493 100644 --- a/tests/CacheTower.Benchmarks/CacheStackBenchmark.cs +++ b/tests/CacheTower.Benchmarks/CacheStackBenchmark.cs @@ -144,7 +144,7 @@ public async Task GetOrSet_NeverStale() await cacheStack.GetOrSetAsync("GetOrSet", (old) => { return Task.FromResult(12); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); } } } @@ -158,7 +158,7 @@ public async Task GetOrSet_AlwaysStale() await cacheStack.GetOrSetAsync("GetOrSet", (old) => { return Task.FromResult(12); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); } } } @@ -175,12 +175,12 @@ public async Task GetOrSet_TwoSimultaneous() { await Task.Delay(30); return 12; - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); var task2 = cacheStack.GetOrSetAsync("GetOrSet", async (old) => { await Task.Delay(30); return 12; - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); await task1; await task2; @@ -200,22 +200,22 @@ public async Task GetOrSet_FourSimultaneous() { await Task.Delay(30); return 12; - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); var task2 = cacheStack.GetOrSetAsync("GetOrSet", async (old) => { await Task.Delay(30); return 12; - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); var task3 = cacheStack.GetOrSetAsync("GetOrSet", async (old) => { await Task.Delay(30); return 12; - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); var task4 = cacheStack.GetOrSetAsync("GetOrSet", async (old) => { await Task.Delay(30); return 12; - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); await task1; await task2; diff --git a/tests/CacheTower.Benchmarks/CacheStackRefreshWaitingBenchmark.cs b/tests/CacheTower.Benchmarks/CacheStackRefreshWaitingBenchmark.cs index c94c3d12..e346f536 100644 --- a/tests/CacheTower.Benchmarks/CacheStackRefreshWaitingBenchmark.cs +++ b/tests/CacheTower.Benchmarks/CacheStackRefreshWaitingBenchmark.cs @@ -27,7 +27,7 @@ public async ValueTask Benchmark() gettingLockSource.SetResult(true); await continueRefreshSource.Task; return 42; - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); await gettingLockSource.Task; @@ -38,7 +38,7 @@ public async ValueTask Benchmark() var task = CacheStack.GetOrSetAsync("RefreshWaiting", (old) => { return Task.FromResult(99); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); awaitingTasks.Add(task.AsTask()); } diff --git a/tests/CacheTower.Benchmarks/Extensions/BaseRefreshWrapperExtensionsBenchmark.cs b/tests/CacheTower.Benchmarks/Extensions/BaseRefreshWrapperExtensionsBenchmark.cs index 59fa2127..d63ed176 100644 --- a/tests/CacheTower.Benchmarks/Extensions/BaseRefreshWrapperExtensionsBenchmark.cs +++ b/tests/CacheTower.Benchmarks/Extensions/BaseRefreshWrapperExtensionsBenchmark.cs @@ -16,7 +16,7 @@ public async Task RefreshValue() await extension.RefreshValueAsync("RefreshValue", () => { return new ValueTask>(new CacheEntry(5, TimeSpan.FromDays(1))); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); await DisposeOf(extension); } } diff --git a/tests/CacheTower.Benchmarks/RealCostOfCacheStackBenchmark.cs b/tests/CacheTower.Benchmarks/RealCostOfCacheStackBenchmark.cs index 0fccff37..dbc33eac 100644 --- a/tests/CacheTower.Benchmarks/RealCostOfCacheStackBenchmark.cs +++ b/tests/CacheTower.Benchmarks/RealCostOfCacheStackBenchmark.cs @@ -166,7 +166,7 @@ public async Task MemoryCacheLayer_CacheStack_GetOrSet() await cacheStack.GetOrSetAsync("Comparision_" + i, (old) => { return Task.FromResult(1); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); } //Set last 200 (complex type) @@ -181,7 +181,7 @@ await cacheStack.GetOrSetAsync("Comparision_" + i, (old) => ExampleDate = new DateTime(2000, 1, 1), DictionaryOfNumbers = new Dictionary() { { "A", 1 }, { "B", 2 }, { "C", 3 } } }); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); } } } diff --git a/tests/CacheTower.Tests/CacheEntryTests.cs b/tests/CacheTower.Tests/CacheEntryTests.cs index 8ca57112..1ef28c1f 100644 --- a/tests/CacheTower.Tests/CacheEntryTests.cs +++ b/tests/CacheTower.Tests/CacheEntryTests.cs @@ -15,9 +15,9 @@ public void GetStaleDate() { var expiry = DateTime.UtcNow; var entry = new CacheEntry(0, expiry); - Assert.IsTrue(expiry.AddDays(-3) - entry.GetStaleDate(new CacheSettings(TimeSpan.FromDays(3))) < TimeSpan.FromSeconds(1)); - Assert.IsFalse(expiry - entry.GetStaleDate(new CacheSettings(TimeSpan.FromDays(3), TimeSpan.FromDays(2))) < TimeSpan.FromSeconds(1)); - Assert.IsTrue(expiry.AddDays(-1) - entry.GetStaleDate(new CacheSettings(TimeSpan.FromDays(3), TimeSpan.FromDays(2))) < TimeSpan.FromSeconds(1)); + Assert.IsTrue(expiry.AddDays(-3) - entry.GetStaleDate(new CacheEntryLifetime(TimeSpan.FromDays(3))) < TimeSpan.FromSeconds(1)); + Assert.IsFalse(expiry - entry.GetStaleDate(new CacheEntryLifetime(TimeSpan.FromDays(3), TimeSpan.FromDays(2))) < TimeSpan.FromSeconds(1)); + Assert.IsTrue(expiry.AddDays(-1) - entry.GetStaleDate(new CacheEntryLifetime(TimeSpan.FromDays(3), TimeSpan.FromDays(2))) < TimeSpan.FromSeconds(1)); } [TestMethod] diff --git a/tests/CacheTower.Tests/CacheStackContextTests.cs b/tests/CacheTower.Tests/CacheStackContextTests.cs index 61411df3..b0922b00 100644 --- a/tests/CacheTower.Tests/CacheStackContextTests.cs +++ b/tests/CacheTower.Tests/CacheStackContextTests.cs @@ -21,13 +21,13 @@ public void ConstructorThrowsOnNullContextFactory() public async Task GetOrSet_ThrowsOnNullKey() { var cacheStack = new CacheStack(() => null, new[] { new MemoryCacheLayer() }, Array.Empty()); - await cacheStack.GetOrSetAsync(null, (old, context) => Task.FromResult(5), new CacheSettings(TimeSpan.FromDays(1))); + await cacheStack.GetOrSetAsync(null, (old, context) => Task.FromResult(5), new CacheEntryLifetime(TimeSpan.FromDays(1))); } [TestMethod, ExpectedException(typeof(ArgumentNullException))] public async Task GetOrSet_ThrowsOnNullGetter() { var cacheStack = new CacheStack(() => null, new[] { new MemoryCacheLayer() }, Array.Empty()); - await cacheStack.GetOrSetAsync("MyCacheKey", null, new CacheSettings(TimeSpan.FromDays(1))); + await cacheStack.GetOrSetAsync("MyCacheKey", null, new CacheEntryLifetime(TimeSpan.FromDays(1))); } [TestMethod] public async Task GetOrSet_CacheMiss_ContextHasValue() @@ -37,7 +37,7 @@ public async Task GetOrSet_CacheMiss_ContextHasValue() { Assert.AreEqual(123, context); return Task.FromResult(5); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); Assert.AreEqual(5, result); @@ -53,14 +53,14 @@ public async Task GetOrSet_CacheMiss_ContextFactoryCalledEachTime() { Assert.AreEqual(0, context); return Task.FromResult(5); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); Assert.AreEqual(5, result1); var result2 = await cacheStack.GetOrSetAsync("GetOrSet_CacheMiss_ContextFactoryCalledEachTime_2", (oldValue, context) => { Assert.AreEqual(1, context); return Task.FromResult(5); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); Assert.AreEqual(5, result2); await DisposeOf(cacheStack); diff --git a/tests/CacheTower.Tests/CacheStackTests.cs b/tests/CacheTower.Tests/CacheStackTests.cs index 3c431502..b28a7e7b 100644 --- a/tests/CacheTower.Tests/CacheStackTests.cs +++ b/tests/CacheTower.Tests/CacheStackTests.cs @@ -155,13 +155,13 @@ public async Task Set_SetsAllTheLayers() public async Task GetOrSet_ThrowsOnNullKey() { var cacheStack = new CacheStack(new[] { new MemoryCacheLayer() }, Array.Empty()); - await cacheStack.GetOrSetAsync(null, (old) => Task.FromResult(5), new CacheSettings(TimeSpan.FromDays(1))); + await cacheStack.GetOrSetAsync(null, (old) => Task.FromResult(5), new CacheEntryLifetime(TimeSpan.FromDays(1))); } [TestMethod, ExpectedException(typeof(ArgumentNullException))] public async Task GetOrSet_ThrowsOnNullGetter() { var cacheStack = new CacheStack(new[] { new MemoryCacheLayer() }, Array.Empty()); - await cacheStack.GetOrSetAsync("MyCacheKey", null, new CacheSettings(TimeSpan.FromDays(1))); + await cacheStack.GetOrSetAsync("MyCacheKey", null, new CacheEntryLifetime(TimeSpan.FromDays(1))); } [TestMethod] public async Task GetOrSet_CacheMiss() @@ -170,7 +170,7 @@ public async Task GetOrSet_CacheMiss() var result = await cacheStack.GetOrSetAsync("GetOrSet_CacheMiss", (oldValue) => { return Task.FromResult(5); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); Assert.AreEqual(5, result); @@ -185,7 +185,7 @@ public async Task GetOrSet_CacheHit() var result = await cacheStack.GetOrSetAsync("GetOrSet_CacheHit", (oldValue) => { return Task.FromResult(27); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); Assert.AreEqual(17, result); @@ -204,7 +204,7 @@ public async Task GetOrSet_CacheHitBackgroundRefresh() { waitingOnBackgroundTask.TrySetResult(27); return Task.FromResult(27); - }, new CacheSettings(TimeSpan.FromDays(2), TimeSpan.Zero)); + }, new CacheEntryLifetime(TimeSpan.FromDays(2), TimeSpan.Zero)); Assert.AreEqual(17, result); await waitingOnBackgroundTask.Task; @@ -230,7 +230,7 @@ public async Task GetOrSet_BackPropagatesToEarlierCacheLayers() var cacheEntryFromStack = await cacheStack.GetOrSetAsync("GetOrSet_BackPropagatesToEarlierCacheLayers", (old) => { return Task.FromResult(14); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromMinutes(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromMinutes(1))); Assert.AreEqual(cacheEntry.Value, cacheEntryFromStack); @@ -252,7 +252,7 @@ public async Task GetOrSet_CacheHitButAllowedStalePoint() var result = await cacheStack.GetOrSetAsync("GetOrSet_CacheHitButAllowedStalePoint", (oldValue) => { return Task.FromResult(27); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.Zero)); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.Zero)); Assert.AreEqual(27, result); await DisposeOf(cacheStack); @@ -273,7 +273,7 @@ public async Task GetOrSet_ConcurrentStaleCacheHits() request2StartLockSource.SetResult(true); await request1LockSource.Task; return 99; - }, new CacheSettings(TimeSpan.FromDays(2), TimeSpan.Zero)); + }, new CacheEntryLifetime(TimeSpan.FromDays(2), TimeSpan.Zero)); await request2StartLockSource.Task; @@ -282,7 +282,7 @@ public async Task GetOrSet_ConcurrentStaleCacheHits() var request2Result = await cacheStack.GetOrSetAsync("GetOrSet_ConcurrentStaleCacheHits", (oldValue) => { return Task.FromResult(99); - }, new CacheSettings(TimeSpan.FromDays(2), TimeSpan.Zero)); + }, new CacheEntryLifetime(TimeSpan.FromDays(2), TimeSpan.Zero)); //Unlock Request 1 to to continue request1LockSource.SetResult(true); @@ -300,7 +300,7 @@ public async Task GetOrSet_ThrowsOnUseAfterDisposal() var cacheStack = new CacheStack(new[] { new MemoryCacheLayer() }, null); await DisposeOf(cacheStack); - await cacheStack.GetOrSetAsync("KeyDoesntMatter", (old) => Task.FromResult(1), new CacheSettings(TimeSpan.FromDays(1))); + await cacheStack.GetOrSetAsync("KeyDoesntMatter", (old) => Task.FromResult(1), new CacheEntryLifetime(TimeSpan.FromDays(1))); } [TestMethod] public async Task GetOrSet_WaitingForRefresh() @@ -314,7 +314,7 @@ public async Task GetOrSet_WaitingForRefresh() gettingLockSource.SetResult(true); await continueRefreshSource.Task; return 42; - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); await gettingLockSource.Task; @@ -325,7 +325,7 @@ public async Task GetOrSet_WaitingForRefresh() var task = cacheStack.GetOrSetAsync("GetOrSet_WaitingForRefresh", (old) => { return Task.FromResult(99); - }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); awaitingTasks.Add(task.AsTask()); } diff --git a/tests/CacheTower.Tests/CacheTower.Tests.csproj b/tests/CacheTower.Tests/CacheTower.Tests.csproj index 5b5fefbe..cca9a8b8 100644 --- a/tests/CacheTower.Tests/CacheTower.Tests.csproj +++ b/tests/CacheTower.Tests/CacheTower.Tests.csproj @@ -1,7 +1,7 @@  - net461;netcoreapp2.1;netcoreapp3.0 + net461;netcoreapp3.0;netcoreapp3.1 false diff --git a/tests/CacheTower.Tests/Extensions/ExtensionContainerTests.cs b/tests/CacheTower.Tests/Extensions/ExtensionContainerTests.cs index c153fdda..3ac069c5 100644 --- a/tests/CacheTower.Tests/Extensions/ExtensionContainerTests.cs +++ b/tests/CacheTower.Tests/Extensions/ExtensionContainerTests.cs @@ -40,12 +40,12 @@ public async Task RefreshWrapperExtension() var refreshedValue = await container.RefreshValueAsync("WrapperTestCacheKey", () => { return new ValueTask>(cacheEntry); - }, new CacheSettings(TimeSpan.FromDays(1))); + }, new CacheEntryLifetime(TimeSpan.FromDays(1))); refreshWrapperMock.Verify(e => e.Register(cacheStackMock.Object), Times.Once); refreshWrapperMock.Verify(e => e.RefreshValueAsync( "WrapperTestCacheKey", - It.IsAny>>>(), new CacheSettings(TimeSpan.FromDays(1)) + It.IsAny>>>(), new CacheEntryLifetime(TimeSpan.FromDays(1)) ), Times.Once ); diff --git a/tests/CacheTower.Tests/Extensions/Redis/RedisLockExtensionTests.cs b/tests/CacheTower.Tests/Extensions/Redis/RedisLockExtensionTests.cs index 46400c54..10768a9c 100644 --- a/tests/CacheTower.Tests/Extensions/Redis/RedisLockExtensionTests.cs +++ b/tests/CacheTower.Tests/Extensions/Redis/RedisLockExtensionTests.cs @@ -53,7 +53,7 @@ public async Task CustomLockTimeout() lockWaiterTask.SetResult(true); await refreshWaiterTask.Task; return new CacheEntry(5, TimeSpan.FromDays(1)); - }, new CacheSettings(TimeSpan.FromHours(3))); + }, new CacheEntryLifetime(TimeSpan.FromHours(3))); await lockWaiterTask.Task; @@ -98,7 +98,7 @@ await connection.GetSubscriber().SubscribeAsync("CacheTower.CacheLock", (channel var cacheEntry = new CacheEntry(13, TimeSpan.FromDays(1)); await extension.RefreshValueAsync("TestKey", - () => new ValueTask>(cacheEntry), new CacheSettings(TimeSpan.FromDays(1))); + () => new ValueTask>(cacheEntry), new CacheEntryLifetime(TimeSpan.FromDays(1))); var waitTask = taskCompletionSource.Task; await Task.WhenAny(waitTask, Task.Delay(TimeSpan.FromSeconds(10))); @@ -131,7 +131,7 @@ public async Task WaitingTaskInSameInstanceUnlocksAndCompletes() await Task.Delay(3000); return cacheEntry; }, - new CacheSettings(TimeSpan.FromDays(1)) + new CacheEntryLifetime(TimeSpan.FromDays(1)) ).AsTask(); await secondaryTaskKickoff.Task; @@ -141,7 +141,7 @@ public async Task WaitingTaskInSameInstanceUnlocksAndCompletes() { return new ValueTask>(cacheEntry); }, - new CacheSettings(TimeSpan.FromDays(1)) + new CacheEntryLifetime(TimeSpan.FromDays(1)) ).AsTask(); var succeedingTask = await Task.WhenAny(primaryTask, secondaryTask); @@ -177,7 +177,7 @@ public async Task WaitingTaskInDifferentInstanceUnlocksAndCompletes() await Task.Delay(3000); return cacheEntry; }, - new CacheSettings(TimeSpan.FromDays(1)) + new CacheEntryLifetime(TimeSpan.FromDays(1)) ).AsTask(); await secondaryTaskKickoff.Task; @@ -187,7 +187,7 @@ public async Task WaitingTaskInDifferentInstanceUnlocksAndCompletes() { return new ValueTask>(cacheEntry); }, - new CacheSettings(TimeSpan.FromDays(1)) + new CacheEntryLifetime(TimeSpan.FromDays(1)) ).AsTask(); var succeedingTask = await Task.WhenAny(primaryTask, secondaryTask); diff --git a/tests/CacheTower.Tests/TestBase.cs b/tests/CacheTower.Tests/TestBase.cs index fbdfec6c..709dfab7 100644 --- a/tests/CacheTower.Tests/TestBase.cs +++ b/tests/CacheTower.Tests/TestBase.cs @@ -15,7 +15,7 @@ protected static Task DisposeOf(IDisposable disposable) } -#if NETCOREAPP3_0 +#if !NET461 protected static async Task DisposeOf(IAsyncDisposable disposable) { await disposable.DisposeAsync();