fix: Unit 4 — backpressure fixes (C-04, C-05, C-07, C-16, C-17, C-25 #2 #3 #6)

C-04 [critical]: route_payload is now async — uses send().await instead
of try_send, so the demux stalls on a full buffer instead of dropping
chunks. Lossless bounded-buffer backpressure per ADR-040 REQ-CH-05.

C-05 [critical]: DEFAULT_BUFFER_CAP changed from 1,048,576 (messages)
to 64 (messages). The old value counted messages, not bytes, giving a
~16 TiB per-channel bound instead of the intended 1 MiB. The new value
is a reasonable message-count bound; the actual memory bound is
enforced by the 16 MiB MAX_CHUNK_LEN per message.

C-07 [critical]: demux loop now skips the payload bytes on
ChunkError::TooLarge before continuing. The parsed length is in the
error variant; the demux reads and discards that many bytes, then
resyncs on the next 8-byte header. Previously it continued without
skipping, causing permanent stream desync.

C-16 [major]: MpscSendStream switched from tokio::sync::mpsc to
futures::channel::mpsc, which exposes poll_ready for proper async
backpressure in poll_write. The ~50 lines of abandoned deliberation
comments are removed. The mux pump now uses futures::StreamExt::next
instead of tokio recv.

C-17 [major]: mux pump writes an EOF chunk when the receiver ends
without a sentinel (handler dropped without shutdown). Previously
the pump exited silently on recv→None, leaving the remote handler
hanging until full transport close.

Tests added:
- C-25 #2: demux_resyncs_after_oversized_chunk
- C-25 #3: backpressure_slow_reader_no_data_loss_other_channel_unaffected
- C-25 #6: mux_pump_writes_eof_on_implicit_close

Verification: 441 tests pass (was 439; +3), clippy clean, fmt clean,
doc warnings unchanged (2 pre-existing, Unit 6 long-tail).
This commit is contained in:
deepseek-v4-pro committed 2026-08-13 07:03:49 +00:00
1 parent 8066d08395
commit 1f06253959
5 files changed
+312 -181

No files matched your search

+20 -19
View File
@@ -16,7 +16,7 @@ use bytes::Bytes;
use parking_lot::Mutex;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tracing::{debug, warn};
use tracing::debug;
use super::mux::{MuxHandle, OpenerLedger};
use super::reassembly::{MpscRecvStream, MpscSendStream, DEFAULT_BUFFER_CAP};
@@ -269,31 +269,28 @@ impl ChannelManager {
/// `channel_id` (the demux's per-chunk route). A zero-length
/// payload is the EOF sentinel — the reassembled stream interprets
/// it as EOF (REQ-CH-01). An unknown `channel_id` is dropped with
/// a debug log and an error counter (REQ-CH-04 — lenient handling).
pub fn route_payload(&self, channel_id: u32, payload: Bytes) {
/// a debug log (REQ-CH-04 — lenient handling).
///
/// Awaits the bounded channel sender — if the handler's read half
/// is slow, the demux stalls here (ADR-040 REQ-CH-05: lossless
/// bounded-buffer backpressure). The demux loop is the only caller;
/// stalling it stalls all channels on this connection, which is the
/// intended behavior (the transport is the shared resource).
pub async fn route_payload(&self, channel_id: u32, payload: Bytes) {
let sender = {
let channels = self.inner.channels.lock();
channels.get(&channel_id).map(|s| s.demux_sender.clone())
};
match sender {
Some(sender) => {
if let Err(e) = sender.try_send(payload) {
use tokio::sync::mpsc::error::TrySendError;
match e {
TrySendError::Full(_) => {
warn!(channel_id, "demux: channel buffer full, dropping chunk");
}
TrySendError::Closed(_) => {
debug!(
channel_id,
"demux: channel receiver dropped, dropping chunk"
);
}
}
if sender.send(payload).await.is_err() {
debug!(
channel_id,
"demux: channel receiver dropped, dropping chunk"
);
}
}
None => {
// REQ-CH-04: lenient unknown-channel handling.
debug!(
channel_id,
"demux: unknown channel_id, dropping chunk (lenient)"
@@ -425,7 +422,9 @@ mod tests {
.open_channel("alknet/tty", "alice", None)
.await
.expect("open");
manager.route_payload(id, Bytes::from_static(b"hello"));
manager
.route_payload(id, Bytes::from_static(b"hello"))
.await;
use tokio::io::AsyncReadExt;
let mut buf = [0u8; 5];
recv.read_exact(&mut buf).await.expect("read");
@@ -435,7 +434,9 @@ mod tests {
#[tokio::test]
async fn route_payload_to_unknown_channel_is_lenient() {
let manager = make_manager_with_runner().await;
manager.route_payload(999, Bytes::from_static(b"data"));
manager
.route_payload(999, Bytes::from_static(b"data"))
.await;
}
#[tokio::test]