Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DateTime Improvements #58

Merged
merged 7 commits into from
Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,7 +38,7 @@ public async ValueTask CacheTower_MemoryCacheLayer_Direct()
var layer = new MemoryCacheLayer();
await LoopActionAsync(Iterations, async () =>
{
await layer.SetAsync("TestKey", new CacheEntry<int>(123, DateTime.UtcNow + TimeSpan.FromDays(1)));
await layer.SetAsync("TestKey", new CacheEntry<int>(123, TimeSpan.FromDays(1)));
await layer.GetAsync<int>("TestKey");

var getOrSetResult = await layer.GetAsync<string>("GetOrSet_TestKey");
Expand Down
3 changes: 2 additions & 1 deletion src/CacheTower/CacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using CacheTower.Internal;

namespace CacheTower
{
Expand Down Expand Up @@ -55,7 +56,7 @@ public class CacheEntry<T> : CacheEntry, IEquatable<CacheEntry<T>>
/// </summary>
/// <param name="value">The value to cache.</param>
/// <param name="timeToLive">The amount of time before the cache entry expires.</param>
public CacheEntry(T value, TimeSpan timeToLive) : this(value, DateTime.UtcNow + timeToLive) { }
public CacheEntry(T value, TimeSpan timeToLive) : this(value, DateTimeProvider.Now + timeToLive) { }
/// <summary>
/// Creates a new <see cref="CacheEntry"/> with the given <paramref name="value"/> and <paramref name="expiry"/>.
/// </summary>
Expand Down
9 changes: 5 additions & 4 deletions src/CacheTower/CacheStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using CacheTower.Extensions;
using CacheTower.Internal;

namespace CacheTower
{
Expand Down Expand Up @@ -102,7 +103,7 @@ public async ValueTask<CacheEntry<T>> SetAsync<T>(string cacheKey, T value, Time
{
ThrowIfDisposed();

var expiry = DateTime.UtcNow + timeToLive;
var expiry = DateTimeProvider.Now + timeToLive;
var cacheEntry = new CacheEntry<T>(value, expiry);
await SetAsync(cacheKey, cacheEntry);
return cacheEntry;
Expand Down Expand Up @@ -192,7 +193,7 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> get
throw new ArgumentNullException(nameof(getter));
}

var currentTime = DateTime.UtcNow;
var currentTime = DateTimeProvider.Now;
var cacheEntryPoint = await GetWithLayerIndexAsync<T>(cacheKey);
if (cacheEntryPoint != default && cacheEntryPoint.CacheEntry.Expiry > currentTime)
{
Expand Down Expand Up @@ -278,7 +279,7 @@ private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Fun
try
{
var previousEntry = await GetAsync<T>(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.
Expand Down Expand Up @@ -329,7 +330,7 @@ private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Fun

//Last minute check to confirm whether waiting is required
var currentEntry = await GetAsync<T>(cacheKey);
if (currentEntry != null && currentEntry.GetStaleDate(settings) > DateTime.UtcNow)
if (currentEntry != null && currentEntry.GetStaleDate(settings) > DateTimeProvider.Now)
{
UnlockWaitingTasks(cacheKey, currentEntry);
return currentEntry;
Expand Down
29 changes: 29 additions & 0 deletions src/CacheTower/Internal/DateTimeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace CacheTower.Internal
{
internal static class DateTimeProvider
{
/// <summary>
/// The current <see cref="DateTime.UtcNow"/>, updated every second.
/// </summary>
public static DateTime Now { get; private set; } = DateTime.UtcNow;

/// <summary>
/// Updates <see cref="Now"/> to the current <see cref="DateTime.UtcNow"/> value. This is automatically called by a timer every second.
/// </summary>
/// <remarks>
/// 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 <see cref="ICacheStack.GetOrSetAsync{T}(string, Func{T, System.Threading.Tasks.Task{T}}, CacheSettings)"/> 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.
/// </remarks>
[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));
}
}
10 changes: 10 additions & 0 deletions tests/CacheTower.Tests/CacheStackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ public async Task GetOrSet_CacheHit()
await using var cacheStack = new CacheStack(new[] { new MemoryCacheLayer() }, Array.Empty<ICacheExtension>());
await cacheStack.SetAsync("GetOrSet_CacheHit", 17, TimeSpan.FromDays(2));

Internal.DateTimeProvider.UpdateTime();

var result = await cacheStack.GetOrSetAsync<int>("GetOrSet_CacheHit", (oldValue) =>
{
return Task.FromResult(27);
Expand All @@ -245,6 +247,8 @@ public async Task GetOrSet_StaleCacheHit()
var cacheEntry = new CacheEntry<int>(17, DateTime.UtcNow.AddDays(2));
await cacheStack.SetAsync("GetOrSet_StaleCacheHit", cacheEntry);

Internal.DateTimeProvider.UpdateTime();

var refreshWaitSource = new TaskCompletionSource<bool>();

var result = await cacheStack.GetOrSetAsync<int>("GetOrSet_StaleCacheHit", (oldValue) =>
Expand All @@ -271,6 +275,8 @@ public async Task GetOrSet_BackPropagatesToEarlierCacheLayers()
var cacheEntry = new CacheEntry<int>(42, TimeSpan.FromDays(1));
await layer2.SetAsync("GetOrSet_BackPropagatesToEarlierCacheLayers", cacheEntry);

Internal.DateTimeProvider.UpdateTime();

var cacheEntryFromStack = await cacheStack.GetOrSetAsync<int>("GetOrSet_BackPropagatesToEarlierCacheLayers", (old) =>
{
return Task.FromResult(14);
Expand All @@ -291,6 +297,8 @@ public async Task GetOrSet_ConcurrentStaleCacheHits_OnlyOneRefresh()
var cacheEntry = new CacheEntry<int>(23, DateTime.UtcNow.AddDays(2));
await cacheStack.SetAsync("GetOrSet_ConcurrentStaleCacheHits_OnlyOneRefresh", cacheEntry);

Internal.DateTimeProvider.UpdateTime();

var refreshWaitSource = new TaskCompletionSource<bool>();
var getterCallCount = 0;

Expand Down Expand Up @@ -339,6 +347,8 @@ public async Task GetOrSet_WaitingForRefresh()

var awaitingTasks = new List<Task<int>>();

Internal.DateTimeProvider.UpdateTime();

for (var i = 0; i < 3; i++)
{
var task = cacheStack.GetOrSetAsync<int>("GetOrSet_WaitingForRefresh", (old) =>
Expand Down