Allocate ephemeral ports for acceptance tests
What does this MR do?
Fixes flaky HTTPS, mTLS, and SSL acceptance tests that intermittently failed with bind: address already in use on a fixed port (37000 and friends).
The tests bound to hardcoded ports. Under CI load, the next test could try to bind a port before the kernel released the previous process's socket. The old port manager waited a fixed 500ms and hoped, which lost the race intermittently and "passed on retry".
This MR replaces the hardcoded ports with per-start ephemeral allocation. RunPagesProcess now asks the OS for a free port for each listener on every start, so no two starts contend for one port. The racy fixed-wait cleanup and the port manager it relied on are removed.
Several helpers capture a listener value before the process starts (the rate limiter tables, the redirect helper, and the supportedListeners loops). For those, assignFreePorts updates the shared listener variables and currentListener re-resolves a captured spec to the port the process bound. The environment-variable config tests configure listeners outside withListeners, so they allocate their own port into a local copy rather than mutating the shared global, which would leak into later tests.
Why ephemeral allocation rather than a release-wait
A release-wait cannot fix this. SO_REUSEADDR is on by default for Go TCP listeners, so a port in TIME_WAIT is not the blocker. When the bind actually fails, a live listener still holds the port, and there is nothing to release from a process that has exited. Fresh ports remove the contention instead of waiting on it.
The test pre-allocates the port (bind :0, read the assigned port, close, hand the number to the binary), which leaves a small window before the binary re-binds. That window is intentional and accepted: top-level acceptance tests run sequentially in a single network namespace, so the collision probability is effectively zero. A comment in allocateFreePort documents this.
Scope
This started as the port fix and the harness changes above. Review then pushed it further: it removed diagnostic-only tests that had no value once the design changed (a rapid-restart test that could not reproduce the race with ephemeral ports, and unit tests that only checked net.Listen), consolidated the port helpers into helpers_test.go where the listener machinery already lives, and trimmed the comments to what the code cannot say.
Verification
- Full acceptance suite: pass, including under
-race(no data races) - Three shuffle seeds: pass, confirming order independence
- Config and env-var tests in isolation: pass, confirming no cross-test global leak
TODO
- Feature flag
- Added feature flag
- This feature does not require a feature flag
- I added the
Changelogtrailer to the commits that need it (test-only change, no changelog entry per repo convention for test fixes) - I added unit tests or they are not required
- I added acceptance tests or they are not required
- I added documentation (or it's not required)
- I followed code review guidelines
- I followed Go Style guidelines