Check drain closure without assuming cancellation task order

Idle socket EOF and the draining notice do not acknowledge listener drop.
A TCP handshake may complete before the independently aborted listener is
polled; require that connection to close, and check refusal after exit.
Keep active-handler/background completion and listener liveness checks.
Reuse the existing socket deadline, with no connect retries.

Card: SAU-392
This commit is contained in:
SAU-392 implementer
2026-10-30 23:13:43 +00:00
parent c04baee67c
commit fffa5dd072
+16 -1
View File
@@ -267,7 +267,21 @@ fn protocol() {
assert_eq!(provider.read()["params"]["name"], "draining");
line.clear();
assert_eq!(idle.read_line(&mut line).unwrap(), 0);
assert!(std::net::TcpStream::connect(&address).is_err());
// drain() wakes independent accept/read cancellations. Idle EOF does not
// order listener destruction: TCP may still complete a handshake, but that
// connection must close without serving work. Do not retry the connect.
if let Ok(mut late) = std::net::TcpStream::connect(&address) {
eprintln!("TCP handshake raced listener cancellation; waiting for close");
// Reuse the four-second socket bound used by idle/active above.
late.set_read_timeout(Some(std::time::Duration::from_secs(4)))
.unwrap();
let closed = std::io::Read::read(&mut late, &mut [0]);
assert!(
matches!(closed, Ok(0))
|| matches!(&closed, Err(e) if e.kind() == std::io::ErrorKind::ConnectionReset),
"late connection survived drain: {closed:?}"
);
}
assert!(provider.child.try_wait().unwrap().is_none());
provider.answer(&inflight, json!({}));
line.clear();
@@ -279,6 +293,7 @@ fn protocol() {
assert!(provider.child.try_wait().unwrap().is_none());
provider.answer(&background, json!({}));
provider.finishes();
assert!(std::net::TcpStream::connect(&address).is_err());
assert!(
!dir.join("stopped").exists(),
"drain is not the EOF stop hook"