BUZZ-175: Route serving event writes through the tenant lock - #7828
TheSentinel454 wants to merge 8 commits into
Conversation
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
|
Gandalf final gate — PASS at head Independently verified:
Calls-to-action for @Tornquist (you merge):
All code-exercising CI (Rust Unit, PostgreSQL, Desktop E2E ×4, Desktop Core, Relay E2E, Rust Lint, Windows Rust) passed at this head. No merge performed — merge is yours. |
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: one P2 compatibility regression, detailed inline. Preserve the legacy acquisition population and add a regression assertion for the migrated callers.
Reviewed HEAD 65257a905daac07aaab68a50c56bf4dac337c4fe against BASE d9e0b3aea49a54c2caa57bf1b5a539838eecc54c. Source-only review on the pinned Blox; no PR code was executed. Existing exact-head Unit Tests, Rust Lint, PostgreSQL Tests, relay E2E and backend integration passed (CI). Those checks do not cover this caller-to-metric routing regression. Raw-SQL confinement and opaque transaction capabilities remain outside this PR’s stated contract.
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
65257a9 to
9b966fb
Compare
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
9b966fb to
1723021
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Source review clear: no actionable exact-diff blockers found. The prior legacy-metrics finding is addressed: typed-only remains the default, and all five pre-existing compatibility populations explicitly retain legacy emission. Event/reaction/roster mention writes now share the owning transaction; the migrated writer families preserve tenant binding and rollback behavior.
Reviewed HEAD 172302149bf551217d9da8986d1f2f9b119f3c55 against BASE 9e178ab098b44f12e55bdd1d624b11acbf4176db, source-only on the pinned Blox. No PR code or tests were executed. Opaque transaction capabilities and full raw-SQL confinement remain outside this caller-migration contract; source-policy tests are syntactic coverage, not connection-provenance proof.
Validation gap: existing exact-head unit, PostgreSQL, and Rust lint checks passed. Sampled relay E2E, backend integration, and desktop integration jobs failed before tests at unauthorized MinIO image pulls (CI). Resolve that external CI gate before treating integration as validated. This is a non-approving review comment, not runtime concurrency certification.
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: one P2 merge-integration regression, detailed inline. Preserve the canvas serialization lock on the relay-facing Db transaction and bind the regression test to that entry point. The legacy-metric fix and exclusive lifecycle-lock repair remain intact.
Bounded follow-up from 172302149bf551217d9da8986d1f2f9b119f3c55; reviewed HEAD ea3358766423c64a88a964031d2b17246baf6d88 against BASE a6a3032e446e6e66e8e41a229ef655ea79f36202. Source-only on the pinned Blox; no PR code or tests executed. Opaque transaction capabilities and full raw-SQL confinement remain out of scope.
Existing CI passed PostgreSQL (491 passed), unit, Rust lint, relay E2E, and backend integration on the synthetic merge of this exact head/base. The canvas lock test exercises the free helper, not the newly bypassing Db method. Desktop runtime behavior was not validated in this review.
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 I'm requesting changes at ea3358766 for the same canvas-lock regression Carl flagged here. I traced it separately and it holds. I also found one more thing that came in with the merge.
Blocking: Db::insert_event_with_thread_metadata no longer takes the canvas lock. At the base, this method went through the free event::insert_event_with_thread_metadata, which takes the per-(community, kind, channel) advisory lock for kind 40100 before inserting (event.rs:1581-1592). Now it calls insert_event_with_thread_metadata_tx directly (event.rs:2267-2284), so it skips that lock. Untagged canvas writes from ingest.rs go through this Db method, while tagged writes go through insert_channel_head_checked, which takes the lock and reads the head before inserting. An untagged revision can therefore commit in the middle of a checked write. The checked writer then gets Inserted even though its revision isn't the live head. The shared community lock doesn't order these two writers against each other. channel_head_untagged_canvas_append_serializes_on_advisory_key still passes because it calls the free helper, not the method that changed.
I think the cleanest fix is to take the canvas lock inside the transaction-bound seam itself, so both the free helper and the Db method get it. Mentions can stay in the same transaction. For the regression test, I'd hold the canvas key on a separate connection, call Db::insert_event_with_thread_metadata with kind 40100, and assert it waits until the key is released. That test fails at this head.
Also worth fixing in this PR:
- The merge brought back
event::soft_delete_event(event.rs:921-948). #6780 removed it on purpose when canvas deletes moved tosoft_delete_event_and_update_thread, which takes the canvas lock. At this head nothing calls it; the only matches are an error string and a test comment. If someone starts using it, it would be a canvas delete that skips the lock. I'd drop it. insert_channel_head_checkedstill opens its transaction withpool.begin()(event.rs:1672). It takes the canvas key first and only gets community admission later, from the insert trigger. It's a serving write path, but it isn't routed through the new helper, and the source-policy check can't see it because the SQL write happens in a helper it calls. Thufir caught this one. I'd route it throughbegin_community_event_write_transaction(community lock, then canvas lock), or list it next toruntime::insert_mentionsas a documented transitional exception.
The rest checks out. The replaceable, roster, NIP-43 membership, push, reminder and admin-delete paths keep their existing advisory keys and check/write order. The community guard runs first on the same connection. The mention-failure tests call the public Db methods and assert that everything rolls back together. The PostgreSQL lane passed 491/491.
|
Addressed the review findings at exact head |
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Source review clear: the prior canvas-lock blocker is fixed; no actionable blockers remain in the repair. The relay-facing Db::insert_event_with_thread_metadata now takes the canvas lock on its admitted transaction, and its regression test calls that same public entry point. Conditional canvas writes now acquire community admission before the canvas lock, and the unused unlocked deletion helper is removed. The earlier legacy-metrics repair remains intact.
Reviewed HEAD b8d73adfd950d682f5ae4c6c9c1be98794155cd2 against BASE a6a3032e446e6e66e8e41a229ef655ea79f36202, with a bounded follow-up from ea3358766423c64a88a964031d2b17246baf6d88. Source-only on the pinned Blox; no PR code or tests were executed. Opaque capabilities and full raw-SQL confinement remain outside this caller-migration contract; the source-policy scan is not transaction-provenance proof.
Existing CI passed unit, Rust lint, PostgreSQL (491 tests, including the corrected public-seam canvas test), relay E2E, backend integration, and desktop integration for this head/base. PostgreSQL ran on their synthetic merge. This is a non-approving review comment, not independent runtime concurrency certification.
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Re-review at b8d73adf. Nothing blocking. All three items from my last review are addressed.
The new commit adds acquire_canvas_event_write_lock_if_needed (event.rs:1523). It takes the per-(community, kind, channel) advisory key only for kind 40100 events with a channel. Db::insert_event_with_thread_metadata now calls it on its admitted transaction (event.rs:2251), and so do the free helper and the serving-lease path in runtime/mod.rs. That means untagged canvas appends serialize with checked writes again. The renamed regression test db_untagged_canvas_append_serializes_on_advisory_key calls the Db method and waits for a waiter to show up in pg_locks, so it now covers the path that regressed. It passed in the PostgreSQL lane.
soft_delete_event is gone. insert_channel_head_checked now opens with begin_community_event_write_transaction and then takes the canvas key, so every canvas writer takes community admission first and the canvas key second. The helper locks only for kind 40100, where the old code locked on the event's own kind. I checked that this doesn't drop coverage. The only production caller is the canvas_revision_spec branch in ingest.rs, and that spec is only built when kind_u32 == KIND_CANVAS. The reaction path calls the helper after its duplicate check. For non-canvas kinds that call does nothing.
CI at this head is green, including the PostgreSQL lane (491/491), relay E2E, integration and all four Desktop Smoke E2E shards.
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com> Signed-off-by: tornquist <tornquist@squareup.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 No blocking defects at 73f5cfeb0. This is a clear review, not an approval.
Since my last review at b8d73adf, the only new commit is the merge of #7706's final head 12a453792, which I reviewed clear on #7706. The merge introduces no hand-written changes: git merge-tree b8d73adf 12a45379 produces exactly this head's tree. The incoming changes are the two lock-mode witness reorders, the let mut folds, and the reconcile-script revert. None of them touch code this PR adds.
This head doesn't merge with current main. #7706 landed as fe9e2409a, and main has also picked up #7932, #7853, #7919, #7854, and #7793 in the files this PR rewrites. git merge-tree reports four conflicts across two files:
runtime/mod.rs,begin_community_write_transaction: main has the squashed #7706 body, and this branch has the legacy-metrics version from172302149. Keep this branch's version.event.rs,insert_event: #7932 skips the transaction for event kinds that aren't listener mentions and keeps it only around the mention outbox enqueue. Taking the tenant lock requires a transaction, so the chokepoint has to reverse #7932 for every kind. That's fine, but please say so in the PR body, since #7932 was a deliberate performance follow-up to the review of #7793.event.rs,soft_delete_event_and_update_thread: #7853 moved the body intosoft_delete_event_and_update_thread_in_txfor the admin delete action. The resolution should open the chokepoint transaction in the outer function and then call_in_tx.execute_delete_with_markerauto-merges with yourbegin_community_event_write_transactionalready in place, so the admin path stays admitted.event.rs,insert_channel_head_checked: main addedacquire_writerplus a plainbegin. The chokepoint call replaces both.
I'll review the resolved head when it's pushed. Main has gained new writers since 09-24, so the source-policy tests in observability_source.rs decide whether anything is left unrouted after the merge.
reviewDecision still shows changes requested because of my earlier review at ea3358766. The canvas-lock issue it raised was fixed in b8d73adf.
73f5cfe to
950d5f6
Compare
|
@buzz-security-review 950d5f6 |
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Blocking at 950d5f629. I'm posting this as a comment rather than requesting changes only because my account already approved this head. The issue below needs a fix before merge.
The rebase resolutions are right. insert_event now always opens the chokepoint transaction, and main's listener-outbox behavior survives because enqueue_mentions_in_transaction returns early for other kinds. insert_channel_head_checked takes the same canvas key inside the admitted transaction. The outer soft_delete_event_and_update_thread opens the chokepoint and calls main's _in_tx. The behavior note in the description covers the #7932 reversal.
Blocking: three event-write owners still start transactions without community admission. The description says every serving event write now has to run in a transaction that holds the community admission lock. These three open Db::begin_event_write_transaction() (runtime/mod.rs:1293), which takes no guard, and then write events through insert_event_in_transaction:
accept_artifact(store/artifact.rs:101), from #7919. It takes the artifact coordinate lock and the head-row lock (:105-126), writes the sidecars, then inserts revision and removal events (:184-197).- Huddle participant admission (
crates/buzz-relay/src/audio/handler.rs:2993), from #7224. It locks the channel row (:3016-3025) and takes a share lock on the huddle-start event (:3106-3113) before inserting the kind48101event (:3144-3150). delete_workflow_by_coordinateandinsert_workflow_deletion(store/workflow/deletion.rs:37,63). This one was already there at73f5cfeb0andb8d73adf. I missed it in both earlier reviews.
The database triggers still take the tenant lock at the first fenced write, so I don't see a way past the fence. What these paths lose is admission at entry. They take domain and row locks first and only meet the tenant lock at their first fenced statement, so a quiescing community rejects them late, after they already hold those locks, and their lock order differs from the writers this PR migrated.
The source-policy test can't see any of the three, so its passing doesn't settle this. It treats any function that accepts &mut Transaction as routed without checking who opened the transaction (tests/observability_source.rs:979-983,1011-1014), and it only scans buzz-db src, so the relay-side huddle owner is outside it. My last review said the source-policy tests would decide whether anything was left unrouted after the merge. That was wrong for this reason.
Fix: open these transactions with Db::begin_community_write_transaction(community) (runtime/mod.rs:1308) before any domain lock. For coverage, either have the policy flag begin_event_write_transaction() callers that reach a guarded helper, or retire that constructor for serving paths. Then add a test showing a fenced community rejects each of these writes with nothing persisted.
Nonblocking: acquire_canvas_event_write_lock_if_needed returns early for non-canvas kinds (store/event.rs:1733), so insert_channel_head_checked now takes no coordinate lock for other kinds. Main locked on the incoming kind unconditionally. The only production caller is canvas-only, but the function doesn't enforce that. Rejecting other kinds, or keeping the unconditional lock there, would close it.
On CI, the one PostgreSQL failure (thread_window_bridge_restarts_both_aux_hops_after_replica_failure) is in a relay test this PR doesn't touch. It passed 10 of 10 isolated runs at both this head and fe9e2409a, so I'm not holding on it. That doesn't prove it's a flake under CI's full parallel load.
Summary
Route serving event writes through one tenant-local transaction entry point. Community admission locking becomes the supported application path, while database triggers remain the authoritative safety backstop.
This migrates normal, replaceable, roster, relay membership, reaction, reminder, admin-delete, and push-owned event writes. Coupled mention writes stay in the same transaction.
The source-policy tests provide syntactic routing coverage. They detect missing route markers but do not prove transaction or connection provenance.
runtime::insert_mentionsremains a transitional raw-transaction path that relies on database fencing.The change removes duplicate transaction-start choices from callers. It adds no cross-community API and removes no schema guard.
Behavior note: #7932 made
insert_eventskip the transaction for kinds that are not listener mentions. Every serving event write now has to run in a transaction that holds the community admission lock, soinsert_eventalways opens the chokepoint transaction again.main'ssoft_delete_event_and_update_thread/_in_txsplit and its removal-marker guard are kept; the outer function now opens the chokepoint transaction.Related issue
BUZZ-175. This is the caller-migration stage that follows #7706 (merged); the branch is rebased directly onto
main. A separate capability migration can make supported helpers require opaque, admitted transactions. Full raw-SQL confinement is not part of this PR.Testing
At
950d5f6293d686b046777b7a0302223d6d3f4d21(rebased onmainatfe9e2409a), on Blox: fmt, clippy forbuzz-dbandbuzz-relaywith-D warnings, ordinarybuzz-dbtests (139 passed, including source policy), the full PostgreSQL lane (790 passed, 0 failed), andgit diff --check.Earlier evidence, from before the rebase:
Gimli exercised the live relay and desktop paths on Blox at
a0ead070024d1b4bd7f2ecb7b6ab099b2227cbba:Artifact: https://buzz.block.builderlab.xyz/media/88c7a2429f9267580e762cea2bf4418e9fc3dbc63d4292e59b32399d6a9b0093.png
The final delta at
65257a905daac07aaab68a50c56bf4dac337c4fechanges source-policy names, comments, and one contract test only. On Blox, format, clippy, security, file-size, and PostgreSQL test discovery passed. The PostgreSQL lane passed 267 of 268 tests;cluster_global_sample_writer_fails_closed_when_activity_is_maskedhitPoolTimedOut. The ordinary suite retains the pre-existing.fetch_all(pool)source-policy failure.Generated with Codex