Found in the QoS1 backlog rework (branch worktree-qos1-backlog), round-3 review finding #7. Narrow race; no loss or duplication, ordering only.
Severity: minor. Violates [MQTT-4.4.0-1] redelivery ordering in a specific race.
Scenario: After a broker restart, client c reconnects with clean_start=0 holding a persisted outbound QoS1 message in AwaitingPubrec and an empty offline queue. Between register_session inserting c into the clients map and bind() running (load_persisted_inflight -> requeue_front), a concurrent publish for c's topic is routed: the queue is still empty so the router puts it on the QoS1 lane. bind() then re-queues the older in-flight message at the front of the queue. The lane arm (gated only by the outbound window) can deliver the newer message before the drain delivers the older re-queued one.
Why it isn't already fixed: The naive fix (gate the QoS1 lane on an empty backlog) deadlocks the normal backlog case, because drain_backlog already returns early while the lane is non-empty — so with both non-empty nothing progresses. That change was tried and reverted.
Proposed fix: Make c route to the queue rather than the lane until bind() completes (e.g. treat the client as "behind" while unbound), so the racing publish lands in the queue after the re-queued in-flight message. Location: crates/mqtt5/src/broker/client_handler/mod.rs (bind/serve) and the queue's behind() in crates/mqtt5/src/broker/storage/client_queue.rs.
Found in the QoS1 backlog rework (branch
worktree-qos1-backlog), round-3 review finding #7. Narrow race; no loss or duplication, ordering only.Severity: minor. Violates [MQTT-4.4.0-1] redelivery ordering in a specific race.
Scenario: After a broker restart, client
creconnects withclean_start=0holding a persisted outbound QoS1 message inAwaitingPubrecand an empty offline queue. Betweenregister_sessioninsertingcinto the clients map andbind()running (load_persisted_inflight->requeue_front), a concurrent publish forc's topic is routed: the queue is still empty so the router puts it on the QoS1 lane.bind()then re-queues the older in-flight message at the front of the queue. The lane arm (gated only by the outbound window) can deliver the newer message before the drain delivers the older re-queued one.Why it isn't already fixed: The naive fix (gate the QoS1 lane on an empty backlog) deadlocks the normal backlog case, because
drain_backlogalready returns early while the lane is non-empty — so with both non-empty nothing progresses. That change was tried and reverted.Proposed fix: Make
croute to the queue rather than the lane untilbind()completes (e.g. treat the client as "behind" while unbound), so the racing publish lands in the queue after the re-queued in-flight message. Location:crates/mqtt5/src/broker/client_handler/mod.rs(bind/serve) and the queue'sbehind()incrates/mqtt5/src/broker/storage/client_queue.rs.