Skip to content

Make reaction ingest atomic#1458

Merged
tlongwell-block merged 3 commits into
mainfrom
perci/p4-reaction-single-tx
Jul 2, 2026
Merged

Make reaction ingest atomic#1458
tlongwell-block merged 3 commits into
mainfrom
perci/p4-reaction-single-tx

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Summary

Make kind:7 reaction ingestion atomic by moving the reaction row upsert, kind:7 event insert, and optional thread metadata insert into one buzz-db transaction.

This removes the previous relay-level multi-query/compensation sequence:

get target event -> add reaction row with NULL reaction_event_id -> insert event/thread metadata -> backfill reaction_event_id -> compensate on failure

New path:

target SELECT in same community -> reaction upsert with reaction_event_id -> duplicate short-circuit -> event/thread insert -> commit

Changed files

  • crates/buzz-db/src/event.rs
    • Added ReactionEventInsertOutcome.
    • Factored insert_event_with_thread_metadata into an internal tx-taking helper.
    • Added insert_reaction_event_with_thread_metadata.
    • Added PG-gated regression coverage for P4 fences.
  • crates/buzz-db/src/reaction.rs
    • Shared ADD_REACTION_SQL between pool and tx paths.
    • Added add_reaction_tx preserving existing reactivation / duplicate semantics.
  • crates/buzz-db/src/lib.rs
    • Exposed the new DB method.
    • Kept mentions best-effort and outside the reaction transaction, matching the existing non-reaction pattern.
  • crates/buzz-relay/src/handlers/ingest.rs
    • Replaced kind:7 target lookup / add / insert / backfill / compensation sequence with the atomic DB helper.

Quinn §4.14 fence table

P4 diff verified against RESEARCH/RELAY_PERF_CORRECTNESS.md §4.14. All six fences hold in code:

  1. Ordering load-bearing: insert_reaction_event_with_thread_metadata body reads exactly: target SELECT → add_reaction_tx!reaction_inserted short-circuits with tx.rollback() returning Duplicate → event insert only after that check. If ordering ever flipped, a duplicate reaction would store a duplicate kind:7. This diff preserves the guarantee.
  2. Community-scoped target lookup: SELECT ... WHERE community_id=$1 AND id=$2 AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1. Explicit. Inv_NonInterference — cross-community A→B target attack returns TargetMissing and the test proves it (reaction_single_tx_cross_community_target_rejected asserts both no event AND no reaction row).
  3. No side-channel writes before commit: Inside the tx: only DB writes (target SELECT, reaction upsert, event/thread insert, counter updates). No Redis, no cache invalidation, no "commit" metrics. Compensation dance is gone by construction — tx.rollback()/tx.commit() is the only exit surface.
  4. NIP-25 semantics stay in ingest: Last-valid e-tag extraction, 64-char emoji cap, empty-content + default, effective_message_author derivation — all preserved in ingest.rs. DB helper is pure plumbing.
  5. Mentions not folded: insert_mentions fires post-commit outside the tx via &self.pool, best-effort with warn! on error — matches the existing non-reaction pattern. Zero cross-contamination.
  6. ON CONFLICT reactivation preserved via shared SQL: ADD_REACTION_SQL is one const string, used by both add_reaction (pool) and add_reaction_tx (tx). Same bind order, same .execute()-then-rows_affected() != 0 shape. Zero SQL divergence risk. The COALESCE(EXCLUDED.reaction_event_id, reactions.reaction_event_id) clause is the exact behavior Quinn named — new gets set, reactivate preserves an existing id, active-duplicate returns false.

Tests

Run at exact rebased SHA 0d4980c857d0caab558dcf5424162263fe132491 after rebasing onto merged main 9967b97f:

. ./bin/activate-hermit
cargo fmt --check
cargo test -p buzz-db
cargo test -p buzz-relay
cargo test -p buzz-db reaction_single_tx -- --ignored --nocapture

Results:

  • cargo fmt --check passed.
  • cargo test -p buzz-db: 79 passed, 53 ignored.
  • cargo test -p buzz-relay: 448 passed, 2 ignored; main test 1 passed; doc tests passed.
  • PG-gated P4 tests: 4 passed.
  • Pre-push hook under Hermit also passed and ran the repo hook suite.

Required PG-gated tests present:

  • reaction_single_tx_duplicate_short_circuit_stores_no_event
  • reaction_single_tx_cross_community_target_rejected
  • reaction_single_tx_event_insert_failure_rolls_back_reaction
  • reaction_single_tx_reactivates_soft_deleted_reaction

npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr and others added 3 commits July 2, 2026 11:13
Collapse kind:7 reaction ingestion into one database transaction so reaction rows, event rows, and thread metadata commit or roll back together. Preserve duplicate short-circuit and soft-delete reactivation semantics by sharing the existing reaction upsert SQL between pool and transaction paths.

Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block tlongwell-block force-pushed the perci/p4-reaction-single-tx branch from 7a08f14 to 9468315 Compare July 2, 2026 16:08
@tlongwell-block tlongwell-block merged commit 835302c into main Jul 2, 2026
29 checks passed
@tlongwell-block tlongwell-block deleted the perci/p4-reaction-single-tx branch July 2, 2026 18:32
wpfleger96 added a commit that referenced this pull request Jul 2, 2026
…n-metrics

* origin/main:
  feat: per-community workspace icon set by admins, served via NIP-11 (#1463)
  perf(relay): batch outbound websocket data frames (#1464)
  Make reaction ingest atomic (#1458)
  Serialize fan-out EVENT frames once (#1459)
  fix: agent reliability — no restart on channel-add, visible dead-letter notice (#1468)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>

# Conflicts:
#	crates/buzz-relay/src/handlers/event.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant