consumer: the BIND reply#2 re-entry guard + session hygiene (review-002 C-1, D-1, N-6, S-2)
- C-1: BindSession::await_inbound is take-guarded — the first read takes the session's stream; the returned BindDataPlane owns it (the TakenInner pairing posture; the session stays the teardown owner), so a second call returns the family's spent shape Ok(None) and can never re-read the live data plane. A failed read restores the inner (the phase is only consumed on success; the producer ends the session on every post-reply#1 failure exit, so a re-call EOFs — never a live data-plane re-read either way). Pinned by the double-await_inbound integration test (the spent shape + the data-plane bytes asserted untouched) and the spent-BIND unit test. - D-1: one normative reply#2 reader (option a) — the session's await_inbound; BindReply1::await_inbound is cut and reply1() is documented as a view (bind_addr + raw_mut), matching its actual usage; the reply1()-twice re-entry path is closed (pinned by the view test). - N-6: the three session constructors' prologues collapsed into one private open_and_negotiate helper (teardown-at-birth in one place); zero behavior change. - S-2: the EOF-propagation doc line on all three reap sites (the dropped teardown_channel handle is deliberate — the graceful shape; clear_all aborts instead). - BindDataPlane is un-lifetimed (owns the stream); into_inner returns the owned Socks5Stream<BiStream>; docs synced (module + session docs, consumer.md §The BIND client). Verification: cargo test 352 green (all-features too), clippy -D warnings clean, fmt clean, cargo doc clean, both wasm checks clean.
This commit is contained in:
1 parent
6d14d35a54
commit
53739b20cb
4 files changed
+459
-139
No files matched your search
+152
-3
@@ -173,7 +173,7 @@ async fn bind_data_plane_accessors_into_inner_and_socket_mut() {
|
||||
.write_all(b"raw")
|
||||
.await
|
||||
.expect("write");
|
||||
let inner = plane.into_inner();
|
||||
let mut inner = plane.into_inner();
|
||||
let mut buf = [0u8; 6];
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
@@ -183,7 +183,7 @@ async fn bind_data_plane_accessors_into_inner_and_socket_mut() {
|
||||
.expect("echo timed out")
|
||||
.expect("read the echo");
|
||||
assert_eq!(&buf, b"preraw", "both write paths rode the same pipe");
|
||||
let _unused = inner;
|
||||
drop(inner);
|
||||
|
||||
assert!(session.close().await, "the entry existed");
|
||||
no_leaked_channels(&topo).await;
|
||||
@@ -484,6 +484,155 @@ async fn bind_drop_teardown_reaps_and_connection_stays_healthy() {
|
||||
fn bind_session_type_is_send() {
|
||||
fn assert_send<T: Send>() {}
|
||||
assert_send::<BindSession>();
|
||||
assert_send::<BindDataPlane>();
|
||||
assert_send::<BindReply1<'static>>();
|
||||
assert_send::<BindDataPlane<'static>>();
|
||||
}
|
||||
|
||||
/// The C-1 re-entry guard (review 002): a second `await_inbound` on
|
||||
/// the same session cannot re-read the data plane — it returns the
|
||||
/// spent shape (`Ok(None)`), and the data-plane bytes survive
|
||||
/// untouched (the first plane's echo round-trip is intact after the
|
||||
/// second call; the pinning test for the silent-corruption path).
|
||||
#[tokio::test]
|
||||
async fn double_await_inbound_returns_the_spent_shape_and_bytes_survive() {
|
||||
let topo = wire_consumer_side_with(
|
||||
harness::echo_dial(),
|
||||
None,
|
||||
Some(harness::echo_accept()),
|
||||
Some(bind_scoped()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut session = BindSession::bind(
|
||||
topo.consumer.as_ref(),
|
||||
("reentry.test", ECHO_BIND_PORT).to_target_addr().unwrap(),
|
||||
)
|
||||
.await
|
||||
.expect("the session binds");
|
||||
|
||||
// The first read: the plane exists (and consumes the phase —
|
||||
// the plane owns the stream; the session stays the teardown
|
||||
// owner).
|
||||
let mut plane = session
|
||||
.await_inbound()
|
||||
.await
|
||||
.expect("reply#2 read")
|
||||
.expect("the plane exists before reply#2");
|
||||
plane.write_all(b"ping").await.expect("write");
|
||||
plane.flush().await.expect("flush");
|
||||
let mut buf = [0u8; 4];
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
plane.read_exact(&mut buf),
|
||||
)
|
||||
.await
|
||||
.expect("echo timed out")
|
||||
.expect("read the echo");
|
||||
assert_eq!(&buf, b"ping", "the first plane's round-trip");
|
||||
drop(plane);
|
||||
|
||||
// The second call: the spent shape — `Ok(None)`, never a
|
||||
// re-read of the live data plane.
|
||||
let second = session
|
||||
.await_inbound()
|
||||
.await
|
||||
.expect("the spent shape is an Ok, not an error");
|
||||
assert!(second.is_none(), "the phase was consumed");
|
||||
|
||||
// The data-plane bytes survive: the first plane's echo
|
||||
// round-trip was complete and no re-read corrupted the pipe —
|
||||
// the channel is closed with the reap (the session owner's
|
||||
// `close`), and a fresh bind on the same registration
|
||||
// round-trips with its own bytes intact.
|
||||
assert!(session.close().await, "the entry existed");
|
||||
no_leaked_channels(&topo).await;
|
||||
|
||||
let mut fresh = BindSession::bind(
|
||||
topo.consumer.as_ref(),
|
||||
("after.test", ECHO_BIND_PORT).to_target_addr().unwrap(),
|
||||
)
|
||||
.await
|
||||
.expect("fresh bind after the spent call");
|
||||
let mut plane = fresh
|
||||
.await_inbound()
|
||||
.await
|
||||
.expect("reply#2")
|
||||
.expect("the plane");
|
||||
plane.write_all(b"again").await.expect("write");
|
||||
plane.flush().await.expect("flush");
|
||||
let mut buf = [0u8; 5];
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
plane.read_exact(&mut buf),
|
||||
)
|
||||
.await
|
||||
.expect("echo timed out")
|
||||
.expect("read the fresh echo");
|
||||
assert_eq!(&buf, b"again", "the fresh session's bytes are intact");
|
||||
drop(plane);
|
||||
fresh.close().await;
|
||||
no_leaked_channels(&topo).await;
|
||||
}
|
||||
|
||||
/// The `reply1()`-twice re-entry path (C-1's other door, closed by
|
||||
/// the D-1 one-reader decision): `reply1()` is a view — fresh handles
|
||||
/// per call, no read phase — so repeated `reply1()` calls carry the
|
||||
/// same advertised address without consuming anything, and the one
|
||||
/// normative reader (`await_inbound`) still yields exactly one plane.
|
||||
#[tokio::test]
|
||||
async fn reply1_twice_is_a_view_and_the_reader_yields_one_plane() {
|
||||
let topo = wire_consumer_side_with(
|
||||
harness::echo_dial(),
|
||||
None,
|
||||
Some(harness::echo_accept()),
|
||||
Some(bind_scoped()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut session = BindSession::bind(
|
||||
topo.consumer.as_ref(),
|
||||
("view.test", ECHO_BIND_PORT).to_target_addr().unwrap(),
|
||||
)
|
||||
.await
|
||||
.expect("the session binds");
|
||||
|
||||
let expected = TargetAddr::Ip(std::net::SocketAddr::from(([127, 0, 0, 1], ECHO_BIND_PORT)));
|
||||
|
||||
// Repeated views: every handle carries the same advertised
|
||||
// address; nothing is consumed.
|
||||
let BindReply1 { bind_addr, .. } = session.reply1().expect("reply#1's handle");
|
||||
assert_eq!(bind_addr, expected);
|
||||
let BindReply1 { bind_addr, .. } = session.reply1().expect("reply#1's handle again");
|
||||
assert_eq!(bind_addr, expected, "a view, not a reader");
|
||||
|
||||
// The one normative reader: exactly one plane.
|
||||
let mut plane = session
|
||||
.await_inbound()
|
||||
.await
|
||||
.expect("reply#2 read")
|
||||
.expect("the plane");
|
||||
plane.write_all(b"once").await.expect("write");
|
||||
plane.flush().await.expect("flush");
|
||||
let mut buf = [0u8; 4];
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10),
|
||||
plane.read_exact(&mut buf),
|
||||
)
|
||||
.await
|
||||
.expect("echo timed out")
|
||||
.expect("read the echo");
|
||||
assert_eq!(&buf, b"once");
|
||||
drop(plane);
|
||||
|
||||
// The re-entry is closed on this door too.
|
||||
assert!(
|
||||
session
|
||||
.await_inbound()
|
||||
.await
|
||||
.expect("spent shape")
|
||||
.is_none(),
|
||||
"no second plane after the reader ran"
|
||||
);
|
||||
session.close().await;
|
||||
no_leaked_channels(&topo).await;
|
||||
}
|
||||
Reference in new issue
Block a user