sprints work
This commit is contained in:
363
src/__Libraries/StellaOps.Provcache/IProvcacheService.cs
Normal file
363
src/__Libraries/StellaOps.Provcache/IProvcacheService.cs
Normal file
@@ -0,0 +1,363 @@
|
||||
namespace StellaOps.Provcache;
|
||||
|
||||
/// <summary>
|
||||
/// High-level service interface for Provcache operations.
|
||||
/// Orchestrates cache store and repository with metrics and invalidation logic.
|
||||
/// </summary>
|
||||
public interface IProvcacheService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a cached decision by VeriKey.
|
||||
/// </summary>
|
||||
/// <param name="veriKey">The cache key.</param>
|
||||
/// <param name="bypassCache">If true, skip cache and force re-evaluation.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The cache result with decision if found.</returns>
|
||||
Task<ProvcacheServiceResult> GetAsync(
|
||||
string veriKey,
|
||||
bool bypassCache = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a decision in the cache.
|
||||
/// </summary>
|
||||
/// <param name="entry">The cache entry to store.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the entry was stored successfully.</returns>
|
||||
Task<bool> SetAsync(ProvcacheEntry entry, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or computes a decision using a factory function for cache misses.
|
||||
/// </summary>
|
||||
/// <param name="veriKey">The cache key.</param>
|
||||
/// <param name="factory">Factory function to create the entry on cache miss.</param>
|
||||
/// <param name="bypassCache">If true, skip cache and force re-computation.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>The cached or newly computed entry.</returns>
|
||||
Task<ProvcacheEntry> GetOrComputeAsync(
|
||||
string veriKey,
|
||||
Func<CancellationToken, Task<ProvcacheEntry>> factory,
|
||||
bool bypassCache = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates a cache entry by VeriKey.
|
||||
/// </summary>
|
||||
/// <param name="veriKey">The cache key.</param>
|
||||
/// <param name="reason">Reason for invalidation (for audit log).</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>True if the entry existed and was invalidated.</returns>
|
||||
Task<bool> InvalidateAsync(
|
||||
string veriKey,
|
||||
string? reason = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates entries by invalidation criteria.
|
||||
/// </summary>
|
||||
/// <param name="request">The invalidation request.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Invalidation result with count of affected entries.</returns>
|
||||
Task<InvalidationResult> InvalidateByAsync(
|
||||
InvalidationRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets cache metrics for monitoring.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Cache metrics.</returns>
|
||||
Task<ProvcacheMetrics> GetMetricsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Prunes expired entries from the cache.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Number of entries pruned.</returns>
|
||||
Task<long> PruneExpiredAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a cache service lookup.
|
||||
/// </summary>
|
||||
public sealed record ProvcacheServiceResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The cache result status.
|
||||
/// </summary>
|
||||
public required ProvcacheResultStatus Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The cache entry if found.
|
||||
/// </summary>
|
||||
public ProvcacheEntry? Entry { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the result came from cache (true) or needs computation (false).
|
||||
/// </summary>
|
||||
public bool WasCached => Status == ProvcacheResultStatus.CacheHit;
|
||||
|
||||
/// <summary>
|
||||
/// Source of the cache hit for diagnostics.
|
||||
/// </summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time taken for the lookup in milliseconds.
|
||||
/// </summary>
|
||||
public double ElapsedMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache hit result.
|
||||
/// </summary>
|
||||
public static ProvcacheServiceResult Hit(ProvcacheEntry entry, string source, double elapsedMs) => new()
|
||||
{
|
||||
Status = ProvcacheResultStatus.CacheHit,
|
||||
Entry = entry,
|
||||
Source = source,
|
||||
ElapsedMs = elapsedMs
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache miss result.
|
||||
/// </summary>
|
||||
public static ProvcacheServiceResult Miss(double elapsedMs) => new()
|
||||
{
|
||||
Status = ProvcacheResultStatus.CacheMiss,
|
||||
Entry = null,
|
||||
Source = null,
|
||||
ElapsedMs = elapsedMs
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a bypassed result (cache was skipped).
|
||||
/// </summary>
|
||||
public static ProvcacheServiceResult Bypassed() => new()
|
||||
{
|
||||
Status = ProvcacheResultStatus.Bypassed,
|
||||
Entry = null,
|
||||
Source = null,
|
||||
ElapsedMs = 0
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates an expired result.
|
||||
/// </summary>
|
||||
public static ProvcacheServiceResult Expired(ProvcacheEntry entry, double elapsedMs) => new()
|
||||
{
|
||||
Status = ProvcacheResultStatus.Expired,
|
||||
Entry = entry,
|
||||
Source = "expired",
|
||||
ElapsedMs = elapsedMs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache result status.
|
||||
/// </summary>
|
||||
public enum ProvcacheResultStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Entry was found in cache and is valid.
|
||||
/// </summary>
|
||||
CacheHit,
|
||||
|
||||
/// <summary>
|
||||
/// Entry was not found in cache.
|
||||
/// </summary>
|
||||
CacheMiss,
|
||||
|
||||
/// <summary>
|
||||
/// Cache was bypassed (force re-computation).
|
||||
/// </summary>
|
||||
Bypassed,
|
||||
|
||||
/// <summary>
|
||||
/// Entry was found but has expired.
|
||||
/// </summary>
|
||||
Expired
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for cache invalidation by criteria.
|
||||
/// </summary>
|
||||
public sealed record InvalidationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The invalidation type.
|
||||
/// </summary>
|
||||
public required InvalidationType Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The value to match for invalidation.
|
||||
/// </summary>
|
||||
public required string Value { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for invalidation (for audit log).
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actor who initiated the invalidation.
|
||||
/// </summary>
|
||||
public string? Actor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an invalidation request by policy hash.
|
||||
/// </summary>
|
||||
public static InvalidationRequest ByPolicyHash(string policyHash, string? reason = null) => new()
|
||||
{
|
||||
Type = InvalidationType.PolicyHash,
|
||||
Value = policyHash,
|
||||
Reason = reason ?? "policy-update"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates an invalidation request by signer set hash.
|
||||
/// </summary>
|
||||
public static InvalidationRequest BySignerSetHash(string signerSetHash, string? reason = null) => new()
|
||||
{
|
||||
Type = InvalidationType.SignerSetHash,
|
||||
Value = signerSetHash,
|
||||
Reason = reason ?? "signer-revocation"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates an invalidation request by feed epoch.
|
||||
/// </summary>
|
||||
public static InvalidationRequest ByFeedEpochOlderThan(string feedEpoch, string? reason = null) => new()
|
||||
{
|
||||
Type = InvalidationType.FeedEpochOlderThan,
|
||||
Value = feedEpoch,
|
||||
Reason = reason ?? "feed-update"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates an invalidation request by key pattern.
|
||||
/// </summary>
|
||||
public static InvalidationRequest ByPattern(string pattern, string? reason = null) => new()
|
||||
{
|
||||
Type = InvalidationType.Pattern,
|
||||
Value = pattern,
|
||||
Reason = reason ?? "pattern-invalidation"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of invalidation criteria.
|
||||
/// </summary>
|
||||
public enum InvalidationType
|
||||
{
|
||||
/// <summary>
|
||||
/// Invalidate by policy hash.
|
||||
/// </summary>
|
||||
PolicyHash,
|
||||
|
||||
/// <summary>
|
||||
/// Invalidate by signer set hash.
|
||||
/// </summary>
|
||||
SignerSetHash,
|
||||
|
||||
/// <summary>
|
||||
/// Invalidate entries with feed epoch older than specified.
|
||||
/// </summary>
|
||||
FeedEpochOlderThan,
|
||||
|
||||
/// <summary>
|
||||
/// Invalidate by key pattern.
|
||||
/// </summary>
|
||||
Pattern,
|
||||
|
||||
/// <summary>
|
||||
/// Invalidate expired entries.
|
||||
/// </summary>
|
||||
Expired
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of an invalidation operation.
|
||||
/// </summary>
|
||||
public sealed record InvalidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of entries invalidated.
|
||||
/// </summary>
|
||||
public required long EntriesAffected { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The invalidation request that was executed.
|
||||
/// </summary>
|
||||
public required InvalidationRequest Request { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the invalidation.
|
||||
/// </summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the invalidation was logged for audit.
|
||||
/// </summary>
|
||||
public bool WasLogged { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache metrics for monitoring and observability.
|
||||
/// </summary>
|
||||
public sealed record ProvcacheMetrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Total cache requests since startup.
|
||||
/// </summary>
|
||||
public long TotalRequests { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total cache hits since startup.
|
||||
/// </summary>
|
||||
public long TotalHits { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total cache misses since startup.
|
||||
/// </summary>
|
||||
public long TotalMisses { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cache hit rate (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public double HitRate => TotalRequests == 0 ? 0.0 : (double)TotalHits / TotalRequests;
|
||||
|
||||
/// <summary>
|
||||
/// Average lookup latency in milliseconds.
|
||||
/// </summary>
|
||||
public double AvgLatencyMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// P99 lookup latency in milliseconds.
|
||||
/// </summary>
|
||||
public double P99LatencyMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current number of entries in cache.
|
||||
/// </summary>
|
||||
public long CurrentEntryCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total invalidations since startup.
|
||||
/// </summary>
|
||||
public long TotalInvalidations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Valkey cache health status.
|
||||
/// </summary>
|
||||
public bool ValkeyCacheHealthy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Postgres repository health status.
|
||||
/// </summary>
|
||||
public bool PostgresRepositoryHealthy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when metrics were collected.
|
||||
/// </summary>
|
||||
public DateTimeOffset CollectedAt { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user