fix(relay): remove the bind race in the relay test fixture - #591
fix(relay): remove the bind race in the relay test fixture#591iamquang95 wants to merge 1 commit into
Conversation
|
@claude review this PR |
|
Claude finished @iamquang95's task in 7m 9s —— View job Review —
|
emlautarom1
left a comment
There was a problem hiding this comment.
As part of the refinement phase, we should try to get a proper solution in place, and retrying here seems like a hack. The original issue describes the problem as something that happens on test-code only code, in particular in CI.
Ideally, we should have something like wiremock, where we can start configure the relay server using a builder, then start it and let it live until the end of the scope (the test). The with_relay_server abstraction is introducing more complications that benefits at this point.
|
For example, tests should ideally look something like: #[tokio::test]
async fn serve_addr_enr_ext_ip() {
let relay = test_relay_server_with(|args| args.p2p.external_ip = Some("222.222.222.222".into())).await.unwrap();
let addr = relay.http_addr.unwrap();
let response = http_get(&format!("http://{addr}/enr")).await.unwrap();
let enr = Record::try_from(response.text().await.unwrap().as_str()).unwrap();
assert_eq!(enr.ip(), Some(Ipv4Addr::new(222, 222, 222, 222)));
}
#[tokio::test]
async fn run_bootnode_auto_p2p() {
let missing_key = test_relay_server_with(|args| args.relay.auto_p2p_key = false).await;
assert!(matches!(
missing_key,
Err(super::CliError::RelayP2PError(
pluto_relay_server::RelayP2PError::FailedToLoadPrivateKey(..)
))
));
let _relay = test_relay_server_with(|args| { }).await; // starts with an auto-generated key
}
#[tokio::test]
async fn taken_monitoring_port_fails_the_relay() {
let taken = net::TcpListener::bind(ANY_ADDR).await.unwrap();
let addr = taken.local_addr().unwrap().to_string();
let err = test_relay_server(|args| args.debug_monitoring.monitor_addr = Some(addr))
.await
.expect_err("relay must not start while its monitoring port is taken");
assert!(matches!(
err,
super::CliError::RelayP2PError(
pluto_relay_server::RelayP2PError::FailedToBindMonitoringListener { .. }
)
));
}The tricky part is how we can retrieve the actually bounded addresses. I think this requires some refactoring to the internals so we can split binding from serving: I explored the work a bit in |
Fix #590
Test fixture (
crates/cli/src/commands/relay.rs) —with_relay_serveris now a bounded attempt loop. Each attempt allocates a fresh data dir and fresh HTTP ports, spawns the relay, andselect!s readiness (/enr, plus/metricswhen monitoring is configured) against the relay task's own exit. A lost bind race retries the whole attempt with new ports, up to 5 times, and only when the error chain carriesio::ErrorKind::AddrInUse— walked viaError::source(), no string matching.Any other startup error is returned immediately with its original type. The test body runs only once the relay serves; afterwards the fixture cancels, joins with a timeout, and returns the relay's exit status instead of dropping it.
retry_getis gone — requests are single-shot, since retries were what hid the bind failure.p2p listeners now bind
127.0.0.1:0, eliminating that race rather than retrying it: libp2p buries theAddrInUseinsideio::Error::other(Transport(..)), out of reach ofError::source, so it could never be detected. The HTTP addresses can't do the same — they're inbound config the relay never reports back, as in Charon.Relay server —
FailedToBindHttpListener { addr, source: io::Error }keeps theio::ErrorKind(was aString);monitoring_serveris split intobind_monitoring_server/serve_monitoring_serverand both failures go toserver_errorsinstead of awarn!, so a monitoring bind failure is fatal (Charon parity); the monitoring address is parsed and bound before the ENR server is spawned, so a failed startup no longer leaks a listener; a server error breaks the select loop instead of returning early, so cancel + joins still run;enr_serverbinds before building state/router.