Skip to content

[repo] e2e harness: piper-edge in front of two relays; ladder for the slot-1 boot race - #549

Merged
ozykhan merged 3 commits into
mainfrom
ozykhan/e2e-edge-two-relays
Sep 5, 2026
Merged

[repo] e2e harness: piper-edge in front of two relays; ladder for the slot-1 boot race#549
ozykhan merged 3 commits into
mainfrom
ozykhan/e2e-edge-two-relays

Conversation

@ozykhan

@ozykhan ozykhan commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #537
Closes #548

Harness (#537)

Every relay e2e spawned one piper-relay on :7000 and pointed piperd straight at it — a topology nobody runs since #530. Now:

  • startCluster in test/e2e/main_test.go starts piper-edge on the public ports (:8443/:8880/:7000) and two piper-relay processes on private ports (1xxxx/2xxxx) in zones zone-a/zone-b, sharing one relaytest database, with PIPER_RELAY_PROXY_PROTOCOL=1 and PIPER_RELAY_ADVERTISE_HOST=127.0.0.1 as a real deployment behind an edge needs. piperd dials the edge and never learns a relay's address.
  • The six relay tests share it and lose their env blocks; the four binaries are built once per test process instead of once per test (TestRelayLoopback 50 s → 1.4 s).
  • piper login still dials one relay's API port directly: the edge routes the API by TLS SNI only, which a CLI can't reach over plain HTTP against a self-signed cert. State is in shared Postgres so either relay answers the same.
  • New TestRelayCluster: two owners in two zones → SIGTERM the relay holding slot 0 → visitor still served through the survivor → restart it (fresh instance id) → back to two owners.

Bug it found (#548)

TestRelayCluster failed on its first assertion, and the failure was real: slot 1 dials the instant slot 0's handshake returns, but the relay acks (tunnel.Serve) before it writes the owner row (acceptTunnelsSetOwner), so the edge places slot 1 on the relay that already holds slot 0 and it sleeps duplicateBackoff — a full minute with no second session. Confirmed in this Mac's production log against Hetzner: race lost with a 16 ms margin on the 20:48 UTC boot, won by 98 ms on the 19:27 one. Details and evidence in #548.

Fix: the duplicate wait is a ladder — duplicateFirstRetry 2 s, ×4, capped at the existing 1 min. Slot 0 keeps its constant 5 s. A lost race now costs 2 s; a genuine one-relay pool still settles to the cap. Unit tests for the ladder and the lost-race recovery; the e2e log shows it end to end:

tunnel: slot 0: connected to relay 127.0.0.1:7000 as alice.localhost
tunnel: slot 1: every relay the edge can offer already holds alice.localhost (edge placed it before the owner row landed, …); retrying in 2s, then up to every 1m0s
tunnel: slot 1: connected to relay 127.0.0.1:7000 as alice.localhost      ← +2.0 s
piper-relay: terminated — draining (deadline 20s)
piper-relay: drained; 0 session(s) cut at the deadline — leaving the pool
tunnel: slot 0: connected to relay 127.0.0.1:7000 as alice.localhost      ← on the restarted relay, +1.3 s

Not touched: the relay-side ordering (ack after SetOwner) that would close the race at the source — noted in #548 as a possible follow-up.

Verification

  • make verify green (gofmt, vet, go test ./..., arm64 cross).
  • make e2e: 11/11 pass in 117 s.

🤖 Generated with Claude Code

@ozykhan ozykhan self-assigned this Sep 5, 2026
ozykhan and others added 3 commits September 6, 2026 00:41
Slot 1 dials the instant slot 0's handshake returns, but the relay acks
before it writes the owner row the edge places on, so the edge can send
slot 1 to the relay that already holds slot 0. Seen in production (16 ms
margin) and on every loopback run. The slot then slept duplicateBackoff:
a minute with no second session, which is a minute a relay roll drops
the box.

The duplicate wait is now a ladder: duplicateFirstRetry (2 s), then x4
up to duplicateBackoff (1 min). Slot 0 keeps its constant 5 s retry; a
genuine one-relay pool still settles to the cap.

Closes #548
Part of #537

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every relay e2e spawned a single piper-relay on :7000 and pointed piperd
straight at it, a topology nobody runs since #530. startCluster in
main_test.go now starts piper-edge on the public ports and two
piper-relay processes on private ports in two zones, sharing one
relaytest database; piperd dials the edge. The four binaries are built
once per test process instead of once per test.

TestRelayCluster asserts what the single-relay harness could not: the
box's two sessions land on two relays in two zones, a visitor is still
served while one relay is drained, and the stranded slot re-places when
the relay returns. It is the test that found #548.

Part of #537

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ladder

Part of #537

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ozykhan
ozykhan force-pushed the ozykhan/e2e-edge-two-relays branch from 770de92 to 802e5ff Compare September 5, 2026 21:45
@ozykhan
ozykhan merged commit aad3d80 into main Sep 5, 2026
3 checks passed
@ozykhan
ozykhan deleted the ozykhan/e2e-edge-two-relays branch September 5, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant