Repository navigation
Conversation
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Rebase & Conflict Resolution Report for PR #8658Target PR: block/buzz#8658 — BUZZ-413: Fence stream and forum message writes 1. Problem & Root CausePR #8658 was marked as
2. GitHub PR Comment Draft (for @ravarora2)Hi @ravarora2,
I rebased PR #8658 onto the latest `main` branch to resolve the merge conflict with commit `d6196a16c1` (#8237, *revoke invitations and restrict claims*):
- In `crates/buzz-db/src/error.rs`: Preserved both the new invitation restriction error variants (`InviteRestricted`, `InviteOwnerMismatch`) and `UserDeletionRestricted`.
- In `crates/buzz-db/src/store/user.rs`: In `set_agent_owner_with_operation`, preserved `lock_admission` alongside `guard_agent_owner_assignment` in canonical order so both invitation admission gating and user-deletion ownership fencing are held before the atomic `UPDATE`.
You can pull the rebased commit directly into your branch:
```bash
git fetch https://github.com/presentyourlove/buzz.git fix/fence-stream-forum-writes-8658
git rebase 59097c94ca0fcb615988facc6877cdc467449c7dOr apply the patch: curl -sL https://github.com/presentyourlove/buzz/commit/59097c94ca0fcb615988facc6877cdc467449c7d.patch | git am |
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
…vent-guards * origin/main: (29 commits) Restyle community invitations (#8728) feat(relay): add incremental artifact reads (#8523) fix(mobile): show agent DM typing and balance reply bubble padding (#8731) perf(relay): route author- and #p-scoped global subscriptions by selector (#8666) fix(desktop): fix the product races behind four E2E flakes (#8680) test(scripts): stop file-size fixtures leaking git repos and fsmonitor daemons (#8727) test(mobile): keep late callbacks and fixed sleeps from failing tests (#8678) fix(mobile): resolve iOS pushes on a fresh connection and retry a stale one (#8722) ci(loadgen): publish buzz-loadgen as ghcr.io/block/buzz-loadgen (#8717) test(relay): fix main's build after the test-state helper rename (#8718) fix(db): keep lease releases durable and deflake PostgreSQL lane tests (#8676) test: dedupe collect_until_eose by event id and uncap two shell tests (#8677) ci: cache the Tauri apt .debs and fit a cold Windows Rust run in its cap (#8679) test(desktop): gate flaky E2E specs on the state they assume (#8681) test(desktop): settle composer edits and press hover-only row actions by keyboard (#8682) test(desktop): measure inline video controls against their player (#8683) relay: open the search pool lazily so a dead reader can't block boot (#8662) fix(mobile): address #8650 second-pass review (#8691) Revert "fix(mobile): push every message in your own Solo chats (#8615)" (#8690) fix: don't notify workflow owners through their attribution tag (#8694) ... Signed-off-by: Ravneet Arora <rarora@squareup.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Review at 0c3ebcb53. The core guard is sound: it checks only the signer, under both the global and community scopes, regardless of profile rows, rechecks the owner after locking, and holds the locks through commit. Five blocking issues, three minor, all inline:
IMPORTANT
- Kind 9035/9036 archive/unarchive requests change
archived_identitiesand publish deltas before the guard runs, so a restricted author's request takes effect and is then refused (event.rsL1884). - The new
AccessDeniedarm on the generic insert path sends community-admission errors to clients asrestricted:(ingest.rsL3730). - The ownership-change refusal is
AccessDenied, so clients seerestricted:,invalid:or an internal error depending on the path. None of these tells them to retry (user_deletion_write_guard.rsL32). - Some callers take identity locks after row locks, against the ordering rules in
user_deletion_fences.rs(event.rsL480). - About nine extra round trips on every event insert, with no measurement (
user_deletion_write_guard.rsL66).
MINOR
6. The refusal mapping is duplicated.
7. The design doc reads as a PR narrative.
8. The test modules are chained through nested #[path] attributes.
| return Err(DbError::EphemeralEventRejected(kind_u16)); | ||
| } | ||
|
|
||
| tx.guard_event_author(event).await?; |
There was a problem hiding this comment.
🤖 IMPORTANT — archive/unarchive takes effect despite the refusal. For kinds 9035/9036, ingest.rs L3082–3089 calls handle_identity_archive_event before the request reaches this inserter. That handler commits archived_identities changes in its own transaction and publishes the relay-signed 8002/8003 deltas and the 13535 snapshot. A restricted author's request therefore changes archive state, then gets refused here and is never stored. The published delta's ["e", request_id] then points at an event that doesn't exist.
Fix: apply the archive change and insert the request in one transaction that runs this guard, and publish after commit. A separate check before the handler would still race a restriction being installed. Please also add archive/unarchive to the HTTP/WS before-and-after-restriction tests, with archived_identities in the unchanged-state snapshot.
There was a problem hiding this comment.
🤖 Fixed in 4688052, included in head 572e30e. Archive/unarchive validation now produces a plan without changing state. The guarded request insert, archived_identities mutation, and relay delta persist in one transaction; notification dispatch follows commit. A rejected request or failed delta insert rolls all three back. Duplicate requests do not repeat the mutation.
Added HTTP/WebSocket before-and-after-fence regressions, unchanged archive/snapshot assertions, delta-failure rollback, and fence commit/rollback races. These passed in the merged tree's PostgreSQL suite (1,189 tests).
Remaining boundary: the snapshot refresh is post-commit. Its failure propagates, and a duplicate retry refreshes it without repeating the archive mutation. There is no durable background refresh queue, so a crash after commit can leave the snapshot stale until a later refresh. This limitation is documented.
There was a problem hiding this comment.
🤖 Follow-up fixed in 3564dc2: replaying an already-processed archive/unarchive request now leaves an already-current snapshot unchanged, including its event ID and timestamp, and does not broadcast a replacement. The publisher compares the saved snapshot with canonical archive state and rechecks that state before taking the no-op path. Missing/stale snapshots are still repaired after a failed publication; the existing post-write drift protection remains.
The new replay regression failed against the previous code, then passed with the fix. It covers repeated archive/unarchive requests and replay after a later opposite operation, asserting that all database rows remain unchanged. Recovery tests cover both missing and stale snapshots and a further retry after repair. A deterministic concurrency test changes canonical state while the publisher is paused before its no-op decision and verifies repair.
Validation: all 6 PostgreSQL archive/fencing tests and 8 existing archive consent/publisher tests passed, as did the normal pre-push hooks. The PostgreSQL fixture exercises the HTTP route and authenticated WebSocket event handler; I did not rerun the separate live WebSocket AUTH simulation. The full repository just ci run was not restarted.
| buzz_db::DbError::UserDeletionRestricted => IngestError::Rejected( | ||
| "restricted: identity is restricted for user deletion".into(), | ||
| ), | ||
| buzz_db::DbError::AccessDenied(reason) => { |
There was a problem hiding this comment.
🤖 IMPORTANT — this arm changes behavior outside this PR's scope. On this path, AccessDenied also comes from community admission (guard_community_write / lock_community_deletion_shared). Examples: community <uuid> is write-fenced (<state>), missing or tombstoned, and deletion-lock admission requires read committed isolation, got …. Those used to surface as error: database error. With this arm, clients see internal state and config text under a restricted: prefix. Once the ownership-change case below has its own variant, nothing in this PR needs the arm. Suggest removing it.
There was a problem hiding this comment.
🤖 Fixed in f1570c4, included in head 572e30e. Removed the generic AccessDenied-to-restricted arm. Only UserDeletionRestricted produces the user-deletion refusal. Other database errors, including community-admission errors, retain their existing caller fallback rather than exposing the internal reason under restricted:. Error-mapping and transport regressions passed.
| // Restart in a new transaction: adding another owner lock now could | ||
| // invert the global-before-scoped ordering. No write has occurred. | ||
| return Err(DbError::AccessDenied( | ||
| "agent ownership changed; retry the write".into(), |
There was a problem hiding this comment.
🤖 IMPORTANT — the ownership-change refusal can't be told apart from a permanent refusal. As AccessDenied, it surfaces three ways depending on the caller:
restricted:on the generic insert pathinvalid:on the put-user and edit pathsInternalthroughevent_write_error, artifact acceptance and command persistence
None of these tells the client to retry, which contradicts the message. begin_community_sweep_transaction also already treats AccessDenied as "community fenced, skip". Suggest a dedicated DbError variant, mapped once in event_write_error to a retryable error: refusal.
There was a problem hiding this comment.
🤖 Fixed in f1570c4, included in head 572e30e. Added DbError::EventAuthorOwnershipChanged and mapped it centrally through event_write_error. An ownership change now returns the retryable error category: WebSocket error: internal server error, or HTTP 500 with the sanitized internal server error message. It no longer uses AccessDenied or becomes restricted:/invalid:.
The PostgreSQL transport regressions force ownership to change while the writer waits for its identity lock, cover generic/membership/edit paths over HTTP and WebSocket, and verify a retry succeeds. This permits a client retry; it does not implement automatic retry behavior.
| return Err(DbError::EphemeralEventRejected(kind_u16)); | ||
| } | ||
|
|
||
| tx.guard_event_author(event).await?; |
There was a problem hiding this comment.
🤖 IMPORTANT — identity locks taken after domain row locks. The ordering rules in user_deletion_fences.rs are: community admission, then all global user locks in one call, then community-scoped agent locks. Because the guard runs at the insert call rather than when the transaction starts, some callers lock rows first. store/artifact.rs accept writes artifact_revisions and upserts artifact_heads before inserting the author's event. On a Move, it then inserts the relay-signed removal marker, which takes a second global user lock after the author's community-scoped lock. Nothing deadlocks today, because normal writers take only shared locks and no exclusive locker touches these rows. But the deletion worker will depend on this order, and once it holds exclusive identity locks and writes these rows, these paths will deadlock and abort.
Two ways to fix it: run the guard before any domain row lock (at admission, or at the top of each caller), or rewrite the ordering rules to match what callers actually do and state the worker-side requirement that follows.
There was a problem hiding this comment.
🤖 Addressed using the second option in 21f8dc6, included in head 572e30e. The documentation now describes the actual call order: domain locks may precede the guard, and later relay-signed inserts can acquire another global identity lock after the first scoped lock. Per-call sorting is explicitly not a transaction-wide guarantee.
The future exclusive fencing transaction must write only restriction/request/checkpoint state while holding identity locks. It must not lock or mutate serving-domain rows; cleanup must use later transactions. Complete exclusive identity sets must also be audited against multi-signer transactions before enabling the worker.
Added production artifact Move concurrency regressions for a fence winning first and a Move waiting for the relay identity lock while a user fence queues. The tested interleavings complete with the expected persisted/rolled-back state and zero recorded database deadlocks. The channel-TTL race also passes. These tests do not establish universal deadlock safety; the real deletion worker remains disabled.
| // Read the signer's restrictions, not the owner's. Check both possible | ||
| // scopes even if the profile/ownership row is absent: those mutable rows | ||
| // must not determine whether a permanent restriction still applies. | ||
| if read_locked(tx, FenceIdentity::user(signer)) |
There was a problem hiding this comment.
🤖 IMPORTANT — cost on the hottest write path, with no measurement. Each persistent event insert now adds roughly nine round trips: the owner read twice, reading, setting and restoring lock_timeout, 2–3 advisory locks, and these two restriction reads. That includes relay-signed side-effect events. Until the deletion worker is enabled, the restriction table is empty in production, so this is pure cost. Two asks:
- Merge these reads into one query:
WHERE pubkey = $1 AND (scope_community_id IS NULL OR scope_community_id = $2). - Add an insert-latency measurement before and after this change to the PR.
There was a problem hiding this comment.
🤖 Combined the two restriction lookups in eb2c2c8 and measured the cost. The query is now pubkey = $1 AND (scope_community_id IS NULL OR scope_community_id = $2); it still checks the signer, not the owner's restriction.
Completed 49,152 successful timed writes at 4aef33b, before the main merge, comparing no guard, the original two-query guard, and the combined query. Covered both shared insert APIs, human/owned-agent signers, and one/eight concurrent writers. Full methodology and tables are in the updated PR description.
Single-writer p50 improved about 5–10% versus the two-query guard. The complete guard still added about 2.2–2.6 ms (65–81%) versus no guard. That is a material remaining cost, not a claim that overhead is solved. Measurements used local PostgreSQL, Cargo's unoptimized test profile, and an empty restriction table; they are not production capacity estimates. All temporary benchmark variants were restored; no bypass is committed.
| } | ||
|
|
||
| /// Keep durable identity restrictions as client refusals on every insert path. | ||
| pub(crate) fn event_write_error(error: buzz_db::DbError) -> IngestError { |
There was a problem hiding this comment.
🤖 MINOR — duplicated mapping. The string restricted: identity is restricted for user deletion is written here and again at L3728. The downcast_ref sites also rebuild DbError::UserDeletionRestricted only to pass it back in here. Taking &DbError and returning Option<IngestError> (with the retryable ownership-change variant handled here too) would replace five match arms with one call per site.
There was a problem hiding this comment.
🤖 Fixed in f1570c4, included in head 572e30e. event_write_error now takes &DbError and returns Option. It centralizes UserDeletionRestricted and EventAuthorOwnershipChanged mapping. Ingest, artifact, and command persistence callers use the helper and preserve their existing fallback for unrelated errors. The repeated refusal string and reconstructed error variants are removed. Mapping/transport regressions passed.
| @@ -0,0 +1,39 @@ | |||
| # User-deletion fencing: event database writes | |||
There was a problem hiding this comment.
🤖 MINOR — the doc describes the PR, not the system. It reads like a PR description: "This PR checks…", "The earlier 510-case live matrix validated a broader revision…", "Current validation results belong in the PR description." That goes stale on merge. Suggest rewriting it to state the rules that hold (which writes are fenced, the lock order, what is still unfenced, and what the worker must do before it's enabled), or moving it into the PR body.
There was a problem hiding this comment.
🤖 Rewritten in 4aef33b, included in head 572e30e. The document now describes lasting behavior: guarded insert functions, transaction flow, signer/owner semantics, errors and atomicity, actual lock order, future exclusive-worker requirements, and remaining unfenced paths. PR history and the old live-matrix narrative are removed. Benchmark methodology/results are in the PR description.
| } | ||
| } | ||
|
|
||
| #[path = "user_deletion_expanded_postgres_tests.rs"] |
There was a problem hiding this comment.
🤖 MINOR — nested #[path] chain. The test modules are chained message → expanded → lifecycle here, and write_guard → lock_order in buzz-db. Registering each one directly in its mod.rs (as with user_deletion_message_postgres_tests) makes them easy to find and keeps them independent.
There was a problem hiding this comment.
🤖 Fixed in b76879c, included in head 572e30e. Each PostgreSQL test module is now registered directly in its package's mod.rs. Shared setup lives in user_deletion_test_support modules instead of depending on chained test-module imports. PostgreSQL test discovery passed across 638 Rust source files, and all 1,189 database/relay PostgreSQL tests passed after merging main.
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
Signed-off-by: Ravneet Arora <rarora@squareup.com>
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Re-review at 3564dc2dc. All eight findings from the review at 0c3ebcb53 are fixed. Archive/unarchive now stores the request, applies the archived_identities change and stores the delta in one guarded transaction. event_write_error matches only the two new error types. Ownership changes have their own retryable error. The lock order is documented as callers actually take it, with the requirements a future worker must meet. The lookup is one query and the PR now includes measurements. The doc describes the system rather than the PR, and the test modules are registered directly.
The atomic archive rewrite introduced one blocking issue and two minor ones:
IMPORTANT
- The archive request and its 8002/8003 delta are no longer indexed in
event_mentions, so#pqueries for the target can't find them (archived_identities.rsL212, inline).
MINOR
2. The relay-signed delta now takes its created_at from the client's request instead of relay time (identity_archive.rs L95, inline).
3. The comment at ingest.rs L622 still says consent is checked "inside handle_identity_archive_event". That function is now a #[cfg(test)] wrapper, and production validation happens in validate_identity_archive_event. Please update the comment.
| ) -> Result<(StoredEvent, bool, Option<StoredEvent>)> { | ||
| let mut tx = self.begin_event_write_transaction(community).await?; | ||
| let (stored, inserted) = | ||
| crate::event::insert_event_in_transaction(&mut tx, request, None).await?; |
There was a problem hiding this comment.
🤖 IMPORTANT — archive requests and deltas are missing from event_mentions. Before this change, the request was stored through Db::insert_event_with_thread_metadata and the delta through Db::insert_event. Both wrappers call insert_mentions_in_transaction after a new insert (event.rs L2289–2292 and L2829–2831). This function calls the low-level insert_event_in_transaction for both events and never indexes their p tags. Single-#p queries and counts use an INNER JOIN event_mentions (event.rs L628–641, L851–861; the relay pushes #p filters down to this path in req.rs L1220–1231). As a result, newly accepted 9035/9036 requests and their 8002/8003 deltas no longer show up in a #p: [target] subscription or history query, which is how a client replays one identity's archive history.
Fix: call crate::insert_mentions_in_transaction(&mut tx, event, None) for the request when inserted, and for the delta when it's inserted, inside this transaction. Please add a regression test that queries #p = target through the production path and expects both events.
| &event.content, | ||
| reason.as_deref(), | ||
| replaced_by.as_deref(), | ||
| // Use the request timestamp for its corresponding relay notification. |
There was a problem hiding this comment.
🤖 MINOR — the relay-signed delta now uses the client's timestamp. build_nipia_delta used to stamp 8002/8003 with relay time. It now copies event.created_at from the request, which ingest accepts anywhere within ±900 s of server time (ingest.rs L2643). So a client can backdate or postdate a relay-signed event by up to 15 minutes. An archive followed by an unarchive can produce deltas whose timestamps are in the wrong order, and a backdated delta can fall before a subscriber's since. The NIP-IA vectors stamp the delta after the request (request + 1). The transaction doesn't need a deterministic timestamp, because a duplicate request never stores a second delta. Suggest building the delta with Timestamp::now().
…ests Two buzz-relay test fixes from the 23-30 Oct flake audit. Neither is covered on main (still fae3f03) or by an open PR. thread_window_bridge_restarts_both_aux_hops_after_replica_failure (3 runs) Runs: 40799617319 (elrond/connect-burst), 40924179886 (elrond/acp-resume-cursor) and 40930100994 (#8697, docs only), all on 10-29 in PostgreSQL Tests, attempt 1 (jobs 123208043033, 123632125866 and 123651641392). None of the branches touches thread_window, and each run passed on a re-run. Signature: failure_postgres_tests.rs:46, "barrier must precede closure completion", left: 2, right: 1. Cause: pause_after_aux_page polls the request until a thread_window_aux span closes, then expects exactly one. The fixture's 1,001 reactions make two replica pages at AUX_LIMIT 1000, and each page is one round trip on the transaction already held. If the loaded runner deschedules the test thread after page 2's query is written, Postgres answers first. The same poll then reads page 2 as well, so both spans close before the test looks. Change: buzz-relay's test_support gains a PausingProxy. It is a trimmed copy of buzz-db's with only the hold-everything switch, because buzz-db's is private to its own unit tests. The replica reader pool connects through it, and AuxPages pauses it when the first aux span closes. That runs inside the request's poll, after page 1's response is consumed and before page 2's query is written. So the proxy holds page 2's query and the page stays pending however fast Postgres answers. The held- transaction check, the writer inserts and pg_terminate_backend use the direct pool. The test resumes the proxy after terminating the reader's backend, so the held page sees the termination and the closure restarts on the writer, as before. The other three pause_after_aux_page callers read from the writer and pass None. Verification: all four pause_after_aux_page tests pass when run alone against local Postgres through scripts/postgres-test-run.sh. Forced check, not committed: after the barrier, the test gave page 2 another 500 ms over ten re-polls and asserted the page count stayed at 1. With the pause the test passed. With the pause removed (None) it failed with left: 2, right: 1, the CI signature. A true same-poll burst needs the OS to preempt between sqlx's write and read, so that was not forced directly. api::media tests that need Postgres (1 run, fails every time it is selected) Run: 40954304982 (#8658, codex/user-deletion-event-guards), 10-29, Flake check (Rust), attempt 1 (job 123730434146). #8658 changes media.rs product code but no tests. Signature: all six test_state() tests panic at the community-seed expect (media.rs:1737 on that branch) after the 30 s sqlx acquire timeout, and the job ends with "flake-check: iteration 1/10 FAILED". Cause: Flake Check selects every test in a file when a hunk lands outside a test body. Its infraTest skips only postgres_tests::, postgres_* binaries and external_infra, and the flake-rust job has no database. These six tests seed relay.example through Postgres but sat in plain `mod tests` without #[ignore]. So any PR that changes non-test code in media.rs fails that lane, and at least 8 open PRs touch the file. No other lane ran them either: the Justfile already noted that api::media's non-ignored tests need Postgres. Change: move test_state, media_get_auth_router, media_get_auth_header, media_get_tags_for, media_request and the six tests into tests::postgres_tests. Mark each test #[ignore = "requires Postgres"], and point test_state at test_support::database_url(), as the module's other states do. The PostgreSQL lane now runs them and Flake Check skips them. Also drop the imports `mod tests` no longer uses, and update the Justfile comment that gave api::media as the reason the infra-free lane is scoped to api::admin. Staging media.rs makes the pre-commit secret scan read the whole file. It flags the module's existing public ES256 test key (TEST_EC_PKCS8_PEM), so the key is now written on one line with a sadscan marker, as invites.rs already does. Its bytes are unchanged. Verification: check-postgres-test-discovery.py accepts the touched files and all 621 files under crates/, and scripts/test-postgres-test-discovery.sh passes. Each of the six tests passes when run alone against local Postgres through scripts/postgres-test-run.sh. They have never run in CI, so the PostgreSQL lane on the PR is their first hosted run. Both: cargo fmt --check and cargo clippy -p buzz-relay --all-targets -- -D warnings are clean under 1.98.1. The full lanes were not run locally. Hosted CI is the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ests (#8740) ## Summary Two buzz-relay test fixes from the 23–30 Oct flake audit. Both are test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### `thread_window_bridge_restarts_both_aux_hops_after_replica_failure` (3 runs) - **Runs:** 40799617319, 40924179886 and 40930100994 (#8697, docs only), all on 10-29 in PostgreSQL Tests, attempt 1 (jobs 123208043033, 123632125866, 123651641392). None of the branches touches thread_window, and each run passed on a re-run. Signature: `failure_postgres_tests.rs:46`, "barrier must precede closure completion", left: 2, right: 1. - **Cause:** `pause_after_aux_page` polls the request until a `thread_window_aux` span closes, then expects exactly one. The fixture's 1,001 reactions make two replica pages at `AUX_LIMIT` 1000, and each page is one round trip on the transaction already held. A loaded runner can deschedule the test thread after page 2's query is written. Postgres then answers first, and the same poll reads page 2 too. - **Change:** `test_support` gains a `PausingProxy`, a trimmed copy of buzz-db's private one with only the hold-everything switch. The replica reader pool connects through it. `AuxPages` pauses it when the first aux span closes. That runs inside the request's poll, after page 1's response is consumed and before page 2's query is written, so page 2 stays pending however fast Postgres answers. The test resumes the proxy after it terminates the reader's backend. The held page then sees the termination, and the closure restarts on the writer, as before. The other three `pause_after_aux_page` callers read from the writer and pass `None`. ### `api::media` tests that need Postgres (fail every time Flake Check selects them) - **Run:** 40954304982 (#8658), 10-29, Flake check (Rust), job 123730434146. All six `test_state()` tests panic at the community-seed `expect` after the 30 s sqlx acquire timeout. - **Cause:** when a hunk lands outside a test body, Flake Check selects every test in the file. It skips only `postgres_tests::`, `postgres_*` binaries and `external_infra`, and its job has no database. These six tests seed `relay.example` through Postgres but sat in plain `mod tests` without `#[ignore]`. So any PR that changes non-test code in `media.rs` fails that lane, and at least 8 open PRs touch the file. No other lane ran these tests either. - **Change:** move `test_state`, the `media_get_*` helpers, `media_request` and the six tests into `tests::postgres_tests`. Mark each test `#[ignore = "requires Postgres"]`, and point `test_state` at `test_support::database_url()`, as the module's other states do. The PostgreSQL lane now runs them, and Flake Check skips them. The change also drops imports `mod tests` no longer uses, and updates the Justfile comment that gave `api::media` as the reason the infra-free lane is scoped to `api::admin`. - When `media.rs` is staged, the pre-commit secret scan reads the whole file and flags the module's existing public ES256 test key (`TEST_EC_PKCS8_PEM`). The key is now written on one line with a sadscan marker, as `invites.rs` already does. Its bytes are unchanged. ### Related issue None found. **Conflicts with #8451 and #8444**, which both already conflict with `main`. The one-line `TEST_EC_PKCS8_PEM` rewrite is the only new conflict hunk with each. The test move merges cleanly with both. ### Testing Verified locally: - Each of the four `pause_after_aux_page` tests passes when run alone against local Postgres through `scripts/postgres-test-run.sh`. - A forced check, not committed: after the barrier, the test gave page 2 another 500 ms over ten re-polls and asserted the page count stayed at 1. It passed with the pause and failed with `None`, with the CI signature (left: 2, right: 1). A true same-poll burst needs the OS to preempt between sqlx's write and read, so it was not forced directly. - `check-postgres-test-discovery.py` accepts the touched files and all 621 files under `crates/`, and `scripts/test-postgres-test-discovery.sh` passes. Each of the six media tests passes when run alone against local Postgres. - `cargo fmt --check` and `cargo clippy -p buzz-relay --all-targets -- -D warnings` are clean under 1.98.1. Only hosted CI can show the full lanes, which were not run locally. The six media tests have never run in CI, so this PR's PostgreSQL lane is their first hosted run. Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8741 test(mobile): RunnerTests title fade and alert wait - #8742 fix(desktop): deep-linked message hold (product change) - #8743 test(desktop): four specs racing scroll settles and focus - #8744 test(desktop): five settle classes and remote-owned-mentions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
Hi @ravarora2! 👋 I've merged the latest Reconciled Changes:
Rebased/Merged Branch:
Hope this helps move PR #8658 forward! 🚀 |
Summary
Make existing user-deletion restrictions apply at the two shared event insertion functions. A fenced signer gets a refusal; the event and its transactional changes roll back. This covers every event kind reaching those functions, including messages, replies, reactions, edits, channel changes, deletion events and artifacts.
The transaction takes shared identity locks and checks the signer's global and community restrictions with one query. It retains the locks through commit or rollback. Stored owner identity coordinates locks; it is not an extra restriction check. Ownership changes produce a retryable internal error. Unrelated database errors keep their existing handling.
Identity archive/unarchive requests now persist the request, archive change and relay delta in one transaction. Notifications follow commit. Duplicate retries do not repeat the mutation. Snapshot refresh still runs after commit: failures propagate, and a retry repairs a missing or stale snapshot. If the snapshot already matches current archive state, it retains its event ID and timestamp and is not broadcast again. The no-op path rechecks canonical state for concurrent changes. There is no durable background retry queue for that refresh.
Some event callers hold domain locks before the identity guard, and some insert events from multiple signers. The documented worker contract now reflects that behavior: exclusive fencing transactions must not touch serving-domain rows. Cleanup belongs in later transactions. Per-call identity sorting alone does not prove every future multi-key workflow safe.
Scope remains event persistence. There are no migrations, user-specific leases, authentication changes, ownership-assignment changes or restriction-insertion behavior changes. Separate replaceable-event writers, read state, uploads, git, audio, ephemeral activity and deletion-worker coordination remain follow-up work. The real user-deletion worker must remain disabled.
This affects a critical shared write path and adds measurable database overhead, even while the restriction table is empty. See the local measurements below and the transaction rules and boundaries.
Related issue
Part of BUZZ-413. Follows merged #8632. No duplicate message-fencing PR found; #8489 covers workflow scaffolding and #8645 covers owned-community handlers.
Testing
Local database benchmark at
4aef33b95, before merging the newermain: 49,152 successful timed writes, excluding warmups. A temporary harness calledDb::insert_eventandDb::insert_event_with_thread_metadatawith kind 9 events, human/agent signers, and one/eight concurrent writers. Signing occurred before timing; timing included pool acquisition, community admission, persistence and commit.Environment: Apple M1 Max, 64 GiB RAM, PostgreSQL 17.11 in local Docker, TCP loopback, Cargo test profile (unoptimized), empty restriction table. Each case used 64 warmups and 512 measured writes per round. Compared the original two-query guard, the combined query, and a temporary no-op guard. Ran three rounds per variant and repeated the combined version. All temporary changes were restored; no benchmark bypass is committed.
Single-writer latency, in milliseconds. Cells show the median of each run's p50/p95 values; combined-query figures use six rounds, others use three.
insert_eventinsert_eventinsert_event_with_thread_metadatainsert_event_with_thread_metadataCombining the lookups reduced single-writer median latency about 5–10% versus two queries in these runs. The full guard still added about 2.2–2.6 ms (65–81%) versus no guard. This is a material cost, not evidence that fencing is free or ready for production capacity assumptions. Actions that also persist relay-generated events pay the guard cost for each additional insert; this benchmark does not measure complete HTTP actions.
Eight concurrent writers, same channel; median throughput across rounds:
insert_eventinsert_eventinsert_event_with_thread_metadatainsert_event_with_thread_metadataThese are local debug measurements, not a production benchmark or capacity estimate. No injected network latency or populated restriction-table load was included in timing. Separate query-plan checks used a temporary table with the same two partial indexes; empty and 50,000-row cases used both indexes. The earlier 510-case lab matrix covered a broader revision and is not evidence for this version; no new UI lab run is claimed.
Generated with Codex