Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ICacheMonitor to allow monitoring extensions to be added #180

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public async ValueTask EvictAsync(string cacheKey)
await EntityCommandWriter.WriteAsync<DbCachedEntry>(Connection, new[] { new EvictCommand(cacheKey) }, default);
}

public string Name => nameof(MongoDbCacheLayer);

/// <inheritdoc/>
public async ValueTask FlushAsync()
{
Expand Down
2 changes: 2 additions & 0 deletions src/CacheTower.Providers.Redis/RedisCacheLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public async ValueTask EvictAsync(string cacheKey)
await Database.KeyDeleteAsync(cacheKey);
}

public string Name => nameof(RedisCacheLayer);

/// <inheritdoc/>
/// <remarks>
/// Flushing the <see cref="RedisCacheLayer"/> performs a database flush in Redis.
Expand Down
89 changes: 73 additions & 16 deletions src/CacheTower/CacheStack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using CacheTower.Extensions;
using CacheTower.Internal;
using CacheTower.Monitoring;

namespace CacheTower
{
Expand All @@ -20,12 +21,14 @@ public class CacheStack : ICacheStack, IFlushableCacheStack, IAsyncDisposable

private ExtensionContainer Extensions { get; }

private ICacheMonitor CacheMonitor { get; }

/// <summary>
/// Creates a new <see cref="CacheStack"/> with the given <paramref name="cacheLayers"/> and <paramref name="extensions"/>.
/// </summary>
/// <param name="cacheLayers">The cache layers to use for the current cache stack. The layers should be ordered from the highest priority to the lowest. At least one cache layer is required.</param>
/// <param name="extensions">The cache extensions to use for the current cache stack.</param>
public CacheStack(ICacheLayer[] cacheLayers, ICacheExtension[] extensions)
public CacheStack(ICacheLayer[] cacheLayers, ICacheExtension[] extensions, ICacheMonitor? cacheMonitor = null)
{
if (cacheLayers == null || cacheLayers.Length == 0)
{
Expand All @@ -38,6 +41,8 @@ public CacheStack(ICacheLayer[] cacheLayers, ICacheExtension[] extensions)
Extensions.Register(this);

WaitingKeyRefresh = new Dictionary<string, TaskCompletionSource<CacheEntry>?>(StringComparer.Ordinal);

CacheMonitor = cacheMonitor ?? new NoOpCacheMonitor();
}

/// <summary>
Expand All @@ -51,7 +56,7 @@ protected void ThrowIfDisposed()
throw new ObjectDisposedException("CacheStack is disposed");
}
}

/// <inheritdoc/>
public async ValueTask FlushAsync()
{
Expand Down Expand Up @@ -91,7 +96,12 @@ public async ValueTask EvictAsync(string cacheKey)
for (int i = 0, l = CacheLayers.Length; i < l; i++)
{
var layer = CacheLayers[i];
await layer.EvictAsync(cacheKey);

await TimingUtils.Time(async t =>
{
await layer.EvictAsync(cacheKey);
await CacheMonitor.Evict(layer.Name, cacheKey, t.TimeElapsed);
});
}

await Extensions.OnCacheEvictionAsync(cacheKey);
Expand Down Expand Up @@ -135,7 +145,12 @@ private async ValueTask InternalSetAsync<T>(string cacheKey, CacheEntry<T> cache
for (int i = 0, l = CacheLayers.Length; i < l; i++)
{
var layer = CacheLayers[i];
await layer.SetAsync(cacheKey, cacheEntry);

await TimingUtils.Time(async t =>
{
await layer.SetAsync(cacheKey, cacheEntry);
await CacheMonitor.Set(layer.Name, cacheKey, t.TimeElapsed);
});
}

await Extensions.OnCacheUpdateAsync(cacheKey, cacheEntry.Expiry, cacheUpdateType);
Expand All @@ -156,10 +171,25 @@ private async ValueTask InternalSetAsync<T>(string cacheKey, CacheEntry<T> cache
var layer = CacheLayers[layerIndex];
if (await layer.IsAvailableAsync(cacheKey))
{
var cacheEntry = await layer.GetAsync<T>(cacheKey);
if (cacheEntry != default)
var entry = await TimingUtils.Time(async t =>
{
var cacheEntry = await layer.GetAsync<T>(cacheKey);

if (cacheEntry != default)
{
await CacheMonitor.GetHit(layer.Name, cacheKey, t.TimeElapsed);
}
else
{
await CacheMonitor.GetMiss(layer.Name, cacheKey, t.TimeElapsed);
}

return cacheEntry;
});

if (entry != default)
{
return entry;
}
}
}
Expand All @@ -175,10 +205,25 @@ private async ValueTask InternalSetAsync<T>(string cacheKey, CacheEntry<T> cache
var layer = CacheLayers[layerIndex];
if (await layer.IsAvailableAsync(cacheKey))
{
var cacheEntry = await layer.GetAsync<T>(cacheKey);
if (cacheEntry != default)
var entry = await TimingUtils.Time(async t =>
{
return (layerIndex, cacheEntry);
var cacheEntry = await layer.GetAsync<T>(cacheKey);

if (cacheEntry != default)
{
await CacheMonitor.GetHit(layer.Name, cacheKey, t.TimeElapsed);
}
else
{
await CacheMonitor.GetMiss(layer.Name, cacheKey, t.TimeElapsed);
}

return cacheEntry;
});

if (entry != default)
{
return (layerIndex, entry);
}
}
}
Expand Down Expand Up @@ -209,21 +254,32 @@ public async ValueTask<T> GetOrSetAsync<T>(string cacheKey, Func<T, Task<T>> get
if (settings.StaleAfter.HasValue && cacheEntry.GetStaleDate(settings) < currentTime)
{
//If the cache entry is stale, refresh the value in the background
_ = RefreshValueAsync(cacheKey, getter, settings, noExistingValueAvailable: false);
_ = TimingUtils.Time(async t =>
{
await RefreshValueAsync(cacheKey, getter, settings, noExistingValueAvailable: false);
await CacheMonitor.RefreshBackground(cacheKey, t.TimeElapsed);
});
}
else if (cacheEntryPoint.LayerIndex > 0)
{
//If a lower-level cache is missing the latest data, attempt to set it in the background
_ = BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry);
_ = TimingUtils.Time(async t =>
{
await BackPopulateCacheAsync(cacheEntryPoint.LayerIndex, cacheKey, cacheEntry);
await CacheMonitor.BackPopulate(cacheKey, t.TimeElapsed);
});
}

return cacheEntry.Value!;
}
else

//Refresh the value in the current thread though because we have no old cache value, we have to lock and wait
return await TimingUtils.Time(async t =>
{
//Refresh the value in the current thread though because we have no old cache value, we have to lock and wait
return (await RefreshValueAsync(cacheKey, getter, settings, noExistingValueAvailable: true))!.Value!;
}
var value = (await RefreshValueAsync(cacheKey, getter, settings, noExistingValueAvailable: true))!.Value!;
await CacheMonitor.RefreshForeground(cacheKey, t.TimeElapsed);
return value;
});
}

