Skip to content

buzz-db: drop the universal community write-fence triggers (contract) - #8323

Draft
TheSentinel454 wants to merge 23 commits into
elrond/drop-floor-guardfrom
elrond/drop-fence-triggers
Draft

TheSentinel454 wants to merge 23 commits into
elrond/drop-floor-guardfrom
elrond/drop-fence-triggers

Conversation

@TheSentinel454

@TheSentinel454 TheSentinel454 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Stack (merge bottom-up): main ← #8325 ← #8326 (0087) ← #8323 (0088) ← #8211 (0089). Migrations are pre-numbered in stack order, so each PR's migration.rs count assumes the ones below it.

Deploy gate (ops, not CI): merge only after #8219, #8318 and #8322 are deployed on every pod, with no rollback below them expected. No tagged relay release contains the fence (it arrived in 0029, after relay-v0.2.1), so there is no release to name as the required stop. The minimum image is any main build at or after 60de1db0b (#8318's merge, which already contains #8219 and #8322). Merging also needs Luke's explicit merge/DDL approval; review readiness is not rollout approval. Relay pods auto-migrate at boot, so the gate has to hold before this rolling deploy starts. Starting it is already too late.

Rollout note: 0088 takes AccessExclusive on each of the 33 tables (including events) and holds every lock until commit. While it waits up to 5s (lock_timeout) on one table, the tables it has already locked stay blocked for reads as well as writes, so serving traffic can stall table by table. If a wait times out, db.migrate() fails and the pod crashloops under BUZZ_AUTO_MIGRATE until 0088 gets through. Apply 0088 in a low-write window, or from a single migrator pod before the rollout, rather than letting every new pod race for it.

Out-of-band pgschema apply: apply 0088 first, or drop community_write_fence_events and community_write_fence_delivery_log (the two partitioned parents) first. The same applies to events_created_at_floor before #8326's 0087. Otherwise pgschema plans the partition-clone drops before the parent drop, and Postgres refuses with 2BP01. The upgrade fixture pre-drops all three for this reason.

What

This is the contract step for BUZZ-315. Migration 0088_retire_community_write_fence.sql:

  • starts with SET LOCAL lock_timeout = '5s', the convention in 0029/0030/0051/0053/0054 (the migrator otherwise runs with lock_timeout = 0). An unbounded AccessExclusive wait across events and 31 other hot tables would queue serving traffic behind it. A timeout aborts the transactional migration, and the pod retries on its next boot.
  • drops the 33 parent community_write_fence_* triggers. Partition clones go with them.
  • drops the five fence-only functions without CASCADE: enforce_community_write_fence, attach_community_write_fence, community_write_fence_excluded_table, community_write_allowed and assert_community_write_allowed.

The desired state follows:

  • schema/schema.sql and the trigger attachments in channels.sql/events.sql are updated.
  • 30 trigger files that held only a fence attachment are deleted, along with the five function files and schema/community_write_fence.sql.
  • Partition-fence reconciliation is removed from 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_tables snapshot 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 read community_write_fence_excluded_table.sql via include_str!, which this PR deletes; excluded_scoped_tables_are_live_control_plane_tables replaces it. It pins EXCLUDED_SCOPED_TABLES to a control-plane list written out in the test (not derived from EXPECTED_SCOPED_TABLES), requires every excluded table to still exist with a community_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/ and scripts/ on the 32 tables. Every serving writer goes through AdmittedTx/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)

Combination Why it is safe
Pod at #8322 + #8318, DB after 0088 Startup and readiness, scoped-table discovery and lease admission no longer touch the dropped objects. #8318 removed the seven in-SQL predicates.
Pod at this commit, DB before 0088 Validators accept either catalog (migrated_catalog_retires_universal_fences_but_keeps_deletion_guards checks startup before the retirement migration, then after it).
#8322 executor, inventory frozen here (empty fences) #8322 ignores the fence list.

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_guards
  • store::deletion::postgres_tests::desired_state_catalog_has_no_universal_fence_and_still_boots
  • The 0088 source assertions in embedded_migrator_contains_consolidated_initial_schema. They pin all 33 drops, the five functions, the retained lock key and tombstone, and the leading lock_timeout.
  • Trigger-only assertions are removed, and the open-write deletion-ordering test now uses an admitted write.

Each test was confirmed to fail when its fix is removed (rc 100, 0/1):

  • with SET LOCAL removed, the source assertion fails;
  • with 0088 dropped from the migrator, the fence triggers remain (parents plus partition clones);
  • with the old desired schema and reconcile script restored, 46 fence triggers remain.

