364 lines
10 KiB
C#
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; }
|
|
}
|