Repository navigation
buzz-db: drop the universal community write-fence triggers (contract) - #8323
TheSentinel454 wants to merge 23 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.
b0e2422 to
93e1c34
Compare
Phase 2 of BUZZ-316. The insert-time application floor check (phase 1) is now the only created_at floor guard, so drop the deferred constraint trigger, its function, the writer-pool buzz.created_at_floor GUC, and the startup catalog/behavior probes that required them. Migration 0062 drops the trigger on events and every partition plus the function, all IF EXISTS. Desired-state schema, pgschema reconciliation and migration asserts drop the same objects. spawn_fence_probe becomes synchronous; the ordered heartbeat probe still fails closed. DESTRUCTIVE: deploy only after phase 1 runs on every writer, with no rollback expected, out-of-band writers stopped, and the replica breaker held closed through the drop until replay catches up. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Address review of the phase-2 floor retirement: - the workspace-wide floor source policy now comes from phase 1's shared crates/*/src walk (which pins buzz-relay coverage); this commit only drops the GUC from its constant-parity message - drop the legacy-0021 half of the writer/probe test, which could not fail, and rename the desired-state test to what it checks - fix stale GUC prose and document the ACCESS EXCLUSIVE lock in 0062 Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
With the floor trigger retired, the source policy is the only guard against rewriting created_at/channel_id on existing event rows. Close two matcher gaps found in review: - `UPDATE events SET(created_at, ...)` with no space before the parenthesis slipped past the " set " split - `INSERT INTO events ... ON CONFLICT ... DO UPDATE SET channel_id = ...` names no table, so it passed even though it can re-home an existing row below the floor; bind each DO UPDATE to the INSERT it belongs to and treat it as an events update when that INSERT targets events Share the SET-list check between both paths, and pin each gap with a negative fixture plus controls: a non-floor-column events upsert, DO NOTHING, and a guarded events insert next to a watermark upsert that sets created_at (the shape of the replaceable-event writer). Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
With the trigger gone the source policy is the only guard. Expose SQL the per-function scan could not see: include_str! is expanded from the file it loads and concat! literals are joined, so both reach the existing insert/update checks. Fail closed on what stays opaque: a non-literal include path, a concat! that spells SQL around a non-literal argument, event-write SQL in a const, static or macro body outside any function, and PREPARE TRANSACTION in any production writer. Cut each function slice to its own item so trailing consts are judged at item level. The replica fence no longer folds pg_prepared_xacts.prepared into the wall: only the retired trigger checked prepared rows at PREPARE, and the insert-time guard's clock is not exposed there. A prepared transaction now fails the sample closed (PreparedTransactions). Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
…annot read Review of 8752ce1 found the opaque-concat rule red on production: the two retention deletes concat a predicate macro into their WHERE, and any "update " around a non-literal argument was a violation. Replace that heuristic with one detector. concat! now becomes a single literal with an {opaque} placeholder per non-literal argument, and every SQL literal is checked on its own: a placeholder (from concat! or format!) in a write target or SET column, a floor-column SET list whose UPDATE is in another literal, an INSERT INTO with no table after it or an events INSERT with a placeholder tail, events written by MERGE INTO or COPY, and a dangling PREPARE are violations. A placeholder in a WHERE or a value is allowed, so the retention deletes and the tenant-severing UPDATE {table} SET community_id = NULL pass. Also recognize concat!/include_str! with [] and {} delimiters, and ban sqlx query_file*! macros, which load SQL the scan cannot see. Document that the prepared-transaction count is cluster-wide on purpose, like the activity scan. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
The per-literal UPDATE check only saw the SET list inside the literal
that holds SET, so format!("UPDATE events SET {}", ..), a split
[..].concat(), and QueryBuilder pushes after SET slipped through. A SET
list that is empty or holds an item with no '=' is now unattributable
when its target is events, a placeholder, or DO UPDATE. Prose that
merely contains update/set is still ignored.
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
The startup floor/roster probes are deleted with the triggers, so their exemption lists and fixtures only kept dead names alive in the policy test. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
Main grew a nip_fi_issuers argument on community bootstrap, ProbeTimings on sample_writer, and a sync spawn_fence_probe; the rebased commits still used the old shapes and the new PreparedTransactions variant had no span kind. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
Main's put_members_on upsert returns columns after its SET list, which the policy read as an unreadable assignment and flagged. Also drop the writer count floor to seven now that the startup probe is gone. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
The prebuilt case holds ROW EXCLUSIVE on events while it migrates. The trigger retirements after 0086 need ACCESS EXCLUSIVE on events, so they hit lock_timeout under that lock. Migrate through 0086 under the writer, then apply the rest once it rolls back. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
The pre-contract step still stopped at 0060 from the original numbering, so the retained-guard snapshot was taken before main's 0061-0086 and the comparison after the full migrator saw their trigger and FK changes. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
A 0086 baseline compared against the full migrator still fails once 0088 and 0089 sit on top, because they drop more triggers. Stop at 0087 for the retained-guard comparison, then run the rest and recheck that the floor objects stay gone. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
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 <[email protected]> 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 <[email protected]> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
…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 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
…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 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
It ran once when the d_tag column was added, updates events fleet-wide without the community deletion lock, and nothing references it. Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Luke Tornquist <[email protected]>
eb8d442 to
c671c71
Compare
6ef31ef to
7d20e7e
Compare
9bd199f to
3e99e19
Compare
## What This is phase 1 of 2 for retiring the SQL function `community_deletion_lock_key(uuid)`. Every relay caller now builds the lock name in Rust, with `buzz_db::deletion::community_deletion_lock_key(CommunityId) -> String`. It then locks with the built-in `hashtextextended($1, 0)`, the same pattern the other advisory locks here use (`channel_ttl_lock_key`, `push_gate_lock_key`). The string is byte-for-byte the one the function hashes (`'buzz-community-deletion:' || target::text`), so the lock is identical. Old and new relays, and the fence functions still in the database (`community_write_allowed`, `assert_community_write_allowed`), keep excluding each other during a rolling deploy. - **Production callers:** exclusive `lock_community_deletion`, shared `lock_community_deletion_shared`, and the bulk shared lock in `bind_nip_fi_issuers_to_active`. The bulk lock now binds a `text[]` of keys, sorted by community id in Rust and locked `WITH ORDINALITY`. That keeps the old `ORDER BY id` lock order, because `Uuid` and Postgres `uuid` both compare bytewise. - **Startup check:** `validate_serving_catalog_on` no longer requires the function to exist, so the phase-2 drop can't make this release refuse to serve or delete. - **Tests and docs:** test lock holders use the helper. `docs/database.md` gives repair scripts the inline expression, which keeps working after the drop. - The test-schema shims in `api/bridge.rs` and `handlers/ingest.rs` stay. Test schemas still copy the fence functions that call the function. **Phase 2 (not here):** a migration and desired-state change drops the function after this release is fully deployed and the fence functions are gone (#8323). It also removes the two test shims. ## Tests - `deletion::postgres_tests::community_deletion_lock_key_matches_the_sql_function` checks that `hashtextextended(rust_key, 0)` equals `community_deletion_lock_key(uuid)` for the nil UUID, the max UUID and 64 random ones. - `migration_schema_fence_objects_are_optional_for_serving_and_deletion` already drops the universal fence objects by hand. It now also drops `community_deletion_lock_key(UUID)`. With both present and absent, it runs the startup and readiness checks, serving leases (shared lock), `bind_nip_fi_issuers_to_active` (bulk shared lock) and `lock_community_deletion` (exclusive lock). Must-fail mutants, each run on Blox against its guarding test: a changed key prefix, the shared, exclusive and bulk locks reverted to the SQL function, and the startup check requiring the function again. ## What got simpler No relay code path calls a database function to build this lock any more. The key has one definition in Rust instead of one in SQL plus about 20 SQL call sites. One fewer object stands between the schema and declarative-only management (`docs/database.md` Rule 1). ## Verification Blox, at `3020a44148d942f1502395640730cd0548a76400` (clean tree): | Gate | Result | |---|---| | `cargo fmt --all --check` | pass | | `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings` | pass | | `cargo nextest run -p buzz-db --lib` | 214/214 | | `scripts/postgres-test-run.sh -p buzz-db --lib` over `deletion::postgres_tests`, `nip_fi_binding::postgres_tests` and the reaction and channel-member lock tests | 101/101 | | Must-fail mutants (prefix, shared, exclusive, bulk, startup check) | 5/5 killed | CI runs the full Postgres lanes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) buzz-review-completed Signed-off-by: Luke Tornquist <[email protected]> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <[email protected]>
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.
scripts/backfill-d-tag.sql(a one-off from whend_tagwas added, an unlocked fleet-wideUPDATE eventswith no references) is deleted too. 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
93e1c3400, restacked onto #8326, plus two review fixes:fence_waits_for_open_write_and_rejects_it_after_transitionis removed. 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.On Blox, fmt, clippy and buzz-db unit pass. The full buzz-db PostgreSQL lane passes except
media_prebuild_waits_for_old_transactions_during_creation_and_cleanup, which fails only on the Blox host and passes in CI. The twofloor_retirementtests were fixed after that run, so they were rerun on their own and pass at this head.Migration numbering:
0088, after #8326's0087; #8211 takes0089on top.