From 30001ace306b24ff03c827603bdacb095f9d7e63 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Fri, 24 Jul 2026 21:08:16 -0700 Subject: [PATCH] testutils: make GetFreePort return distinct ports per process GetFreePort finds a free port by binding to :0 and then releasing it before returning the number. Two calls in quick succession could therefore hand back the same port, because the OS is free to re-offer the just-released one. That is the TestEtcdComponentIntegration flake: clientPort and peerPort both came back as 36517, etcd tried to bind its client and peer URLs to one port, failed with "address already in use", and the test timed out waiting for readiness. Track every port handed out in this process and skip a repeat, rebinding to rotate to a fresh ephemeral port. This fixes all call sites at once (the etcd test takes two or three ports) rather than patching each one, and it keeps the "free for both TCP and UDP" guarantee intact. Part of MIR-720 Authored-by: Claude Code (autonomous agent) --- pkg/testutils/port.go | 36 ++++++++++++++++++++++++++++++++---- pkg/testutils/port_test.go | 22 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 pkg/testutils/port_test.go diff --git a/pkg/testutils/port.go b/pkg/testutils/port.go index faf7325d4..29bbc56e8 100644 --- a/pkg/testutils/port.go +++ b/pkg/testutils/port.go @@ -2,10 +2,22 @@ package testutils import ( "net" + "sync" "testing" ) -// GetFreePort returns a port that is free for both TCP and UDP. +// handedOutPorts remembers every port GetFreePort has returned in this process, +// so a later call never returns the same number. GetFreePort releases the port +// before returning it (see below), so without this two calls in quick +// succession can hand back the same port — the cause of the etcd +// client/peer-port collision flake (MIR-720). +var ( + handedOutMu sync.Mutex + handedOutPorts = map[int]struct{}{} +) + +// GetFreePort returns a port that is free for both TCP and UDP, and distinct +// from any port already handed out by GetFreePort in this process. // This is important for QUIC-based servers (like the RPC server) which bind both protocols. // The function binds both protocols to verify availability, then releases them. // Fails the test if a suitable port cannot be obtained after several attempts. @@ -13,7 +25,7 @@ func GetFreePort(t testing.TB) int { t.Helper() // Try a few times to find a port free for both TCP and UDP - for range 10 { + for range 20 { // First, get an available TCP port tcpListener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -29,12 +41,28 @@ func GetFreePort(t testing.TB) int { continue } - // Both succeeded - close and return the port + // Reject a port we've already handed out. Because the listeners are + // released before returning, the OS can re-offer a just-freed port to + // the next caller; rebinding on the next iteration rotates to a fresh + // ephemeral port. + handedOutMu.Lock() + _, seen := handedOutPorts[port] + if !seen { + handedOutPorts[port] = struct{}{} + } + handedOutMu.Unlock() + if seen { + tcpListener.Close() + udpConn.Close() + continue + } + + // Both succeeded and the port is new - close and return the port tcpListener.Close() udpConn.Close() return port } - t.Fatalf("failed to find a port free for both TCP and UDP after 10 attempts") + t.Fatalf("failed to find a port free for both TCP and UDP after 20 attempts") return 0 } diff --git a/pkg/testutils/port_test.go b/pkg/testutils/port_test.go new file mode 100644 index 000000000..caafbc178 --- /dev/null +++ b/pkg/testutils/port_test.go @@ -0,0 +1,22 @@ +package testutils + +import "testing" + +// TestGetFreePortReturnsDistinctPorts pins the guarantee that resolves MIR-720: +// GetFreePort releases each port before returning it, so successive calls used +// to be able to hand back the same number (etcd then failed to bind its client +// and peer URLs to one port). Every port must now be unique within the process. +func TestGetFreePortReturnsDistinctPorts(t *testing.T) { + const n = 200 + seen := make(map[int]bool, n) + for i := 0; i < n; i++ { + port := GetFreePort(t) + if port <= 0 { + t.Fatalf("call %d: GetFreePort returned non-positive port %d", i, port) + } + if seen[port] { + t.Fatalf("call %d: GetFreePort returned duplicate port %d", i, port) + } + seen[port] = true + } +}