Found in the QoS1 backlog/takeover rework (branch worktree-qos1-backlog, commit 6024b21 + round-3 follow-ups), round-3 review finding #5. Not a regression from main; a residual edge case in the new takeover path.
Severity: major (spec violation), but a narrow timing race. No message loss, duplication, crash, or unbounded memory.
Scenario: Client c (handler A1) is subscribed to t and is behind / has a full QoS1 lane. A publish to t snapshots a delivery plan for A1 under the router read locks, then drops the locks. In the small window before that plan executes, c reconnects with clean_start=true (handler A2): register_session strips A1's subscriptions and A2 binds (clear(None), fresh session). The stale plan then executes, finds A1's lane closed, and falls through to queue_behind, pushing the message onto c's queue with a fresh sequence number. A2 drains it and delivers it — a message routed under the prior session's subscription reaches a clean session that never subscribed to t.
Why it isn't already fixed: A sequence cutoff can't help — the stale push gets a fresh seq after the clean bind. The correct fix needs the delivery plan to carry the generation it was built under and to be dropped on mismatch only for clean-start takeovers (dropping on a resumed takeover would lose a message the resumed session legitimately should get, since the publisher's PUBACK already committed).
Proposed fix: Tag DeliveryPlan with the source generation; in execute_plan, before queue_behind, drop the message if the client's current generation differs and that generation belongs to a clean-start session. Location: crates/mqtt5/src/broker/router.rs (plan_delivery / execute_plan / queue_behind).
Found in the QoS1 backlog/takeover rework (branch
worktree-qos1-backlog, commit6024b21+ round-3 follow-ups), round-3 review finding #5. Not a regression frommain; a residual edge case in the new takeover path.Severity: major (spec violation), but a narrow timing race. No message loss, duplication, crash, or unbounded memory.
Scenario: Client
c(handler A1) is subscribed totand is behind / has a full QoS1 lane. A publish totsnapshots a delivery plan for A1 under the router read locks, then drops the locks. In the small window before that plan executes,creconnects withclean_start=true(handler A2):register_sessionstrips A1's subscriptions and A2 binds (clear(None), fresh session). The stale plan then executes, finds A1's lane closed, and falls through toqueue_behind, pushing the message ontoc's queue with a fresh sequence number. A2 drains it and delivers it — a message routed under the prior session's subscription reaches a clean session that never subscribed tot.Why it isn't already fixed: A sequence cutoff can't help — the stale push gets a fresh seq after the clean bind. The correct fix needs the delivery plan to carry the generation it was built under and to be dropped on mismatch only for clean-start takeovers (dropping on a resumed takeover would lose a message the resumed session legitimately should get, since the publisher's PUBACK already committed).
Proposed fix: Tag
DeliveryPlanwith the source generation; inexecute_plan, beforequeue_behind, drop the message if the client's current generation differs and that generation belongs to a clean-start session. Location:crates/mqtt5/src/broker/router.rs(plan_delivery/execute_plan/queue_behind).