Files
git.stella-ops.org/src/__Libraries/StellaOps.Provcache/Revocation/InMemoryRevocationLedger.cs
2026-02-01 21:37:40 +02:00

139 lines
4.4 KiB
C#

using Microsoft.Extensions.Logging;
using StellaOps.Provcache.Entities;
using System.Collections.Concurrent;
using System.Text.Json;
namespace StellaOps.Provcache;
/// <summary>
/// In-memory implementation of the revocation ledger for testing and non-persistent scenarios.
/// For production use, inject a PostgreSQL-backed implementation from StellaOps.Provcache.Postgres.
/// </summary>
public sealed class InMemoryRevocationLedger : IRevocationLedger
{
private readonly ConcurrentDictionary<long, RevocationEntry> _entries = new();
private readonly ILogger<InMemoryRevocationLedger> _logger;
private long _currentSeqNo;
public InMemoryRevocationLedger(ILogger<InMemoryRevocationLedger> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public Task<RevocationEntry> RecordAsync(
RevocationEntry entry,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(entry);
var seqNo = Interlocked.Increment(ref _currentSeqNo);
var recordedEntry = entry with { SeqNo = seqNo };
_entries[seqNo] = recordedEntry;
_logger.LogInformation(
"Recorded revocation {RevocationId} of type {Type} for key {Key}, invalidated {Count} entries",
entry.RevocationId, entry.RevocationType, entry.RevokedKey, entry.EntriesInvalidated);
return Task.FromResult(recordedEntry);
}
/// <inheritdoc />
public Task<IReadOnlyList<RevocationEntry>> GetEntriesSinceAsync(
long sinceSeqNo,
int limit = 1000,
CancellationToken cancellationToken = default)
{
var entries = _entries.Values
.Where(e => e.SeqNo > sinceSeqNo)
.OrderBy(e => e.SeqNo)
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<RevocationEntry>>(entries);
}
/// <inheritdoc />
public Task<IReadOnlyList<RevocationEntry>> GetEntriesByTypeAsync(
string revocationType,
DateTimeOffset? since = null,
int limit = 1000,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(revocationType);
var query = _entries.Values
.Where(e => e.RevocationType == revocationType);
if (since.HasValue)
{
query = query.Where(e => e.RevokedAt > since.Value);
}
var entries = query
.OrderBy(e => e.SeqNo)
.Take(limit)
.ToList();
return Task.FromResult<IReadOnlyList<RevocationEntry>>(entries);
}
/// <inheritdoc />
public Task<long> GetLatestSeqNoAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(Interlocked.Read(ref _currentSeqNo));
}
/// <inheritdoc />
public Task<IReadOnlyList<RevocationEntry>> GetRevocationsForKeyAsync(
string revokedKey,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(revokedKey);
var entries = _entries.Values
.Where(e => e.RevokedKey == revokedKey)
.OrderBy(e => e.SeqNo)
.ToList();
return Task.FromResult<IReadOnlyList<RevocationEntry>>(entries);
}
/// <inheritdoc />
public Task<RevocationLedgerStats> GetStatsAsync(CancellationToken cancellationToken = default)
{
var allEntries = _entries.Values.ToList();
var totalEntries = allEntries.Count;
var latestSeqNo = Interlocked.Read(ref _currentSeqNo);
var totalInvalidated = allEntries.Sum(e => (long)e.EntriesInvalidated);
var entriesByType = allEntries
.GroupBy(e => e.RevocationType)
.ToDictionary(g => g.Key, g => (long)g.Count());
var oldestEntry = allEntries.MinBy(e => e.SeqNo)?.RevokedAt;
var newestEntry = allEntries.MaxBy(e => e.SeqNo)?.RevokedAt;
return Task.FromResult(new RevocationLedgerStats
{
TotalEntries = totalEntries,
LatestSeqNo = latestSeqNo,
EntriesByType = entriesByType,
TotalEntriesInvalidated = totalInvalidated,
OldestEntryAt = oldestEntry,
NewestEntryAt = newestEntry
});
}
/// <summary>
/// Clears all entries (for testing).
/// </summary>
public void Clear()
{
_entries.Clear();
Interlocked.Exchange(ref _currentSeqNo, 0);
}
}