diff --git a/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs b/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs index 1edf0fa8..1521ec4f 100644 --- a/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs +++ b/benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using CacheManager.Core; +using CacheTower.Internal; using CacheTower.Providers.Memory; using EasyCaching.InMemory; using LazyCache; @@ -37,7 +38,7 @@ public async ValueTask CacheTower_MemoryCacheLayer_Direct() var layer = new MemoryCacheLayer(); await LoopActionAsync(Iterations, async () => { - await layer.SetAsync("TestKey", new CacheEntry(123, DateTime.UtcNow + TimeSpan.FromDays(1))); + await layer.SetAsync("TestKey", new CacheEntry(123, TimeSpan.FromDays(1))); await layer.GetAsync("TestKey"); var getOrSetResult = await layer.GetAsync("GetOrSet_TestKey"); diff --git a/src/CacheTower/CacheEntry.cs b/src/CacheTower/CacheEntry.cs index c16f2aa3..717df318 100644 --- a/src/CacheTower/CacheEntry.cs +++ b/src/CacheTower/CacheEntry.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; +using CacheTower.Internal; namespace CacheTower { @@ -55,7 +56,7 @@ public class CacheEntry : CacheEntry, IEquatable> /// /// The value to cache. /// The amount of time before the cache entry expires. - public CacheEntry(T value, TimeSpan timeToLive) : this(value, DateTime.UtcNow + timeToLive) { } + public CacheEntry(T value, TimeSpan timeToLive) : this(value, DateTimeProvider.Now + timeToLive) { } /// /// Creates a new with the given and . /// diff --git a/src/CacheTower/CacheStack.cs b/src/CacheTower/CacheStack.cs index 81eaa0ea..59a2bc65 100644 --- a/src/CacheTower/CacheStack.cs +++ b/src/CacheTower/CacheStack.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using CacheTower.Extensions; +using CacheTower.Internal; namespace CacheTower { @@ -102,7 +103,7 @@ public async ValueTask> SetAsync(string cacheKey, T value, Time { ThrowIfDisposed(); - var expiry = DateTime.UtcNow + timeToLive; + var expiry = DateTimeProvider.Now + timeToLive; var cacheEntry = new CacheEntry(value, expiry); await SetAsync(cacheKey, cacheEntry); return cacheEntry; @@ -192,7 +193,7 @@ public async ValueTask GetOrSetAsync(string cacheKey, Func> get throw new ArgumentNullException(nameof(getter)); } - var currentTime = DateTime.UtcNow; + var currentTime = DateTimeProvider.Now; var cacheEntryPoint = await GetWithLayerIndexAsync(cacheKey); if (cacheEntryPoint != default && cacheEntryPoint.CacheEntry.Expiry > currentTime) { @@ -278,7 +279,7 @@ private async ValueTask> RefreshValueAsync(string cacheKey, Fun try { var previousEntry = await GetAsync(cacheKey); - if (previousEntry != default && noExistingValueAvailable && previousEntry.Expiry < DateTime.UtcNow) + if (previousEntry != default && noExistingValueAvailable && previousEntry.Expiry < DateTimeProvider.Now) { //The Cache Stack will always return an unexpired value if one exists. //If we are told to refresh because one doesn't and we find one, we return the existing value, ignoring the refresh. @@ -329,7 +330,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(settings) > DateTimeProvider.Now) { UnlockWaitingTasks(cacheKey, currentEntry); return currentEntry; diff --git a/src/CacheTower/Internal/DateTimeProvider.cs b/src/CacheTower/Internal/DateTimeProvider.cs new file mode 100644 index 00000000..1e98951f --- /dev/null +++ b/src/CacheTower/Internal/DateTimeProvider.cs @@ -0,0 +1,29 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace CacheTower.Internal +{ + internal static class DateTimeProvider + { + /// + /// The current , updated every second. + /// + public static DateTime Now { get; private set; } = DateTime.UtcNow; + + /// + /// Updates to the current value. This is automatically called by a timer every second. + /// + /// + /// This is intended to only be triggered by the internal timer or by unit tests that require it. + /// The reason why tests need it is due to the fast turn around of setting a value and testing the outcome. + /// Real applications aren't immediately setting a cache value manually, calling and then comparing whether the results are the same. + /// The alternative for the tests is just "waiting" an extra second between setting a value and retrieving it however that makes the testing slower and the tests more confusing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UpdateTime() => Now = DateTime.UtcNow; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Establishes timer and prevents it being garbage collected")] + private static readonly Timer DateTimeTimer = new(state => UpdateTime(), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); + } +} diff --git a/tests/CacheTower.Tests/CacheStackTests.cs b/tests/CacheTower.Tests/CacheStackTests.cs index 46340a35..2575f8f1 100644 --- a/tests/CacheTower.Tests/CacheStackTests.cs +++ b/tests/CacheTower.Tests/CacheStackTests.cs @@ -231,6 +231,8 @@ public async Task GetOrSet_CacheHit() await using var cacheStack = new CacheStack(new[] { new MemoryCacheLayer() }, Array.Empty()); await cacheStack.SetAsync("GetOrSet_CacheHit", 17, TimeSpan.FromDays(2)); + Internal.DateTimeProvider.UpdateTime(); + var result = await cacheStack.GetOrSetAsync("GetOrSet_CacheHit", (oldValue) => { return Task.FromResult(27); @@ -245,6 +247,8 @@ public async Task GetOrSet_StaleCacheHit() var cacheEntry = new CacheEntry(17, DateTime.UtcNow.AddDays(2)); await cacheStack.SetAsync("GetOrSet_StaleCacheHit", cacheEntry); + Internal.DateTimeProvider.UpdateTime(); + var refreshWaitSource = new TaskCompletionSource(); var result = await cacheStack.GetOrSetAsync("GetOrSet_StaleCacheHit", (oldValue) => @@ -271,6 +275,8 @@ public async Task GetOrSet_BackPropagatesToEarlierCacheLayers() var cacheEntry = new CacheEntry(42, TimeSpan.FromDays(1)); await layer2.SetAsync("GetOrSet_BackPropagatesToEarlierCacheLayers", cacheEntry); + Internal.DateTimeProvider.UpdateTime(); + var cacheEntryFromStack = await cacheStack.GetOrSetAsync("GetOrSet_BackPropagatesToEarlierCacheLayers", (old) => { return Task.FromResult(14); @@ -291,6 +297,8 @@ public async Task GetOrSet_ConcurrentStaleCacheHits_OnlyOneRefresh() var cacheEntry = new CacheEntry(23, DateTime.UtcNow.AddDays(2)); await cacheStack.SetAsync("GetOrSet_ConcurrentStaleCacheHits_OnlyOneRefresh", cacheEntry); + Internal.DateTimeProvider.UpdateTime(); + var refreshWaitSource = new TaskCompletionSource(); var getterCallCount = 0; @@ -339,6 +347,8 @@ public async Task GetOrSet_WaitingForRefresh() var awaitingTasks = new List>(); + Internal.DateTimeProvider.UpdateTime(); + for (var i = 0; i < 3; i++) { var task = cacheStack.GetOrSetAsync("GetOrSet_WaitingForRefresh", (old) =>