mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
An auto-join could add a new community member to a default channel after it was archived (jedwards27, second review on #8594). The auto-join lists the effective default channels and `join_channel_as_self` checks the archive state before the write, but `put_members` never read `archived_at`, and nothing ordered an archive against the membership write. An archive landing between the list and the insert still created the row and emitted the kind:40099, 39000/39002 and 44100. `put_members` and `put_members_with_event` now take the membership lock, the shared TTL lock and then the channel row `FOR NO KEY UPDATE`, the order a huddle join already uses, and refuse a target who is not an active member with `AccessDenied("channel is archived")` while `archived_at` is set. An auto-join that loses the race is logged and counted as a failed channel, with no row and no side effects. Active members keep their rows; only admission is refused. The review suggested treating archive and unarchive as admission changes so they take the membership lock. This fix locks the row instead, because the row is what every archive writes: `archive_channel`, a kind:9002 `archived` tag and the TTL reaper's single UPDATE, which takes no advisory lock at all and so would still race under the membership-lock version. It also leaves every archive writer unchanged. The refusal is not scoped to self-joins: the only production callers are kind:9000 (already refused on an archived channel at ingest), kind:9021 and the auto-join. No restore or import path re-adds members through `put_members`, and no test relies on adding to an archived channel. The cost is that a membership write now holds the channel row for its transaction. Writers of that row (settings writes, huddle joins, TTL refreshes, the reaper) wait for it, and reads do not. NOSTR.md "Default channels" now says a channel archived or made private while the join runs is one of the logged, unretried failures. Tests: - `put_member_waits_for_a_concurrent_archive` (buzz-db Postgres): an uncommitted reaper-style archive makes a join wait. Once the archive commits the join is refused with no row, and re-adding a removed member is refused too. In the other order, `archive_channel` waits for an admission that holds the locks, and the admitted member stays. - `auto_join_skips_a_default_channel_archived_while_it_joins` (buzz-relay Postgres): a flagged channel is archived in a held transaction. The auto-join lists it, passes the join body's check, then waits in `put_member`. After the archive commits there is no membership row, no kind:39002 entry, no kind:44100 and no `member_joined` notice. - Mutations: dropping the row lock from `lock_channel_for_member_put` fails both tests, and so does dropping the archived refusal from `put_members_on`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>