From 77fadb43c716cdd1a9e1aebadb5ab249161874bb Mon Sep 17 00:00:00 2001 From: Mike Goodfellow Date: Thu, 12 Aug 2021 14:40:09 +0100 Subject: [PATCH] Add ICacheMonitor to allow monitoring extensions to be added --- .../MongoDbCacheLayer.cs | 2 + .../RedisCacheLayer.cs | 2 + src/CacheTower/CacheStack.cs | 89 +++++++++++++++---- src/CacheTower/ICacheLayer.cs | 2 + src/CacheTower/Monitoring/DisposableTimer.cs | 22 +++++ src/CacheTower/Monitoring/ICacheMonitor.cs | 16 ++++ src/CacheTower/Monitoring/NoOpCacheMonitor.cs | 43 +++++++++ src/CacheTower/Monitoring/TimingUtils.cs | 20 +++++ .../FileSystem/FileCacheLayerBase.cs | 2 + .../Providers/Memory/MemoryCacheLayer.cs | 2 + 10 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 src/CacheTower/Monitoring/DisposableTimer.cs create mode 100644 src/CacheTower/Monitoring/ICacheMonitor.cs create mode 100644 src/CacheTower/Monitoring/NoOpCacheMonitor.cs create mode 100644 src/CacheTower/Monitoring/TimingUtils.cs diff --git a/src/CacheTower.Providers.Database.MongoDB/MongoDbCacheLayer.cs b/src/CacheTower.Providers.Database.MongoDB/MongoDbCacheLayer.cs index c863f739..84f3cfb8 100644 --- a/src/CacheTower.Providers.Database.MongoDB/MongoDbCacheLayer.cs +++ b/src/CacheTower.Providers.Database.MongoDB/MongoDbCacheLayer.cs @@ -54,6 +54,8 @@ public async ValueTask EvictAsync(string cacheKey) await EntityCommandWriter.WriteAsync(Connection, new[] { new EvictCommand(cacheKey) }, default); } + public string Name => nameof(MongoDbCacheLayer); + /// public async ValueTask FlushAsync() { diff --git a/src/CacheTower.Providers.Redis/RedisCacheLayer.cs b/src/CacheTower.Providers.Redis/RedisCacheLayer.cs index 8d5bb6da..493bd4e2 100644 --- a/src/CacheTower.Providers.Redis/RedisCacheLayer.cs +++ b/src/CacheTower.Providers.Redis/RedisCacheLayer.cs @@ -55,6 +55,8 @@ public async ValueTask EvictAsync(string cacheKey) await Database.KeyDeleteAsync(cacheKey); } + public string Name => nameof(RedisCacheLayer); + /// /// /// Flushing the performs a database flush in Redis. diff --git a/src/CacheTower/CacheStack.cs b/src/CacheTower/CacheStack.cs index a26425ea..c6cb5976 100644 --- a/src/CacheTower/CacheStack.cs +++ b/src/CacheTower/CacheStack.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using CacheTower.Extensions; using CacheTower.Internal; +using CacheTower.Monitoring; namespace CacheTower { @@ -20,12 +21,14 @@ public class CacheStack : ICacheStack, IFlushableCacheStack, IAsyncDisposable private ExtensionContainer Extensions { get; } + private ICacheMonitor CacheMonitor { get; } + /// /// Creates a new with the given and . /// /// 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. /// The cache extensions to use for the current cache stack. - public CacheStack(ICacheLayer[] cacheLayers, ICacheExtension[] extensions) + public CacheStack(ICacheLayer[] cacheLayers, ICacheExtension[] extensions, ICacheMonitor? cacheMonitor = null) { if (cacheLayers == null || cacheLayers.Length == 0) { @@ -38,6 +41,8 @@ public CacheStack(ICacheLayer[] cacheLayers, ICacheExtension[] extensions) Extensions.Register(this); WaitingKeyRefresh = new Dictionary?>(StringComparer.Ordinal); + + CacheMonitor = cacheMonitor ?? new NoOpCacheMonitor(); } /// @@ -51,7 +56,7 @@ protected void ThrowIfDisposed() throw new ObjectDisposedException("CacheStack is disposed"); } } - + /// public async ValueTask FlushAsync() { @@ -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); @@ -135,7 +145,12 @@ private async ValueTask InternalSetAsync(string cacheKey, CacheEntry 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); @@ -156,10 +171,25 @@ private async ValueTask InternalSetAsync(string cacheKey, CacheEntry cache var layer = CacheLayers[layerIndex]; if (await layer.IsAvailableAsync(cacheKey)) { - var cacheEntry = await layer.GetAsync(cacheKey); - if (cacheEntry != default) + var entry = await TimingUtils.Time(async t => { + var cacheEntry = await layer.GetAsync(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; } } } @@ -175,10 +205,25 @@ private async ValueTask InternalSetAsync(string cacheKey, CacheEntry cache var layer = CacheLayers[layerIndex]; if (await layer.IsAvailableAsync(cacheKey)) { - var cacheEntry = await layer.GetAsync(cacheKey); - if (cacheEntry != default) + var entry = await TimingUtils.Time(async t => { - return (layerIndex, cacheEntry); + var cacheEntry = await layer.GetAsync(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); } } } @@ -209,21 +254,32 @@ public async ValueTask GetOrSetAsync(string cacheKey, Func> 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(int fromIndexExclusive, string cacheKey, CacheEntry cacheEntry) @@ -321,7 +377,8 @@ private async ValueTask BackPopulateCacheAsync(int fromIndexExclusive, string throw; } } - else if (noExistingValueAvailable) + + if (noExistingValueAvailable) { TaskCompletionSource completionSource; diff --git a/src/CacheTower/ICacheLayer.cs b/src/CacheTower/ICacheLayer.cs index e7f93f60..533dd492 100644 --- a/src/CacheTower/ICacheLayer.cs +++ b/src/CacheTower/ICacheLayer.cs @@ -8,6 +8,8 @@ namespace CacheTower /// public interface ICacheLayer { + string Name { get; } + /// /// Flushes the cache layer, removing every item from the cache. /// diff --git a/src/CacheTower/Monitoring/DisposableTimer.cs b/src/CacheTower/Monitoring/DisposableTimer.cs new file mode 100644 index 00000000..394b4027 --- /dev/null +++ b/src/CacheTower/Monitoring/DisposableTimer.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/CacheTower/Monitoring/ICacheMonitor.cs b/src/CacheTower/Monitoring/ICacheMonitor.cs new file mode 100644 index 00000000..9ebec40d --- /dev/null +++ b/src/CacheTower/Monitoring/ICacheMonitor.cs @@ -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); + } +} diff --git a/src/CacheTower/Monitoring/NoOpCacheMonitor.cs b/src/CacheTower/Monitoring/NoOpCacheMonitor.cs new file mode 100644 index 00000000..acf36265 --- /dev/null +++ b/src/CacheTower/Monitoring/NoOpCacheMonitor.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/CacheTower/Monitoring/TimingUtils.cs b/src/CacheTower/Monitoring/TimingUtils.cs new file mode 100644 index 00000000..3e29db75 --- /dev/null +++ b/src/CacheTower/Monitoring/TimingUtils.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace CacheTower.Monitoring +{ + internal static class TimingUtils + { + public static async Task Time(Func func) + { + using var timer = new DisposableTimer(); + await func(timer); + } + + public static async Task Time(Func> func) + { + using var timer = new DisposableTimer(); + return await func(timer); + } + } +} \ No newline at end of file diff --git a/src/CacheTower/Providers/FileSystem/FileCacheLayerBase.cs b/src/CacheTower/Providers/FileSystem/FileCacheLayerBase.cs index 70d78525..aad972b7 100644 --- a/src/CacheTower/Providers/FileSystem/FileCacheLayerBase.cs +++ b/src/CacheTower/Providers/FileSystem/FileCacheLayerBase.cs @@ -235,6 +235,8 @@ public async ValueTask EvictAsync(string cacheKey) } } + public string Name => "FileCacheLayer"; + /// public async ValueTask FlushAsync() { diff --git a/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs b/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs index 4fc10599..5f86a05b 100644 --- a/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs +++ b/src/CacheTower/Providers/Memory/MemoryCacheLayer.cs @@ -42,6 +42,8 @@ public ValueTask EvictAsync(string cacheKey) return new ValueTask(); } + public string Name => nameof(MemoryCacheLayer); + /// public ValueTask FlushAsync() {