refactor(provcache): inject TimeProvider and IGuidProvider for determinism - DET-005
Refactored 8 files across StellaOps.Provcache, StellaOps.Provcache.Postgres, and StellaOps.Provcache.Valkey: Core Provcache library: - EvidenceChunker: Added IGuidProvider for ChunkId generation in ChunkAsync/ChunkStreamAsync - LazyFetchOrchestrator: Added IGuidProvider for ChunkId generation when storing fetched chunks - MinimalProofExporter: Added IGuidProvider for ChunkId generation in ImportAsync - FeedEpochAdvancedEvent: Added optional eventId/timestamp parameters to static Create() - SignerRevokedEvent: Added optional eventId/timestamp parameters to static Create() Postgres implementation: - PostgresProvcacheRepository: Added TimeProvider and IGuidProvider for IncrementHitCountAsync, GetStatisticsAsync, LogRevocationAsync, and MapToEntity - PostgresEvidenceChunkRepository: Added TimeProvider and IGuidProvider for GetManifestAsync and MapToEntity Valkey implementation: - ValkeyProvcacheStore: Added TimeProvider for TTL calculations in GetAsync, SetAsync, SetManyAsync All constructors use optional parameters with defaults to system implementations for backward compatibility. Added StellaOps.Determinism.Abstractions project references where needed.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Provcache.Entities;
|
||||
|
||||
namespace StellaOps.Provcache.Postgres;
|
||||
@@ -11,13 +12,19 @@ public sealed class PostgresEvidenceChunkRepository : IEvidenceChunkRepository
|
||||
{
|
||||
private readonly ProvcacheDbContext _context;
|
||||
private readonly ILogger<PostgresEvidenceChunkRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public PostgresEvidenceChunkRepository(
|
||||
ProvcacheDbContext context,
|
||||
ILogger<PostgresEvidenceChunkRepository> logger)
|
||||
ILogger<PostgresEvidenceChunkRepository> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -131,7 +138,7 @@ public sealed class PostgresEvidenceChunkRepository : IEvidenceChunkRepository
|
||||
TotalChunks = chunks.Count,
|
||||
TotalSize = chunks.Sum(c => (long)c.BlobSize),
|
||||
Chunks = metadata,
|
||||
GeneratedAt = DateTimeOffset.UtcNow
|
||||
GeneratedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -240,11 +247,11 @@ public sealed class PostgresEvidenceChunkRepository : IEvidenceChunkRepository
|
||||
};
|
||||
}
|
||||
|
||||
private static ProvcacheEvidenceChunkEntity MapToEntity(EvidenceChunk chunk, string proofRoot)
|
||||
private ProvcacheEvidenceChunkEntity MapToEntity(EvidenceChunk chunk, string proofRoot)
|
||||
{
|
||||
return new ProvcacheEvidenceChunkEntity
|
||||
{
|
||||
ChunkId = chunk.ChunkId == Guid.Empty ? Guid.NewGuid() : chunk.ChunkId,
|
||||
ChunkId = chunk.ChunkId == Guid.Empty ? _guidProvider.NewGuid() : chunk.ChunkId,
|
||||
ProofRoot = proofRoot,
|
||||
ChunkIndex = chunk.ChunkIndex,
|
||||
ChunkHash = chunk.ChunkHash,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Provcache.Entities;
|
||||
|
||||
namespace StellaOps.Provcache.Postgres;
|
||||
@@ -12,14 +13,20 @@ public sealed class PostgresProvcacheRepository : IProvcacheRepository
|
||||
{
|
||||
private readonly ProvcacheDbContext _context;
|
||||
private readonly ILogger<PostgresProvcacheRepository> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public PostgresProvcacheRepository(
|
||||
ProvcacheDbContext context,
|
||||
ILogger<PostgresProvcacheRepository> logger)
|
||||
ILogger<PostgresProvcacheRepository> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
@@ -193,12 +200,13 @@ public sealed class PostgresProvcacheRepository : IProvcacheRepository
|
||||
/// <inheritdoc />
|
||||
public async Task IncrementHitCountAsync(string veriKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
await _context.ProvcacheItems
|
||||
.Where(e => e.VeriKey == veriKey)
|
||||
.ExecuteUpdateAsync(
|
||||
setters => setters
|
||||
.SetProperty(e => e.HitCount, e => e.HitCount + 1)
|
||||
.SetProperty(e => e.LastAccessedAt, DateTimeOffset.UtcNow),
|
||||
.SetProperty(e => e.LastAccessedAt, now),
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -206,7 +214,7 @@ public sealed class PostgresProvcacheRepository : IProvcacheRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<ProvcacheStatistics> GetStatisticsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var hourFromNow = now.AddHours(1);
|
||||
|
||||
var totalEntries = await _context.ProvcacheItems
|
||||
@@ -266,12 +274,12 @@ public sealed class PostgresProvcacheRepository : IProvcacheRepository
|
||||
{
|
||||
var revocation = new ProvcacheRevocationEntity
|
||||
{
|
||||
RevocationId = Guid.NewGuid(),
|
||||
RevocationId = _guidProvider.NewGuid(),
|
||||
RevocationType = type,
|
||||
TargetHash = targetHash,
|
||||
Reason = reason,
|
||||
EntriesAffected = entriesAffected,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
CreatedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
_context.Revocations.Add(revocation);
|
||||
@@ -329,7 +337,7 @@ public sealed class PostgresProvcacheRepository : IProvcacheRepository
|
||||
HitCount = entry.HitCount,
|
||||
CreatedAt = entry.CreatedAt,
|
||||
ExpiresAt = entry.ExpiresAt,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
UpdatedAt = _timeProvider.GetUtcNow(),
|
||||
LastAccessedAt = entry.LastAccessedAt
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Provcache/StellaOps.Provcache.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
private readonly IConnectionMultiplexer _connectionMultiplexer;
|
||||
private readonly ProvcacheOptions _options;
|
||||
private readonly ILogger<ValkeyProvcacheStore> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
||||
private IDatabase? _database;
|
||||
@@ -24,11 +25,13 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
public ValkeyProvcacheStore(
|
||||
IConnectionMultiplexer connectionMultiplexer,
|
||||
IOptions<ProvcacheOptions> options,
|
||||
ILogger<ValkeyProvcacheStore> logger)
|
||||
ILogger<ValkeyProvcacheStore> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_connectionMultiplexer = connectionMultiplexer ?? throw new ArgumentNullException(nameof(connectionMultiplexer));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
@@ -65,7 +68,7 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
// Optionally refresh TTL on read (sliding expiration)
|
||||
if (_options.SlidingExpiration)
|
||||
{
|
||||
var ttl = entry.ExpiresAt - DateTimeOffset.UtcNow;
|
||||
var ttl = entry.ExpiresAt - _timeProvider.GetUtcNow();
|
||||
if (ttl > TimeSpan.Zero)
|
||||
{
|
||||
await db.KeyExpireAsync(redisKey, ttl).ConfigureAwait(false);
|
||||
@@ -169,7 +172,7 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
var redisKey = BuildKey(entry.VeriKey);
|
||||
var value = JsonSerializer.Serialize(entry, _jsonOptions);
|
||||
|
||||
var ttl = entry.ExpiresAt - DateTimeOffset.UtcNow;
|
||||
var ttl = entry.ExpiresAt - _timeProvider.GetUtcNow();
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogDebug("Skipping expired entry for VeriKey {VeriKey}", entry.VeriKey);
|
||||
@@ -205,13 +208,14 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
var db = await GetDatabaseAsync().ConfigureAwait(false);
|
||||
var batch = db.CreateBatch();
|
||||
var tasks = new List<Task>();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
foreach (var entry in entryList)
|
||||
{
|
||||
var redisKey = BuildKey(entry.VeriKey);
|
||||
var value = JsonSerializer.Serialize(entry, _jsonOptions);
|
||||
|
||||
var ttl = entry.ExpiresAt - DateTimeOffset.UtcNow;
|
||||
var ttl = entry.ExpiresAt - now;
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Security.Cryptography;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Provcache;
|
||||
|
||||
@@ -87,11 +88,16 @@ public sealed class EvidenceChunker : IEvidenceChunker
|
||||
{
|
||||
private readonly ProvcacheOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
public EvidenceChunker(ProvcacheOptions options, TimeProvider? timeProvider = null)
|
||||
public EvidenceChunker(
|
||||
ProvcacheOptions options,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -122,7 +128,7 @@ public sealed class EvidenceChunker : IEvidenceChunker
|
||||
|
||||
chunks.Add(new EvidenceChunk
|
||||
{
|
||||
ChunkId = Guid.NewGuid(),
|
||||
ChunkId = _guidProvider.NewGuid(),
|
||||
ProofRoot = string.Empty, // Will be set after computing Merkle root
|
||||
ChunkIndex = chunkIndex,
|
||||
ChunkHash = chunkHash,
|
||||
@@ -171,7 +177,7 @@ public sealed class EvidenceChunker : IEvidenceChunker
|
||||
|
||||
yield return new EvidenceChunk
|
||||
{
|
||||
ChunkId = Guid.NewGuid(),
|
||||
ChunkId = _guidProvider.NewGuid(),
|
||||
ProofRoot = string.Empty, // Caller must compute after all chunks
|
||||
ChunkIndex = chunkIndex,
|
||||
ChunkHash = chunkHash,
|
||||
|
||||
@@ -80,6 +80,17 @@ public sealed record FeedEpochAdvancedEvent
|
||||
/// <summary>
|
||||
/// Creates a new FeedEpochAdvancedEvent.
|
||||
/// </summary>
|
||||
/// <param name="feedId">The feed identifier.</param>
|
||||
/// <param name="previousEpoch">The previous epoch identifier.</param>
|
||||
/// <param name="newEpoch">The new epoch identifier.</param>
|
||||
/// <param name="effectiveAt">When the new epoch became effective.</param>
|
||||
/// <param name="advisoriesAdded">Number of advisories added (for metrics).</param>
|
||||
/// <param name="advisoriesModified">Number of advisories modified (for metrics).</param>
|
||||
/// <param name="advisoriesWithdrawn">Number of advisories withdrawn (for metrics).</param>
|
||||
/// <param name="tenantId">Tenant ID if multi-tenant.</param>
|
||||
/// <param name="correlationId">Correlation ID for tracing.</param>
|
||||
/// <param name="eventId">Optional event ID (defaults to new GUID).</param>
|
||||
/// <param name="timestamp">Optional timestamp (defaults to current UTC time).</param>
|
||||
public static FeedEpochAdvancedEvent Create(
|
||||
string feedId,
|
||||
string previousEpoch,
|
||||
@@ -89,12 +100,14 @@ public sealed record FeedEpochAdvancedEvent
|
||||
int? advisoriesModified = null,
|
||||
int? advisoriesWithdrawn = null,
|
||||
string? tenantId = null,
|
||||
string? correlationId = null)
|
||||
string? correlationId = null,
|
||||
Guid? eventId = null,
|
||||
DateTimeOffset? timestamp = null)
|
||||
{
|
||||
return new FeedEpochAdvancedEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
EventId = eventId ?? Guid.NewGuid(),
|
||||
Timestamp = timestamp ?? DateTimeOffset.UtcNow,
|
||||
FeedId = feedId,
|
||||
PreviousEpoch = previousEpoch,
|
||||
NewEpoch = newEpoch,
|
||||
|
||||
@@ -71,6 +71,15 @@ public sealed record SignerRevokedEvent
|
||||
/// <summary>
|
||||
/// Creates a new SignerRevokedEvent.
|
||||
/// </summary>
|
||||
/// <param name="anchorId">The trust anchor ID that owns the revoked key.</param>
|
||||
/// <param name="keyId">The revoked key identifier.</param>
|
||||
/// <param name="signerHash">Hash of the revoked signer's certificate/public key.</param>
|
||||
/// <param name="effectiveAt">When the revocation became effective.</param>
|
||||
/// <param name="reason">Reason for the revocation.</param>
|
||||
/// <param name="actor">Actor who initiated the revocation.</param>
|
||||
/// <param name="correlationId">Correlation ID for tracing.</param>
|
||||
/// <param name="eventId">Optional event ID (defaults to new GUID).</param>
|
||||
/// <param name="timestamp">Optional timestamp (defaults to current UTC time).</param>
|
||||
public static SignerRevokedEvent Create(
|
||||
Guid anchorId,
|
||||
string keyId,
|
||||
@@ -78,12 +87,14 @@ public sealed record SignerRevokedEvent
|
||||
DateTimeOffset effectiveAt,
|
||||
string? reason = null,
|
||||
string? actor = null,
|
||||
string? correlationId = null)
|
||||
string? correlationId = null,
|
||||
Guid? eventId = null,
|
||||
DateTimeOffset? timestamp = null)
|
||||
{
|
||||
return new SignerRevokedEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
EventId = eventId ?? Guid.NewGuid(),
|
||||
Timestamp = timestamp ?? DateTimeOffset.UtcNow,
|
||||
AnchorId = anchorId,
|
||||
KeyId = keyId,
|
||||
SignerHash = signerHash,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Provenance.Attestation;
|
||||
|
||||
namespace StellaOps.Provcache;
|
||||
@@ -15,6 +16,7 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
private readonly IEvidenceChunkRepository _chunkRepository;
|
||||
private readonly ISigner? _signer;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<MinimalProofExporter> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions s_jsonOptions = new()
|
||||
@@ -29,12 +31,14 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
IEvidenceChunkRepository chunkRepository,
|
||||
ISigner? signer = null,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null,
|
||||
ILogger<MinimalProofExporter>? logger = null)
|
||||
{
|
||||
_provcacheService = provcacheService ?? throw new ArgumentNullException(nameof(provcacheService));
|
||||
_chunkRepository = chunkRepository ?? throw new ArgumentNullException(nameof(chunkRepository));
|
||||
_signer = signer;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<MinimalProofExporter>.Instance;
|
||||
}
|
||||
|
||||
@@ -192,7 +196,7 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
|
||||
chunksToStore.Add(new EvidenceChunk
|
||||
{
|
||||
ChunkId = Guid.NewGuid(),
|
||||
ChunkId = _guidProvider.NewGuid(),
|
||||
ProofRoot = bundle.Digest.ProofRoot,
|
||||
ChunkIndex = bundleChunk.Index,
|
||||
ChunkHash = bundleChunk.Hash,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Provcache;
|
||||
|
||||
@@ -13,6 +14,7 @@ public sealed class LazyFetchOrchestrator
|
||||
private readonly IEvidenceChunkRepository _repository;
|
||||
private readonly ILogger<LazyFetchOrchestrator> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a lazy fetch orchestrator.
|
||||
@@ -20,14 +22,17 @@ public sealed class LazyFetchOrchestrator
|
||||
/// <param name="repository">The chunk repository for local storage.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
/// <param name="timeProvider">Optional time provider.</param>
|
||||
/// <param name="guidProvider">Optional GUID provider.</param>
|
||||
public LazyFetchOrchestrator(
|
||||
IEvidenceChunkRepository repository,
|
||||
ILogger<LazyFetchOrchestrator> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,7 +159,7 @@ public sealed class LazyFetchOrchestrator
|
||||
// Convert FetchedChunk to EvidenceChunk for storage
|
||||
var evidenceChunk = new EvidenceChunk
|
||||
{
|
||||
ChunkId = Guid.NewGuid(),
|
||||
ChunkId = _guidProvider.NewGuid(),
|
||||
ProofRoot = proofRoot,
|
||||
ChunkIndex = fetchedChunk.Index,
|
||||
ChunkHash = fetchedChunk.Hash,
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="../../Router/__Libraries/StellaOps.Messaging/StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="../../Provenance/StellaOps.Provenance.Attestation/StellaOps.Provenance.Attestation.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user