Skip to content

Commit e902eaa

Browse files
authored
Implement optional back population on get operations (#292)
* Add GetAsync overload for back-population Adds a `GetAsync<T>(string, bool)` overload to `ICacheStack` that allows the caller to specify if the cache entry should be back-populated to higher cache layers if it is resolved using a lower layer. * Add unit tests for get with back propagation * Update test names * Add unit tests for null keys
1 parent 87de169 commit e902eaa

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

src/CacheTower/CacheStack.cs

+31
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,37 @@ private async ValueTask InternalSetAsync<T>(string cacheKey, CacheEntry<T> cache
228228
return default;
229229
}
230230

231+
/// <inheritdoc/>
232+
public async ValueTask<CacheEntry<T>?> GetAsync<T>(string cacheKey, bool backPopulate)
233+
{
234+
if (!backPopulate)
235+
{
236+
return await GetAsync<T>(cacheKey).ConfigureAwait(false);
237+
}
238+
239+
ThrowIfDisposed();
240+
241+
if (cacheKey == null)
242+
{
243+
throw new ArgumentNullException(nameof(cacheKey));
244+
}
245+
246+
var cacheEntryPoint = await GetWithLayerIndexAsync<T>(cacheKey).ConfigureAwait(false);
247+
if (cacheEntryPoint == default)
248+
{
249+
return default;
250+
}
251+
252+
if (cacheEntryPoint.LayerIndex == 0)
253+
{
254+
return cacheEntryPoint.CacheEntry;
255+
}
256+
257+
_ = BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntryPoint.CacheEntry);
258+
259+
return cacheEntryPoint.CacheEntry;
260+
}
261+
231262
[MethodImpl(MethodImplOptions.AggressiveInlining)]
232263
private async ValueTask<(int LayerIndex, CacheEntry<T> CacheEntry)> GetWithLayerIndexAsync<T>(string cacheKey)
233264
{

src/CacheTower/ICacheStack.cs

+20
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,32 @@ public interface ICacheStack
5050
/// The entry returned corresponds to the first cache layer that contains it.
5151
/// <br/>
5252
/// If no cache layer contains it, Null is returned.
53+
/// <br/>
54+
/// <br/>
55+
/// Use <see cref="GetAsync{T}(string, bool)"/> to optionally perform back-population to upper cache layers if the entry is found in a lower cache layer.
5356
/// </remarks>
5457
/// <typeparam name="T">The type of value in the cache entry.</typeparam>
5558
/// <param name="cacheKey">The cache entry's key.</param>
5659
/// <returns></returns>
5760
ValueTask<CacheEntry<T>?> GetAsync<T>(string cacheKey);
5861
/// <summary>
62+
/// Retrieves the <see cref="CacheEntry{T}"/> for a given <paramref name="cacheKey"/> and
63+
/// optionally back-populates the entry if it is found in a lower cache layer.
64+
/// </summary>
65+
/// <remarks>
66+
/// The entry returned corresponds to the first cache layer that contains it.
67+
/// <br/>
68+
/// If no cache layer contains it, Null is returned.
69+
/// <br/>
70+
/// <br/>
71+
/// Specifying a <paramref name="backPopulate"/> value of <see langword="false"/> is equivalent to calling <see cref="GetAsync{T}(string)"/>.
72+
/// </remarks>
73+
/// <typeparam name="T">The type of value in the cache entry.</typeparam>
74+
/// <param name="cacheKey">The cache entry's key.</param>
75+
/// <param name="backPopulate"><see langword="true"/> to back-populate the entry to upper cache layers if it is found in a lower cache layer; otherwise, <see langword="false"/>.</param>
76+
/// <returns></returns>
77+
ValueTask<CacheEntry<T>?> GetAsync<T>(string cacheKey, bool backPopulate);
78+
/// <summary>
5979
/// Attempts to retrieve the value for the given <paramref name="cacheKey"/>.
6080
/// When unavailable, will fallback to use <paramref name="valueFactory"/> to generate the value, storing it in the cache.
6181
/// </summary>

tests/CacheTower.Tests/CacheStackTests.cs

+80
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,86 @@ public async Task Get_ThrowsOnUseAfterDisposal()
154154

155155
await cacheStack.GetAsync<int>("KeyDoesntMatter");
156156
}
157+
[DataTestMethod, ExpectedException(typeof(ArgumentNullException))]
158+
[DataRow(true)]
159+
[DataRow(false)]
160+
public async Task Get_ThrowsOnNullKeyWithBackPopulation(bool enabled)
161+
{
162+
await using var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }));
163+
await cacheStack.GetAsync<int>(null, enabled);
164+
}
165+
[TestMethod]
166+
public async Task Get_BackPopulatesToEarlierCacheLayers()
167+
{
168+
var layer1 = new MemoryCacheLayer();
169+
var layer2 = new MemoryCacheLayer();
170+
var layer3 = new MemoryCacheLayer();
171+
172+
await using var cacheStack = new CacheStack(null, new(new[] { layer1, layer2, layer3 }));
173+
var cacheEntry = new CacheEntry<int>(42, TimeSpan.FromDays(1));
174+
await layer2.SetAsync("Get_BackPopulatesToEarlierCacheLayers", cacheEntry);
175+
176+
Internal.DateTimeProvider.UpdateTime();
177+
178+
var cacheEntryFromStack = await cacheStack.GetAsync<int>("Get_BackPopulatesToEarlierCacheLayers", true);
179+
180+
Assert.IsNotNull(cacheEntryFromStack);
181+
Assert.AreEqual(cacheEntry.Value, cacheEntryFromStack.Value);
182+
183+
//Give enough time for the background task back propagation to happen
184+
await Task.Delay(2000);
185+
186+
Assert.AreEqual(cacheEntry, await layer1.GetAsync<int>("Get_BackPopulatesToEarlierCacheLayers"));
187+
Assert.IsNull(await layer3.GetAsync<int>("Get_BackPopulatesToEarlierCacheLayers"));
188+
}
189+
[TestMethod]
190+
public async Task Get_DoesNotBackPopulateToEarlierCacheLayers()
191+
{
192+
var layer1 = new MemoryCacheLayer();
193+
var layer2 = new MemoryCacheLayer();
194+
var layer3 = new MemoryCacheLayer();
195+
196+
await using var cacheStack = new CacheStack(null, new(new[] { layer1, layer2, layer3 }));
197+
var cacheEntry = new CacheEntry<int>(42, TimeSpan.FromDays(1));
198+
await layer2.SetAsync("Get_DoesNotBackPopulateToEarlierCacheLayers", cacheEntry);
199+
200+
Internal.DateTimeProvider.UpdateTime();
201+
202+
var cacheEntryFromStack = await cacheStack.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayers", false);
203+
204+
Assert.IsNotNull(cacheEntryFromStack);
205+
Assert.AreEqual(cacheEntry.Value, cacheEntryFromStack.Value);
206+
207+
//Give enough time for the background task back propagation to happen if it had been requested
208+
await Task.Delay(2000);
209+
210+
Assert.IsNull(await layer1.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayers"));
211+
Assert.IsNull(await layer3.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayers"));
212+
}
213+
[TestMethod]
214+
public async Task Get_DoesNotBackPopulateToEarlierCacheLayersByDefault()
215+
{
216+
var layer1 = new MemoryCacheLayer();
217+
var layer2 = new MemoryCacheLayer();
218+
var layer3 = new MemoryCacheLayer();
219+
220+
await using var cacheStack = new CacheStack(null, new(new[] { layer1, layer2, layer3 }));
221+
var cacheEntry = new CacheEntry<int>(42, TimeSpan.FromDays(1));
222+
await layer2.SetAsync("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault", cacheEntry);
223+
224+
Internal.DateTimeProvider.UpdateTime();
225+
226+
var cacheEntryFromStack = await cacheStack.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault");
227+
228+
Assert.IsNotNull(cacheEntryFromStack);
229+
Assert.AreEqual(cacheEntry.Value, cacheEntryFromStack.Value);
230+
231+
//Give enough time for the background task back propagation to happen if it had been requested
232+
await Task.Delay(2000);
233+
234+
Assert.IsNull(await layer1.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault"));
235+
Assert.IsNull(await layer3.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault"));
236+
}
157237

158238
[TestMethod, ExpectedException(typeof(ArgumentNullException))]
159239
public async Task Set_ThrowsOnNullKey()

0 commit comments

Comments
 (0)