65 lines
2.9 KiB
C#
65 lines
2.9 KiB
C#
using System.Text.Json;
|
|
|
|
namespace JobTrackerApi.Services;
|
|
|
|
public sealed record AccountDeletionTombstone(string SchemaVersion, Guid RequestId, string OwnerKey, DateTimeOffset CompletedAtUtc);
|
|
|
|
public sealed class AccountDeletionTombstoneStore(AppPaths paths)
|
|
{
|
|
private const string SchemaVersion = "jobtracker.account-deletion-tombstone.v1";
|
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
|
private string LedgerPath => Path.Combine(paths.AccountDeletionTombstonesRoot, "tombstones.jsonl");
|
|
|
|
public async Task AppendAsync(Guid requestId, string ownerKey, DateTimeOffset completedAtUtc, CancellationToken cancellationToken)
|
|
{
|
|
await _gate.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
var existing = await ReadUnsafeAsync(cancellationToken);
|
|
if (existing.Any(item => item.RequestId == requestId)) return;
|
|
Directory.CreateDirectory(paths.AccountDeletionTombstonesRoot);
|
|
var line = JsonSerializer.Serialize(new AccountDeletionTombstone(SchemaVersion, requestId, ownerKey, completedAtUtc));
|
|
await File.AppendAllTextAsync(LedgerPath, line + Environment.NewLine, cancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
_gate.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<IReadOnlyList<AccountDeletionTombstone>> ReadAsync(CancellationToken cancellationToken)
|
|
{
|
|
await _gate.WaitAsync(cancellationToken);
|
|
try { return await ReadUnsafeAsync(cancellationToken); }
|
|
finally { _gate.Release(); }
|
|
}
|
|
|
|
private async Task<IReadOnlyList<AccountDeletionTombstone>> ReadUnsafeAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!File.Exists(LedgerPath)) return Array.Empty<AccountDeletionTombstone>();
|
|
var result = new List<AccountDeletionTombstone>();
|
|
foreach (var line in await File.ReadAllLinesAsync(LedgerPath, cancellationToken))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
try
|
|
{
|
|
var item = JsonSerializer.Deserialize<AccountDeletionTombstone>(line);
|
|
if (item is null
|
|
|| item.SchemaVersion != SchemaVersion
|
|
|| item.RequestId == Guid.Empty
|
|
|| item.OwnerKey.Length != 64
|
|
|| item.OwnerKey.Any(character => !Uri.IsHexDigit(character)))
|
|
throw new InvalidOperationException("The account-deletion tombstone ledger contains an invalid record.");
|
|
result.Add(item);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// A partial/corrupt line is never ignored by replay callers: expose a sentinel so
|
|
// startup fails closed instead of declaring the tombstone set complete.
|
|
throw new InvalidOperationException("The account-deletion tombstone ledger contains an invalid record.");
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|