Severity: LOW (replay-only; inert under the default replay_ttl = 0)
Component: crates/transport-quic-broker
Summary
BrokerState::publish enforces the global backlog byte budget (total_backlog_bytes > max_backlog_bytes) by popping from the current room's backlog only. When one room has already filled the global budget and then goes quiet, a different now-active room's first records are immediately evicted — so replay preferentially discards the freshest data of the active stream while retaining staler data from quiet rooms, inverting the replay feature's intent.
Location
crates/transport-quic-broker/src/state.rs:228-236 (BrokerState::publish):
room.backlog.push_back(BacklogRecord { record: Arc::clone(&record), bytes: record_bytes, appended_at: Instant::now() });
room.backlog_bytes += record_bytes;
total_backlog_bytes += record_bytes;
while room.backlog.len() > self.max_backlog
|| total_backlog_bytes > self.max_backlog_bytes // GLOBAL budget…
{
let Some(dropped) = room.backlog.pop_front() else { break; }; // …enforced by popping THIS room only
room.backlog_bytes = room.backlog_bytes.saturating_sub(dropped.bytes);
total_backlog_bytes = total_backlog_bytes.saturating_sub(dropped.bytes);
}
Concrete failure scenario (with replay_ttl > 0)
- Room A is the sole active stream and fills its backlog until
total_backlog_bytes ≈ max_backlog_bytes (each A-publish self-evicts A's front, so A alone reaches the global budget).
- Room A goes quiet but its backlog persists (up to
FINISHED_ROOM_TTL/UNFINISHED_ROOM_TTL, ~60s).
- Room B (a different, now-active preview) publishes its first record.
total_backlog_bytes is ≈ budget (from A), so adding B's record trips total > max_backlog_bytes.
- The
while loop pops from B (the current room), discarding the record B just appended. B empties (pop_front → None → break) and retains nothing.
- A late subscriber joining B during A's TTL window gets an empty backlog snapshot, while A's stale records survive.
Impact
Silent replay starvation of the active room under global byte pressure — the opposite of what replay is for. No error, no metric. Inert with the default replay_ttl = 0 (no backlog retained); only reachable when replay is enabled. The broker tests exercise single-room eviction only (src/tests.rs:204-236, 1008-1011), never the multi-room global-budget interaction.
Suggested fix
When enforcing the global byte budget, evict from the globally-largest / oldest room (or bound per-room bytes), not from whichever room is currently publishing.
Dedup
Distinct from #797 (performance of the purge_expired_rooms O(max_rooms) sweep + recompute) — this is an eviction-target-selection correctness/fairness defect, orthogonal to sweep cost. Related to but distinct from the closed #372 (wait_for_subscriber reset not decrementing total_backlog_bytes — an accounting bug on finished-room reuse); this is the publish eviction path choosing the wrong room.
Filed from a meticulous automated code-review pass; verified against source and deduplicated against the existing open/closed issue set.
Severity: LOW (replay-only; inert under the default
replay_ttl = 0)Component:
crates/transport-quic-brokerSummary
BrokerState::publishenforces the global backlog byte budget (total_backlog_bytes > max_backlog_bytes) by popping from the current room's backlog only. When one room has already filled the global budget and then goes quiet, a different now-active room's first records are immediately evicted — so replay preferentially discards the freshest data of the active stream while retaining staler data from quiet rooms, inverting the replay feature's intent.Location
crates/transport-quic-broker/src/state.rs:228-236(BrokerState::publish):Concrete failure scenario (with
replay_ttl > 0)total_backlog_bytes ≈ max_backlog_bytes(each A-publish self-evicts A's front, so A alone reaches the global budget).FINISHED_ROOM_TTL/UNFINISHED_ROOM_TTL, ~60s).total_backlog_bytesis ≈ budget (from A), so adding B's record tripstotal > max_backlog_bytes.whileloop pops from B (the current room), discarding the record B just appended. B empties (pop_front → None → break) and retains nothing.Impact
Silent replay starvation of the active room under global byte pressure — the opposite of what replay is for. No error, no metric. Inert with the default
replay_ttl = 0(no backlog retained); only reachable when replay is enabled. The broker tests exercise single-room eviction only (src/tests.rs:204-236,1008-1011), never the multi-room global-budget interaction.Suggested fix
When enforcing the global byte budget, evict from the globally-largest / oldest room (or bound per-room bytes), not from whichever room is currently publishing.
Dedup
Distinct from #797 (performance of the
purge_expired_roomsO(max_rooms) sweep + recompute) — this is an eviction-target-selection correctness/fairness defect, orthogonal to sweep cost. Related to but distinct from the closed #372 (wait_for_subscriberreset not decrementingtotal_backlog_bytes— an accounting bug on finished-room reuse); this is thepublisheviction path choosing the wrong room.Filed from a meticulous automated code-review pass; verified against source and deduplicated against the existing open/closed issue set.