Test-fixture changes:

  • push.rs loses two cleanup DELETEs that called the dropped function. Per-test databases make them redundant.
  • The 0049 thread-window test releases its held writer before running the full migrator, because 0088's DROP TRIGGER correctly 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 when d_tag was added, an unlocked fleet-wide UPDATE events with 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_default and writer_pool_metrics_stop_after_session_timeout_setup_failure hang on clean 177551dda.

Verification

Head 93e1c3400, restacked onto #8326, plus two review fixes:

  • The upgrade fixture now pre-drops the two fence parents and the floor trigger.
  • fence_waits_for_open_write_and_rejects_it_after_transition is removed. With the trigger gone, its wait came from the pubkey_allowlist FK, not the deletion lock. begin_quiescing_waits_for_open_admitted_writer_note_update and community_fence_rejects_fresh_begin_transaction_after_fence cover 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 two floor_retirement tests were fixed after that run, so they were rerun on their own and pass at this head.

Migration numbering: 0088, after #8326's 0087; #8211 takes 0089 on top.

@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch from c3661fe to 647a82a Compare October 14, 2026 15:45
@TheSentinel454
TheSentinel454 force-pushed the elrond/fence-objects-optional branch from 5bc79c9 to 797181c Compare October 14, 2026 15:56
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch from 647a82a to bad9013 Compare October 14, 2026 15:56
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch from bad9013 to 525bc80 Compare October 16, 2026 14:01
@TheSentinel454
TheSentinel454 changed the base branch from elrond/fence-objects-optional to elrond/fence-retire-stack-base October 16, 2026 14:01
@TheSentinel454
TheSentinel454 force-pushed the elrond/fence-retire-stack-base branch from 3d8789b to 0b09dc2 Compare October 16, 2026 14:11
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch 2 times, most recently from 1981a4b to d897bee Compare October 16, 2026 14:22
TheSentinel454 added a commit that referenced this pull request Oct 16, 2026
## 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>
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch from c523219 to c910fbd Compare November 10, 2026 16:07
@TheSentinel454
TheSentinel454 changed the base branch from elrond/fence-retire-stack-base to main November 10, 2026 16:07
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch from c910fbd to fd5d9f5 Compare November 10, 2026 16:38
@TheSentinel454
TheSentinel454 changed the base branch from main to elrond/drop-floor-guard November 10, 2026 16:38

@loganj loganj left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 with cannot 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 c910fbde55 and at fd5d9f5dd1, and it fails the same way when run alone.
  • Fix (test): the fixture's pre-drop list at :428-440 already handles this for the earlier retired events triggers. Add DROP TRIGGER IF EXISTS community_write_fence_events ON events; and DROP TRIGGER IF EXISTS community_write_fence_delivery_log ON delivery_log; to it.
  • Ops consequence: an out-of-band pgschema apply of 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_feedback and rate_limit_violations were 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_transaction from push enqueue; admitting quiescing in guard_community_write; the NIP-FI bind without deletion_state='active'; hiding channels in EXCLUDED_SCOPED_TABLES; 0088 skipping the users drop. 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 only events, reactions and event_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 from EXPECTED_SCOPED_TABLES, and move the raw-connection helpers to &mut AdmittedTx or pin them.
  • scripts/backfill-d-tag.sql runs a fleet-wide UPDATE events with 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) and session_replication_role = replica in admission_fixture.
  • The body is stale: 0066 (now 0088), "32" triggers (33), "main is at 0064", "Pending at 525bc80a", and the numbering paragraph.

Comment thread crates/buzz-db/src/store/deletion.rs Resolved
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch 3 times, most recently from b0e2422 to 93e1c34 Compare November 10, 2026 18:18
Elrond and others added 10 commits November 12, 2026 09:57
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]>
TheSentinel454 and others added 13 commits November 12, 2026 09:57
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]>
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-fence-triggers branch from eb8d442 to c671c71 Compare November 12, 2026 14:58
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-floor-guard branch from 6ef31ef to 7d20e7e Compare November 12, 2026 14:58
@TheSentinel454
TheSentinel454 force-pushed the elrond/drop-floor-guard branch 2 times, most recently from 9bd199f to 3e99e19 Compare November 12, 2026 22:30
TheSentinel454 added a commit that referenced this pull request Nov 13, 2026
## 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]>

This branch has not been deployed

No deployments
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.

2 participants