feat(siteruntime): OperationTrackingStore site-local SQLite (#23 M3)

This commit is contained in:
Joseph Doherty
2026-05-20 13:51:09 -04:00
parent 1c38dd540f
commit b86d7c61ab
5 changed files with 767 additions and 0 deletions
@@ -0,0 +1,87 @@
using ScadaLink.Commons.Types;
namespace ScadaLink.Commons.Interfaces;
/// <summary>
/// Site-local source of truth for cached-operation tracking
/// (<c>ExternalSystem.CachedCall</c> / <c>Database.CachedWrite</c>) — alongside the
/// Store-and-Forward buffer, this is the row that <c>Tracking.Status(id)</c>
/// reads (Audit Log #23 / M3). One row per <see cref="TrackedOperationId"/>;
/// terminal rows are purged after a configurable retention window
/// (default 7 days).
/// </summary>
/// <remarks>
/// <para>
/// The store is intentionally a thin write-API on top of SQLite — not a
/// dispatcher. Status transitions follow
/// <c>Submitted → Retrying → Delivered / Parked / Failed / Discarded</c>; rows
/// in a terminal state never roll back. Implementations must:
/// <list type="bullet">
/// <item><description><see cref="RecordEnqueueAsync"/> is insert-if-not-exists
/// (caller-supplied id is the idempotency key — duplicate enqueues are no-ops).</description></item>
/// <item><description><see cref="RecordAttemptAsync"/> only updates non-terminal rows.</description></item>
/// <item><description><see cref="RecordTerminalAsync"/> only flips a non-terminal row to terminal.</description></item>
/// <item><description><see cref="PurgeTerminalAsync"/> deletes terminal rows whose
/// <c>TerminalAtUtc</c> is strictly older than the supplied threshold.</description></item>
/// </list>
/// </para>
/// </remarks>
public interface IOperationTrackingStore
{
/// <summary>
/// Insert a new tracking row in <c>Submitted</c> state with <c>RetryCount = 0</c>.
/// Idempotent — a duplicate id is silently ignored (the existing row is left
/// untouched), matching the at-least-once semantics of the calling site
/// store-and-forward path.
/// </summary>
Task RecordEnqueueAsync(
TrackedOperationId id,
string kind,
string? targetSummary,
string? sourceInstanceId,
string? sourceScript,
CancellationToken ct = default);
/// <summary>
/// Advance an in-flight tracking row's status, retry counter, and most-
/// recent error/HTTP-status. Terminal rows (<see cref="RecordTerminalAsync"/>
/// already applied) are NOT mutated — the operation has reached its final
/// outcome and any late-arriving attempt telemetry is dropped on the floor.
/// </summary>
Task RecordAttemptAsync(
TrackedOperationId id,
string status,
int retryCount,
string? lastError,
int? httpStatus,
CancellationToken ct = default);
/// <summary>
/// Flip a non-terminal tracking row to terminal — sets
/// <c>TerminalAtUtc = now</c> and writes the final status / error. A row
/// already in terminal state is left untouched (first-write-wins).
/// </summary>
Task RecordTerminalAsync(
TrackedOperationId id,
string status,
string? lastError,
int? httpStatus,
CancellationToken ct = default);
/// <summary>
/// Return the latest snapshot for the supplied id, or <c>null</c> when no
/// tracking row exists (purged or never recorded).
/// </summary>
Task<TrackingStatusSnapshot?> GetStatusAsync(
TrackedOperationId id,
CancellationToken ct = default);
/// <summary>
/// Delete terminal rows whose <c>TerminalAtUtc</c> is strictly older than
/// <paramref name="olderThanUtc"/>. Non-terminal rows are kept regardless
/// of age (the operation is still in flight).
/// </summary>
Task PurgeTerminalAsync(
DateTime olderThanUtc,
CancellationToken ct = default);
}