docs backend evidence community safety SEC01D_RETRY_ROLLBACK - AlexandreZanata/Abastevo GitHub Wiki
SEC01-D — Retry, rollback and stable 429s
Date: 2026-10-11. Status: LOCAL_DONE. Scope: mail-failure rollback,
cooldown/duplicate convergence, exhausted-budget stability, hourly
rotation, legitimate second client, Retry-After + frontend states —
no SMTP, no SEC02+ scope.
Head: maintained dev + this task.
Plan: remediation plan
(SEC01, AUD-01). Contract: B-BR-A01/A02/A04/A05 + B-BR-F03/F04;
BUC-A01/A04 + BUC-F. Constraint: free use stays free inside
ceilings; aggregate mail suppression stays nil (no oracle); no
PG-state leak; hashed accounting only.
Validation: PASS
Changes
db/queries/account/accounts.sql(+ regenerated sqlc):DeleteCodeByIDcompensation delete.account/application:Store.DeleteCode;RequestCoderolls back the admitted row whenSendCodefails (zero rows/mail, budget intact, recovered retry succeeds).account/adapters(Mem + PG):DeleteCode(unknown IDs converge no-op).feedback/application+feedback/adapters/pgstore.go:TryInsertCommentreplays an identical live retry (same ID, same account) converged without consuming budget; any other ID reuse fails closed (ErrCommentNotFound, no oracle). PG checks existence before insert so replays never abort the tx; concurrent same-ID races fail closed.account/adapters/http/http.go: budget 429s carry stable codes plusRetry-After(admission-exhausted3600 upper bound as the hourly window slides;session-budget-exhausted60 polite hint — budget frees on revoke/expiry/user action, hint guarantees no slot); refusals stay no-store with no identity echo.feedback/adapters/http/http.go: reportQuotaDeniedErrorcarries its exactRetry-After; commentquota-exceededmaps to 429 +Retry-After: 60(same hint semantics).- Tests: unit rollback (fail → zero rows, recover → 1/1), cooldown duplicate (1 row/1 mail), hourly rotation (exhausted nil + zero growth, post-window fresh succeeds), duplicate-username stability, duplicate comment-ID replay + foreign-rebind refusal; HTTP 429 code/header/no-leak tests; real-PostGIS rollback, duplicate/rotation (account) and duplicate replay (feedback).
Validation (actual)
go test ./internal/modules/account/... ./internal/modules/feedback/...green (incl. 4 new rollback/rotation + 1 duplicate-ID + 2 HTTP metadata tests).-race -tags=integrationgreen on disposable local PostGIS:account/adapters(incl. 2 new SEC01-D),feedback/adapters(incl. 1 new SEC01-D); pre-existing SEC01-C parallel/purge suites re-green.go build ./...,go vet,gofmt -lclean,sqlc vetclean,git diff --checkclean,scan-secrets.shpassed (synthetic addresses only).- RED pre-GREEN: new rollback test failed before DeleteCode compensation (row retained); PG duplicate replay failed before the check-before-insert reorder (aborted-tx unique violation); feedback quota 429 lacked headers before this task.
Limits / owed
- Session/comment
Retry-After: 60is a polite hint, not a slot guarantee (budget frees on revoke/delete/expiry); aggregate 3600 is an upper bound (slots free sooner as the window slides). Documented here and in code; frontend should pair the countdown with manage-sessions/delete guidance. - Client idempotency keys (lost-response double submit with two server IDs) remain two rows by design; same-ID replay covers store-level retries only.
- Multi-replica strict serializability beyond advisory xact locks not proven here. No VPS/phone/shared-edge change; no production readiness claim.
Next microtask: SEC02-A proxy-fairness regressions.