Skip to content

test(e2e): give test nodes 10 s for DHT requests and an OS-assigned port - #248

Merged
Nic-dorman merged 6 commits into
mainfrom
nic/v2-1852-ant-client-e2e-macos-is-flaky-4-fail-then-pass-reruns-in-the
Nov 5, 2026
Merged

Nic-dorman merged 6 commits into
mainfrom
nic/v2-1852-ant-client-e2e-macos-is-flaky-4-fail-then-pass-reruns-in-the

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Follow-up (c9b13f4): Hermes's non-blocking nit. A comment in e2e_cost_estimate.rs still said 10 nodes. Comment only; optional re-review ac94cd2..c9b13f4.

Round 2 (ac94cd2): test nodes bind port 0, which replaces the node-create retry from round 1. See flake 2 below. Review range dec642c..ac94cd2.

Fixes the two E2E flakes behind V2-1852:

  1. "need 7": all 4 fail-then-pass macOS E2E runs in the ticket.
  2. "Failed to create transport" when a test node is created. It hit this PR's own CI twice, and twice before (10-23 and 10-29).

The ticket's fifth run, Merkle ubuntu with "Got 14 merkle candidates, need 16", is a different failure. It's now tracked in V2-1893.

Flake 1: "need 7"

All 4 fail-then-pass macOS E2E runs in the last 50 were one test, e2e_cost_estimate::test_estimate_all_stored_partial_sample_is_incomplete:

  • Runs: 40015142708, 40349637751, 40683863980, 40844173225.
  • Failure: each failed its upload before payment, with InsufficientPeers … witnessed close group initial lookup found 6 peers, need 7.

Cause: the test harness gives every DHT request 5 s. saorsa-core uses a node's connection_timeout for two things: the connection timeout, and the request timeout of every DHT operation (network.rs: request_timeout: config.connection_timeout). MiniTestnet set it to 5 s, against 25 s in production. On a loaded macOS runner, a slow but live peer that answers after 5 s isn't counted, so the lookup comes up one short.

The 5 s came in with the saorsa-client port (5d25103), with no comment. ant-node's harness has the same trap at 2 s, set to keep teardown short (857398c).

Flake 2: "Failed to create transport"

spawn_node panicked with create P2P node: Transport(SetupFailed("Failed to create dual-stack network nodes: Failed to create transport")) before any test logic ran:

Run Date Job Test
39942512923 10-23 Merkle E2E macOS test_external_merkle_partial_payment_is_partial_upload
40914982712 10-29 E2E macOS test_attack_zero_amount_payment
41883601747 11-04 (this PR) E2E ubuntu test_file_download_bytes_written, the first test in its binary
41897795306 11-04 (this PR) E2E macOS test_idempotent_chunk_storage, all 4 retries from round 1

