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 Changelog trailer 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
Edited by Armin Pašalić

Merge request reports

Loading
Loading