Align Sudoku read/write ownership with TLS

Make Sudoku reads input-only and remove the shared readiness dispatcher. Keep write-side buffering and draining unchanged, with explicit flush ownership documented for streams and session handoffs.

Cover independent reader/writer readiness, sticky failures, stacked masks, protocol handoffs, and TLS/REALITY over Sudoku under backpressure.
This commit is contained in:
cfal
2026-10-30 09:48:42 +00:00
parent b6534d8293
commit cd79e3d26d
4 changed files with 681 additions and 281 deletions
+147 -80
View File
@@ -217,73 +217,122 @@ fn tls_reads_leave_output_and_writer_readiness_untouched() {
}
#[test]
fn masked_tls_reads_preserve_underlying_writer_readiness() {
fn masked_tls_reads_leave_output_and_writer_readiness_untouched() {
use crate::sudoku_mask::{Role, SudokuMaskConfig, SudokuStream};
for reality in [false, true] {
for synchronous_progress in [false, true] {
for operation in [
WriteOperation::Write,
WriteOperation::WriteVectored,
WriteOperation::Flush,
WriteOperation::Shutdown,
WriteOperation::DrainTls,
] {
let state = Arc::new(Mutex::new(WriteReadiness::default()));
let config: crate::sudoku_mask::SudokuMaskConfig<()> =
serde_json::from_value(serde_json::json!({"protocol":null})).unwrap();
let io = crate::sudoku_mask::SudokuStream::new(
Box::new(SingleWriteWaiter(state.clone())),
config.prepare().unwrap(),
crate::sudoku_mask::Role::Client,
);
let session = if reality {
completed_reality_connection()
} else {
completed_ordinary_connections().0
};
let mut stream = CryptoTlsStream::new(io, session, None);
let writer = Arc::new(WakeCount::default());
let reader = Arc::new(WakeCount::default());
let (write_waker, read_waker) = (writer.clone().into(), reader.clone().into());
let mut write_cx = Context::from_waker(&write_waker);
let mut read_cx = Context::from_waker(&read_waker);
saturate_tls_output(&mut stream, &mut write_cx);
assert!(
operation
.poll(&mut stream, &mut write_cx, &BACKPRESSURE_PAYLOAD)
.is_pending(),
"expected {operation:?} to block"
);
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
if synchronous_progress {
state.lock().unwrap().allowance = usize::MAX;
}
// TLS reads no longer drain TLS output, but the mask still drains accepted wire.
assert!(
Pin::new(&mut stream)
.poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0]))
.is_pending()
);
if !synchronous_progress {
for (role, peer_role) in [(Role::Client, Role::Server), (Role::Server, Role::Client)] {
for output_ready in [false, true] {
for operation in [
WriteOperation::Write,
WriteOperation::WriteVectored,
WriteOperation::Flush,
WriteOperation::Shutdown,
WriteOperation::DrainTls,
WriteOperation::Ping,
] {
let state = Arc::new(Mutex::new(WriteReadiness::default()));
let config: SudokuMaskConfig<()> =
serde_json::from_value(serde_json::json!({"protocol":null})).unwrap();
let config = config.prepare().unwrap();
let io = SudokuStream::new(
Box::new(SingleWriteWaiter(state.clone())),
config.clone(),
role,
);
let (session, mut peer) = if reality {
let (client, server) = crate::reality::established_pair();
(server, client)
} else {
completed_ordinary_connections()
};
let mut stream = CryptoTlsStream::new(io, session, None);
let writer = Arc::new(WakeCount::default());
let reader = Arc::new(WakeCount::default());
let (write_waker, read_waker) = (writer.clone().into(), reader.clone().into());
let mut write_cx = Context::from_waker(&write_waker);
let mut read_cx = Context::from_waker(&read_waker);
saturate_tls_output(&mut stream, &mut write_cx);
assert!(
operation
.poll(&mut stream, &mut write_cx, &BACKPRESSURE_PAYLOAD)
.is_pending(),
"expected {operation:?} to block"
);
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
// Cancel the reader: only the transport readiness notification remains.
let readiness = state.lock().unwrap().waker.take().unwrap();
peer.writer().write_all(b"incoming").unwrap();
let mut tls_wire = Vec::new();
peer.write_tls(&mut tls_wire).unwrap();
let polls = {
let mut state = state.lock().unwrap();
state.input = mask_wire(config, peer_role, &tls_wire);
if output_ready {
state.allowance = usize::MAX;
}
(state.write_polls, state.flush_polls, state.output.len())
};
let mut bytes = [0; 8];
let mut buf = ReadBuf::new(&mut bytes);
assert!(matches!(
Pin::new(&mut stream).poll_read(&mut read_cx, &mut buf),
Poll::Ready(Ok(()))
));
assert_eq!(buf.filled(), b"incoming");
assert!(
Pin::new(&mut stream)
.poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0]))
.is_pending()
);
let readiness = {
let mut state = state.lock().unwrap();
assert_eq!(
(state.write_polls, state.flush_polls, state.output.len()),
polls
);
state.waker.take().unwrap()
};
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
assert!(readiness.will_wake(&write_waker));
// Reader cancellation cannot consume or replace transport write readiness.
readiness.wake();
assert!(reader.0.load(Ordering::Relaxed) > 0);
assert_eq!(writer.0.load(Ordering::Relaxed), 1);
assert_eq!(reader.0.load(Ordering::Relaxed), 0);
state.lock().unwrap().allowance = usize::MAX;
assert!(matches!(
Pin::new(&mut stream).poll_flush(&mut write_cx),
Poll::Ready(Ok(()))
));
}
assert!(
writer.0.load(Ordering::Relaxed) > 0,
"lost writer: reality={reality}, synchronous={synchronous_progress}, operation={operation:?}"
);
state.lock().unwrap().allowance = usize::MAX;
assert!(matches!(
Pin::new(&mut stream).poll_flush(&mut write_cx),
Poll::Ready(Ok(()))
));
}
}
}
}
fn mask_wire(
config: Arc<crate::sudoku_mask::Prepared>,
role: crate::sudoku_mask::Role,
input: &[u8],
) -> Vec<u8> {
let state = Arc::new(Mutex::new(WriteReadiness {
allowance: usize::MAX,
..Default::default()
}));
let mut stream = crate::sudoku_mask::SudokuStream::new(
Box::new(SingleWriteWaiter(state.clone())),
config,
role,
);
let mut cx = Context::from_waker(noop_waker_ref());
assert!(
matches!(Pin::new(&mut stream).poll_write(&mut cx, input), Poll::Ready(Ok(n)) if n == input.len())
);
assert!(matches!(
Pin::new(&mut stream).poll_flush(&mut cx),
Poll::Ready(Ok(()))
));
std::mem::take(&mut state.lock().unwrap().output)
}
#[test]
fn split_tls_tasks_keep_raw_and_ping_write_readiness() {
for raw_read in [false, true] {
@@ -337,29 +386,47 @@ fn split_tls_tasks_keep_raw_and_ping_write_readiness() {
#[test]
fn tls_receive_errors_wake_writers_without_outbound_progress() {
for session in [
completed_ordinary_connections().0,
completed_reality_connection(),
] {
let state = Arc::new(Mutex::new(WriteReadiness::default()));
let mut stream = CryptoTlsStream::new(SingleWriteWaiter(state.clone()), session, None);
let writer = Arc::new(WakeCount::default());
let waker = writer.clone().into();
let mut write_cx = Context::from_waker(&waker);
let mut read_cx = Context::from_waker(noop_waker_ref());
saturate_tls_output(&mut stream, &mut write_cx);
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
state.lock().unwrap().input = vec![23, 3, 3, 0, 1, 0];
assert!(
matches!(Pin::new(&mut stream).poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0])),
for masked in [false, true] {
for session in [
completed_ordinary_connections().0,
completed_reality_connection(),
] {
let state = Arc::new(Mutex::new(WriteReadiness::default()));
let config: crate::sudoku_mask::SudokuMaskConfig<()> =
serde_json::from_value(serde_json::json!({"protocol":null})).unwrap();
let config = config.prepare().unwrap();
let mut io: Box<dyn AsyncStream> = Box::new(SingleWriteWaiter(state.clone()));
if masked {
io = Box::new(crate::sudoku_mask::SudokuStream::new(
io,
config.clone(),
crate::sudoku_mask::Role::Client,
));
}
let mut stream = CryptoTlsStream::new(io, session, None);
let writer = Arc::new(WakeCount::default());
let waker = writer.clone().into();
let mut write_cx = Context::from_waker(&waker);
let mut read_cx = Context::from_waker(noop_waker_ref());
saturate_tls_output(&mut stream, &mut write_cx);
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
let invalid_record = vec![23, 3, 3, 0, 1, 0];
state.lock().unwrap().input = if masked {
mask_wire(config, crate::sudoku_mask::Role::Server, &invalid_record)
} else {
invalid_record
};
assert!(
matches!(Pin::new(&mut stream).poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0])),
Poll::Ready(Err(error)) if error.kind() == io::ErrorKind::InvalidData)
);
assert!(writer.0.load(Ordering::Relaxed) > 0);
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut write_cx, b"next"),
Poll::Ready(Err(_))
));
assert_eq!(state.lock().unwrap().allowance, 0);
);
assert!(writer.0.load(Ordering::Relaxed) > 0);
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut write_cx, b"next"),
Poll::Ready(Err(_))
));
assert_eq!(state.lock().unwrap().allowance, 0);
}
}
}
+212
View File
@@ -95,3 +95,215 @@ impl TcpServerHandler for ServerHandler {
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::pin::Pin;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::{
async_stream::{AsyncFlushMessage, test_streams::FlushGatedStream},
client_proxy_selector::ClientProxySelector,
h2mux::{MUX_DESTINATION_HOST, MUX_DESTINATION_PORT},
resolver::NativeResolver,
vless::vless_server_handler::VlessTcpServerHandler,
};
#[tokio::test]
async fn socks_negotiation_flushes_through_client_and_server_masks() {
use crate::{
address::NetLocation,
socks_handler::{SocksTcpClientHandler, SocksTcpServerHandler},
};
for auth in [None, Some(("user".to_owned(), "pass".to_owned()))] {
for layers in [1, 2] {
tokio::time::timeout(std::time::Duration::from_secs(5), async {
let target = NetLocation::from_str("127.0.0.1:443", None).unwrap();
let mut client: Box<dyn TcpClientHandler> =
Box::new(SocksTcpClientHandler::new(auth.clone()));
let mut server: Box<dyn TcpServerHandler> =
Box::new(SocksTcpServerHandler::new(
auth.clone(),
false,
"0.0.0.0".parse().unwrap(),
Arc::new(ClientProxySelector::new(vec![])),
Arc::new(NativeResolver::new()),
));
for layer in 0..layers {
let config = super::super::tests::config(
serde_json::json!({"password":format!("layer-{layer}")}),
);
client = Box::new(ClientHandler {
config: config.clone(),
inner: client,
});
server = Box::new(ServerHandler {
config,
inner: server,
});
}
let (client_io, server_io) = tokio::io::duplex(64);
let connect = async {
let mut setup = client
.setup_client_tcp_stream(
Box::new(FlushGatedStream::new(client_io)),
target.clone().into(),
)
.await
.unwrap();
let mut tail = Vec::new();
setup.client_stream.read_to_end(&mut tail).await.unwrap();
assert!(tail.is_empty());
};
let accept = async {
let TcpServerSetupResult::TcpForward {
remote_location,
mut stream,
connection_success_response,
need_initial_flush,
..
} = server
.setup_server_stream(Box::new(FlushGatedStream::new(server_io)))
.await
.unwrap()
else {
panic!("expected TCP forwarding")
};
assert_eq!(remote_location, target);
assert!(need_initial_flush);
stream
.write_all(&connection_success_response.unwrap())
.await
.unwrap();
if need_initial_flush {
stream.flush().await.unwrap();
}
};
tokio::join!(connect, accept);
})
.await
.unwrap_or_else(|_| {
panic!(
"SOCKS handshake stalled: auth={}, layers={layers}",
auth.is_some()
)
});
}
}
}
#[tokio::test]
async fn forwarding_and_session_handoffs_flush_through_stacked_masks() {
const USER_ID: &str = "b0e80a62-8a51-47f0-91f1-f0f7faf8d9d4";
for (command, session) in [(1, false), (1, true), (2, false), (3, false)] {
for layers in [1, 2] {
tokio::time::timeout(std::time::Duration::from_secs(5), async {
let mut handler: Box<dyn TcpServerHandler> = Box::new(
VlessTcpServerHandler::new(
USER_ID,
true,
Arc::new(ClientProxySelector::new(vec![])),
Arc::new(NativeResolver::new()),
None,
None,
false,
)
.unwrap(),
);
let config = super::super::tests::config(serde_json::json!({}));
let (io, peer) = tokio::io::duplex(1);
let mut peer: Box<dyn AsyncStream> = Box::new(FlushGatedStream::new(peer));
for _ in 0..layers {
handler = Box::new(ServerHandler {
config: config.clone(),
inner: handler,
});
peer = Box::new(SudokuStream::new(peer, config.clone(), Role::Client));
}
let server = async {
match handler
.setup_server_stream(Box::new(FlushGatedStream::new(io)))
.await
.unwrap()
{
TcpServerSetupResult::TcpForward {
mut stream,
need_initial_flush,
connection_success_response,
..
} => {
assert_eq!(command, 1);
assert!(!session);
assert!(need_initial_flush);
stream
.write_all(&connection_success_response.unwrap())
.await
.unwrap();
if need_initial_flush {
stream.flush().await.unwrap();
}
}
TcpServerSetupResult::BidirectionalUdp {
mut stream,
need_initial_flush,
..
} => {
assert_eq!(command, 2);
assert!(need_initial_flush);
if need_initial_flush {
futures::future::poll_fn(|cx| {
Pin::new(&mut stream).poll_flush_message(cx)
})
.await
.unwrap();
}
}
TcpServerSetupResult::SessionBasedUdp {
mut stream,
need_initial_flush,
..
} => {
assert_eq!(command, 3);
assert!(need_initial_flush);
if need_initial_flush {
futures::future::poll_fn(|cx| {
Pin::new(&mut stream).poll_flush_message(cx)
})
.await
.unwrap();
}
}
TcpServerSetupResult::Session(_) => assert!(session),
_ => panic!("unexpected setup result"),
}
};
let client = async {
let mut request = vec![0];
request.extend_from_slice(&crate::uuid_util::parse_uuid(USER_ID).unwrap());
request.extend_from_slice(&[0, command]);
if session {
request.extend_from_slice(&MUX_DESTINATION_PORT.to_be_bytes());
request.extend_from_slice(&[2, MUX_DESTINATION_HOST.len() as u8]);
request.extend_from_slice(MUX_DESTINATION_HOST.as_bytes());
} else if command != 3 {
request.extend_from_slice(&[1, 187, 1, 127, 0, 0, 1]);
}
peer.write_all(&request).await.unwrap();
peer.flush().await.unwrap();
// Drain framing bytes too, so the one-byte transport can finish flushing.
let mut response = Vec::new();
peer.read_to_end(&mut response).await.unwrap();
assert_eq!(response, [0, 0]);
};
tokio::join!(server, client);
})
.await
.unwrap_or_else(|_| {
panic!("handoff stalled: command={command}, session={session}, layers={layers}")
});
}
}
}
}
+321 -201
View File
@@ -5,7 +5,6 @@ use std::{
task::{Context, Poll, ready},
};
use futures::task::{ArcWake, AtomicWaker, waker_ref};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use crate::async_stream::{AsyncPing, AsyncStream};
@@ -18,19 +17,6 @@ use super::{
const WIRE_CAPACITY: usize = 32 * 1024;
const READ_BUDGET: usize = 64 * 1024;
#[derive(Default)]
struct StreamWake {
reader: AtomicWaker,
writer: AtomicWaker,
}
impl ArcWake for StreamWake {
fn wake_by_ref(this: &Arc<Self>) {
this.reader.wake();
this.writer.wake();
}
}
struct Failure {
kind: io::ErrorKind,
message: String,
@@ -48,9 +34,10 @@ impl Failure {
}
}
/// Writes may buffer output; callers must flush before waiting for a response.
/// Reads only drive input.
pub(crate) struct SudokuStream {
inner: Box<dyn AsyncStream>,
wake: Arc<StreamWake>,
encoder: Encoder,
decoder: Decoder,
read_wire: Box<[u8]>,
@@ -69,7 +56,6 @@ impl SudokuStream {
pub fn new(inner: Box<dyn AsyncStream>, config: Arc<Prepared>, role: Role) -> Self {
Self {
inner,
wake: Arc::default(),
encoder: Encoder::new(config.clone(), role),
decoder: Decoder::new(config, role),
read_wire: vec![0; WIRE_CAPACITY].into_boxed_slice(),
@@ -124,19 +110,6 @@ impl AsyncRead for SudokuStream {
if output.remaining() == 0 {
return Poll::Ready(Ok(()));
}
// Reads also drive writes; a single transport waker must notify both split tasks.
let wake = this.wake.clone();
wake.reader.register(cx.waker());
let waker = waker_ref(&wake);
let cx = &mut Context::from_waker(&waker);
// Handshakes can switch to reading after write_all without an explicit flush.
// Drive accepted wire bytes, but never block reading on a stalled writer.
let pending = this.write_start < this.write_end;
let start = this.write_start;
let drained = this.poll_drain(cx);
if pending && (drained.is_ready() || start != this.write_start) {
wake.writer.wake();
}
let initial = output.filled().len();
let mut work = 0;
loop {
@@ -191,10 +164,6 @@ impl AsyncWrite for SudokuStream {
input: &[u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
let wake = this.wake.clone();
wake.writer.register(cx.waker());
let waker = waker_ref(&wake);
let cx = &mut Context::from_waker(&waker);
if this.shutdown {
return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into()));
}
@@ -213,10 +182,6 @@ impl AsyncWrite for SudokuStream {
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let this = self.get_mut();
let wake = this.wake.clone();
wake.writer.register(cx.waker());
let waker = waker_ref(&wake);
let cx = &mut Context::from_waker(&waker);
ready!(this.poll_drain(cx))?;
match ready!(Pin::new(&mut this.inner).poll_flush(cx)) {
Ok(()) => Poll::Ready(Ok(())),
@@ -229,9 +194,6 @@ impl AsyncWrite for SudokuStream {
return Poll::Ready(Ok(()));
}
ready!(self.as_mut().poll_flush(cx))?;
let wake = self.wake.clone();
let waker = waker_ref(&wake);
let cx = &mut Context::from_waker(&waker);
ready!(Pin::new(&mut self.inner).poll_shutdown(cx))?;
self.shutdown = true;
Poll::Ready(Ok(()))
@@ -244,10 +206,6 @@ impl AsyncPing for SudokuStream {
}
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<bool>> {
let this = self.get_mut();
let wake = this.wake.clone();
wake.writer.register(cx.waker());
let waker = waker_ref(&wake);
let cx = &mut Context::from_waker(&waker);
ready!(this.poll_drain(cx))?;
Pin::new(&mut this.inner).poll_write_ping(cx)
}
@@ -303,7 +261,13 @@ mod tests {
struct State {
reads: VecDeque<Read>,
writes: VecDeque<Write>,
read_waker: Option<std::task::Waker>,
write_waker: Option<std::task::Waker>,
read_polls: usize,
write_polls: usize,
flush_blocked: bool,
shutdown_blocked: bool,
ping_blocked: bool,
sent: Vec<u8>,
events: Vec<&'static str>,
}
@@ -311,10 +275,11 @@ mod tests {
impl AsyncRead for Script {
fn poll_read(
self: Pin<&mut Self>,
_: &mut Context<'_>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let mut state = self.0.lock().unwrap();
state.read_polls += 1;
match state.reads.pop_front().unwrap_or(Read::Pending) {
Read::Bytes(bytes) => {
let n = bytes.len().min(buf.remaining());
@@ -324,7 +289,10 @@ mod tests {
}
Poll::Ready(Ok(()))
}
Read::Pending => Poll::Pending,
Read::Pending => {
state.read_waker = Some(cx.waker().clone());
Poll::Pending
}
Read::Error => Poll::Ready(Err(io::ErrorKind::ConnectionReset.into())),
Read::Eof => Poll::Ready(Ok(())),
}
@@ -337,6 +305,7 @@ mod tests {
buf: &[u8],
) -> Poll<io::Result<usize>> {
let mut state = self.0.lock().unwrap();
state.write_polls += 1;
match state.writes.pop_front().unwrap_or(Write::Count(usize::MAX)) {
Write::Count(n) => {
let n = n.min(buf.len());
@@ -350,12 +319,22 @@ mod tests {
Write::Error => Poll::Ready(Err(io::ErrorKind::ConnectionReset.into())),
}
}
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
self.0.lock().unwrap().events.push("flush");
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let mut state = self.0.lock().unwrap();
state.events.push("flush");
if state.flush_blocked {
state.write_waker = Some(cx.waker().clone());
return Poll::Pending;
}
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
self.0.lock().unwrap().events.push("shutdown");
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let mut state = self.0.lock().unwrap();
state.events.push("shutdown");
if state.shutdown_blocked {
state.write_waker = Some(cx.waker().clone());
return Poll::Pending;
}
Poll::Ready(Ok(()))
}
}
@@ -363,8 +342,13 @@ mod tests {
fn supports_ping(&self) -> bool {
true
}
fn poll_write_ping(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<bool>> {
self.0.lock().unwrap().events.push("ping");
fn poll_write_ping(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<bool>> {
let mut state = self.0.lock().unwrap();
state.events.push("ping");
if state.ping_blocked {
state.write_waker = Some(cx.waker().clone());
return Poll::Pending;
}
Poll::Ready(Ok(true))
}
}
@@ -389,145 +373,180 @@ mod tests {
out
}
#[test]
fn split_tasks_keep_write_readiness_after_read_drains_or_is_cancelled() {
#[derive(Clone, Copy, Debug)]
enum Operation {
Write,
Flush,
Shutdown,
Ping,
}
fn write_side(
stream: &mut SudokuStream,
cx: &mut Context<'_>,
operation: Operation,
) -> Poll<io::Result<()>> {
match operation {
Operation::Write => Pin::new(stream)
#[derive(Clone, Copy, Debug)]
enum Operation {
Write,
WriteVectored,
Flush,
Shutdown,
Ping,
}
impl Operation {
fn poll(self, stream: &mut SudokuStream, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self {
Self::Write => Pin::new(stream)
.poll_write(cx, b"next")
.map(|r| r.map(|_| ())),
Operation::Flush => Pin::new(stream).poll_flush(cx),
Operation::Shutdown => Pin::new(stream).poll_shutdown(cx),
Operation::Ping => Pin::new(stream).poll_write_ping(cx).map(|r| r.map(|_| ())),
}
}
for operation in [
Operation::Write,
Operation::Flush,
Operation::Shutdown,
Operation::Ping,
] {
for (drain_immediately, cancel_reader) in [(false, false), (false, true), (true, false)]
{
let (mut stream, state) = scripted(Role::Client);
state
.lock()
.unwrap()
.writes
.extend([Write::Pending, Write::Pending]);
if !drain_immediately {
state.lock().unwrap().writes.push_back(Write::Pending);
}
let writer = Arc::new(WakeCounter(AtomicUsize::new(0)));
let reader = Arc::new(WakeCounter(AtomicUsize::new(0)));
let (write_waker, read_waker) = (writer.clone().into(), reader.clone().into());
let mut write_cx = Context::from_waker(&write_waker);
let mut read_cx = Context::from_waker(&read_waker);
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut write_cx, b"one"),
Poll::Ready(Ok(3))
));
assert!(write_side(&mut stream, &mut write_cx, operation).is_pending());
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
assert!(
Pin::new(&mut stream)
.poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0]))
.is_pending()
);
if !drain_immediately {
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
let readiness = state.lock().unwrap().write_waker.take().unwrap();
readiness.wake();
assert!(reader.0.load(Ordering::Relaxed) > 0);
}
assert!(
writer.0.load(Ordering::Relaxed) > 0,
"writer lost readiness; operation {operation:?}"
);
if !cancel_reader && !drain_immediately {
assert!(
Pin::new(&mut stream)
.poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0]))
.is_pending()
);
}
assert!(matches!(
write_side(&mut stream, &mut write_cx, operation),
Poll::Ready(Ok(()))
));
let mut decoder =
Decoder::new(super::super::tests::config(json!({})), Role::Server);
let decoded: Vec<_> = state
.lock()
.unwrap()
.sent
.iter()
.filter_map(|b| decoder.decode_byte(*b).transpose())
.collect::<io::Result<_>>()
.unwrap();
assert_eq!(
decoded,
if matches!(operation, Operation::Write) {
b"onenext".as_slice()
} else {
b"one"
}
);
Self::WriteVectored => Pin::new(stream)
.poll_write_vectored(cx, &[io::IoSlice::new(b""), io::IoSlice::new(b"next")])
.map(|r| r.map(|_| ())),
Self::Flush => Pin::new(stream).poll_flush(cx),
Self::Shutdown => Pin::new(stream).poll_shutdown(cx),
Self::Ping => Pin::new(stream).poll_write_ping(cx).map(|r| r.map(|_| ())),
}
}
}
#[test]
fn read_drain_progress_and_errors_wake_a_blocked_writer() {
for fail in [false, true] {
let (mut stream, state) = scripted(Role::Client);
state
.lock()
.unwrap()
.writes
.extend([Write::Pending, Write::Pending]);
let writer = Arc::new(WakeCounter(AtomicUsize::new(0)));
let waker = writer.clone().into();
let mut write_cx = Context::from_waker(&waker);
let mut read_cx = Context::from_waker(std::task::Waker::noop());
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut write_cx, b"one"),
Poll::Ready(Ok(3))
));
assert!(Pin::new(&mut stream).poll_flush(&mut write_cx).is_pending());
if fail {
state.lock().unwrap().writes.push_back(Write::Error);
} else {
state
.lock()
.unwrap()
.writes
.extend([Write::Count(1), Write::Pending]);
fn reads_leave_buffered_output_and_writer_readiness_untouched() {
for operation in [
Operation::Write,
Operation::WriteVectored,
Operation::Flush,
Operation::Shutdown,
Operation::Ping,
] {
for (role, peer_role) in [(Role::Client, Role::Server), (Role::Server, Role::Client)] {
for output_ready in [false, true] {
let (mut stream, state) = scripted(role);
state.lock().unwrap().writes.extend([
Write::Count(1),
Write::Pending,
Write::Pending,
]);
let writer = Arc::new(WakeCounter(AtomicUsize::new(0)));
let reader = Arc::new(WakeCounter(AtomicUsize::new(0)));
let (write_waker, read_waker) = (writer.clone().into(), reader.clone().into());
let mut write_cx = Context::from_waker(&write_waker);
let mut read_cx = Context::from_waker(&read_waker);
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut write_cx, b"one"),
Poll::Ready(Ok(3))
));
assert!(operation.poll(&mut stream, &mut write_cx).is_pending());
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
let (sent, polls, events, read_polls) = {
let mut state = state.lock().unwrap();
if !output_ready {
state.writes.push_back(Write::Pending);
}
state
.reads
.push_back(Read::Bytes(wire(b"incoming", peer_role)));
(
state.sent.clone(),
state.write_polls,
state.events.clone(),
state.read_polls,
)
};
assert!(matches!(
Pin::new(&mut stream).poll_read(&mut read_cx, &mut ReadBuf::new(&mut [])),
Poll::Ready(Ok(()))
));
assert_eq!(state.lock().unwrap().read_polls, read_polls);
let mut bytes = [0; 8];
let mut buf = ReadBuf::new(&mut bytes);
assert!(matches!(
Pin::new(&mut stream).poll_read(&mut read_cx, &mut buf),
Poll::Ready(Ok(()))
));
assert_eq!(buf.filled(), b"incoming");
assert!(
Pin::new(&mut stream)
.poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0]))
.is_pending()
);
let readiness = {
let mut state = state.lock().unwrap();
assert_eq!(
(&state.sent, state.write_polls, &state.events),
(&sent, polls, &events)
);
state.writes.clear();
state.write_waker.take().unwrap()
};
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
assert!(readiness.will_wake(&write_waker));
// Abandon the reader; write readiness must reach only the writer.
readiness.wake();
assert_eq!(writer.0.load(Ordering::Relaxed), 1);
assert_eq!(reader.0.load(Ordering::Relaxed), 0);
assert!(matches!(
operation.poll(&mut stream, &mut write_cx),
Poll::Ready(Ok(()))
));
assert_eq!(writer.0.load(Ordering::Relaxed), 1);
let readiness = state.lock().unwrap().read_waker.take().unwrap();
assert!(readiness.will_wake(&read_waker));
readiness.wake();
assert_eq!(reader.0.load(Ordering::Relaxed), 1);
assert_eq!(writer.0.load(Ordering::Relaxed), 1);
let mut decoder =
Decoder::new(super::super::tests::config(json!({})), peer_role);
let decoded: Vec<_> = state
.lock()
.unwrap()
.sent
.iter()
.filter_map(|b| decoder.decode_byte(*b).transpose())
.collect::<io::Result<_>>()
.unwrap();
assert_eq!(
decoded,
if matches!(operation, Operation::Write | Operation::WriteVectored) {
b"onenext".as_slice()
} else {
b"one"
}
);
}
}
}
}
#[test]
fn reads_leave_transport_control_readiness_untouched() {
for operation in [Operation::Flush, Operation::Shutdown, Operation::Ping] {
let (mut stream, state) = scripted(Role::Client);
{
let mut state = state.lock().unwrap();
state.flush_blocked = matches!(operation, Operation::Flush);
state.shutdown_blocked = matches!(operation, Operation::Shutdown);
state.ping_blocked = matches!(operation, Operation::Ping);
}
let writer = Arc::new(WakeCounter(AtomicUsize::new(0)));
let reader = Arc::new(WakeCounter(AtomicUsize::new(0)));
let waker = writer.clone().into();
let read_waker = reader.clone().into();
let mut write_cx = Context::from_waker(&waker);
let mut read_cx = Context::from_waker(&read_waker);
assert!(operation.poll(&mut stream, &mut write_cx).is_pending());
let events = state.lock().unwrap().events.clone();
assert!(
Pin::new(&mut stream)
.poll_read(&mut read_cx, &mut ReadBuf::new(&mut [0]))
.is_pending()
);
assert!(writer.0.load(Ordering::Relaxed) > 0);
match Pin::new(&mut stream).poll_flush(&mut write_cx) {
Poll::Ready(Err(error)) if fail => {
assert_eq!(error.kind(), io::ErrorKind::ConnectionReset)
}
Poll::Ready(Ok(())) if !fail => {}
result => panic!("unexpected flush result: {result:?}"),
}
let readiness = {
let mut state = state.lock().unwrap();
assert_eq!(state.events, events);
assert_eq!(state.write_polls, 0);
state.flush_blocked = false;
state.shutdown_blocked = false;
state.ping_blocked = false;
state.write_waker.take().unwrap()
};
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
assert!(readiness.will_wake(&waker));
readiness.wake();
assert_eq!(writer.0.load(Ordering::Relaxed), 1);
assert_eq!(reader.0.load(Ordering::Relaxed), 0);
assert!(matches!(
operation.poll(&mut stream, &mut write_cx),
Poll::Ready(Ok(()))
));
}
}
@@ -618,6 +637,78 @@ mod tests {
}
}
#[test]
fn read_and_write_failures_remain_independent() {
for failure in [Write::Count(0), Write::Error] {
let (mut stream, state) = scripted(Role::Client);
state
.lock()
.unwrap()
.writes
.extend([Write::Pending, failure]);
let mut cx = Context::from_waker(std::task::Waker::noop());
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut cx, b"one"),
Poll::Ready(Ok(3))
));
// Neither a pending write error nor a recorded one may prevent input progress.
for failed in [false, true] {
state
.lock()
.unwrap()
.reads
.push_back(Read::Bytes(wire(b"ok", Role::Server)));
let mut bytes = [0; 2];
let mut buf = ReadBuf::new(&mut bytes);
assert!(matches!(
Pin::new(&mut stream).poll_read(&mut cx, &mut buf),
Poll::Ready(Ok(()))
));
assert_eq!(buf.filled(), b"ok");
assert_eq!(stream.write_error.is_some(), failed);
assert!(matches!(
Pin::new(&mut stream).poll_flush(&mut cx),
Poll::Ready(Err(_))
));
}
assert_eq!(state.lock().unwrap().write_polls, 2);
}
for input in [Read::Error, Read::Bytes(vec![0; 4]), Read::Eof] {
let eof = matches!(input, Read::Eof);
let (mut stream, state) = scripted(Role::Server);
state.lock().unwrap().writes.push_back(Write::Pending);
state.lock().unwrap().reads.push_back(input);
let mut cx = Context::from_waker(std::task::Waker::noop());
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut cx, b"one"),
Poll::Ready(Ok(3))
));
for _ in 0..2 {
let result = Pin::new(&mut stream).poll_read(&mut cx, &mut ReadBuf::new(&mut [0]));
assert!(if eof {
matches!(result, Poll::Ready(Ok(())))
} else {
matches!(result, Poll::Ready(Err(_)))
});
assert_eq!(state.lock().unwrap().write_polls, 1);
}
assert!(matches!(
Pin::new(&mut stream).poll_flush(&mut cx),
Poll::Ready(Ok(()))
));
let state = state.lock().unwrap();
let mut decoder = Decoder::new(super::super::tests::config(json!({})), Role::Client);
let decoded: Vec<_> = state
.sent
.iter()
.filter_map(|b| decoder.decode_byte(*b).transpose())
.collect::<io::Result<_>>()
.unwrap();
assert_eq!(decoded, b"one");
}
}
#[test]
fn decoded_prefix_precedes_errors_and_pending_with_prefilled_output() {
for tail in [Read::Pending, Read::Error, Read::Eof] {
@@ -660,6 +751,13 @@ mod tests {
#[test]
fn padding_only_yields_without_false_eof_and_truncated_tail_is_discarded() {
let (mut stream, state) = scripted(Role::Server);
state.lock().unwrap().writes.push_back(Write::Pending);
let writer = Arc::new(WakeCounter(AtomicUsize::new(0)));
let write_waker = writer.clone().into();
assert!(matches!(
Pin::new(&mut stream).poll_write(&mut Context::from_waker(&write_waker), b"pending"),
Poll::Ready(Ok(7))
));
state.lock().unwrap().reads.extend([
Read::Bytes(vec![0xff; READ_BUDGET + 1]),
Read::Bytes(vec![0; 3]),
@@ -675,12 +773,15 @@ mod tests {
.is_pending()
);
assert_eq!(wakes.0.load(Ordering::Relaxed), 1);
assert_eq!(writer.0.load(Ordering::Relaxed), 0);
assert_eq!(state.lock().unwrap().write_polls, 1);
let mut buf = ReadBuf::new(&mut bytes);
assert!(matches!(
Pin::new(&mut stream).poll_read(&mut cx, &mut buf),
Poll::Ready(Ok(()))
));
assert!(buf.filled().is_empty());
assert_eq!(state.lock().unwrap().write_polls, 1);
}
#[tokio::test]
@@ -724,30 +825,49 @@ mod tests {
}
#[tokio::test]
async fn read_drives_accepted_write_without_an_explicit_flush() {
let config = super::super::tests::config(json!({"ascii":"ascii"}));
let (a, b) = tokio::io::duplex(1);
let mut client = SudokuStream::new(Box::new(a), config.clone(), Role::Client);
let mut server = SudokuStream::new(Box::new(b), config, Role::Server);
tokio::time::timeout(std::time::Duration::from_secs(5), async {
tokio::join!(
async {
client.write_all(b"question").await.unwrap();
let mut reply = [0; 6];
client.read_exact(&mut reply).await.unwrap();
assert_eq!(&reply, b"answer");
},
async {
let mut request = [0; 8];
server.read_exact(&mut request).await.unwrap();
assert_eq!(&request, b"question");
server.write_all(b"answer").await.unwrap();
server.flush().await.unwrap();
async fn explicit_flush_before_read_drains_stacked_masks_and_buffered_transports() {
for layers in [1, 2] {
for buffered in [false, true] {
let (a, b) = tokio::io::duplex(1);
let (mut client, mut server): (Box<dyn AsyncStream>, Box<dyn AsyncStream>) =
if buffered {
use crate::async_stream::test_streams::FlushGatedStream;
(
Box::new(FlushGatedStream::new(a)),
Box::new(FlushGatedStream::new(b)),
)
} else {
(Box::new(a), Box::new(b))
};
for layer in 0..layers {
let config = super::super::tests::config(
json!({"ascii":"ascii", "password":format!("layer-{layer}")}),
);
client = Box::new(SudokuStream::new(client, config.clone(), Role::Client));
server = Box::new(SudokuStream::new(server, config, Role::Server));
}
);
})
.await
.unwrap();
tokio::time::timeout(std::time::Duration::from_secs(5), async {
tokio::join!(
async {
client.write_all(b"question").await.unwrap();
client.flush().await.unwrap();
let mut reply = [0; 6];
client.read_exact(&mut reply).await.unwrap();
assert_eq!(&reply, b"answer");
},
async {
let mut request = [0; 8];
server.read_exact(&mut request).await.unwrap();
assert_eq!(&request, b"question");
server.write_all(b"answer").await.unwrap();
server.flush().await.unwrap();
}
);
})
.await
.unwrap();
}
}
}
#[test]
+1
View File
@@ -41,6 +41,7 @@ pub enum TcpServerSetupResult {
proxy_selector: Arc<ClientProxySelector>,
},
/// Long-lived work, polled outside the setup deadline and cancelled with its owner.
/// The producer/session owns output flushing; `need_initial_flush` does not apply.
Session(Pin<Box<dyn Future<Output = ()> + Send>>),
}