Skip to content

Commit 71f8b13

Browse files
committed
Initial support for forward propagation after a threshold
1 parent cc7cd0c commit 71f8b13

24 files changed

+201
-130
lines changed

src/CacheTower.Extensions.Redis/RedisLockExtension.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public void Register(ICacheStack cacheStack)
5959
RegisteredStack = cacheStack;
6060
}
6161

62-
public async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Func<ValueTask<CacheEntry<T>>> valueProvider, CacheSettings settings)
62+
public async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Func<ValueTask<CacheEntry<T>>> valueProvider, CacheEntryLifetime settings)
6363
{
6464
var hasLock = await Database.StringSetAsync(cacheKey, RedisValue.EmptyString, expiry: LockTimeout, when: When.NotExists);
6565

@@ -82,7 +82,7 @@ public async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Func
8282
}
8383
}
8484

85-
private async Task<CacheEntry<T>> WaitForResult<T>(string cacheKey, CacheSettings settings)
85+
private async Task<CacheEntry<T>> WaitForResult<T>(string cacheKey, CacheEntryLifetime settings)
8686
{
8787
var delayedResultSource = new TaskCompletionSource<bool>();
8888
var waitList = new[] { delayedResultSource };

src/CacheTower/CacheEntry.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@ namespace CacheTower
77
{
88
public abstract class CacheEntry
99
{
10+
/// <summary>
11+
/// The absolute expiry date of the <see cref="CacheEntry"/>.
12+
/// </summary>
1013
public DateTime Expiry { get; }
14+
/// <summary>
15+
/// The number of in-memory cache hits the <see cref="CacheEntry"/> has had.
16+
/// </summary>
17+
public int CacheHitCount => _CacheHitCount;
18+
19+
internal int _CacheHitCount;
20+
internal bool _HasBeenForwardPropagated;
1121

1222
protected CacheEntry(DateTime expiry)
1323
{
@@ -18,9 +28,9 @@ protected CacheEntry(DateTime expiry)
1828
}
1929

2030
[MethodImpl(MethodImplOptions.AggressiveInlining)]
21-
public DateTime GetStaleDate(CacheSettings cacheSettings)
31+
public DateTime GetStaleDate(CacheEntryLifetime entryLifetime)
2232
{
23-
return Expiry - cacheSettings.TimeToLive + cacheSettings.StaleAfter;
33+
return Expiry - entryLifetime.TimeToLive + entryLifetime.StaleAfter;
2434
}
2535
}
2636

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace CacheTower
6+
{
7+
public struct CacheEntryLifetime
8+
{
9+
public TimeSpan TimeToLive { get; }
10+
public TimeSpan StaleAfter { get; }
11+
12+
public CacheEntryLifetime(TimeSpan timeToLive)
13+
{
14+
TimeToLive = timeToLive;
15+
StaleAfter = TimeSpan.Zero;
16+
}
17+
18+
public CacheEntryLifetime(TimeSpan timeToLive, TimeSpan staleAfter)
19+
{
20+
TimeToLive = timeToLive;
21+
StaleAfter = staleAfter;
22+
}
23+
}
24+
}

src/CacheTower/CacheSettings.cs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Text;
4+
using CacheTower.Providers.Memory;
45

56
namespace CacheTower
67
{
78
public struct CacheSettings
89
{
9-
public TimeSpan TimeToLive { get; }
10-
public TimeSpan StaleAfter { get; }
11-
12-
public CacheSettings(TimeSpan timeToLive)
13-
{
14-
TimeToLive = timeToLive;
15-
StaleAfter = TimeSpan.Zero;
16-
}
17-
18-
public CacheSettings(TimeSpan timeToLive, TimeSpan staleAfter)
19-
{
20-
TimeToLive = timeToLive;
21-
StaleAfter = staleAfter;
22-
}
10+
/// <summary>
11+
/// The number of cache hits before forward propagating from a <see cref="MemoryCacheLayer" /> to higher level caches.
12+
/// </summary>
13+
public uint ForwardPropagateAfterXCacheHits { get; set; }
2314
}
2415
}

src/CacheTower/CacheStack.cs

Lines changed: 102 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading;
77
using System.Threading.Tasks;
88
using CacheTower.Extensions;
9+
using CacheTower.Providers.Memory;
910
using Microsoft.Extensions.DependencyInjection;
1011

1112
namespace CacheTower
@@ -89,17 +90,17 @@ public async ValueTask EvictAsync(string cacheKey)
8990
}
9091
}
9192

