feat(config-db): secured-write TryMarkExpiredAsync CAS + CountAsync (arch-review S2/P3)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 05:00:23 -04:00
parent 5cfa85ea91
commit 0fad6817c2
3 changed files with 124 additions and 0 deletions
@@ -79,4 +79,34 @@ public interface ISecuredWriteRepository
string? verifierComment,
DateTime decidedAtUtc,
CancellationToken ct = default);
/// <summary>
/// Atomically flips a row from <c>Pending</c> to <c>Expired</c>, stamping the
/// expiry as the decision time, but ONLY if the row is still <c>Pending</c>.
/// This is the multi-node-safe expiry primitive: the conditional
/// <c>WHERE Status='Pending'</c> makes the Pending-&gt;Expired transition atomic at
/// the row level, so two nodes sweeping the same overdue write concurrently
/// produce exactly one winner. The expiry is a system decision — no verifier is
/// recorded and <see cref="PendingSecuredWrite.VerifierUser"/> stays <c>null</c>.
/// The loser observes <c>false</c> (the row was already decided or expired).
/// </summary>
/// <param name="id">Identity of the pending secured write.</param>
/// <param name="expiredAtUtc">UTC instant the write was deemed expired.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>
/// A task resolving to <c>true</c> if this caller won the expiry (exactly one
/// row transitioned), or <c>false</c> if the row was no longer <c>Pending</c>.
/// </returns>
Task<bool> TryMarkExpiredAsync(long id, DateTime expiredAtUtc, CancellationToken ct = default);
/// <summary>
/// Returns the number of rows matching the optional <paramref name="status"/> and
/// <paramref name="siteId"/> filters. A <c>null</c> filter argument matches every
/// row. Mirrors <see cref="QueryAsync"/>'s filter composition without paging.
/// </summary>
/// <param name="status">Status filter; <c>null</c> matches every status.</param>
/// <param name="siteId">Site id filter; <c>null</c> matches every site.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>A task that resolves to the count of matching rows.</returns>
Task<int> CountAsync(string? status, string? siteId, CancellationToken ct = default);
}