Files
git.stella-ops.org/src/__Libraries/StellaOps.Provcache/IProvcacheService.cs
StellaOps Bot 2a06f780cf sprints work
2025-12-25 12:19:12 +02:00

364 lines
10 KiB
C#

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; }
}