92-
public async ValueTask<CacheEntry<T>> SetAsync<T>(string cacheKey, T value, TimeSpan timeToLive)
93+
public async ValueTask<CacheEntry<T>> SetAsync<T>(string cacheKey, T value, TimeSpan timeToLive, CacheSettings settings = default)
9394
{
9495
ThrowIfDisposed();
9596

9697
var expiry = DateTime.UtcNow + timeToLive;
9798
var cacheEntry = new CacheEntry<T>(value, expiry);
98-
await SetAsync(cacheKey, cacheEntry);
99+
await SetAsync(cacheKey, cacheEntry, settings);
99100
return cacheEntry;
100101
}
101102

102-
public async ValueTask SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry)
103+
public async ValueTask SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry, CacheSettings settings = default)
103104
{
104105
ThrowIfDisposed();
105106

@@ -113,16 +114,23 @@ public async ValueTask SetAsync<T>(string cacheKey, CacheEntry<T> cacheEntry)
113114
throw new ArgumentNullException(nameof(cacheEntry));
114115
}
115116

116-
for (int i = 0, l = CacheLayers.Length; i < l; i++)
117+
if (settings.ForwardPropagateAfterXCacheHits > 0 && CacheLayers[0] is MemoryCacheLayer memoryCacheLayer)
117118
{
118-
var layer = CacheLayers[i];
119-
if (layer is ISyncCacheLayer syncLayerOne)
120-
{
121-
syncLayerOne.Set(cacheKey, cacheEntry);
122-
}
123-
else
119+
memoryCacheLayer.Set(cacheKey, cacheEntry);
120+
}
121+
else
122+
{
123+
for (int i = 0, l = CacheLayers.Length; i < l; i++)
124124
{
125-
await (layer as IAsyncCacheLayer).SetAsync(cacheKey, cacheEntry);
125+
var layer = CacheLayers[i];
126+
if (layer is ISyncCacheLayer syncLayerOne)
127+
{
128+
syncLayerOne.Set(cacheKey, cacheEntry);
129+
}
130+
else
131+
{
132+
await (layer as IAsyncCacheLayer).SetAsync(cacheKey, cacheEntry);
133+
}
126134
}
127135
}
128136
}
@@ -201,7 +209,7 @@ public async ValueTask<CacheEntry<T>> GetAsync<T>(string cacheKey)
201209
return default;
202210
}
203211

204-
public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> getter, CacheSettings settings)
212+
public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> getter, CacheEntryLifetime entryLifetime, CacheSettings settings = default)
205213
{
206214
ThrowIfDisposed();
207215

@@ -220,13 +228,14 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> get
220228
{
221229
var cacheEntry = cacheEntryPoint.CacheEntry;
222230
var currentTime = DateTime.UtcNow;
223-
if (cacheEntry.GetStaleDate(settings) < currentTime)
231+
var isStale = cacheEntry.GetStaleDate(entryLifetime) < currentTime;
232+
if (isStale)
224233
{
225234
if (cacheEntry.Expiry < currentTime)
226235
{
227236
//Refresh the value in the current thread though short circuit if we're unable to establish a lock
228237
//If the lock isn't established, it will instead use the stale cache entry (even if past the allowed stale period)
229-
var refreshedCacheEntry = await RefreshValueAsync(cacheKey, getter, settings, waitForRefresh: false);
238+
var refreshedCacheEntry = await RefreshValueAsync(cacheKey, getter, entryLifetime, settings, waitForRefresh: false);
230239
if (refreshedCacheEntry != default)
231240
{
232241
cacheEntry = refreshedCacheEntry;
@@ -235,62 +244,56 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> get
235244
else
236245
{
237246
//Refresh the value in the background
238-
_ = RefreshValueAsync(cacheKey, getter, settings, waitForRefresh: false);
247+
_ = RefreshValueAsync(cacheKey, getter, entryLifetime, settings, waitForRefresh: false);
239248
}
240249
}
241-
else if (cacheEntryPoint.LayerIndex > 0)
250+
else
242251
{
243-
//If a lower-level cache is missing the latest data, attempt to set it in the background
244-
_ = BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry);
252+
if (cacheEntryPoint.LayerIndex > 0)
253+
{
254+
//If a lower-level cache (eg. a memory cache) is missing the latest data, attempt to set it in the background
255+
_ = BackPropagateCacheEntryAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry);
256+
}
257+
else if (!cacheEntry._HasBeenForwardPropagated && settings.ForwardPropagateAfterXCacheHits > 0 && cacheEntry.CacheHitCount >= settings.ForwardPropagateAfterXCacheHits)
258+
{
259+
//If enabled, we push the local cache entry to higher-level caches, doing so in the background
260+
_ = ForwardPropagateCacheEntryAsync(cacheEntryPoint.LayerIndex + 1, cacheKey, cacheEntry);
261+
}
245262
}
246263

247264
return cacheEntry.Value;
248265
}
249266
else
250267
{
251268
//Refresh the value in the current thread though because we have no old cache value, we have to lock and wait
252-
return (await RefreshValueAsync(cacheKey, getter, settings, waitForRefresh: true)).Value;
269+
return (await RefreshValueAsync(cacheKey, getter, entryLifetime, settings, waitForRefresh: true)).Value;
253270
}
254271
}
255272

