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:
1 parent
8066d08395
commit
1f06253959
5 files changed
+312
-181
No files matched your search
+20
-19
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user