fix(site-call-audit): same-rank freshness tiebreaker — retrying calls no longer freeze RetryCount/LastError at first write
Within an equal NON-terminal rank the newest UpdatedAtUtc wins, unfreezing the Attempted-phase RetryCount/LastError/HttpStatus. Terminal ranks (>=3) are excluded from the tiebreaker so terminal immutability is preserved (Delivered never overwrites Parked); equal stamps stay idempotent, lower rank stays a no-op.
This commit is contained in:
+35
-8
@@ -24,9 +24,17 @@ public class SiteCallAuditRepository : ISiteCallAuditRepository
|
||||
private const int SqlErrorUniqueIndexViolation = 2601;
|
||||
private const int SqlErrorPrimaryKeyViolation = 2627;
|
||||
|
||||
// Monotonic status ordering. Lower rank wins on tie (same-rank upserts are
|
||||
// no-ops, including terminal-over-terminal):
|
||||
// Monotonic status ordering:
|
||||
// Submitted < Forwarded < Attempted == Skipped < Delivered == Failed == Parked == Discarded.
|
||||
// A higher incoming rank always wins. WITHIN an equal NON-terminal rank
|
||||
// (Attempted/Skipped, rank 2 — and the transient Submitted/Forwarded ranks),
|
||||
// the newest UpdatedAtUtc wins so a retrying call's live RetryCount/LastError
|
||||
// no longer freezes at first-write (Task 11). Equal terminal ranks (rank 3)
|
||||
// stay immutable — the freshness tiebreaker is deliberately scoped to
|
||||
// rank < 3, so a later terminal NEVER flips an earlier one (Delivered cannot
|
||||
// overwrite Parked). Still idempotent (equal stamps are inert) and still
|
||||
// regression-proof (a lower rank is always a no-op).
|
||||
private const int TerminalRank = 3;
|
||||
private static readonly Dictionary<string, int> StatusRank = new(StringComparer.Ordinal)
|
||||
{
|
||||
["Submitted"] = 0,
|
||||
@@ -95,10 +103,16 @@ VALUES
|
||||
idText);
|
||||
}
|
||||
|
||||
// Step 2: monotonic update. The CASE expression maps the stored Status
|
||||
// string to the same rank table the caller uses; we only mutate if the
|
||||
// incoming rank is strictly greater. Same-rank (including
|
||||
// terminal-over-terminal) is a no-op — first-write-wins at each rank.
|
||||
// Step 2: monotonic update with a same-rank freshness tiebreaker. The
|
||||
// CASE expression maps the stored Status string to the same rank table
|
||||
// the caller uses. We mutate when EITHER the incoming rank is strictly
|
||||
// greater, OR the incoming rank equals the stored rank AND that rank is
|
||||
// non-terminal (< TerminalRank) AND the incoming UpdatedAtUtc is strictly
|
||||
// newer than the stored one — so a retrying call's Attempted-phase
|
||||
// RetryCount/LastError/HttpStatus stay live instead of freezing at the
|
||||
// first Attempted packet (Task 11). Terminal ranks are excluded from the
|
||||
// tiebreaker, so a later terminal NEVER overwrites an earlier one; equal
|
||||
// stamps are inert (idempotent replay) and a lower rank is always a no-op.
|
||||
//
|
||||
// SourceNode-stamping: SourceNode is updated via
|
||||
// COALESCE(@SourceNode, SourceNode). The operator returns @SourceNode
|
||||
@@ -125,7 +139,7 @@ SET Status = {siteCall.Status},
|
||||
IngestedAtUtc = {siteCall.IngestedAtUtc},
|
||||
SourceNode = COALESCE({siteCall.SourceNode}, SourceNode)
|
||||
WHERE TrackedOperationId = {idText}
|
||||
AND {incomingRank} > (CASE Status
|
||||
AND ( {incomingRank} > (CASE Status
|
||||
WHEN 'Submitted' THEN 0
|
||||
WHEN 'Forwarded' THEN 1
|
||||
WHEN 'Attempted' THEN 2
|
||||
@@ -135,7 +149,20 @@ WHERE TrackedOperationId = {idText}
|
||||
WHEN 'Parked' THEN 3
|
||||
WHEN 'Discarded' THEN 3
|
||||
ELSE -1
|
||||
END);",
|
||||
END)
|
||||
OR ( {incomingRank} = (CASE Status
|
||||
WHEN 'Submitted' THEN 0
|
||||
WHEN 'Forwarded' THEN 1
|
||||
WHEN 'Attempted' THEN 2
|
||||
WHEN 'Skipped' THEN 2
|
||||
WHEN 'Delivered' THEN 3
|
||||
WHEN 'Failed' THEN 3
|
||||
WHEN 'Parked' THEN 3
|
||||
WHEN 'Discarded' THEN 3
|
||||
ELSE -1
|
||||
END)
|
||||
AND {incomingRank} < {TerminalRank}
|
||||
AND UpdatedAtUtc < {siteCall.UpdatedAtUtc} ) );",
|
||||
ct);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user