256-
private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string cacheKey, CacheEntry<T> cacheEntry)
273+
private async ValueTask BackPropagateCacheEntryAsync<T>(int fromIndexExclusive, string cacheKey, CacheEntry<T> cacheEntry)
257274
{
258275
ThrowIfDisposed();
259276

260-
var hasLock = false;
261-
lock (WaitingKeyRefresh)
262-
{
263-
#if NETSTANDARD2_0
264-
hasLock = !WaitingKeyRefresh.ContainsKey(cacheKey);
265-
if (hasLock)
266-
{
267-
WaitingKeyRefresh[cacheKey] = Array.Empty<TaskCompletionSource<object>>();
268-
}
269-
#elif NETSTANDARD2_1
270-
hasLock = WaitingKeyRefresh.TryAdd(cacheKey, Array.Empty<TaskCompletionSource<object>>());
271-
#endif
272-
}
273-
274-
if (hasLock)
277+
if (TryGetKeyRefreshLock(cacheKey))
275278
{
276279
try
277280
{
278281
for (; --fromIndexExclusive >= 0;)
279282
{
280-
var previousLayer = CacheLayers[fromIndexExclusive];
281-
if (previousLayer is ISyncCacheLayer prevSyncLayer)
283+
var cacheLayer = CacheLayers[fromIndexExclusive];
284+
if (cacheLayer is ISyncCacheLayer syncLayer)
282285
{
283-
if (prevSyncLayer.IsAvailable(cacheKey))
286+
if (syncLayer.IsAvailable(cacheKey))
284287
{
285-
prevSyncLayer.Set(cacheKey, cacheEntry);
288+
syncLayer.Set(cacheKey, cacheEntry);
286289
}
287290
}
288291
else
289292
{
290-
var prevAsyncLayer = previousLayer as IAsyncCacheLayer;
291-
if (await prevAsyncLayer.IsAvailableAsync(cacheKey))
293+
var asyncCacheLayer = cacheLayer as IAsyncCacheLayer;
294+
if (await asyncCacheLayer.IsAvailableAsync(cacheKey))
292295
{
293-
await prevAsyncLayer.SetAsync(cacheKey, cacheEntry);
296+
await asyncCacheLayer.SetAsync(cacheKey, cacheEntry);
294297
}
295298
}
296299
}
@@ -302,25 +305,48 @@ private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string
302305
}
303306
}
304307

305-
private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Func<T, Task<T>> getter, CacheSettings settings, bool waitForRefresh)
308+
private async ValueTask ForwardPropagateCacheEntryAsync<T>(int fromIndexExclusive, string cacheKey, CacheEntry<T> cacheEntry)
306309
{
307310
ThrowIfDisposed();
308311

309-
var hasLock = false;
310-
lock (WaitingKeyRefresh)
312+
if (TryGetKeyRefreshLock(cacheKey) && cacheEntry._HasBeenForwardPropagated)
311313
{
312-
#if NETSTANDARD2_0
313-
hasLock = !WaitingKeyRefresh.ContainsKey(cacheKey);
314-
if (hasLock)
314+
try
315315
{
316-
WaitingKeyRefresh[cacheKey] = Array.Empty<TaskCompletionSource<object>>();
316+
for (; ++fromIndexExclusive < CacheLayers.Length;)
317+
{
318+
var cacheLayer = CacheLayers[fromIndexExclusive];
319+
if (cacheLayer is ISyncCacheLayer syncLayer)
320+
{
321+
if (syncLayer.IsAvailable(cacheKey))
322+
{
323+
syncLayer.Set(cacheKey, cacheEntry);
324+
}
325+
}
326+
else
327+
{
328+
var asyncCacheLayer = cacheLayer as IAsyncCacheLayer;
329+
if (await asyncCacheLayer.IsAvailableAsync(cacheKey))
330+
{
331+
await asyncCacheLayer.SetAsync(cacheKey, cacheEntry);
332+
}
333+
}
334+
}
335+
336+
cacheEntry._HasBeenForwardPropagated = true;
337+
}
338+
finally
339+
{
340+
UnlockWaitingTasks(cacheKey, cacheEntry);
317341
}
318-
#elif NETSTANDARD2_1
319-
hasLock = WaitingKeyRefresh.TryAdd(cacheKey, Array.Empty<TaskCompletionSource<object>>());
320-
#endif
321342
}
343+
}
322344

