Skip to content

Commit 082e9cd

Browse files
authored
Merge pull request #196 from TurnerSoftware/detailed-cache-entry-status
Prevent returning expired entries
2 parents d251195 + 879df6d commit 082e9cd

3 files changed

Lines changed: 67 additions & 16 deletions

File tree

src/CacheTower/CacheEntryStatus.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace CacheTower
6+
{
7+
/// <summary>
8+
/// Describes the status of the entry - whether it is expired, stale or access to an entry was a miss.
9+
/// </summary>
10+
internal enum CacheEntryStatus
11+
{
12+
/// <summary>
13+
/// When the cache entry is considered stale.
14+
/// </summary>
15+
Stale,
16+
/// <summary>
17+
/// When the cache entry is considered expired.
18+
/// </summary>
19+
Expired,
20+
/// <summary>
21+
/// When no cache entry was found.
22+
/// </summary>
23+
Miss
24+
}
25+
}

src/CacheTower/CacheStack.cs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -203,26 +203,34 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> get
203203

204204
var currentTime = DateTimeProvider.Now;
205205
var cacheEntryPoint = await GetWithLayerIndexAsync<T>(cacheKey);
206-
if (cacheEntryPoint != default && cacheEntryPoint.CacheEntry.Expiry > currentTime)
206+
if (cacheEntryPoint != default)
207207
{
208-
var cacheEntry = cacheEntryPoint.CacheEntry;
209-
if (settings.StaleAfter.HasValue && cacheEntry.GetStaleDate(settings) < currentTime)
208+
if (cacheEntryPoint.CacheEntry.Expiry > currentTime)
210209
{
211-
//If the cache entry is stale, refresh the value in the background
212-
_ = RefreshValueAsync(cacheKey, getter, settings, noExistingValueAvailable: false);
210+
var cacheEntry = cacheEntryPoint.CacheEntry;
211+
if (settings.StaleAfter.HasValue && cacheEntry.GetStaleDate(settings) < currentTime)
212+
{
213+
//If the cache entry is stale, refresh the value in the background
214+
_ = RefreshValueAsync(cacheKey, getter, settings, CacheEntryStatus.Stale);
215+
}
216+
else if (cacheEntryPoint.LayerIndex > 0)
217+
{
218+
//If a lower-level cache is missing the latest data, attempt to set it in the background
219+
_ = BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry);
220+
}
221+
222+
return cacheEntry.Value!;
213223
}
214-
else if (cacheEntryPoint.LayerIndex > 0)
224+
else
215225
{
216-
//If a lower-level cache is missing the latest data, attempt to set it in the background
217-
_ = BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry);
226+
//Refresh the value in the current thread because we only have expired data (we never return expired data)
227+
return (await RefreshValueAsync(cacheKey, getter, settings, CacheEntryStatus.Expired))!.Value!;
218228
}
219-
220-
return cacheEntry.Value!;
221229
}
222230
else
223231
{
224-
//Refresh the value in the current thread though because we have no old cache value, we have to lock and wait
225-
return (await RefreshValueAsync(cacheKey, getter, settings, noExistingValueAvailable: true))!.Value!;
232+
//Refresh the value in the current thread because we have no existing data
233+
return (await RefreshValueAsync(cacheKey, getter, settings, CacheEntryStatus.Miss))!.Value!;
226234
}
227235
}
228236

@@ -264,7 +272,7 @@ private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string
264272
}
265273
}
266274

267-
private async ValueTask<CacheEntry<T>?> RefreshValueAsync<T>(string cacheKey, Func<T, Task<T>> getter, CacheSettings settings, bool noExistingValueAvailable)
275+
private async ValueTask<CacheEntry<T>?> RefreshValueAsync<T>(string cacheKey, Func<T, Task<T>> getter, CacheSettings settings, CacheEntryStatus entryStatus)
268276
{
269277
ThrowIfDisposed();
270278

@@ -287,7 +295,7 @@ private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string
287295
try
288296
{
289297
var previousEntry = await GetAsync<T>(cacheKey);
290-
if (previousEntry != default && noExistingValueAvailable && previousEntry.Expiry < DateTimeProvider.Now)
298+
if (previousEntry != default && entryStatus == CacheEntryStatus.Miss && previousEntry.Expiry >= DateTimeProvider.Now)
291299
{
292300
//The Cache Stack will always return an unexpired value if one exists.
293301
//If we are told to refresh because one doesn't and we find one, we return the existing value, ignoring the refresh.
@@ -307,7 +315,11 @@ private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string
307315

308316
var value = await getter(oldValue!);
309317
var refreshedEntry = new CacheEntry<T>(value, settings.TimeToLive);
310-
var cacheUpdateType = noExistingValueAvailable ? CacheUpdateType.AddEntry : CacheUpdateType.AddOrUpdateEntry;
318+
var cacheUpdateType = entryStatus switch
319+
{
320+
CacheEntryStatus.Miss => CacheUpdateType.AddEntry,
321+
_ => CacheUpdateType.AddOrUpdateEntry
322+
};
311323
await InternalSetAsync(cacheKey, refreshedEntry, cacheUpdateType);
312324

313325
UnlockWaitingTasks(cacheKey, refreshedEntry);
@@ -321,7 +333,7 @@ private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string
321333
throw;
322334
}
323335
}
324-
else if (noExistingValueAvailable)
336+
else if (entryStatus != CacheEntryStatus.Stale)
325337
{
326338
TaskCompletionSource<CacheEntry> completionSource;
327339

tests/CacheTower.Tests/CacheStackTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,5 +451,19 @@ public async Task GetOrSet_WaitingForRefresh()
451451
Assert.AreEqual(42, await awaitingTasks[1]);
452452
Assert.AreEqual(42, await awaitingTasks[2]);
453453
}
454+
[TestMethod]
455+
public async Task GetOrSet_ExpiredCacheHit()
456+
{
457+
await using var cacheStack = new CacheStack(new[] { new MemoryCacheLayer() }, Array.Empty<ICacheExtension>());
458+
await cacheStack.SetAsync("GetOrSet_CacheHit", 17, TimeSpan.FromDays(-1));
459+
460+
var result = await cacheStack.GetOrSetAsync<int>("GetOrSet_CacheHit", (oldValue) =>
461+
{
462+
Assert.AreEqual(17, oldValue);
463+
return Task.FromResult(27);
464+
}, new CacheSettings(TimeSpan.FromDays(1)));
465+
466+
Assert.AreEqual(27, result);
467+
}
454468
}
455469
}

0 commit comments

Comments
 (0)