Skip to content

Commit

Permalink
Implement optional back population on get operations (#292)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wazzamatazz authored Aug 4, 2024
1 parent 87de169 commit e902eaa
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/CacheTower/CacheStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,37 @@ private async ValueTask InternalSetAsync<T>(string cacheKey, CacheEntry<T> cache
return default;
}

/// <inheritdoc/>
public async ValueTask<CacheEntry<T>?> GetAsync<T>(string cacheKey, bool backPopulate)
{
if (!backPopulate)
{
return await GetAsync<T>(cacheKey).ConfigureAwait(false);
}

ThrowIfDisposed();

if (cacheKey == null)
{
throw new ArgumentNullException(nameof(cacheKey));
}

var cacheEntryPoint = await GetWithLayerIndexAsync<T>(cacheKey).ConfigureAwait(false);
if (cacheEntryPoint == default)
{
return default;
}

if (cacheEntryPoint.LayerIndex == 0)
{
return cacheEntryPoint.CacheEntry;
}

_ = BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntryPoint.CacheEntry);

return cacheEntryPoint.CacheEntry;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private async ValueTask<(int LayerIndex, CacheEntry<T> CacheEntry)> GetWithLayerIndexAsync<T>(string cacheKey)
{
Expand Down
20 changes: 20 additions & 0 deletions src/CacheTower/ICacheStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,32 @@ public interface ICacheStack
/// The entry returned corresponds to the first cache layer that contains it.
/// <br/>
/// If no cache layer contains it, Null is returned.
/// <br/>
/// <br/>
/// 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.
/// </remarks>
/// <typeparam name="T">The type of value in the cache entry.</typeparam>
/// <param name="cacheKey">The cache entry's key.</param>
/// <returns></returns>
ValueTask<CacheEntry<T>?> GetAsync<T>(string cacheKey);
/// <summary>
/// Retrieves the <see cref="CacheEntry{T}"/> for a given <paramref name="cacheKey"/> and
/// optionally back-populates the entry if it is found in a lower cache layer.
/// </summary>
/// <remarks>
/// The entry returned corresponds to the first cache layer that contains it.
/// <br/>
/// If no cache layer contains it, Null is returned.
/// <br/>
/// <br/>
/// Specifying a <paramref name="backPopulate"/> value of <see langword="false"/> is equivalent to calling <see cref="GetAsync{T}(string)"/>.
/// </remarks>
/// <typeparam name="T">The type of value in the cache entry.</typeparam>
/// <param name="cacheKey">The cache entry's key.</param>
/// <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>
/// <returns></returns>
ValueTask<CacheEntry<T>?> GetAsync<T>(string cacheKey, bool backPopulate);
/// <summary>
/// Attempts to retrieve the value for the given <paramref name="cacheKey"/>.
/// When unavailable, will fallback to use <paramref name="valueFactory"/> to generate the value, storing it in the cache.
/// </summary>
Expand Down
80 changes: 80 additions & 0 deletions tests/CacheTower.Tests/CacheStackTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,86 @@ public async Task Get_ThrowsOnUseAfterDisposal()

await cacheStack.GetAsync<int>("KeyDoesntMatter");
}
[DataTestMethod, ExpectedException(typeof(ArgumentNullException))]
[DataRow(true)]
[DataRow(false)]
public async Task Get_ThrowsOnNullKeyWithBackPopulation(bool enabled)
{
await using var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }));
await cacheStack.GetAsync<int>(null, enabled);
}
[TestMethod]
public async Task Get_BackPopulatesToEarlierCacheLayers()
{
var layer1 = new MemoryCacheLayer();
var layer2 = new MemoryCacheLayer();
var layer3 = new MemoryCacheLayer();

await using var cacheStack = new CacheStack(null, new(new[] { layer1, layer2, layer3 }));
var cacheEntry = new CacheEntry<int>(42, TimeSpan.FromDays(1));
await layer2.SetAsync("Get_BackPopulatesToEarlierCacheLayers", cacheEntry);

Internal.DateTimeProvider.UpdateTime();

var cacheEntryFromStack = await cacheStack.GetAsync<int>("Get_BackPopulatesToEarlierCacheLayers", true);

Assert.IsNotNull(cacheEntryFromStack);
Assert.AreEqual(cacheEntry.Value, cacheEntryFromStack.Value);

//Give enough time for the background task back propagation to happen
await Task.Delay(2000);

Assert.AreEqual(cacheEntry, await layer1.GetAsync<int>("Get_BackPopulatesToEarlierCacheLayers"));
Assert.IsNull(await layer3.GetAsync<int>("Get_BackPopulatesToEarlierCacheLayers"));
}
[TestMethod]
public async Task Get_DoesNotBackPopulateToEarlierCacheLayers()
{
var layer1 = new MemoryCacheLayer();
var layer2 = new MemoryCacheLayer();
var layer3 = new MemoryCacheLayer();

await using var cacheStack = new CacheStack(null, new(new[] { layer1, layer2, layer3 }));
var cacheEntry = new CacheEntry<int>(42, TimeSpan.FromDays(1));
await layer2.SetAsync("Get_DoesNotBackPopulateToEarlierCacheLayers", cacheEntry);

Internal.DateTimeProvider.UpdateTime();

var cacheEntryFromStack = await cacheStack.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayers", false);

Assert.IsNotNull(cacheEntryFromStack);
Assert.AreEqual(cacheEntry.Value, cacheEntryFromStack.Value);

//Give enough time for the background task back propagation to happen if it had been requested
await Task.Delay(2000);

Assert.IsNull(await layer1.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayers"));
Assert.IsNull(await layer3.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayers"));
}
[TestMethod]
public async Task Get_DoesNotBackPopulateToEarlierCacheLayersByDefault()
{
var layer1 = new MemoryCacheLayer();
var layer2 = new MemoryCacheLayer();
var layer3 = new MemoryCacheLayer();

await using var cacheStack = new CacheStack(null, new(new[] { layer1, layer2, layer3 }));
var cacheEntry = new CacheEntry<int>(42, TimeSpan.FromDays(1));
await layer2.SetAsync("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault", cacheEntry);

Internal.DateTimeProvider.UpdateTime();

var cacheEntryFromStack = await cacheStack.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault");

Assert.IsNotNull(cacheEntryFromStack);
Assert.AreEqual(cacheEntry.Value, cacheEntryFromStack.Value);

//Give enough time for the background task back propagation to happen if it had been requested
await Task.Delay(2000);

Assert.IsNull(await layer1.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault"));
Assert.IsNull(await layer3.GetAsync<int>("Get_DoesNotBackPopulateToEarlierCacheLayersByDefault"));
}

[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public async Task Set_ThrowsOnNullKey()
Expand Down

0 comments on commit e902eaa

Please sign in to comment.