SIGN IN SIGN UP

fix(outbox): let a failed row finish its retry — version CAS instead of a status list

A `failed` row is scheduled for retry, not settled: claimDue() selects it on
purpose. Listing it in TERMINAL_OUTBOX_STATUSES (#109) meant the retry could
never complete — claimDue re-selected the row, publish succeeded, markSent()
refused to touch it, so the status stayed `failed`, `attempts` never grew (so
maxAttempts/DLQ never fired), `nextAttemptAt` stayed in the past, and the row
was re-claimed on every flush. The returning ACK bounced as
message-not-in-flight. Nothing short of a manual dlq could settle it.
Reported by @lichtpfad with 766 log lines over two msgIds (#113).

The race #109 was guarding — a fast ACK/NACK landing between publish() and
markSent(), where the late markSent() drags the settled row back into flight —
is now handled per row: claimDue() hands out the row `version`, the flush loop
passes it to markSent(), and the update applies only while the row is untouched.
That covers any concurrent transition, not just the statuses someone enumerated.
TERMINAL_OUTBOX_STATUSES keeps its real meaning: acked and dlq.

The ack-hardening assertion on the set's contents is rewritten to state the new
rule; the behaviour it protected is covered by the version-CAS tests.

Closes #113
A
Alexander Vasiliev committed
55f5a4ed0670a10bf4d11ba3f2dda2e5676a633b
Parent: 7208903