Skip to content

Commit 04d037d

Browse files
authored
Merge pull request #41 from sshaplygin/fix/test-port-race
test(client): stop nowhere() handing out a freed port
2 parents 4db0005 + 7c0a136 commit 04d037d

1 file changed

Lines changed: 34 additions & 5 deletions

File tree

crates/ytsaurus-client/tests/request_shape.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//! two, and [`Proxy`] rather than [`capture`].
1313
1414
use std::io::{BufRead, BufReader, Read, Write};
15-
use std::net::TcpListener;
15+
use std::net::{TcpListener, TcpStream};
1616

1717
use ytsaurus_client::{
1818
Client, ClientError, DataFormat, Method, OperationFilter, OperationParameters, RetryPolicy,
@@ -1022,11 +1022,40 @@ fn refusal() -> Vec<u8> {
10221022
}
10231023

10241024
/// An address nothing is listening on, for a proxy that has gone away.
1025+
///
1026+
/// It has one job — a connection to it must be *refused*, at once — and one
1027+
/// hazard behind that job: the port must be one no other stub in the suite can
1028+
/// end up bound to, or "nowhere" becomes somewhere.
1029+
///
1030+
/// The obvious way is to bind `127.0.0.1:0`, read the port the OS chose, and
1031+
/// drop the listener so nothing is left answering. That refuses connections —
1032+
/// until the OS hands the freed port to another test's `TcpListener::bind`. The
1033+
/// range is cycled through before a port is reused, so within one binary it
1034+
/// almost never comes round; across a suite that binds thousands of sockets it
1035+
/// does, and then a request meant to go nowhere reaches a live listener (or the
1036+
/// stub that took the port receives a stray request). It is the kind of flake
1037+
/// that passes on a re-run and cannot be reproduced on demand.
1038+
///
1039+
/// A **privileged** port sidesteps the hazard entirely: `bind("127.0.0.1:0")`
1040+
/// draws from the ephemeral range only — 32768+ on Linux, 49152+ on macOS — so
1041+
/// nothing in this suite can ever be assigned a port below 1024, and no test
1042+
/// process binds one deliberately. Nothing is listening there, so a connection
1043+
/// is refused immediately on both platforms. The refusal is *verified* rather
1044+
/// than assumed: the first candidate that actually refuses is the one returned,
1045+
/// so a machine that happens to run something on one of these ports is skipped
1046+
/// rather than silently turning "nowhere" into a live host.
10251047
fn nowhere() -> String {
1026-
let listener = TcpListener::bind("127.0.0.1:0").expect("binds");
1027-
let address = listener.local_addr().expect("has an address");
1028-
drop(listener);
1029-
address.to_string()
1048+
for port in 1u16..=16 {
1049+
let address = format!("127.0.0.1:{port}");
1050+
let socket = address.parse().expect("a valid loopback address");
1051+
match TcpStream::connect_timeout(&socket, std::time::Duration::from_millis(200)) {
1052+
Err(error) if error.kind() == std::io::ErrorKind::ConnectionRefused => return address,
1053+
// A listener answered, or the connect timed out: not nowhere. Try
1054+
// the next reserved port rather than hand back a live one.
1055+
_ => continue,
1056+
}
1057+
}
1058+
panic!("no reserved loopback port refused a connection; cannot address nowhere");
10301059
}
10311060

10321061
/// A client that discovers, though it is talking to a listener on loopback.

0 commit comments

Comments
 (0)