Commit Graph

7 Commits

Author SHA1 Message Date
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc
f4dbd208f5 fix(db): use microsecond stamps in claim_due_reminder to close same-second ABA
The rollback guard `delivered_at = $stamp` was sound only at second
granularity because `claim_due_reminder` wrote `Utc::now().timestamp()`.
Two claims in the same wall-clock second produced the same i64 stamp, so
a stale rollback from a slow pod could match (and silently clobber) a
live claim freshly written by a fast pod. That's the very ABA hazard the
guard exists to prevent.

Switch the claim write to `Utc::now().timestamp_micros()`. The column
stays `BIGINT` — microsecond-epoch values fit, and no reader interprets
`delivered_at` as a whole-seconds count (every read is `IS NULL` /
`IS NOT NULL`; verified across schema.sql:138, migrations/0003:17,
event.rs:1049). No migration needed.

Tests:
- Add `rollback_under_same_wall_clock_second_does_not_clobber`: two
  back-to-back claims (no sleep) with a rollback between, assert the
  stamps are distinct, then prove the stale stamp_a rollback against
  the live stamp_b row returns false and the row is unchanged. Pins
  the actual race Eva [4] flagged — under seconds-granularity stamp_a
  == stamp_b on any modern host, the assertion fails, and the stale
  rollback silently clobbers.
- Update `rollback_with_matching_stamp_reverts_then_reclaim_wins`
  comment that previously cited 1-second resolution as the reason for
  not comparing stamps. No longer true.
- Update `rollback_with_stale_stamp_does_not_clobber_live_claim`
  comment: stamp_old+1 is now a 1-microsecond delta, not 1 second.

Adversarial: reverted `claim_due_reminder` to `Utc::now().timestamp()`,
ran the new test → loud fail at `assert_ne!(stamp_a, stamp_b)` with
`stamp_a=1782498896, stamp_b=1782498896`. Restored.

Validation:
- cargo fmt --check (buzz-db, buzz-relay): OK
- cargo clippy --tests --no-deps -- -D warnings (buzz-db, buzz-relay): OK
- cargo test -p buzz-db --lib --include-ignored --test-threads=1: 88 pass
- cargo test -p buzz-relay --lib: 369 pass
- cargo check -p buzz-relay: OK

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
2026-06-26 14:37:55 -04:00
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc
a88c18f029 fix(relay): reorder reminder scheduler to claim FIRST, publish SECOND
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>
2026-06-26 14:28:53 -04:00
Tyler
73cc31cc52 chore: remove LLM-slop comments across the codebase (#1277)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Mari <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Quinn <96f056ad5f2305c8ddf637dc65d048aa4c12d7daeb8867690e34fca46b0ef64c@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
2026-06-25 11:03:05 -04:00
Tyler
959f25982b Avoid duplicate thread event fetches (#1129)
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
2026-06-19 12:46:08 -04:00
Will Pfleger
26563968cb feat(relay): add NIP-ER push scheduler with cross-pod delivery (#957)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
2026-06-17 10:43:22 -04:00
tlongwell-block
2300248d3b Add automatic database migrations (#988)
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
2026-06-16 08:39:19 -04:00
Will Pfleger
d99ad131f1 refactor: rename sprout backend to buzz (#958)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Will Pfleger <wpfleger@squareup.com>
Signed-off-by: Will Pfleger <wpfleger96@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1fgdl5qqnh3k3f2xkqrvt7cujalhm623x4s7fdjdj5yrtp5fzjl9qrjpucw <4a1bfa0013bc6d14a8d600d8bf6392efefbd2a26ac3c96c9b2a106b0d12297ca@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub16v54tttfqacx9ycvc3k0ut0npj564ahcuajzy6qjvh57ntmsf4uq4806j2 <d32955ad69077062930cc46cfe2df30ca9aaf6f8e76422681265e9e9af704d78@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Will Pfleger <wpfleger96@gmail.com>
2026-06-10 19:29:51 -04:00