Repository navigation
Conversation
|
Nice fix. I traced One regression gap before merge: the current tests pause before |
49a1ed6 to
d7faef7
Compare
|
Addressed in |
b383a46 to
3de3aa8
Compare
3de3aa8 to
c30c157
Compare
c30c157 to
49bb9f4
Compare
49bb9f4 to
27245a9
Compare
a5c27a2 to
4dcbfe7
Compare
4dcbfe7 to
154da58
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Reviewed exact head 5f780df6a1cb4cbad65ba7578ef22e463f9599fa with the Royal Court. Requesting changes for one material delivery-order race.
Blocking: cancellation is not a cutover fence for historical output.
register_subscription_if_current swaps the registry entry and then calls pending.commit(), which cancels the predecessor (crates/buzz-relay/src/handlers/req.rs:463-486, crates/buzz-relay/src/connection.rs:75-80). CLOSE similarly cancels/removes the pending lineage under the lifecycle lock and then queues CLOSED (crates/buzz-relay/src/handlers/close.rs:34-49). But the old handle_req does not validate its lease at each historical EVENT or before EOSE. Cancellation is observed only by the outer tokio::select! when the handler future yields (crates/buzz-relay/src/connection.rs:770-783), while the historical per-row loop uses synchronous conn.send and yields only after each 100 accepted events (crates/buzz-relay/src/handlers/req.rs:385-426).
On a multithreaded runtime, the old handler can therefore be inside that loop while another task commits a same-ID replacement or processes CLOSE. It can then queue old-filter EVENTs and possibly EOSE after the replacement owns the same sub_id, or queue an EVENT after CLOSED. The client cannot distinguish those stale frames. The current deterministic race tests verify registry/map/refcount/gauge state, but not the outbound ordering contract.
Please add a current-lease/epoch fence for every historical send and EOSE that is serialized strongly enough with replacement commit and CLOSE; a check that can race between “current?” and send is insufficient. Add deterministic paused-old-delivery tests proving:
- no old-filter
EVENTorEOSEis queued after a same-ID replacement commits; and - no
EVENTis queued afterCLOSEDforCLOSE.
I independently traced disconnect draining and the bounded pubsub desired-state reconciliation and found no additional blocker. Focused tests for predecessor restoration/commit, CLOSE during retain, disconnect before registration, and queue-full retain reconciliation all pass locally on the exact clean head; CI is also green. Those are useful state-integrity checks, but they do not cover this output cutover race.
|
Addressed the delivery-order race at The fix adds a historical-output fence that holds the existing Added deterministic regressions for both requested orderings:
Verification on the exact patch that became this commit:
Please re-review the exact new head. |
cd68377 to
47173d0
Compare
|
Carl (Logan's Buzz instance) — independent VERIFY at exact head Attribution note: the original CHANGES_REQUESTED review was authored by a separate Carl installation posting via Wes's GitHub account. This comment is an independent verification of that finding by Logan's Carl instance, posting via Logan's credentials — not a self-dismissal. An earlier version of this comment misattributed the original review as this instance's own; the technical content below is unchanged. I re-reviewed this read-only on a fresh checkout and traced the actual cutover serialization:
The two regressions deterministically hold the lifecycle mutex, park stale senders behind it, perform replacement commit / CLOSE ordering while locked, and then prove the stale senders reject and the outbound queue contains only the post-cutover frame ( No remaining finding in the scope of the original CHANGES_REQUESTED review. The dungeon door may stop pretending it is a concurrency primitive. |
Verified resolved at exact head 47173d0; see #2382 (comment)
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Reviewed exact head 47173d076914f1a2f45de7f2013ceccbcc91750e with the Royal Court. Requesting changes for one remaining lifecycle/output race.
Blocking: stale pre-registration failures can close a newer same-ID subscription.
The new lifecycle fence covers registration and historical EVENT/EOSE output, but the pre-registration rejection paths still queue CLOSED directly with conn.send (crates/buzz-relay/src/handlers/req.rs:84-142, 193-275). Those paths run after awaits such as get_accessible_channel_ids_cached and is_member (req.rs:135-145, 177-198). The task-level cancellation select in connection.rs:799-810 does not make the resumed handler lose every poll: when an awaited DB future becomes ready at the same time the lease is cancelled, the handler branch can be polled, observe the DB error, synchronously queue CLOSED, and return before cancellation wins.
A concrete ordering is:
- REQ A for
sub_id = xwaits in access resolution. - Newer same-ID REQ B validates and commits, cancelling A’s lease while leaving B registered.
- A’s access-resolution future also resolves with an error; A resumes and directly queues
["CLOSED","x",...]. - The registry and topic refcount correctly retain B, but the client receives a terminal frame for B’s identifier and can no longer distinguish it from B failing.
The same stale terminal-frame hazard exists when CLOSE x removes the lineage while A is awaiting: A can queue an additional error CLOSED after the acknowledgement. The recently added historical sender solved precisely this class of output-order ambiguity for EVENT/EOSE; terminal REQ output needs the same lease-aware serialization.
Please route every REQ-scoped CLOSED (and its paired REQ-scoped NOTICE, if retained) through a helper that validates the exact pending lease while holding the lifecycle lock through queue insertion. Add a deterministic regression that pauses A inside pre-registration access resolution, commits B (and preferably also exercises CLOSE), then releases A with an error and proves no stale terminal frame is queued and B remains registered.
I independently traced disconnect draining, registration/topic serialization, historical-output fencing, and bounded pubsub desired-state reconciliation at this head. CI is green and git diff --check passes; no additional blocker found in those paths.
47173d0 to
e8e0fcc
Compare
|
Addressed at The finding was valid. I audited the full REQ-owned output family rather than changing only the two awaited database-error branches. Every terminal rejection in
Deterministic regressions:
Both tests fail when the request-output fence is replaced with the old direct-send behavior, and pass at the pushed head. Exact-head Blox validation at
The local publication hook additionally passed branch skew, file-size, Desktop checks/typecheck/tests, Rust tests, and Tauri checks. CI is running on the new head. |
|
Addressing the blocking review above (automated reviewer posting via Wes's GitHub account). Head is now The blocking finding is fixedSome background first, since the mechanism is easy to misread. A relay client opens a subscription by sending a Every subscription-scoped terminal frame — The requested deterministic regression exists: The pause point is a The second hazard is real, pre-existing, and tracked separatelyOur reviewers independently found — and I confirmed — a related hazard on the live event fan-out path: the fan-out snapshots its recipient list, awaits an access filter, then queues events without rechecking whether the subscription generation is still current. So a close or same-ID replacement during that await can still be followed by one stale live event. That is not caused or worsened by this pull request. The relevant files ( It is filed as #6319 with the mechanism, both orderings, a second instance on the revocation-eviction path, and acceptance criteria. This PR's description now states the limitation plainly under "Scope and limitation" rather than leaving it implicit. Verification at this head
|
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent
Verdict: APPROVE
Reviewed: bbd20fae75ecc3bd7a83cc12a65379fac22a2b79..4b86e9222424cc9e68ea7a80394586039929e788 (exact head 4b86e9222424cc9e68ea7a80394586039929e788)
Risk: high — relay subscription cancellation, replacement lineage, and reconnect cleanup affect message correctness and resource ownership.
Behavior/contracts traced: Both review lanes traced REQ admission through pending lease ownership, subscription commit/replacement, CLOSE serialization, historical delivery fencing, pubsub topic retain/release, disconnect cleanup, and late-task reconciliation. The new lifecycle lock and lineage model install pending ownership before spawn, serialize CLOSE with registration/commit, cancel predecessors only after successful replacement, and prevent stale historical/rejection output from surviving cancellation.
Findings: no unresolved author-actionable defect. The potentially confusing pre-admission same-ID rejection behavior is present at the pinned base and is not introduced by this PR.
Author action: none for the reviewed code.
Verification owner: CI/integration environment for live Redis race coverage; branch owner/merge gate for the current base conflict. Any rebased head requires fresh review.
Validation: at matching clean exact head, the lanes reported cargo fmt --all -- --check PASS and cargo test -p buzz-pubsub PASS (26 passed, 11 Redis-dependent ignored). cargo test -p buzz-relay reached 902 passed with two failures outside the changed subscription/pubsub surface (telemetry::tests::trace_context_lookup_does_not_enable_callsites and api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo); exact-head GitHub checks are currently green. git diff --check passed.
Manual/native evidence: no live Redis-backed relay race journey was run.
Residual risk: live Redis timing remains independently unwitnessed, and GitHub currently reports the branch CONFLICTING/DIRTY. Those are integration/confidence gates rather than demonstrated PR defects; approval does not carry across the required conflict-resolution head.
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Reviewed: direct diff bbd20fae75ecc3bd7a83cc12a65379fac22a2b79..4b86e9222424cc9e68ea7a80394586039929e788 (exact head 4b86e9222424cc9e68ea7a80394586039929e788)
Risk: high — relay subscription lifecycle, cancellation, output ordering, and Redis topic ownership are messaging-correctness boundaries.
Behavior/contracts traced: pending REQ lease creation; same-ID replacement and predecessor lineage; CLOSE versus registration/commit; fenced REQ-scoped rejection, historical EVENT, and EOSE output; disconnect draining; pubsub retain/release and bounded-queue reconciliation.
Findings: no blocking defect found. Independent systems/integration and adversarial passes found that the lease/lifecycle lock prevents a superseded or closed handler from registering or emitting request-scoped stale output after ownership changes. The prior stale pre-registration rejection defect is covered by the exact-lease output fence and deterministic same-ID replacement regression. The nearby behavior where a replacement rejected before pending ownership begins can emit CLOSED while an older same-ID subscription remains active is pre-existing at the pinned base, not introduced here.
Author action: none for the reviewed code. The branch is currently not mergeable against live main (mergeable_state: dirty; the supplied base is not an ancestor of this head), so it must be reconciled before merge. Any resulting head change invalidates this approval and requires delta review.
Verification owner: CI/integration owns Redis-backed and merge-readiness gates; reviewer owns review of any updated head.
Validation at exact head:
cargo check -p buzz-relay— passcargo fmt --all -- --check— passcargo test -p buzz-pubsub— pass: 26 passed, 11 Redis-dependent ignoredcargo test -p buzz-relay --lib/cargo test -p buzz-relay— 902 passed, 2 failed, 44 ignored locally; failures were unchanged telemetry callsite filtering and mesh-demo timeout paths, with no evidence connecting them to this diff- Exact-head GitHub checks — all required reported checks passed, including Unit Tests, Rust Lint, Security, Relay E2E, Backend Integration, server cross-compiles, and release candidate
Manual/native evidence: no live Redis race exercise was independently run. The PR reports live-local release-binary health/readiness and channel/message/thread smoke coverage; this review does not relabel that author evidence as independent observation.
Residual risk: the 11 Redis-dependent pubsub tests were not independently exercised locally. CI's exact-head relay/backend integration passes reduce that gap. Live event fan-out generation revalidation remains explicitly outside this PR and is not represented as fixed here.
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
🤖 Dismissing: this approval was for the old head 4b86e92. The PR is now a rebuilt single commit on current main and needs a fresh review.
db5d0bb to
6290cfd
Compare
TheSentinel454
left a comment
There was a problem hiding this comment.
🤖 Both inline comments are addressed in 6290cfd. The six new tests now have CI lanes, and the reconcile wake-up only fires on a full queue. Nothing new from me.
6290cfd to
0534c17
Compare
A REQ handler is spawned and can pause in its access checks, but it took its subscription ID only when it registered. A CLOSE during that pause was acknowledged and then the REQ registered anyway; an older same-ID REQ that finished late could replace the newer one or close it with its refusal. The read loop now admits each REQ and sync REQ in arrival order before it spawns the handler, recording its owner token in the connection's subscription map. Claims and in-handler refusals (CLOSED and its NOTICE) act only while the request still owns its token, so a CLOSE or a newer same-ID request fences a pending one. A pubsub retain no longer waits on a full subscriber command queue while the caller holds the lifecycle lock: it leaves a coalesced wake-up, and the subscriber subscribes every retained topic it is missing. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
The lifecycle map now records whether the newest admitted request for an ID has claimed it. A channel-access eviction that removes an old holder while a newer same-ID request is still pending retires only the holder: the client already replaced it, so the relay keeps the pending request's token and sends no CLOSED for the ID. Tests: a revoke of a replaced holder keeps the pending replacement; the pubsub subscriber recovers a dropped subscribe hint on a live Redis link; the bridge search test waits for the claim itself, not for admission. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
The bridge search test now checks the retired live entry at the before_search_query hook, after the claim and before the timeout CLOSED that would also retire it. The pubsub subscriber test waits for Redis to report no subscribers on the released topic before it retains it again. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
…ests in CI A queued Subscribe is processed in order, and a closed queue means the subscriber restarts and re-subscribes the whole desired map. Only a subscribe hint dropped by a full queue needs the reconciliation wake-up, so first retains no longer each make the subscriber rescan the map. The new relay lifecycle tests and the full-queue pubsub test join the infra-free unit lanes (Justfile and scripts/run-tests.sh). The Redis subscriber test runs in a Redis-backed backend-integration step, so buzz-pubsub joins that archive. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
Rebasing onto main's per-filter live scopes (#8519) lengthened the claim_admitted_live calls; rustfmt now wraps them. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
0534c17 to
85f64df
Compare
Summary
A REQ handler runs as a spawned task and can pause in its access checks. Before this change it took its subscription ID only when it registered. So:
REQ x, thenCLOSE xwhile the REQ was still in its access checks: the relay sentCLOSED x, and then the REQ registeredxanyway and sent events andEOSEunder an ID the client had closed.REQ x(A), then a newerREQ x(B): if A finished its checks after B registered, A replaced B, or A's refusal retired B and sent aCLOSEDthe client could not tell from B failing.Now the read loop admits each REQ and sync REQ, in arrival order, before it spawns the handler: the newest request owns the ID from the moment it arrives. A
CLOSEor a newer same-ID request fences an older one that is still pending. A fenced request does not register, and does not sendNOTICE,CLOSED,EVENTorEOSEfor the ID.This builds on the owner token and lifecycle lock that #8522 added. That PR fenced output after registration; this one moves the token to admission, so the same fence also covers the time before registration.
The pubsub retain no longer waits on a full subscriber command queue. Callers hold a connection's lifecycle lock while they retain. A retain past a full queue now leaves a coalesced wake-up, and the subscriber subscribes every retained topic it is missing. A retain that fits in the queue leaves no wake-up, so first retains do not each make the subscriber rescan the map.
Behavior
close::admitruns in the read loop before spawn. It records the request's token in the connection's subscription map, with a flag that says whether the request has claimed yet. It also applies the subscription limit, which moves there from the handlers.claim_admitted_live,claim_admitted_search, the sync claim) register only while the request still owns its token.close_if_owner. The two REQ refusals that also send aNOTICEsend it in the same critical section.CLOSEDfor the ID, because the client already replaced that holder, and the newer request still claims.Client-visible changes to the subscription limit:
CLOSED "error: too many subscriptions"instead of the other refusal.Scope
Live fan-out does not check the token. A same-ID request that has arrived but not yet claimed does not stop live events to the old holder. That path is unchanged here.
If a handler panics before it claims, its token stays in the map until a
CLOSE, a same-ID REQ, or the disconnect removes it. Until then it counts toward the subscription limit.Testing
close_fences_a_req_whose_handler_has_not_run(REQ and sync REQ):REQ xthenCLOSE xbefore the handler runs gives onlyCLOSED xand no registration.only_the_newest_same_id_req_answers: of two same-ID REQs, only the newer one answers.superseded_or_closed_requests_cannot_claim: an older request cannot claim after a newer one, and a closed request cannot claim or close.revoke_of_a_replaced_holder_spares_the_pending_replacement: a revoke of the old holder sends noCLOSED, keeps the pending request's token, and the pending request then claims its own channel.queue_full_retain_does_not_wait_and_schedules_reconciliation(pubsub).live_subscriber_recovers_a_dropped_subscribe_hint(pubsub, requires Redis): it runs the real subscriber on a live link and drops a topic's subscribe hint with a full queue. The topic is still delivered without a reconnect and withoutConfirm. The same holds after the topic is released, unsubscribed and retained again.close_owned, both connection tests fail. With no owner check inclaim_admitted_live, the claim test fails. With noreplacedcheck in the eviction, the revoke test fails. With no reconcile arm in the subscriber, or nonotify_oneinretain_topic, the Redis subscriber test fails.just test-unitandscripts/run-tests.sh. The Redis subscriber test runs in a new Redis-backed backend-integration step.cargo test -p buzz-relay --lib(with--skip config::tests, which hangs locally when run in one process),cargo test -p buzz-pubsub -- --include-ignoredagainst a local Redis,cargo clippy -p buzz-relay -p buzz-pubsub --all-targets -- -D warnings,cargo fmt --all -- --check.🤖 Generated with Claude Code