mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
Pre-build §5c gate for the multi-tenant relay rewrite: the NIP-ER
reminder producer at `crates/buzz-relay/src/main.rs:454-487` inherited
the old relay's publish-then-claim shape, with the comment
"duplicate fan-out on the next tick is harmless (subscribers
dedup by event ID)"
That premise is wrong — subscribers do NOT dedup reminder fan-outs by
event id, so a publish that wins fan-out but loses the claim leaks a
duplicate delivery to every connected client. Reorder to:
claim_due_reminder → if Ok(Some(stamp))
publish_event → if Ok(()) we're done
if Err(_) rollback_reminder_claim(..., stamp)
This mirrors the `claim → side-effect` ordering Max + Sami landed for
the workflow cron producer (`df997d7cc` + `910d82228`) and keeps the
DB claim row as the single cross-pod side-effect boundary.
DB API change (single prod caller, `main.rs` only)
claim_due_reminder returns `Result<Option<i64>>` instead of
`Result<bool>`. The `Some(stamp)` carries the `delivered_at` value
the winner wrote, which the rollback uses as its `AND delivered_at
= $stamp` guard. Without that guard, a stale rollback could clobber
another pod's later claim — a silent duplicate-publish window.
rollback_reminder_claim(id, created_at, stamp) -> Result<bool> is
the inverse: stamp-guarded UPDATE that sets delivered_at = NULL.
Returns true on revert, false when the row's delivered_at no longer
equals stamp (caller leaves it alone; bounded duplicate fan-out is
the worst case, never silent data loss).
Tests (`#[ignore = "requires Postgres"]`, follows F1/F4 conventions)
two_concurrent_claims_yield_exactly_one_winner
Two `claim_due_reminder` calls under `tokio::join!` on the same
reminder. Asserts exactly one Some(stamp), one None, and a third
claim still loses. The §5c "two-pod concurrency" acceptance test
Eva called for. Adversarial: drop the `AND delivered_at IS NULL`
guard from the claim UPDATE and both calls win — verified.
rollback_with_matching_stamp_reverts_then_reclaim_wins
Happy path: claim → rollback with matching stamp returns true →
second claim wins (no longer held). Adversarial: replace
`SET delivered_at = NULL` with `SET delivered_at = delivered_at`
(no-op rollback) and the second claim fails to win — verified.
rollback_with_stale_stamp_does_not_clobber_live_claim
ABA guard: claim with stamp_old → manually advance delivered_at
to stamp_new (simulates a later pod's claim) → rollback with
stamp_old returns false WITHOUT reverting; re-read confirms
delivered_at is still stamp_new. Adversarial: drop the
`AND delivered_at = $3` guard and the stale rollback clobbers
the live claim — verified.
Comment fix
The misleading "subscribers dedup by event ID" comment is replaced
with the actual invariant: claim is the side-effect boundary,
rollback is best-effort, on rollback failure bounded duplicate
fan-out is the worst case.
Validation
cargo fmt --check -p buzz-db -p buzz-relay ✅
cargo clippy -p buzz-db --tests --no-deps ✅
cargo clippy -p buzz-relay --tests --no-deps ✅
cargo test -p buzz-db --lib -- --include-ignored
--test-threads=1 ✅ 87 pass
cargo test -p buzz-relay --lib ✅ 369 pass
cargo check -p buzz-relay ✅
Refs §5c reminder scheduler claim-first reorder gate.
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>