Skip to content

relay: keep the reservation, not only acquire it - #3

Open
mkotelnikov wants to merge 1 commit into
mainfrom
feat/relay-supervisor
Open

relay: keep the reservation, not only acquire it#3
mkotelnikov wants to merge 1 commit into
mainfrom
feat/relay-supervisor

Conversation

@mkotelnikov

Copy link
Copy Markdown
Contributor

relay: keep the reservation, not only acquire it

A browser peer's (or hub's) WebSocket to the relay drops — tab suspended, laptop asleep, network switch, relay redeploy, proxy reload — and the reservation never comes back, so the peer stays unreachable until the page is reloaded. For a hub this takes down every member of its mesh.

Root cause (measured, not assumed)

libp2p re-dials a lost relay once, as the connection closes. A probe against a real relay (libp2p 3.3.8, circuit-relay-v2 4.2.11), restarted on the same port and key after a 2 s outage:

  • Without help: no reservation reappeared within 45 s.
  • One explicit dialRelay: the reservation was back within about 1 s. So re-dialling is enough, and the node never needs rebuilding.

The existing keepalive in join.ts watches the hub connection. That can stay up over WebRTC after the relay link is gone, so the loss went unnoticed. The p2p-demo prototype had watched the relay; the watcher was pointed at the hub when it was ported to httpeers-stack.

Changes (apps/httpeers-stack)

  • superviseRelay({ node, relayAddr }) in src/reservation.ts:
    • Detects the loss from the relay's connection:close, from self:peer:update, and from a 10 s backstop check. Only a reservation on this relay counts.
    • Re-dials with jittered exponential backoff (1 s → 30 s) and never gives up.
    • poke() retries at once; stop() ends everything.
  • watchPageWake(onWake) in src/browser/page-wake.ts: online, visibilitychange (to visible only), pageshow and resume call poke(). A suspended tab's timers are throttled, so the backoff alone could wait minutes after the page is usable again.
  • Wiring:
    • startHub runs the supervisor.
    • startBrowserHub and startBrowserPeer run it together with watchPageWake.
    • All three unwind it on a failed start and stop it first in stop(), so a disconnected page stays disconnected.
  • join.ts: the comment claiming the reservation is "entirely libp2p-managed" now says what is libp2p's job and what is ours.

Tests

Each was watched failing before its implementation.

  • tests/e2e/relay-supervisor.test.ts, with a real relay that really restarts:
    • the reservation comes back after a 3 s outage;
    • stop() ends re-dialling;
    • poke() retries despite a 60 s backoff;
    • startHub gets its reservation back.
  • tests/reservation.test.ts: a backoff table with hand-computed values. Removing the doubling, the cap or the jitter each fails it.
  • tests/browser-page-wake.test.ts: the four events, hidden versus visible, and unsubscribe.
  • Mutation checks: turning stop() into a no-op, or dropping a removeEventListener, fails the corresponding test.

Gate

  • tsc --noEmit is clean, and Biome is clean on the changed files.
  • After the rebase onto a614eb4: the new suites plus hub and hub-startup pass, 26/26.
  • The full suite before the rebase: 340/348. The 8 failures are all in tests/e2e/browser.test.ts (2 in Chromium, 6 in Firefox) and are pre-existing: a clean origin/main worktree fails the same 8 with identical messages.
  • typecheck:tests also fails on origin/main, in browser-session.test.ts: 8431536 added connectionKind to BrowserPeerHandle without updating the test's fake.

Not in this PR

  • Not exercised in a real browser against a real outage.
  • A relay-lost / reconnecting session phase; the page still shows live while restoring.
  • Caddy's stream_close_delay (deployment, statewalker/httpeers).
  • Our own STUN server is tracked in Deploy a STUN server at stun.httpeers.net httpeers#1.

This is step 1 of the agreed plan: supervisors, then a prototype where each hub relays signalling for its own members, with a TURN fallback so no application traffic crosses the relay or a hub.

Do not auto-merge. Ready for review.

🤖 Generated with Claude Code

libp2p re-dials a lost relay once and then never again: after the relay
came back on the same port and key, no reservation reappeared within 45 s,
while one explicit dialRelay restored it within a second (probe against
libp2p 3.3.8 / circuit-relay-v2 4.2.11). The join keepalive watched the
hub connection, which can stay up over WebRTC after the relay link is
gone, so a relay restart left peers -- and hubs -- unreachable until
reloaded.

- superviseRelay (reservation.ts): detects the loss (relay
  connection:close, self:peer:update, a 10 s backstop), re-dials with
  jittered exponential backoff (1 s -> 30 s) and never gives up; poke()
  retries at once, stop() ends it.
- watchPageWake (browser/page-wake.ts): online, visibilitychange to
  visible, pageshow and resume poke the supervisor, because a suspended
  tab's timers are throttled.
- Wired into startHub, startBrowserHub and startBrowserPeer; unwound on a
  failed start and stopped first in stop().
- join.ts: the comment no longer claims the reservation is entirely
  libp2p-managed.

Tests: a real relay that really restarts (tests/e2e/relay-supervisor),
the backoff table, and the page-wake events.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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