Bound the live view connect stages separately - #415
Closed
robertjamesprior wants to merge 7 commits into
Closed
robertjamesprior wants to merge 7 commits into
robertjamesprior wants to merge 7 commits into
Conversation
onMessage is assigned directly to ws.onmessage, so a throw from createPeer or setRemoteOffer became a discarded rejection: no error, no overlay, no parent-frame message. The only thing left running was the 15s connect watchdog, which closed the socket and reconnected into the identical failure indefinitely. Route a throw to onDisconnected, cap consecutive attempts that never reach a connected peer, and post the terminal reason to the parent frame so embedders can react. An unsupported browser is now terminal too rather than retried. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bound check incremented before comparing, so giving up after three attempts reported four, contradicting its own message. Count only attempts that opened a socket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
robertjamesprior
force-pushed
the
hypeship/bound-live-view-connect-retries
branch
from
September 25, 2026 21:21
ed29f17 to
3397bfa
Compare
Contributor
Author
|
Collapsed into #417. The retry this was stacked to bound is gone — retries are removed entirely, so the stage bounds and the per-stage reason are now the whole change rather than a layer on top of one. #417 carries both and targets main directly. Branch |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #403 — retarget to
mainonce that lands.Summary
A connect walks transport (socket open) → signaling (
signal/provide+ offer) → media(ICE starts). One 15s watchdog covered all three, so a socket that never opened burned
the full budget and reported only
"connection timeout", and a stuck media path wasindistinguishable from a stuck socket.
Each stage now arms its own bound and reports its own reason:
transportconnect(), before the socket openssignalingws.onopenmediasetRemoteOffer, once a peer existsmediais terminal because a peer that exists but never reaches ICEcheckingis thedeterministic class — a retry repeats it. That also caps the wait on that path, and
transport/signaling now fail on their own bounds instead of one shared clock.
KERNEL_CONNECTION_TIMEOUT'sreasonis now"<stage> timeout"rather than"connection timeout", andKERNEL_CONNECTION_FAILEDcarries the same stage. Bothevents are recent enough that this reads as filling them in rather than changing them,
but it is a payload change.
Calibration
The bounds come from 130 connects against real headful sessions —
us-east,eu-west,ap-southeast, plus relay-only (iceTransportPolicy: 'relay') in two of them. A localChromium loaded the live view with the
WebSocketandRTCPeerConnectionconstructorsinstrumented before any page script ran; the client itself is unchanged.
mediais local, not network-bound. ICE reachescheckingas soon as the localdescription is set, because the remote candidates arrive in the offer. Direct media is
1–16ms even in
ap-southeast, where transport is 700ms. Relay-only is a flat ~90ms(the TURN allocation), independent of the last mile. 2s is ~20x the worst case.
transportstays at 15s, deliberately off the measurement. It is last-mile RTT(35ms intra-region → 238ms
eu-west→ 700msap-southeast), but it is also the stagethe live-view proxy's own wake path lands in, and that path can spend ~12s waking a
browser before the socket opens. A bound set from the measured p99 would fire on every
slow wake and never let the wake finish. This is the one stage where a stale bound is
cheaper than a tight one.
signalingis one round trip on an open socket. p99 under 200ms, including coldsessions (the first connect after session creation is the slow one — ~135ms offer vs
~20ms median).
All 130 connects succeeded; this measures the healthy path and does not reproduce the
field failure.
Consequence worth stating: only
mediagets faster. The transport stage keeps thebudget it had, so a transport-class failure still spends up to ~47s across three attempts
(3 × 15s + 2 × 1s) before the terminal event — the same total as before, now with a
reason attached. What changes is that the stuck-media class, which is the failure we can
actually see in the field, ends in ~2.5s instead of 15.4s.
Validation on a real image
Hotpatch A/B in one session on the deployed image
(the currently deployed headful image, serving
app.416afd11.js) against a localbuild of this branch (
app.47e216f9.js). The live view was embedded in an iframe on alocal parent page, so the parent-frame messages are observable — the client only posts
them when embedded. Faults were injected before any page script ran:
throw—RTCPeerConnectionconstruction throws. The failing boundary from the field.media-stall— candidate lines stripped from the remote offer andaddIceCandidateneutralised, so ICE never leaves
new.fb437e9)throwKERNEL_CONNECTION_TIMEOUT15373ms, reasonconnection timeoutKERNEL_CONNECTION_FAILED432ms, reasoninjected: RTCPeerConnection blockedmedia-stallKERNEL_CONNECTION_TIMEOUT15373ms, reasonconnection timeoutKERNEL_CONNECTION_TIMEOUT+KERNEL_CONNECTION_FAILED2528ms, reasonmedia timeout, attempts 1waited 15.4s and said only
connection timeout.retry loop on the deterministic class.
media-stall,KERNEL_PLAYINGstill fired (baseline 15376ms, patched 2532ms). The<video>elementemits
playingeven when the peer never connected, so it is not on its own a"media is flowing" signal. In the field failure signature (
throw) it never fired.packaging, and the stall is injected at the
RTCPeerConnectionboundary rather than bya real network fault.
Not in this PR
or satcom numbers. That matters most for
signaling, which is the bound derivedtightest from measurement.
signal/providere-arming the media stage on a renegotiation is unhandled; a secondprovide replaces the peer and restarts the bound, which is the desired behavior, but
there is no test for it.
Testing
35 pass, 0 fail in
images/chromium-headful/client(bun test tests), including 4 newcases covering stage entry, the terminal media timeout, and ICE
checkingclearing themedia bound.
vue-cli-service lintclean on changed files.CI
Test Live View clientandTest for the server/ directory(incl. both docker builds,server unit, server e2e) were dispatched against
9a7a14cand both passed — this repo'sworkflows only trigger on PRs targeting
main, so a stacked PR gets no PR checks of itsown. Rerun them on retarget.