Skip to content

Commit 2a01ced

Browse files
mikemcdougallclaudeMike McDougall
authored
fix: complete issue 193 requirements (#193) (#195)
* feat: implement Redis metadata cache with in-memory fallback (#37) - Add ICacheService and ICacheHealthChecker abstractions in Honua.Core - Add CacheOptions configuration with TTL settings - Implement RedisCacheService using IDistributedCache from Aspire - Support automatic fallback to in-memory cache when Redis unavailable - Create CachingLayerCatalog decorator for transparent caching - Integrate cache health status into ReadinessCheckService - Add cache metrics (hits/misses/evictions) using PerformanceMetrics - Add configuration documentation in appsettings.json - Add comprehensive unit tests for caching functionality Configuration options: - Cache:Enabled - enable/disable caching (default: true) - Cache:DefaultTtlSeconds - default TTL (default: 300) - Cache:LayerTtlSeconds - layer metadata TTL (default: 300) - Cache:ServiceTtlSeconds - service metadata TTL (default: 300) - Cache:EnableFallback - use in-memory when Redis unavailable (default: true) - Cache:FallbackMaxEntries - max in-memory entries (default: 1000) - Cache:RetryIntervalSeconds - retry Redis connection interval (default: 30) - Cache:KeyPrefix - key prefix for Redis (default: "honua:") * fix: complete issue 193 requirements (#193) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Mike McDougall <mike@honua.io>
1 parent e8bacc3 commit 2a01ced

32 files changed

+1423
-289
lines changed

src/Honua.Core/Features/Infrastructure/Monitoring/DefaultPerformanceMonitor.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Honua. All rights reserved.
22
// Licensed under the Elastic License 2.0. See LICENSE in the project root.
33

4+
using System.Collections.Concurrent;
45
using System.Diagnostics;
56
using System.Globalization;
67

@@ -13,8 +14,10 @@ namespace Honua.Core.Features.Infrastructure.Monitoring;
1314
/// This implementation provides comprehensive performance monitoring using the .NET Metrics API
1415
/// and integrates with OpenTelemetry for telemetry export.
1516
/// </remarks>
16-
internal sealed class DefaultPerformanceMonitor : IPerformanceMonitor
17+
internal sealed class DefaultPerformanceMonitor : IPerformanceMonitor, ICacheMetricsSnapshotProvider
1718
{
19+
private readonly ConcurrentDictionary<string, CacheOperationCounters> _cacheCounters = new(StringComparer.OrdinalIgnoreCase);
20+
1821
/// <inheritdoc />
1922
public void RecordDatabaseQuery(string queryType, string layerId, TimeSpan duration, int recordCount)
2023
{
@@ -72,6 +75,37 @@ public void RecordCacheMetrics(string cacheType, string operation)
7275
};
7376

7477
PerformanceMetrics.CacheOperationCount.Add(1, tags);
78+
79+
var normalizedType = string.IsNullOrWhiteSpace(cacheType) ? "unknown" : cacheType;
80+
var normalizedOperation = string.IsNullOrWhiteSpace(operation) ? "unknown" : operation;
81+
var counters = _cacheCounters.GetOrAdd(normalizedType, _ => new CacheOperationCounters());
82+
counters.Record(normalizedOperation);
83+
}
84+
85+
/// <inheritdoc />
86+
public CacheMetricsSnapshot GetCacheMetricsSnapshot()
87+
{
88+
var types = new Dictionary<string, CacheTypeMetricsSnapshot>(StringComparer.OrdinalIgnoreCase);
89+
long totalHits = 0;
90+
long totalMisses = 0;
91+
long totalEvictions = 0;
92+
93+
foreach (var (cacheType, counters) in _cacheCounters)
94+
{
95+
var snapshot = counters.Snapshot();
96+
types[cacheType] = snapshot;
97+
totalHits += snapshot.Hits;
98+
totalMisses += snapshot.Misses;
99+
totalEvictions += snapshot.Evictions;
100+
}
101+
102+
return new CacheMetricsSnapshot
103+
{
104+
TotalHits = totalHits,
105+
TotalMisses = totalMisses,
106+
TotalEvictions = totalEvictions,
107+
Types = types
108+
};
75109
}
76110

77111
/// <inheritdoc />
@@ -149,4 +183,38 @@ public void Dispose()
149183
PerformanceMetrics.OperationCount.Add(1, tagPairs);
150184
}
151185
}
186+
187+
private sealed class CacheOperationCounters
188+
{
189+
private long _hits;
190+
private long _misses;
191+
private long _evictions;
192+
193+
public void Record(string operation)
194+
{
195+
switch (operation.ToLowerInvariant())
196+
{
197+
case "hit":
198+
Interlocked.Increment(ref _hits);
199+
break;
200+
case "miss":
201+
Interlocked.Increment(ref _misses);
202+
break;
203+
case "eviction":
204+
case "pattern_eviction":
205+
Interlocked.Increment(ref _evictions);
206+
break;
207+
}
208+
}
209+
210+
public CacheTypeMetricsSnapshot Snapshot()
211+
{
212+
return new CacheTypeMetricsSnapshot
213+
{
214+
Hits = Interlocked.Read(ref _hits),
215+
Misses = Interlocked.Read(ref _misses),
216+
Evictions = Interlocked.Read(ref _evictions)
217+
};
218+
}
219+
}
152220
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Honua. All rights reserved.
2+
// Licensed under the Elastic License 2.0. See LICENSE in the project root.
3+
4+
namespace Honua.Core.Features.Infrastructure.Monitoring;
5+
6+
/// <summary>
7+
/// Provides access to database performance metrics snapshots.
8+
/// </summary>
9+
public interface IDatabasePerformanceMetricsProvider
10+
{
11+
/// <summary>
12+
/// Gets a snapshot of database performance metrics.
13+
/// </summary>
14+
DatabasePerformanceMetricsSnapshot GetMetrics();
15+
}
16+
17+
/// <summary>
18+
/// Provides access to cache performance metrics snapshots.
19+
/// </summary>
20+
public interface ICacheMetricsSnapshotProvider
21+
{
22+
/// <summary>
23+
/// Gets a snapshot of cache performance metrics.
24+
/// </summary>
25+
CacheMetricsSnapshot GetCacheMetricsSnapshot();
26+
}
27+
28+
/// <summary>
29+
/// Snapshot of database performance metrics.
30+
/// </summary>
31+
public sealed record DatabasePerformanceMetricsSnapshot
32+
{
33+
/// <summary>
34+
/// Cache hit rate as a decimal (0.0 to 1.0).
35+
/// </summary>
36+
public double CacheHitRate { get; init; }
37+
38+
/// <summary>
39+
/// Total number of cache hits.
40+
/// </summary>
41+
public long CacheHits { get; init; }
42+
43+
/// <summary>
44+
/// Total number of cache misses.
45+
/// </summary>
46+
public long CacheMisses { get; init; }
47+
48+
/// <summary>
49+
/// Performance metrics by operation type.
50+
/// </summary>
51+
public Dictionary<string, DatabaseOperationMetricsSnapshot> Operations { get; init; } = new();
52+
}
53+
54+
/// <summary>
55+
/// Snapshot of performance metrics for a specific database operation.
56+
/// </summary>
57+
public sealed record DatabaseOperationMetricsSnapshot
58+
{
59+
/// <summary>
60+
/// Total number of operations executed.
61+
/// </summary>
62+
public long Count { get; init; }
63+
64+
/// <summary>
65+
/// Total execution time in milliseconds.
66+
/// </summary>
67+
public long TotalTimeMs { get; init; }
68+
69+
/// <summary>
70+
/// Maximum execution time in milliseconds.
71+
/// </summary>
72+
public long MaxTimeMs { get; init; }
73+
74+
/// <summary>
75+
/// Average execution time in milliseconds.
76+
/// </summary>
77+
public double AvgTimeMs { get; init; }
78+
}
79+
80+
/// <summary>
81+
/// Snapshot of cache performance metrics.
82+
/// </summary>
83+
public sealed record CacheMetricsSnapshot
84+
{
85+
/// <summary>
86+
/// Total number of cache hits.
87+
/// </summary>
88+
public long TotalHits { get; init; }
89+
90+
/// <summary>
91+
/// Total number of cache misses.
92+
/// </summary>
93+
public long TotalMisses { get; init; }
94+
95+
/// <summary>
96+
/// Total number of cache evictions.
97+
/// </summary>
98+
public long TotalEvictions { get; init; }
99+
100+
/// <summary>
101+
/// Metrics by cache type.
102+
/// </summary>
103+
public Dictionary<string, CacheTypeMetricsSnapshot> Types { get; init; } = new();
104+
}
105+
106+
/// <summary>
107+
/// Snapshot of cache metrics for a specific cache type.
108+
/// </summary>
109+
public sealed record CacheTypeMetricsSnapshot
110+
{
111+
/// <summary>
112+
/// Number of cache hits for this type.
113+
/// </summary>
114+
public long Hits { get; init; }
115+
116+
/// <summary>
117+
/// Number of cache misses for this type.
118+
/// </summary>
119+
public long Misses { get; init; }
120+
121+
/// <summary>
122+
/// Number of evictions for this type.
123+
/// </summary>
124+
public long Evictions { get; init; }
125+
}
126+
127+
/// <summary>
128+
/// Default database metrics provider when no implementation is registered.
129+
/// </summary>
130+
internal sealed class NullDatabasePerformanceMetricsProvider : IDatabasePerformanceMetricsProvider
131+
{
132+
public DatabasePerformanceMetricsSnapshot GetMetrics() => new();
133+
}

src/Honua.Core/Features/Infrastructure/Monitoring/PerformanceMonitoringServiceCollectionExtensions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Elastic License 2.0. See LICENSE in the project root.
33

44
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
56

67
namespace Honua.Core.Features.Infrastructure.Monitoring;
78

@@ -17,7 +18,10 @@ public static class PerformanceMonitoringServiceCollectionExtensions
1718
/// <returns>The service collection for chaining</returns>
1819
public static IServiceCollection AddDefaultPerformanceMonitor(this IServiceCollection services)
1920
{
20-
services.AddSingleton<IPerformanceMonitor, DefaultPerformanceMonitor>();
21+
services.AddSingleton<DefaultPerformanceMonitor>();
22+
services.AddSingleton<IPerformanceMonitor>(sp => sp.GetRequiredService<DefaultPerformanceMonitor>());
23+
services.AddSingleton<ICacheMetricsSnapshotProvider>(sp => sp.GetRequiredService<DefaultPerformanceMonitor>());
24+
services.TryAddSingleton<IDatabasePerformanceMetricsProvider, NullDatabasePerformanceMetricsProvider>();
2125
return services;
2226
}
2327
}

0 commit comments

Comments
 (0)