Cause: a UDP port collision with the UPnP gateway search.

  • Each test node bound 127.0.0.1 on a port the harness picked: a random base in 20000-60000, plus the node's index. That range overlaps the OS ephemeral range (Linux 32768-60999, macOS 49152-65535).
  • Every node and client starts a UPnP gateway search when its transport comes up (saorsa-transport upnp.rs, enabled by default, in local mode too).
  • The search binds 0.0.0.0:0 (igd-next's default SearchOptions) and runs for up to 2 s (DEFAULT_DISCOVERY_TIMEOUT). There is no gateway on a CI runner, so it uses the full 2 s.
  • A 127.0.0.1:P bind while another socket holds 0.0.0.0:P fails with "address in use". So when a search socket happened to get the port of a node not yet created, that node failed.
  • saorsa-core formats the error with {} (transport_handle.rs:545), which drops the underlying io::Error. That's why the log only says "Failed to create transport".
  • Round 1's retry used the same port 4 times over about 1.5 s, inside one search window, so it failed all 4 times on 41897795306.

Reproduced on dev2:

  • Base port pinned (local patch only), 0.0.0.0:<base+3> held by a plain UDP socket.
  • Result: create P2P node after 4 attempts: Transport error: Setup failed: Failed to create dual-stack network nodes: Failed to create transport, the same string as run 41897795306, in 4.8 s.
  • Sampling the test process's sockets with ss showed each node holding only its own 127.0.0.1 socket, and one short-lived 0.0.0.0:<ephemeral> socket appearing with each node spawn.

Fix: nodes bind port 0, so the OS picks a free port. The OS never assigns a port that is in use, whatever holds it: a search socket, another process, or an IPv6 dual-stack socket. Later nodes bootstrap from the addresses the bootstrap nodes actually bound (listen_addrs()). The retry is removed: it only covered this race, and any other failure should show on the first attempt. Nothing else read the fixed ports; no test restarts a node on the same address.

Changes

  1. TEST_CORE_CONNECTION_TIMEOUT_SECS = 10, with a comment explaining the double use. ant-node moves to the same name and value in its own PR (V2-1869), and each harness says to keep the two in step.
  2. teardown() shuts nodes down concurrently, as ant-node's does, so the DHT leave waits overlap instead of adding up.
  3. e2e_cost_estimate.rs uses DEFAULT_NODE_COUNT (14) instead of a hard-coded 10. It was the only e2e file left on 10, and the merkle test in it keeps 20. This helps a little; change 1 is the actual fix.
  4. spawn_node binds port 0 (round 2), and start_inner builds the bootstrap list from the nodes' listen_addrs(). This removes TEST_PORT_RANGE_MIN/MAX, the random base port and the 4-attempt node-create retry.

Linear issue

Closes V2-1852

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Compatibility

  • Wire: none
  • Storage: none
  • API: none. Test harness only (ant-core/tests/).

Semver impact

  • breaking
  • feature
  • fix

Test evidence

All runs below were on dev2 (Linux, i5-8500, 4 cores), with debug builds and -D warnings as in CI.

Which timeout holds. I ran the flaky test with --exact at 14 nodes, with its CPU capped by systemd-run -p CPUQuota:

CPU cap 5 s (main) 10 s 15 s 20 s 25 s
1.5 cores 3/3 fail (1 at the upload, 2 at the re-estimate after it) 3/3 pass 3/3 3/3 3/3
1 core 3/3 fail (upload) 3/3 pass 2/2 2/2 2/2

Run time doesn't depend on the value: about 210 s at 1.5 cores and about 272 s at 1 core. 10 s is the lowest value tried that held.

What it costs. Full targets at full CPU:

Target main (5 s, serial teardown) 10 s, serial this PR (10 s, concurrent)
e2e_chunk (13 tests) 499 s, of which teardown 72 s 504 s / 78 s 437 s / 13 s
e2e_pointer (17 tests, silenced-peer cases) 1149 s / 97 s not run 1069 s / 15 s

All runs passed. The suites get faster overall, and e2e_pointer's silenced-peer tests are no slower at 10 s.

Round 2 (port 0), on dev2 at ac94cd2:

  • cargo clippy -p ant-core --all-targets --all-features -- -D warnings: clean. cargo fmt --check: clean.
  • CI's E2E command (--test-threads=1, all 9 binaries): 69 passed, 0 failed: adr0004 3, chunk 13, cost_estimate 7, data 7, file 10, manifest 7, native_payment 4, payment 8, security 10. No "create P2P node" in the log.
  • e2e_merkle (35 nodes): 8 passed, 0 failed (2023 s).

CI on round 2 (ac94cd2, run 42012971431): everything green except the inherited Security Audit (RUSTSEC-2026-0387, V2-1743). E2E 69/69 on ubuntu and macOS, Merkle E2E 8/8 on both. No "create P2P node" or panic in any E2E or Merkle log.

CI on round 1 (dec642c): macOS E2E passed e2e_cost_estimate 7/7, with no "need 7"; flake 1 held. The macOS E2E job still failed, on flake 2 (e2e_payment), which round 2 fixes.

New dependency

none. futures is already a dependency of ant-core.

ADR

n/a

Mitigation / rollback

Revert. If a test that waits out a dead peer turns out too slow, lower TEST_CORE_CONNECTION_TIMEOUT_SECS. Don't go to 5 s, which is known to fail.

🤖 Generated with Claude Code

Nic-dorman and others added 2 commits November 4, 2026 14:57
e2e_cost_estimate.rs was the only e2e file still starting
MiniTestnet::start(10). Every other one uses DEFAULT_NODE_COUNT
(CLOSE_GROUP_SIZE * 2 = 14), which 41e5bda introduced on 04-21 because
a smaller testnet leaves too few peers ready on slow macOS runners.
This file landed a day later (#44) with the hard-coded 10.

All 4 E2E macOS fail-then-pass runs in the last 50 (40015142708,
40349637751, 40683863980, 40844173225) are one test in this file,
test_estimate_all_stored_partial_sample_is_incomplete. Each one failed
its upload with "witnessed close group initial lookup found 6 peers,
need 7".

On dev2, with the test's CPU capped to imitate a slow runner:
- 2 cores: 10 and 14 nodes both pass 3/3.
- 1.5 cores: at 10 nodes the upload fails 3/3 ("5-6 of 7"). At 14
  nodes it gets past the upload 2/3; those 2 runs then fail the
  re-estimate (a non-zero cost for the stored file).
- 1 core: both fail the upload 3/3.

So the larger testnet moves the cliff the CI failure sits on, but
doesn't remove it under heavy starvation. The merkle-estimate test
keeps its 20 nodes (merkle pools need 16).

Closes V2-1852

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oncurrently

MiniTestnet set each node's saorsa-core connection_timeout to 5 s.
saorsa-core also uses that value as the request timeout for every DHT
operation (network.rs: request_timeout: config.connection_timeout),
so a witnessed close-group lookup gave each peer 5 s to answer, against
25 s in production. On loaded macOS runners slow but live peers missed
it, and uploads failed with "found 6 peers, need 7" (V2-1852). The 5 s
came in with the saorsa-client port (5d25103), with no comment.

The value is now TEST_CORE_CONNECTION_TIMEOUT_SECS = 10, the same name
and value as ant-node's e2e harness (V2-1869), and each says to keep
the two in step.

dev2, test_estimate_all_stored_partial_sample_is_incomplete, 14 nodes,
CPU capped:
- 5 s: failed 3/3 at 1 core; at 1.5 cores, 1/3 failed the upload and
  2/3 failed the re-estimate after it.
- 10, 15, 20 and 25 s: passed every run at 1.5 cores (3/3 each) and at
  1 core (10 s 3/3, the rest 2/2). Run time didn't change with the value.

teardown() now shuts the nodes down concurrently, as ant-node's does
(857398c), so their DHT leave waits overlap instead of adding up.
dev2, full CPU:
- e2e_chunk, 13 tests: 499 s with 72 s of teardown on main (5 s,
  serial); 504 s / 78 s at 10 s serial; 437 s / 13 s at 10 s concurrent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman Nic-dorman changed the title test(e2e): run the cost-estimate tests on the default 14-node testnet test(e2e): give test nodes 10 s for DHT requests and tear them down concurrently Nov 4, 2026
Nic-dorman and others added 2 commits November 4, 2026 19:31
E2E ubuntu on this PR failed in harness setup, not in a test:
test_file_download_bytes_written panicked at "create P2P node:
Transport(SetupFailed("Failed to create dual-stack network nodes:
Failed to create transport"))". It isn't new: E2E macOS hit the same
panic on 2026-10-29 (run 40914982712, test_attack_zero_amount_payment,
another branch, before the timeout and teardown changes here).

ant-node's e2e harness already handles this race. A freshly chosen
loopback port can be momentarily held by another socket, so it retries
P2PNode::new up to 4 times on the same port, backing off 250 ms per
attempt (eb049e3). spawn_node now does the same, building a fresh
config per attempt because P2PNode::new consumes it. A real transport
misconfiguration still fails, after the last attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ky-4-fail-then-pass-reruns-in-the

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node creation now and then failed with "Failed to create transport"
(V2-1852). Each node bound 127.0.0.1 on a random port in 20000-60000,
which overlaps the OS ephemeral range. Every node and client starts a
UPnP gateway search that holds a 0.0.0.0:<ephemeral> UDP socket for up
to 2 s, and a 127.0.0.1 bind to a port a wildcard socket holds fails
with "address in use". The four same-port retries all fell inside one
search window, so they didn't help.

Nodes now bind port 0, and later nodes bootstrap from the address the
bootstrap nodes actually bound (listen_addrs()). The OS never assigns a
port that is in use. The retry is removed: it only covered this race,
and any other failure should show on the first attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman Nic-dorman changed the title test(e2e): give test nodes 10 s for DHT requests and tear them down concurrently test(e2e): give test nodes 10 s for DHT requests and an OS-assigned port Nov 5, 2026

@MaidSafe-Hermes MaidSafe-Hermes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Focused round-2 re-review of dec642c957cfa8858e38fc57ff027fd43c4e0927..ac94cd277fd6e38b927d5d7e4d33c0a4da9d3191 (current head ac94cd277fd6e38b927d5d7e4d33c0a4da9d3191). No code findings.

The port-0 change addresses the fixed-port collision. I verified against the pinned saorsa-core source (c3c4c81) that the transport records the actual bound socket address, then exposes the actual listener addresses. With local mode and IPv4-only enabled, those are loopback addresses, so later nodes receive the OS-assigned, usable bootstrap port. Removing retries is reasonable for the diagnosed collision: the kernel now chooses an available port at bind time.

Review coverage: 2/2 seats — sol-1 (gpt-6.1-sol, xhigh) returned UNCERTAIN because its evidence bundle omitted the transport implementation; I resolved that uncertainty by inspecting the exact pinned source. glm-1 (z-ai/glm-5.3) returned APPROVE. The seats did not independently agree; the final disposition follows the source verification.

The required linear-link and pr-template checks pass. E2E/Merkle and other CI jobs are still pending; Security Audit is red for the inherited RUSTSEC-2026-0387 / V2-1743 finding, which is not a required check. This approval is a code-review verdict, not a merge-readiness claim.

@MaidSafe-Hermes MaidSafe-Hermes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — no blocking code findings on ac94cd2.

I checked the port-0 bootstrap path against the exact saorsa-core revision pinned by Cargo.lock (c3c4c81e63db7e3048f488f7cf0fec4cff3131d4): P2PNode::listen_addrs() returns runtime transport listener addresses, the transport records the bound socket addresses, and local mode uses loopback. The later nodes therefore bootstrap from the actual OS-assigned ports. DEFAULT_NODE_COUNT is 2 × CLOSE_GROUP_SIZE (14); CI exercises the changed test harness and reports E2E and Merkle E2E passing on Ubuntu and macOS.

Review coverage: 2/2 seats reviewed — sol-1 (gpt-6.1-sol, xhigh) returned UNCERTAIN; glm-1 (z-ai/glm-5.3) returned APPROVE. The seats did not agree. I resolved the material uncertainty in sol-1’s report by checking the pinned source, constant definition and CI evidence; this verdict is not presented as seat agreement.

Non-blocking nit: e2e_cost_estimate.rs still says “Use 10 nodes” although DEFAULT_NODE_COUNT is 14. The Security Audit check is failing for the inherited RUSTSEC-2026-0387 / V2-1743 issue noted in the PR; this code approval is not a merge-readiness claim.

The test has used DEFAULT_NODE_COUNT since this PR's first round, but
the comment still said 10 nodes (Hermes, non-blocking).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman merged commit 5172960 into main Nov 5, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants