-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCacheService.cs
More file actions
134 lines (108 loc) · 5.49 KB
/
CacheService.cs
File metadata and controls
134 lines (108 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
using System.Collections.Concurrent;
namespace Nhs.Appointments.Core.Caching;
public record LazySlideCacheOptions<T>(Func<Task<T>> UpdateOperation, TimeSpan SlideThreshold, TimeSpan AbsoluteExpiration);
public record CacheOptions<T>(Func<Task<T>> UpdateOperation, TimeSpan AbsoluteExpiration, TryPatternOptions<T> TryPatternOptions = null);
public record TryPatternOptions<T>(bool UseTryPattern = true, T DefaultResponse = default);
public interface ICacheService
{
Task<T> GetLazySlidingCacheValue<T>(string cacheKey, LazySlideCacheOptions<T> options);
Task<T> GetCacheValue<T>(string cacheKey, CacheOptions<T> options);
}
public class CacheService(ICacheStore cacheStore, TimeProvider timeProvider) : ICacheService
{
private static readonly ConcurrentDictionary<string, SemaphoreSlim> LazySlidingCacheLocks = new();
internal static string LazySlideCacheKey(string cacheKey) => $"LazySlide:{cacheKey}";
public async Task<T> GetLazySlidingCacheValue<T>(string cacheKey, LazySlideCacheOptions<T> options)
{
//intentionally prefix cache key indicating that it is a lazy sliding value
var lazySlideCacheKey = LazySlideCacheKey(cacheKey);
if (options.AbsoluteExpiration <= options.SlideThreshold)
{
throw new ArgumentException("Configuration is not supported, AbsoluteExpiration must be greater than the SlideThreshold");
}
var utcNow = timeProvider.GetUtcNow();
var lazySlideCacheLock =
LazySlidingCacheLocks.GetOrAdd(lazySlideCacheKey, _ => new SemaphoreSlim(1, 1));
//we want to wait until the lock is free before continuing
await lazySlideCacheLock.WaitAsync();
var slidePerformed = false;
try
{
if (await cacheStore.TryGetAsync<LazySlideCacheObject>(lazySlideCacheKey, out var lazySlideCacheObject))
{
ArgumentNullException.ThrowIfNull(lazySlideCacheObject);
//check if we want to update the existing cache in the background lazily...
if (lazySlideCacheObject.DateTimeUpdated.Add(options.SlideThreshold) < utcNow)
{
//Sliding cache functionality
//Update the cache value so the NEXT request gets a newer version of the latest expensive value fetch
//This approach means the cache entry is never guaranteed to be the exact latest value (unless a cache value does not exist) - but it is recent enough to not have a big impact
//The performance gain is a sufficient benefit to the value being potentially slightly behind the latest value
_ = SlideCache(lazySlideCacheKey, options, lazySlideCacheLock, (T)lazySlideCacheObject.Value, utcNow);
slidePerformed = true;
}
//return the current cached value regardless of whether sliding was invoked
return (T)lazySlideCacheObject.Value;
}
var value = await options.UpdateOperation();
await cacheStore.SetAsync(lazySlideCacheKey, new LazySlideCacheObject(value, utcNow),
utcNow.Add(options.AbsoluteExpiration));
return value;
}
finally
{
//the lock was released earlier if a slide was performed
if (!slidePerformed)
{
lazySlideCacheLock.Release();
}
}
}
public async Task<T> GetCacheValue<T>(string cacheKey, CacheOptions<T> options)
{
if (await cacheStore.TryGetAsync<CacheObject<T>>(cacheKey, out var cacheObj))
{
ArgumentNullException.ThrowIfNull(cacheObj);
if (cacheObj.Value != null)
{
return cacheObj.Value;
}
}
T newValue;
if (options.TryPatternOptions?.UseTryPattern ?? false)
{
var tryResult = await TryPattern.TryAsync(options.UpdateOperation);
if (!tryResult.Completed)
{
return options.TryPatternOptions.DefaultResponse;
}
newValue = tryResult.Result;
}
else
{
newValue = await options.UpdateOperation();
}
await cacheStore.SetAsync(cacheKey, new CacheObject<T>(newValue), options.AbsoluteExpiration);
return newValue;
}
private async Task SlideCache<T>(string lazySlideCacheKey, LazySlideCacheOptions<T> options, SemaphoreSlim lazySlideCacheLock, T lazyValue, DateTimeOffset dateTime)
{
try
{
//update the cache datetime prematurely so that concurrent waiting threads do not trigger their own slide operation
await cacheStore.SetAsync(lazySlideCacheKey, new LazySlideCacheObject(lazyValue, dateTime),
dateTime.Add(options.AbsoluteExpiration));
}
finally
{
//can release other threads now that the cache time has been updated
lazySlideCacheLock.Release();
}
//then update the actual value now that no locks are being held
var value = await options.UpdateOperation();
await cacheStore.SetAsync(lazySlideCacheKey, new LazySlideCacheObject(value, dateTime),
dateTime.Add(options.AbsoluteExpiration));
}
internal record LazySlideCacheObject(object Value, DateTimeOffset DateTimeUpdated);
internal record CacheObject<T>(T Value);
}