private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string cacheKey, CacheEntry<T> cacheEntry)
Expand Down Expand Up @@ -321,7 +377,8 @@ private async ValueTask BackPopulateCacheAsync<T>(int fromIndexExclusive, string
throw;
}
}
else if (noExistingValueAvailable)

if (noExistingValueAvailable)
{
TaskCompletionSource<CacheEntry> completionSource;

Expand Down
2 changes: 2 additions & 0 deletions src/CacheTower/ICacheLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ namespace CacheTower
/// </summary>
public interface ICacheLayer
{
string Name { get; }

/// <summary>
/// Flushes the cache layer, removing every item from the cache.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/CacheTower/Monitoring/DisposableTimer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Diagnostics;

namespace CacheTower.Monitoring
{
internal class DisposableTimer : IDisposable
{
private readonly Stopwatch Stopwatch;

public DisposableTimer()
{
Stopwatch = Stopwatch.StartNew();
}

public TimeSpan TimeElapsed => Stopwatch.Elapsed;

public void Dispose()
{
Stopwatch.Stop();
}
}
}
16 changes: 16 additions & 0 deletions src/CacheTower/Monitoring/ICacheMonitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;

namespace CacheTower.Monitoring
{
public interface ICacheMonitor
{
Task Evict(string layerName, string cacheKey, TimeSpan timeTaken);
Task Set(string layerName, string cacheKey, TimeSpan timeTaken);
Task GetHit(string layerName, string cacheKey, TimeSpan timeTaken);
Task GetMiss(string layerName, string cacheKey, TimeSpan timeTaken);
Task RefreshForeground(string cacheKey, TimeSpan timeTaken);
Task RefreshBackground(string cacheKey, TimeSpan timeTaken);
Task BackPopulate(string cacheKey, TimeSpan timeTaken);
}
}
43 changes: 43 additions & 0 deletions src/CacheTower/Monitoring/NoOpCacheMonitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;

namespace CacheTower.Monitoring
{
public class NoOpCacheMonitor : ICacheMonitor
{
public Task Evict(string layerName, string cacheKey, TimeSpan timeTaken)
{
return Task.CompletedTask;
}

public Task Set(string layerName, string cacheKey, TimeSpan timeTaken)
{
return Task.CompletedTask;
}

public Task GetHit(string layerName, string cacheKey, TimeSpan timeTaken)
{
return Task.CompletedTask;
}

public Task GetMiss(string layerName, string cacheKey, TimeSpan timeTaken)
{
return Task.CompletedTask;
}

public Task RefreshForeground(string cacheKey, TimeSpan timeTaken)
{
return Task.CompletedTask;
}

public Task RefreshBackground(string cacheKey, TimeSpan timeTaken)
{
return Task.CompletedTask;
}

public Task BackPopulate(string cacheKey, TimeSpan timeTaken)
{
return Task.CompletedTask;
}
}
}
20 changes: 20 additions & 0 deletions src/CacheTower/Monitoring/TimingUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;

namespace CacheTower.Monitoring
{
internal static class TimingUtils
{
public static async Task Time(Func<DisposableTimer, Task> func)
{
using var timer = new DisposableTimer();
await func(timer);
}

public static async Task<T> Time<T>(Func<DisposableTimer, Task<T>> func)
{
using var timer = new DisposableTimer();
return await func(timer);
}
}
}
2 changes: 2 additions & 0 deletions src/CacheTower/Providers/FileSystem/FileCacheLayerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ public async ValueTask EvictAsync(string cacheKey)
}
}

public string Name => "FileCacheLayer";

/// <inheritdoc/>
public async ValueTask FlushAsync()
{
Expand Down
2 changes: 2 additions & 0 deletions src/CacheTower/Providers/Memory/MemoryCacheLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public ValueTask EvictAsync(string cacheKey)
return new ValueTask();
}

public string Name => nameof(MemoryCacheLayer);

/// <inheritdoc/>
public ValueTask FlushAsync()
{
Expand Down