Skip to content

Make the Headscale e2e reliable under CI load - #83

Merged
danReynolds merged 1 commit into
mainfrom
danreynolds/e2e-reliability
Jul 22, 2026
Merged

Make the Headscale e2e reliable under CI load#83
danReynolds merged 1 commit into
mainfrom
danreynolds/e2e-reliability

Conversation

@danReynolds

Copy link
Copy Markdown
Owner

The e2e suite flaked intermittently on CI — 30s "Future not completed" timeouts, a different test each run, never reproducible locally, and it even failed on a changelog-only PR. Two root causes, both fixed at the source rather than by blanket timeout inflation:

1. Data-plane tests raced path establishment (the structural fix)

The two-node group never established the WireGuard/DERP tunnel before its timed tests, so the first data-plane op — or a peer→self inbound request — raced establishment (endpoint discovery + DERP negotiation) inside its 30s window. awaitDataPath() now brings the path up once in setUpAll with a retrying 90s-budget probe against the peer's /hello; a successful outbound round-trip warms the tunnel bidirectionally, so inbound tests are fast too. Tests now start from a guaranteed-warm precondition.

2. Control-plane transition budgets were under-sized

recordUntil defaulted to 30s; a credential-reconnect against a containerized Headscale on a loaded runner can exceed it. Here the timeout is the wait mechanism, so it was right-sized: recordUntil 30s→60s, the broadcast reconnect wait 10s→60s, a local onNodeChanges first-emit 2s→10s.

Verification

  • Green locally, 36/36.
  • Green again under full CPU saturation (10 yes hogs on 10 cores, load avg ~39) — deliberately simulating the contended-runner condition that triggered the flakes.
  • Test-only change: no library code, no assertions weakened.

Honest caveat: local can't perfectly reproduce a network-latency flake, so the ultimate proof is CI staying green across subsequent runs — but the structural warm-up removes the race entirely, and the stress run demonstrates the margins hold under heavy scheduling contention.

🤖 Generated with Claude Code

Two timing-sensitive test classes flaked intermittently on contended CI
runners (30s "Future not completed" timeouts, a different test each run,
never reproducible locally). Root causes and principled fixes:

1. Data-plane tests raced WireGuard/DERP path establishment. The two-node
   group never established the tunnel before its timed tests, so the first
   data-plane op -- or a peer->self inbound request -- raced establishment
   (endpoint discovery + DERP negotiation) inside its own 30s window. Added
   awaitDataPath(): the group's setUpAll now brings the path up once, up
   front, with a retrying 90s-budget probe against the peer's /hello. A
   successful outbound round-trip warms the tunnel both directions, so inbound
   tests are fast too. This is a structural readiness guarantee, not a bigger
   timeout.

2. Control-plane transition budgets were too tight. recordUntil defaulted to
   30s; a credential-reconnect (up without authkey) against a containerized
   Headscale on a loaded runner can exceed that. The timeout IS the wait
   mechanism here, so it was simply under-budgeted: recordUntil 30s -> 60s,
   the broadcast reconnect wait 10s -> 60s, and a local onNodeChanges first-
   emit 2s -> 10s for headroom.

Verified: e2e green locally (36/36), and green again under full CPU
saturation (10 hogs on 10 cores, load avg ~39) to simulate a contended
runner -- the condition that triggered the flakes. Test-only; no library or
assertion changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danReynolds
danReynolds merged commit f2ca5ab into main Jul 22, 2026
3 checks passed
@danReynolds
danReynolds deleted the danreynolds/e2e-reliability branch July 22, 2026 22:15
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.

1 participant