Repository navigation
buzz-db: drop the universal community write-fence triggers (contract) - #8323
TheSentinel454 wants to merge 10 commits into
Conversation
c3661fe to
647a82a
Compare
5bc79c9 to
797181c
Compare
647a82a to
bad9013
Compare
bad9013 to
525bc80
Compare
3d8789b to
0b09dc2
Compare
1981a4b to
d897bee
Compare
## What This is the expand step for retiring the 32 universal `community_write_fence_*` triggers and their SQL functions (BUZZ-315). Relay pods auto-migrate at boot (`BUZZ_AUTO_MIGRATE`). The drop migration therefore runs while the previous release is still serving, and that release must not depend on the objects it drops. Today the app depends on them in four places, and this PR removes all four from its own code paths. Seven in-SQL `community_write_allowed` predicates in fleet sweeps and push (`push_enqueue.rs`, `channel.rs`, `event.rs`, `push.rs`, `relay_invite.rs`, `workflow.rs`) are #8318's scope. The release is independent of the fence functions only with both this PR and #8318. **It changes no schema.** The objects stay and keep working until the follow-up drop PR. | Old dependency | Now | |---|---| | Startup and readiness (`validate_serving_catalog_on`) require `community_write_allowed`, `assert_community_write_allowed`, `enforce_community_write_fence` and all 32 triggers | No longer required. Still required: the tenant tables, `community_deletion_lock_key` and the enabled community tombstone trigger. | | Serving-lease admission runs `SELECT assert_community_write_allowed($1)` | Uses `guard_community_write`, the same shared lock and state check as ordinary serving writes, in the same transaction as the lease `INSERT`. Fenced or missing communities still map to `AccessDenied`. | | Live scoped-table discovery calls `community_write_fence_excluded_table()` | Uses a Rust `EXCLUDED_SCOPED_TABLES` list. A unit test pins it equal to the SQL list while both exist. | | The deletion executor compares the live `fenced_tables` against the frozen inventory | Only tenant-table equality is compared. Inventories **still record** live fences, so an older executor that validates an inventory frozen by this release still matches. The digest still covers the full frozen manifest. | `lock_community_deletion_shared` now refuses transactions that are not READ COMMITTED, so every lock-then-read admission gets the check: `guard_community_write`, serving-lease finalization (`guard_transaction_with_serving_lease`), lease renewal/verification, owner preparation and owner-mutation admission. The old SQL assertion did this check for the lease path, and the trigger backed up the rest. The isolation level comes back from the existing `pg_advisory_xact_lock_shared` statement, so the hot path makes no extra round trip. The refusal comes before any lifecycle-state read. ## Mixed versions | Combination | Why it is safe | |---|---| | Previous release, this DB | No schema change, so all old checks still see the 32 fences. | | This release, DB with fence objects | Extra objects are tolerated, and the trigger backstop still applies. | | This release **plus #8318**, DB without fence objects | Serving, readiness, lease admission and deletion proven by `migration_schema_fence_objects_are_optional_for_serving_and_deletion`, which drops them inline. Without #8318, serving and deletion still work, but the push matcher, exhausted-job reaper, invite retention, ephemeral reaper and startup d_tag backfill fail with `42883 community_write_allowed(uuid) does not exist` (seen in deploy-stack E2E). | | Older executor, inventory frozen by this release | `fenced_tables` is still populated from the live catalog (32). | This PR does not depend on #8318 to merge and can ship alongside it, but it is not drop-safe alone. The drop PR (#8323) needs #8219, #8318 and this PR deployed on every pod, and every rollback image after the drop must include #8318. ## Tests - `store::deletion::postgres_tests::migration_schema_fence_objects_are_optional_for_serving_and_deletion` migrates to 0060. It then runs the following with the fence objects present, then again after dropping them inline: - serving startup - readiness - the deletion catalog check - `inventory_schema` - lease acquire/release - `AccessDenied` for quiescing, fenced, tombstoned and missing communities - `store::deletion::postgres_tests::deletion_lock_admission_rejects_pinned_snapshot_isolation` checks that REPEATABLE READ and SERIALIZABLE are refused by both `guard_community_write` and `guard_transaction_with_serving_lease`. The lease case uses a never-acquired lease, so if the isolation check is skipped it fails as stale instead. - `buzz-deletion` `postgres_tests::approved_legacy_fence_inventory_accepts_current_catalog` checks that a legacy digest with 32 fences validates against a live catalog with none, and that an unknown tenant table is still rejected. - `store::deletion::tests::scoped_table_exclusions_match_sql_during_transition` checks exclusion-list parity. - `store::deletion::postgres_tests::serving_lease_acquire_waits_for_fence_lock_then_refuses` holds the exclusive deletion lock and checks that lease acquisition stays pending. It then commits `fenced` and requires `AccessDenied` with no lease row persisted. Removing the lease path's `guard_community_write` call fails this test, while the other four lease/admission tests above still pass. - `buzz-deletion` `serving_guard_heartbeats_long_operation_through_quiescing` now requires `AccessDenied` for a lease acquired during quiescing, rather than any error. Each test was confirmed to fail when its fix is removed (rc 100, 0/1): the isolation refusal, the three validators restored to the base version, the fence-list comparison restored, and one name dropped from the Rust list. ## What got simpler The lease path loses its special SQL assertion and its error-code translation and reuses the ordinary app guard. The serving and deletion catalog checks each lose one requirement. No new abstraction. The `fenced_tables` reader stays only for mixed-version executors; the drop PR deletes it. **Identified debt (pre-existing, outside this diff):** `runtime::postgres_tests::writer_pool_rejects_non_read_committed_database_default` hangs on clean `177551dda` too. ## Verification (Blox, tree identical to this commit) | Gate | Result | |---|---| | `cargo fmt --all -- --check` | pass | | `cargo clippy --workspace --all-targets -- -D warnings` | pass | | `cargo nextest run -p buzz-db --lib` | 167/167 | | `scripts/test-postgres-test-discovery.sh` | pass | | `scripts/postgres-test-run.sh -p` buzz-admin / audit / deletion / push-gateway / relay / search / workflow | all rc 0 (4, 7, 12, 11, 402, 20, 2) | | `scripts/postgres-test-run.sh -p buzz-db` | 510 pass, 1 fail: the pre-existing hang above, interrupted | | Round 2 (this head): `-p buzz-db` / `-p buzz-deletion` | 511 pass + the same pre-existing hang interrupted / 12/12 | The new PG tests are selected by the `postgres-ci` filter `(test(/postgres_tests::/) or binary(/^postgres_/)) and not test(/(^|::)external_infra[^:]*::/)`. buzz-review-completed --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
c523219 to
c910fbd
Compare
c910fbd to
fd5d9f5
Compare
loganj
left a comment
There was a problem hiding this comment.
🤖 Larry and Auto: an independent adversarial review of fd5d9f5dd1. Source review only, plus local Postgres 17 runs and mutation tests. Most of the review was done at c910fbde55. The restack onto #8326 changes only the migration number (0087 to 0088; the SQL is byte-identical), ARCHITECTURE.md and schema/triggers/events.sql, and we checked that delta.
Approving. The production change is sound. One test at this head fails, and it shows a real ordering problem on the pgschema deployment path.
Must-fix (error): migration_schema_desired_pgschema_reconcile_matches_populated_upgrade fails.
- At
relay_invite/upgrade_postgres_tests.rs:496, pgschema stops withcannot drop trigger community_write_fence_events on table events_p2026_04 because trigger community_write_fence_events on table events requires it (SQLSTATE 2BP01). pgschema plans the partition-clone drops before the parent drop. - It passes on main and fails here, both at
c910fbde55and atfd5d9f5dd1, and it fails the same way when run alone. - Fix (test): the fixture's pre-drop list at
:428-440already handles this for the earlier retiredeventstriggers. AddDROP TRIGGER IF EXISTS community_write_fence_events ON events;andDROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log;to it. - Ops consequence: an out-of-band
pgschema applyof the new desired state onto a database that has not run 0088 fails the same way. Please add one line to the rollout notes: run 0088 first, or drop those two parent triggers before the apply.
Fixed by the restack: media_edit_index_upgrade_builds_inline_or_verifies_prebuild timed out (55P03) at the retirement migration at c910fbde55, because it held ROW EXCLUSIVE on events through the full migrator. At fd5d9f5dd1 it passes, because #8326's base now stops the held-writer part at 0086. Any later PR that drops something on events must keep that order.
What we checked
- Every serving writer is still admitted in the app. Auto went through 174 production DML sites on the 33 tables, and checked the 29 that a keyword scan could not place by hand. Each one goes through
AdmittedTx,guard_transaction,sweep_admitted_communities, the deletion executor, or a raw-connection helper whose every caller passes an admitted transaction. We found no unadmitted writer. - The drop list is complete. The 33 drops match every desired-state attachment.
product_feedbackandrate_limit_violationswere already dropped in 0030. No production SQL calls the dropped functions, here or at main. - Rollout: #8219, #8318 and #8322 are on main, so this is a proper contract step. No tagged relay release ever contained the fence (it arrived in 0029, after relay-v0.2.1), so only untagged main builds older than #8318/#8322 are exposed. The body's image gate is the right control.
- Mutations, all caught: dropping
guard_transactionfrom push enqueue; admittingquiescinginguard_community_write; the NIP-FI bind withoutdeletion_state='active'; hidingchannelsinEXCLUDED_SCOPED_TABLES; 0088 skipping theusersdrop. One survives, in a test (see the inline comment).
Should-fix: docs/database.md's "retire a trigger" step 2 asks for release notes that name the step-1 release as a required stop. There is no such relay release. Say so in the body, and name the minimum image instead.
Optional
- The source policy that forces writers through admission (
GUARDED_TABLE_WRITE_MARKERS,tests/observability_source.rs:838) covers onlyevents,reactionsandevent_mentions. With the trigger gone, nothing static stops a new raw writer on the other 30 tables, but docs/database.md now says every serving writer goes through admission. You could derive the markers fromEXPECTED_SCOPED_TABLES, and move the raw-connection helpers to&mut AdmittedTxor pin them. scripts/backfill-d-tag.sqlruns a fleet-wideUPDATE eventswith no deletion lock. The trigger used to make it fail on a quiescing community; now it writes through. Retire it, or make it take the lock.- Lock window: the migration holds ACCESS EXCLUSIVE on all 33 tables and every partition until commit, and waits up to 5 s for each. The body's single-migrator, low-write-window advice covers this. A shorter
lock_timeout, or locking the hottest tables first, would shorten the worst case. - Two test helpers no longer have anything to do:
test_support::reject_legacy_sql_fence_predicate(:231, 9 callers) andsession_replication_role = replicainadmission_fixture. - The body is stale: 0066 (now 0088), "32" triggers (33), "main is at 0064", "Pending at
525bc80a", and the numbering paragraph.
| DROP TRIGGER IF EXISTS community_write_fence_community_nip_fi_issuers ON community_nip_fi_issuers; | ||
| DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log; | ||
| DROP TRIGGER IF EXISTS community_write_fence_event_mentions ON event_mentions; | ||
| DROP TRIGGER IF EXISTS community_write_fence_events ON events; |
There was a problem hiding this comment.
🤖 Must-fix (error), test and ops note. pgschema cannot reproduce this drop on a populated database. It plans DROP TRIGGER community_write_fence_events ON events_p2026_* before the parent drop, and Postgres refuses with 2BP01, because the clones belong to the parent trigger. This makes migration_schema_desired_pgschema_reconcile_matches_populated_upgrade fail (relay_invite/upgrade_postgres_tests.rs:496). The same goes for community_write_fence_delivery_log (:15).
Fix: add both parent drops to the fixture's pre-drop list (upgrade_postgres_tests.rs:428-440). Also note in the rollout section that an out-of-band apply needs 0088 to run first, or these two parent triggers dropped first.
| // A raw writer transaction on purpose: this pins that the fenced-table | ||
| // trigger itself takes the shared deletion lock, not the chokepoint. | ||
| let mut open_write = db.pool.begin().await.expect("open write transaction"); | ||
| let mut open_write = crate::begin_community_write_transaction( |
There was a problem hiding this comment.
🤖 Optional (test validity). fence_waits_for_open_write_and_rejects_it_after_transition no longer tests the deletion lock. The wait it sees comes from the foreign key: the pubkey_allowlist insert takes KEY SHARE on communities, and the FOR UPDATE in begin_quiescing waits on that. Without the trigger, a write that lands after the fence also succeeds. Auto moved the shared deletion lock to the wrong key, and this test still passed; begin_quiescing_waits_for_open_admitted_writer_note_update caught it. Fix: use a table with no FK to communities, or assert the advisory-lock wait in pg_locks, or delete this test because :7394 covers it.
Contract step for BUZZ-315. Migration 0061 drops the 32 community_write_fence_* triggers (partition clones follow) and the five fence-only functions: enforce_community_write_fence, attach_community_write_fence, community_write_fence_excluded_table, community_write_allowed and assert_community_write_allowed. It runs under SET LOCAL lock_timeout = '5s' so the AccessExclusive DDL cannot queue serving traffic indefinitely. The desired-state schema, reconcile script and migration source assertions follow. New inventories leave the legacy fenced_tables snapshot empty, and the live-fence catalog reader is deleted. community_deletion_lock_key and the community tombstone stay. Deploy gate: merge only after the app-side admission PR, the tenant-admitted fleet sweeps and the fence-object tolerance PR are deployed on every pod, with no rollback below them expected. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Rewrite comments and docs that still described the dropped community-write triggers as a backstop. Remove the admission fixture's raw witness insert, which no longer witnesses a trigger bypass once the triggers are gone. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
…plane list The retired parity test read the SQL exclusion function, which this change deletes. Pin EXCLUDED_SCOPED_TABLES to a control-plane list written out in the test instead of deriving it from the tenant manifest, and check each excluded table still exists with community provenance. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
…unity_id
`community_deletion_checkpoints` and `community_deletion_executor_heartbeats`
have no `community_id` column, so `live_scoped_tables_on` never sees them and
their `EXCLUDED_SCOPED_TABLES` entries were dead. The retired SQL predicate
listed them, but it only acted on tables with community provenance.
`excluded_scoped_tables_are_live_control_plane_tables` asserts every
exclusion is a live provenance table and failed on both
("stale exclusion community_deletion_checkpoints").
No runtime change: the exclusion filters a community_id-column query.
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
…ge list Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Main reached migration 0086 while this change waited, and its 0066 now creates community_nip_fi_issuers, so the retirement moves to 0087. The migration count, the retirement lookup and the pre-retirement fixture follow the new number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Main added a bootstrap test that a fresh desired-state schema keeps only the explicitly declared legacy fences, and database.md rules that point at attach_community_write_fence and community_write_fence_excluded_table. With every universal fence object dropped, the test has nothing to check (desired_state_catalog_has_no_universal_fence_and_still_boots covers the empty catalog) and the rules name functions that no longer exist. The provenance rules now point at EXCLUDED_SCOPED_TABLES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
fd5d9f5 to
83560c8
Compare
…ture pgschema plans the partition-clone drops of community_write_fence_events and community_write_fence_delivery_log before their parents, and Postgres refuses with 2BP01. The fixture already pre-drops the other retired partitioned-parent triggers; add these two. Delete fence_waits_for_open_write_and_rejects_it_after_transition. With the trigger gone, the wait it saw came from the pubkey_allowlist FK's KEY SHARE on communities, not the deletion lock, so it no longer tested admission. begin_quiescing_waits_for_open_admitted_writer_note_update covers the wait and community_fence_rejects_fresh_begin_transaction_after_fence covers the post-fence rejection. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luke Tornquist <tornquist@squareup.com>
…ture With 8323's fence drops on top of 0087, pgschema also plans the events_created_at_floor partition-clone drops before the parent and Postgres refuses with 2BP01. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Luke Tornquist <tornquist@squareup.com>
What
This is the contract step for BUZZ-315. Migration
0088_retire_community_write_fence.sql:SET LOCAL lock_timeout = '5s', the convention in 0029/0030/0051/0053/0054 (the migrator otherwise runs withlock_timeout = 0). An unbounded AccessExclusive wait acrosseventsand 31 other hot tables would queue serving traffic behind it. A timeout aborts the transactional migration, and the pod retries on its next boot.community_write_fence_*triggers. Partition clones go with them.CASCADE:enforce_community_write_fence,attach_community_write_fence,community_write_fence_excluded_table,community_write_allowedandassert_community_write_allowed.The desired state follows:
schema/schema.sqland the trigger attachments inchannels.sql/events.sqlare updated.schema/community_write_fence.sql.scripts/reconcile-schema-after-pgschema.sql.Both boot paths, desired-template and
run_migrations, end with zero universal fences.New inventories leave the legacy
fenced_tablessnapshot empty.#8322's executor ignores it, and the field remains so existing frozen digests stay valid. The live-fence catalog reader is deleted. The transitional exclusion-parity test readcommunity_write_fence_excluded_table.sqlviainclude_str!, which this PR deletes;excluded_scoped_tables_are_live_control_plane_tablesreplaces it. It pinsEXCLUDED_SCOPED_TABLESto a control-plane list written out in the test (not derived fromEXPECTED_SCOPED_TABLES), requires every excluded table to still exist with acommunity_id, and requires the live provenance tables to be exactly tenant ∪ control-plane, so moving a tenant table into the exclusion list fails.Kept:
community_deletion_lock_key: app admission, deletion code, relay lock tests and the bridge mini-schemas still call it.enforce_community_tombstone/communities_deletion_tombstone: independent of the dropped functions; BUZZ-318 owns them.Writer inventory: 189 DML sites across
crates/andscripts/on the 32 tables. Every serving writer goes throughAdmittedTx/begin_community_*_write_transaction,guard_transaction, the deletion executor under its generation GUC, or a rolled-back startup probe. The exceptions are #8318's seven statements. Direct maintenance scripts lose the trigger backstop and stay outside the supported-writer contract.Mixed versions (rollout of this PR)
migrated_catalog_retires_universal_fences_but_keeps_deletion_guardschecks startup before the retirement migration, then after it).Not safe: a pod at #8322 without #8318 on a DB after 0088. Serving and deletion keep working, but the push matcher, exhausted-job reaper, invite retention sweep, ephemeral channel reaper and startup d_tag backfill fail with
42883 community_write_allowed(uuid) does not exist(observed in deploy-stack E2E). Rollback images after 0088 must include #8318, and no image older than #8219 + #8318 + #8322 may run against a post-0088 DB.Tests
runtime::migration::postgres_tests::migrated_catalog_retires_universal_fences_but_keeps_deletion_guardsstore::deletion::postgres_tests::desired_state_catalog_has_no_universal_fence_and_still_boots0088source assertions inembedded_migrator_contains_consolidated_initial_schema. They pin all 33 drops, the five functions, the retained lock key and tombstone, and the leadinglock_timeout.Each test was confirmed to fail when its fix is removed (rc 100, 0/1):
SET LOCALremoved, the source assertion fails;Test-fixture changes:
push.rsloses two cleanupDELETEs that called the dropped function. Per-test databases make them redundant.DROP TRIGGERcorrectly waits on that writer's lock.What got simpler
33 triggers, 5 SQL functions, 37 schema files and the live-fence catalog reader are deleted, along with their boot/catalog probes. The diff over the stack base is 275 insertions and 607 deletions.
Identified debt (pre-existing, outside this diff):
writer_pool_rejects_non_read_committed_database_defaultandwriter_pool_metrics_stop_after_session_timeout_setup_failurehang on clean177551dda.Verification
Head
a79e49857, restacked onto #8326, plus the upgrade-fixture fix from review (pre-drops of the two fence parents and the floor trigger) and removal offence_waits_for_open_write_and_rejects_it_after_transition. With the trigger gone, its wait came from thepubkey_allowlistFK, not the deletion lock;begin_quiescing_waits_for_open_admitted_writer_note_updateandcommunity_fence_rejects_fresh_begin_transaction_after_fencecover both halves. CI skips the PostgreSQL job on stacked PRs, so the buzz-db PG lane runs on Blox; it is running at this head.Migration numbering:
0088, after #8326's0087; #8211 takes0089on top.