323-
if (hasLock)
345+
private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Func<T, Task<T>> getter, CacheEntryLifetime entryLifetime, CacheSettings settings, bool waitForRefresh)
346+
{
347+
ThrowIfDisposed();
348+
349+
if (TryGetKeyRefreshLock(cacheKey))
324350
{
325351
try
326352
{
@@ -335,14 +361,14 @@ private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Fun
335361
}
336362

337363
var value = await getter(oldValue);
338-
var refreshedEntry = await SetAsync(cacheKey, value, settings.TimeToLive);
364+
var refreshedEntry = await SetAsync(cacheKey, value, entryLifetime.TimeToLive, settings);
339365

340-
_ = Extensions.OnValueRefreshAsync(cacheKey, settings.TimeToLive);
366+
_ = Extensions.OnValueRefreshAsync(cacheKey, entryLifetime.TimeToLive);
341367

342368
UnlockWaitingTasks(cacheKey, refreshedEntry);
343369

344370
return refreshedEntry;
345-
}, settings);
371+
}, entryLifetime);
346372
}
347373
catch
348374
{
@@ -369,7 +395,7 @@ private async ValueTask<CacheEntry<T>> RefreshValueAsync<T>(string cacheKey, Fun
369395

370396
//Last minute check to confirm whether waiting is required
371397
var currentEntry = await GetAsync<T>(cacheKey);
372-
if (currentEntry != null && currentEntry.GetStaleDate(settings) > DateTime.UtcNow)
398+
if (currentEntry != null && currentEntry.GetStaleDate(entryLifetime) > DateTime.UtcNow)
373399
{
374400
UnlockWaitingTasks(cacheKey, currentEntry);
375401
return currentEntry;
@@ -400,6 +426,25 @@ private void UnlockWaitingTasks(string cacheKey, CacheEntry cacheEntry)
400426
}
401427
}
402428

429+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
430+
private bool TryGetKeyRefreshLock(string cacheKey)
431+
{
432+
var hasLock = false;
433+
lock (WaitingKeyRefresh)
434+
{
435+
#if NETSTANDARD2_0
436+
hasLock = !WaitingKeyRefresh.ContainsKey(cacheKey);
437+
if (hasLock)
438+
{
439+
WaitingKeyRefresh[cacheKey] = Array.Empty<TaskCompletionSource<object>>();
440+
}
441+
#elif NETSTANDARD2_1
442+
hasLock = WaitingKeyRefresh.TryAdd(cacheKey, Array.Empty<TaskCompletionSource<object>>());
443+
#endif
444+
}
445+
return hasLock;
446+
}
447+
403448
#if NETSTANDARD2_0
404449
public void Dispose()
405450
{

0 commit comments

Comments
 (0)