From 74e0845234a676c455ddaca44a891876aff21f21 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Wed, 11 Nov 2026 00:48:46 +0800 Subject: [PATCH 1/6] perf(client-runtime): a saved environment whose direct route stalls connects over T3 Connect 6.9x sooner (median 5.29 s -> 764 ms, injected stall) Co-Authored-By: Claude Opus 5.5 --- docs/internals/remote.md | 4 +- .../client-runtime/src/connection/driver.ts | 50 +++++- .../src/connection/supervisor.test.ts | 143 ++++++++++++++++++ 3 files changed, 192 insertions(+), 5 deletions(-) diff --git a/docs/internals/remote.md b/docs/internals/remote.md index 0c328c50317c5..779c46dc8ff36 100644 --- a/docs/internals/remote.md +++ b/docs/internals/remote.md @@ -37,7 +37,9 @@ network receives no credential. That check is not proof of a working route: when every route stays silent, each is still tried. A route that fails to connect, including a blocked one such as a signed-out T3 Connect, moves on to the next; only an incompatible server stops the walk, because it is the same -server on every route. While connected over a later route the +server on every route. A direct route that answered the check but is still +connecting when the check's head start ends races T3 Connect instead of holding +it back, and the first to connect wins; the other closes. While connected over a later route the [supervisor](../../packages/client-runtime/src/connection/supervisor.ts) preflights the earlier ones and replaces the session when one would connect. Preflight includes authorization so a route that answers but rejects this diff --git a/packages/client-runtime/src/connection/driver.ts b/packages/client-runtime/src/connection/driver.ts index 45f12d0b74509..88e76901c50c0 100644 --- a/packages/client-runtime/src/connection/driver.ts +++ b/packages/client-runtime/src/connection/driver.ts @@ -4,6 +4,7 @@ import * as Option from "effect/Option"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import type * as Result from "effect/Result"; import * as Scope from "effect/Scope"; import * as FetchHttpClient from "effect/http/FetchHttpClient"; import * as HttpClient from "effect/http/HttpClient"; @@ -84,8 +85,10 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") let transient: ConnectionAttemptError | undefined; let blocked: ConnectionAttemptError | undefined; // Each route gets its own scope so a half-open session closes before the next try. - const attempt = Effect.fnUntraced(function* (route: ConnectionRoute) { - const routeScope = yield* Scope.fork(attemptScope); + const attempt = Effect.fnUntraced(function* ( + route: ConnectionRoute, + routeScope: Scope.Closeable, + ) { const result = yield* connectRoute(route).pipe( Scope.provide(routeScope), Effect.onExit((exit) => (Exit.isSuccess(exit) ? Effect.void : Scope.close(routeScope, exit))), @@ -97,8 +100,43 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") } return result; }); + const relayIndex = routes.findIndex((route) => route.target._tag === "RelayConnectionTarget"); + let relayTried = false; + // A direct route still connecting when the head start ends races relay. The + // first to connect wins; closing the other scope also ends a late session. + const attemptWithRelay = Effect.fnUntraced(function* ( + route: ConnectionRoute, + relay: ConnectionRoute, + ) { + const directScope = yield* Scope.fork(attemptScope); + const direct = yield* Effect.forkChild(attempt(route, directScope)); + const early = yield* Effect.raceFirst( + Fiber.join(direct).pipe(Effect.asSome), + Fiber.join(headStart).pipe(Effect.as(Option.none())), + ); + if (Option.isSome(early)) return early.value; + relayTried = true; + const relayScope = yield* Scope.fork(attemptScope); + const settle = + (scope: Scope.Closeable) => + (result: Result.Result) => + result._tag === "Success" || result.failure.reason === "unsupported" + ? Effect.succeed({ result, scope }) + : Effect.fail(result); + const raced = yield* Effect.race( + Fiber.join(direct).pipe(Effect.flatMap(settle(directScope))), + attempt(relay, relayScope).pipe(Effect.flatMap(settle(relayScope))), + ).pipe(Effect.result); + yield* Fiber.interrupt(direct); + const winner = raced._tag === "Success" ? raced.success.scope : undefined; + for (const scope of [directScope, relayScope]) { + if (scope !== winner) yield* Scope.close(scope, Exit.void); + } + return raced._tag === "Success" ? raced.success.result : raced.failure; + }); const silent: Array<{ readonly route: ConnectionRoute; readonly index: number }> = []; for (const [index, route] of routes.entries()) { + if (index === relayIndex && relayTried) continue; const check = checks.length === 0 ? "unchecked" @@ -110,7 +148,11 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") silent.push({ route, index }); continue; } - const result = yield* attempt(route); + const relay = routes[relayIndex]; + const result = + check === "answered" && relay !== undefined && index < relayIndex && !relayTried + ? yield* attemptWithRelay(route, relay) + : yield* attempt(route, yield* Scope.fork(attemptScope)); if (result._tag === "Success") return result.success; // An incompatible server is the same server on every route. if (result.failure.reason === "unsupported") return yield* result.failure; @@ -125,7 +167,7 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") ...silent.filter((_, position) => late[position] === "silent"), ]; for (const { route } of retry) { - const result = yield* attempt(route); + const result = yield* attempt(route, yield* Scope.fork(attemptScope)); if (result._tag === "Success") return result.success; if (result.failure.reason === "unsupported") return yield* result.failure; } diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index f284c48e82772..d60f901cba5bc 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -39,6 +39,7 @@ import { type SupervisorConnectionState, } from "./model.ts"; import * as RpcSession from "../rpc/session.ts"; +import { environmentMismatchError } from "./errors.ts"; import { presentConnectionState } from "./presentation.ts"; import * as EnvironmentSupervisor from "./supervisor.ts"; import * as ConnectionWakeups from "./wakeups.ts"; @@ -1548,6 +1549,148 @@ describe("EnvironmentSupervisor routes", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect.each(["hangs", "fails late", "rejects its identity late"] as const)( + "connects over relay when an answering LAN route's readiness %s", + (lanReadiness) => + Effect.gen(function* () { + const lanWaiting = yield* Deferred.make(); + const harness = yield* makeHarness({ + checkRoute: (route) => + Effect.succeed( + route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked", + ), + prepare: (_attempt, target) => Effect.succeed(preparedFor(target)), + ready: (attempt) => + attempt === 1 + ? Deferred.succeed(lanWaiting, undefined).pipe( + Effect.andThen( + lanReadiness === "hangs" + ? Effect.never + : Effect.sleep("5 seconds").pipe( + Effect.andThen( + Effect.fail( + lanReadiness === "fails late" + ? transient("LAN readiness failed.") + : environmentMismatchError({ + expected: TARGET.environmentId, + actual: EnvironmentId.make("environment-2"), + }), + ), + ), + ), + ), + ) + : Effect.void, + }); + const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* Deferred.await(lanWaiting); + yield* TestClock.adjust("500 millis"); + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target._tag).toBe( + "RelayConnectionTarget", + ); + // The stuck LAN session is closed rather than left beside the relay one. + expect(yield* Ref.get(harness.sessionCount)).toBe(2); + expect(yield* Ref.get(harness.releaseCount)).toBe(1); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("keeps waiting for a slow LAN route when relay fails its identity check", () => + Effect.gen(function* () { + const lanWaiting = yield* Deferred.make(); + const attempted: string[] = []; + const harness = yield* makeHarness({ + checkRoute: (route) => + Effect.succeed(route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked"), + prepare: (_attempt, target) => { + attempted.push(target._tag); + return target._tag === "RelayConnectionTarget" + ? Effect.fail( + environmentMismatchError({ + expected: TARGET.environmentId, + actual: EnvironmentId.make("environment-2"), + }), + ) + : Effect.succeed(preparedFor(target)); + }, + ready: () => + Deferred.succeed(lanWaiting, undefined).pipe(Effect.andThen(Effect.sleep("3 seconds"))), + }); + const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* Deferred.await(lanWaiting); + yield* TestClock.adjust("3 seconds"); + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target._tag).toBe( + "BearerConnectionTarget", + ); + expect(attempted).toEqual(["BearerConnectionTarget", "RelayConnectionTarget"]); + expect(yield* Ref.get(harness.releaseCount)).toBe(0); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("keeps one session when LAN and relay become ready together", () => + Effect.gen(function* () { + const waiting = yield* Ref.make(0); + const bothWaiting = yield* Deferred.make(); + const ready = yield* Deferred.make(); + const harness = yield* makeHarness({ + checkRoute: (route) => + Effect.succeed(route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked"), + prepare: (_attempt, target) => Effect.succeed(preparedFor(target)), + ready: () => + Ref.updateAndGet(waiting, (count) => count + 1).pipe( + Effect.flatMap((count) => + count === 2 ? Deferred.succeed(bothWaiting, undefined) : Effect.void, + ), + Effect.andThen(Deferred.await(ready)), + ), + }); + const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* TestClock.adjust("500 millis"); + yield* Deferred.await(bothWaiting); + yield* Deferred.succeed(ready, undefined); + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + expect(yield* Ref.get(harness.sessionCount)).toBe(2); + expect(yield* Ref.get(harness.releaseCount)).toBe(1); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("closes both racing routes when the user disconnects", () => + Effect.gen(function* () { + const lanWaiting = yield* Deferred.make(); + const relayWaiting = yield* Deferred.make(); + const harness = yield* makeHarness({ + checkRoute: (route) => + Effect.succeed(route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked"), + prepare: (_attempt, target) => Effect.succeed(preparedFor(target)), + ready: (attempt) => + Deferred.succeed(attempt === 1 ? lanWaiting : relayWaiting, undefined).pipe( + Effect.andThen(Effect.never), + ), + }); + const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* Deferred.await(lanWaiting); + yield* TestClock.adjust("500 millis"); + yield* Deferred.await(relayWaiting); + yield* supervisor.disconnect; + yield* awaitState(supervisor.state, (state) => state.phase === "available"); + expect(yield* Ref.get(harness.sessionCount)).toBe(2); + expect(yield* Ref.get(harness.releaseCount)).toBe(2); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("tries a slow tailnet route that answered before a dead LAN route", () => Effect.gen(function* () { const tailnetTarget = new BearerConnectionTarget({ From 415315ae3f04594dae5538177352db0a0234949a Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Wed, 11 Nov 2026 01:12:54 +0800 Subject: [PATCH 2/6] ci: rerun after a runner setup timeout Co-Authored-By: Claude Opus 5.5 From a4f18d0b91bf721312f0c8961a946deba7c64bf2 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Wed, 11 Nov 2026 01:27:04 +0800 Subject: [PATCH 3/6] fix(client-runtime): give each answering route its own relay head start Co-Authored-By: Claude Opus 5.5 --- .../client-runtime/src/connection/driver.ts | 5 +- .../src/connection/supervisor.test.ts | 76 ++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/packages/client-runtime/src/connection/driver.ts b/packages/client-runtime/src/connection/driver.ts index 88e76901c50c0..e4e36391f5155 100644 --- a/packages/client-runtime/src/connection/driver.ts +++ b/packages/client-runtime/src/connection/driver.ts @@ -110,10 +110,7 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") ) { const directScope = yield* Scope.fork(attemptScope); const direct = yield* Effect.forkChild(attempt(route, directScope)); - const early = yield* Effect.raceFirst( - Fiber.join(direct).pipe(Effect.asSome), - Fiber.join(headStart).pipe(Effect.as(Option.none())), - ); + const early = yield* Fiber.join(direct).pipe(Effect.timeoutOption(ROUTE_CHECK_HEAD_START_MS)); if (Option.isSome(early)) return early.value; relayTried = true; const relayScope = yield* Scope.fork(attemptScope); diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index d60f901cba5bc..98537ebfe9b26 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -1554,6 +1554,7 @@ describe("EnvironmentSupervisor routes", () => { (lanReadiness) => Effect.gen(function* () { const lanWaiting = yield* Deferred.make(); + const relayMayConnect = yield* Deferred.make(); const harness = yield* makeHarness({ checkRoute: (route) => Effect.succeed( @@ -1580,7 +1581,7 @@ describe("EnvironmentSupervisor routes", () => { ), ), ) - : Effect.void, + : Deferred.await(relayMayConnect), }); const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { initiallyDesired: true, @@ -1588,6 +1589,9 @@ describe("EnvironmentSupervisor routes", () => { yield* Deferred.await(lanWaiting); yield* TestClock.adjust("500 millis"); + // A late LAN failure does not end the race while relay is still connecting. + yield* TestClock.adjust("5 seconds"); + yield* Deferred.succeed(relayMayConnect, undefined); yield* awaitState(supervisor.state, (state) => state.phase === "connected"); expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target._tag).toBe( "RelayConnectionTarget", @@ -1598,6 +1602,76 @@ describe("EnvironmentSupervisor routes", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect("gives a later answering route its own head start before racing relay", () => + Effect.gen(function* () { + const secondLanTarget = new BearerConnectionTarget({ + environmentId: TARGET.environmentId, + label: TARGET.label, + connectionId: "bearer:second-lan", + }); + const entry: ConnectionCatalogEntry = { + ...LAN_THEN_RELAY_ENTRY, + alternateRoutes: [ + { + target: secondLanTarget, + profile: Option.some( + new BearerConnectionProfile({ + connectionId: secondLanTarget.connectionId, + environmentId: TARGET.environmentId, + label: TARGET.label, + httpBaseUrl: "http://192.168.56.1:3773/", + wsBaseUrl: "ws://192.168.56.1:3773/", + }), + ), + }, + { target: RELAY_TARGET, profile: Option.none() }, + ], + }; + const isFirstLan = (target: ConnectionTarget) => + connectionRouteId(target) === LAN_TARGET.connectionId; + const firstLanPreparing = yield* Deferred.make(); + const secondLanWaiting = yield* Deferred.make(); + const attempted: string[] = []; + const harness = yield* makeHarness({ + checkRoute: (route) => + Effect.succeed( + isFirstLan(route.target) || route.target._tag !== "BearerConnectionTarget" + ? "unchecked" + : "answered", + ), + // The first LAN route spends the whole head start failing. + prepare: (_attempt, target) => { + attempted.push(connectionRouteId(target)); + return isFirstLan(target) + ? Deferred.succeed(firstLanPreparing, undefined).pipe( + Effect.andThen(Effect.sleep("1 second")), + Effect.andThen(Effect.fail(transient("First LAN route failed."))), + ) + : Effect.succeed(preparedFor(target)); + }, + ready: () => + Deferred.succeed(secondLanWaiting, undefined).pipe( + Effect.andThen(Effect.sleep("200 millis")), + ), + }); + const supervisor = yield* EnvironmentSupervisor.make(entry, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* Deferred.await(firstLanPreparing); + yield* TestClock.adjust("1 second"); + yield* Deferred.await(secondLanWaiting); + yield* TestClock.adjust("200 millis"); + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + expect( + connectionRouteId( + Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target, + ), + ).toBe(secondLanTarget.connectionId); + expect(attempted).toEqual([LAN_TARGET.connectionId, secondLanTarget.connectionId]); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("keeps waiting for a slow LAN route when relay fails its identity check", () => Effect.gen(function* () { const lanWaiting = yield* Deferred.make(); From cf13b3ccb7d25f6c009dc042de70dfa2072fffb0 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Fri, 13 Nov 2026 14:14:21 +0800 Subject: [PATCH 4/6] fix(client-runtime): a failed direct route moves on to the next while T3 Connect connects Once the head start ended, the relay race waited on T3 Connect after the raced direct route failed, so later direct routes were never tried while relay hung. Relay now runs beside the rest of the direct-route walk and races each later direct attempt; a direct failure moves on to the next route. Co-Authored-By: Claude Opus 5.5 --- docs/internals/remote.md | 6 +- .../client-runtime/src/connection/driver.ts | 88 +++++++++++++------ .../src/connection/supervisor.test.ts | 70 +++++++++++++++ 3 files changed, 133 insertions(+), 31 deletions(-) diff --git a/docs/internals/remote.md b/docs/internals/remote.md index fc26d2d593a24..57c416a0a3b12 100644 --- a/docs/internals/remote.md +++ b/docs/internals/remote.md @@ -38,8 +38,10 @@ when every route stays silent, each is still tried. A route that fails to connect, including a blocked one such as a signed-out T3 Connect, moves on to the next; only an incompatible server stops the walk, because it is the same server on every route. A direct route that answered the check but is still -connecting when the check's head start ends races T3 Connect instead of holding -it back, and the first to connect wins; the other closes. While connected over a later route the +connecting when the check's head start ends starts T3 Connect instead of holding +it back. T3 Connect then races that route and every direct route after it, so a +failed direct route still moves on to the next one. The first to connect wins and +the others close. While connected over a later route the [supervisor](../../packages/client-runtime/src/connection/supervisor.ts) preflights the earlier ones and replaces the session when one would connect. Preflight includes authorization so a route that answers but rejects this diff --git a/packages/client-runtime/src/connection/driver.ts b/packages/client-runtime/src/connection/driver.ts index 7ce51be85282a..f09ae0731ac9e 100644 --- a/packages/client-runtime/src/connection/driver.ts +++ b/packages/client-runtime/src/connection/driver.ts @@ -102,39 +102,61 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") return result; }); const relayIndex = routes.findIndex((route) => route.target._tag === "RelayConnectionTarget"); - let relayTried = false; - // A direct route still connecting when the head start ends races relay. The - // first to connect wins; closing the other scope also ends a late session. - const attemptWithRelay = Effect.fnUntraced(function* ( - route: ConnectionRoute, - relay: ConnectionRoute, - ) { + const relayRoute = routes[relayIndex]; + type Settled = { + readonly result: Result.Result; + readonly scope: Scope.Closeable; + }; + let relay: + | { + readonly fiber: Fiber.Fiber< + Settled, + Result.Result + >; + readonly scope: Scope.Closeable; + } + | undefined; + // A direct route still connecting when the head start ends starts relay, which + // then races every direct attempt after it. The first to connect wins. A failed + // direct route moves on to the next one while relay keeps connecting. + const attemptRacingRelay = Effect.fnUntraced(function* (route: ConnectionRoute) { const directScope = yield* Scope.fork(attemptScope); const direct = yield* Effect.forkChild(attempt(route, directScope)); - const early = yield* Fiber.join(direct).pipe(Effect.timeoutOption(ROUTE_CHECK_HEAD_START_MS)); - if (Option.isSome(early)) return early.value; - relayTried = true; - const relayScope = yield* Scope.fork(attemptScope); - const settle = - (scope: Scope.Closeable) => - (result: Result.Result) => - result._tag === "Success" || result.failure.reason === "unsupported" - ? Effect.succeed({ result, scope }) - : Effect.fail(result); + if (relay === undefined) { + const early = yield* Fiber.join(direct).pipe(Effect.timeoutOption(ROUTE_CHECK_HEAD_START_MS)); + if (Option.isSome(early)) return early.value; + const relayScope = yield* Scope.fork(attemptScope); + const fiber = yield* Effect.forkChild( + attempt(relayRoute!, relayScope).pipe( + Effect.flatMap((result) => + result._tag === "Success" || result.failure.reason === "unsupported" + ? Effect.succeed({ result, scope: relayScope }) + : Effect.fail(result), + ), + ), + ); + relay = { fiber, scope: relayScope }; + } + const racing = relay; const raced = yield* Effect.race( - Fiber.join(direct).pipe(Effect.flatMap(settle(directScope))), - attempt(relay, relayScope).pipe(Effect.flatMap(settle(relayScope))), - ).pipe(Effect.result); - yield* Fiber.interrupt(direct); - const winner = raced._tag === "Success" ? raced.success.scope : undefined; - for (const scope of [directScope, relayScope]) { - if (scope !== winner) yield* Scope.close(scope, Exit.void); + Fiber.join(direct).pipe(Effect.map((result): Settled => ({ result, scope: directScope }))), + // A failed relay leaves the direct attempt to finish on its own. + Fiber.join(racing.fiber).pipe(Effect.catch(() => Effect.never)), + ); + if (raced.scope === directScope) { + if (raced.result._tag === "Success" || raced.result.failure.reason === "unsupported") { + yield* Fiber.interrupt(racing.fiber); + yield* Scope.close(racing.scope, Exit.void); + } + } else { + yield* Fiber.interrupt(direct); + yield* Scope.close(directScope, Exit.void); } - return raced._tag === "Success" ? raced.success.result : raced.failure; + return raced.result; }); const silent: Array<{ readonly route: ConnectionRoute; readonly index: number }> = []; for (const [index, route] of routes.entries()) { - if (index === relayIndex && relayTried) continue; + if (index === relayIndex && relay !== undefined) continue; const check = checks.length === 0 ? "unchecked" @@ -146,15 +168,23 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") silent.push({ route, index }); continue; } - const relay = routes[relayIndex]; const result = - check === "answered" && relay !== undefined && index < relayIndex && !relayTried - ? yield* attemptWithRelay(route, relay) + relay !== undefined || + (check === "answered" && relayRoute !== undefined && index < relayIndex) + ? yield* attemptRacingRelay(route) : yield* attempt(route, yield* Scope.fork(attemptScope)); if (result._tag === "Success") return result.success; // An incompatible server is the same server on every route. if (result.failure.reason === "unsupported") return yield* result.failure; } + if (relay !== undefined) { + const settled = yield* Fiber.join(relay.fiber).pipe(Effect.option); + if (Option.isSome(settled)) { + const { result } = settled.value; + if (result._tag === "Success") return result.success; + return yield* result.failure; + } + } const late = yield* Effect.forEach(silent, ({ index }) => Fiber.join(checks[index]!), { concurrency: "unbounded", }); diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index 0fbe6060c157a..281add7b1193a 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -1903,6 +1903,76 @@ describe("EnvironmentSupervisor routes", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect("tries the next answering LAN route when the raced one fails while relay hangs", () => + Effect.gen(function* () { + const secondLanTarget = new BearerConnectionTarget({ + environmentId: TARGET.environmentId, + label: TARGET.label, + connectionId: "bearer:second-lan", + }); + const entry: ConnectionCatalogEntry = { + ...LAN_THEN_RELAY_ENTRY, + alternateRoutes: [ + { + target: secondLanTarget, + profile: Option.some( + new BearerConnectionProfile({ + connectionId: secondLanTarget.connectionId, + environmentId: TARGET.environmentId, + label: TARGET.label, + httpBaseUrl: "http://192.168.56.1:3773/", + wsBaseUrl: "ws://192.168.56.1:3773/", + }), + ), + }, + { target: RELAY_TARGET, profile: Option.none() }, + ], + }; + const firstLanWaiting = yield* Deferred.make(); + const relayWaiting = yield* Deferred.make(); + const attempted: string[] = []; + const harness = yield* makeHarness({ + checkRoute: (route) => + Effect.succeed(route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked"), + prepare: (_attempt, target) => { + attempted.push(connectionRouteId(target)); + return Effect.succeed(preparedFor(target)); + }, + ready: (attempt) => + attempt === 1 + ? Deferred.succeed(firstLanWaiting, undefined).pipe( + Effect.andThen(Effect.sleep("1 second")), + Effect.andThen(Effect.fail(transient("First LAN readiness failed."))), + ) + : attempt === 2 + ? Deferred.succeed(relayWaiting, undefined).pipe(Effect.andThen(Effect.never)) + : Effect.void, + }); + const supervisor = yield* EnvironmentSupervisor.make(entry, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* Deferred.await(firstLanWaiting); + yield* TestClock.adjust("500 millis"); + yield* Deferred.await(relayWaiting); + yield* TestClock.adjust("500 millis"); + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + expect( + connectionRouteId( + Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target, + ), + ).toBe(secondLanTarget.connectionId); + expect(attempted).toEqual([ + LAN_TARGET.connectionId, + connectionRouteId(RELAY_TARGET), + secondLanTarget.connectionId, + ]); + // The failed LAN session and the still-connecting relay session both close. + expect(yield* Ref.get(harness.sessionCount)).toBe(3); + expect(yield* Ref.get(harness.releaseCount)).toBe(2); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("keeps waiting for a slow LAN route when relay fails its identity check", () => Effect.gen(function* () { const lanWaiting = yield* Deferred.make(); From 426246eeffbef22de3b2b5d0a3fcba2b009e7f49 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Nov 2026 07:45:32 +0000 Subject: [PATCH 5/6] test(client-runtime): trim relay hedge tests to the cases that catch a regression Drop the identity-mismatch readiness row (same path as the late transient failure) and the simultaneous-ready case (the hang case already asserts the loser closes), and share the two-LAN entry across tests. Co-Authored-By: Claude Opus 5.5 --- .../src/connection/supervisor.test.ts | 150 ++++-------------- 1 file changed, 33 insertions(+), 117 deletions(-) diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index ffca73e2d818f..b1c6e83584480 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -110,6 +110,30 @@ const LAN_THEN_RELAY_ENTRY: ConnectionCatalogEntry = { enabled: true, }; +const SECOND_LAN_TARGET = new BearerConnectionTarget({ + environmentId: TARGET.environmentId, + label: TARGET.label, + connectionId: "bearer:second-lan", +}); +const TWO_LANS_THEN_RELAY_ENTRY: ConnectionCatalogEntry = { + ...LAN_THEN_RELAY_ENTRY, + alternateRoutes: [ + { + target: SECOND_LAN_TARGET, + profile: Option.some( + new BearerConnectionProfile({ + connectionId: SECOND_LAN_TARGET.connectionId, + environmentId: TARGET.environmentId, + label: TARGET.label, + httpBaseUrl: "http://192.168.56.1:3773/", + wsBaseUrl: "ws://192.168.56.1:3773/", + }), + ), + }, + { target: RELAY_TARGET, profile: Option.none() }, + ], +}; + const SSH_ENTRY: ConnectionCatalogEntry = { target: new SshConnectionTarget({ environmentId: TARGET.environmentId, @@ -1842,29 +1866,6 @@ describe("EnvironmentSupervisor routes", () => { it.effect("shares one head start across several slow LAN routes", () => Effect.gen(function* () { - const secondLanTarget = new BearerConnectionTarget({ - environmentId: TARGET.environmentId, - label: TARGET.label, - connectionId: "bearer:second-lan", - }); - const entry: ConnectionCatalogEntry = { - ...LAN_THEN_RELAY_ENTRY, - alternateRoutes: [ - { - target: secondLanTarget, - profile: Option.some( - new BearerConnectionProfile({ - connectionId: secondLanTarget.connectionId, - environmentId: TARGET.environmentId, - label: TARGET.label, - httpBaseUrl: "http://192.168.56.1:3773/", - wsBaseUrl: "ws://192.168.56.1:3773/", - }), - ), - }, - { target: RELAY_TARGET, profile: Option.none() }, - ], - }; const checking = yield* Ref.make(0); const checkingLans = yield* Deferred.make(); const harness = yield* makeHarness({ @@ -1880,7 +1881,7 @@ describe("EnvironmentSupervisor routes", () => { : Effect.succeed("unchecked"), prepare: (_attempt, target) => Effect.succeed(preparedFor(target)), }); - const supervisor = yield* EnvironmentSupervisor.make(entry, { + const supervisor = yield* EnvironmentSupervisor.make(TWO_LANS_THEN_RELAY_ENTRY, { initiallyDesired: true, }).pipe(Effect.provide(harness.dependencies)); @@ -1920,7 +1921,7 @@ describe("EnvironmentSupervisor routes", () => { }).pipe(Effect.provide(TestClock.layer())), ); - it.effect.each(["hangs", "fails late", "rejects its identity late"] as const)( + it.effect.each(["hangs", "fails late"] as const)( "connects over relay when an answering LAN route's readiness %s", (lanReadiness) => Effect.gen(function* () { @@ -1939,16 +1940,7 @@ describe("EnvironmentSupervisor routes", () => { lanReadiness === "hangs" ? Effect.never : Effect.sleep("5 seconds").pipe( - Effect.andThen( - Effect.fail( - lanReadiness === "fails late" - ? transient("LAN readiness failed.") - : environmentMismatchError({ - expected: TARGET.environmentId, - actual: EnvironmentId.make("environment-2"), - }), - ), - ), + Effect.andThen(Effect.fail(transient("LAN readiness failed."))), ), ), ) @@ -1975,29 +1967,6 @@ describe("EnvironmentSupervisor routes", () => { it.effect("gives a later answering route its own head start before racing relay", () => Effect.gen(function* () { - const secondLanTarget = new BearerConnectionTarget({ - environmentId: TARGET.environmentId, - label: TARGET.label, - connectionId: "bearer:second-lan", - }); - const entry: ConnectionCatalogEntry = { - ...LAN_THEN_RELAY_ENTRY, - alternateRoutes: [ - { - target: secondLanTarget, - profile: Option.some( - new BearerConnectionProfile({ - connectionId: secondLanTarget.connectionId, - environmentId: TARGET.environmentId, - label: TARGET.label, - httpBaseUrl: "http://192.168.56.1:3773/", - wsBaseUrl: "ws://192.168.56.1:3773/", - }), - ), - }, - { target: RELAY_TARGET, profile: Option.none() }, - ], - }; const isFirstLan = (target: ConnectionTarget) => connectionRouteId(target) === LAN_TARGET.connectionId; const firstLanPreparing = yield* Deferred.make(); @@ -2025,7 +1994,7 @@ describe("EnvironmentSupervisor routes", () => { Effect.andThen(Effect.sleep("200 millis")), ), }); - const supervisor = yield* EnvironmentSupervisor.make(entry, { + const supervisor = yield* EnvironmentSupervisor.make(TWO_LANS_THEN_RELAY_ENTRY, { initiallyDesired: true, }).pipe(Effect.provide(harness.dependencies)); @@ -2038,36 +2007,13 @@ describe("EnvironmentSupervisor routes", () => { connectionRouteId( Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target, ), - ).toBe(secondLanTarget.connectionId); - expect(attempted).toEqual([LAN_TARGET.connectionId, secondLanTarget.connectionId]); + ).toBe(SECOND_LAN_TARGET.connectionId); + expect(attempted).toEqual([LAN_TARGET.connectionId, SECOND_LAN_TARGET.connectionId]); }).pipe(Effect.provide(TestClock.layer())), ); it.effect("tries the next answering LAN route when the raced one fails while relay hangs", () => Effect.gen(function* () { - const secondLanTarget = new BearerConnectionTarget({ - environmentId: TARGET.environmentId, - label: TARGET.label, - connectionId: "bearer:second-lan", - }); - const entry: ConnectionCatalogEntry = { - ...LAN_THEN_RELAY_ENTRY, - alternateRoutes: [ - { - target: secondLanTarget, - profile: Option.some( - new BearerConnectionProfile({ - connectionId: secondLanTarget.connectionId, - environmentId: TARGET.environmentId, - label: TARGET.label, - httpBaseUrl: "http://192.168.56.1:3773/", - wsBaseUrl: "ws://192.168.56.1:3773/", - }), - ), - }, - { target: RELAY_TARGET, profile: Option.none() }, - ], - }; const firstLanWaiting = yield* Deferred.make(); const relayWaiting = yield* Deferred.make(); const attempted: string[] = []; @@ -2088,7 +2034,7 @@ describe("EnvironmentSupervisor routes", () => { ? Deferred.succeed(relayWaiting, undefined).pipe(Effect.andThen(Effect.never)) : Effect.void, }); - const supervisor = yield* EnvironmentSupervisor.make(entry, { + const supervisor = yield* EnvironmentSupervisor.make(TWO_LANS_THEN_RELAY_ENTRY, { initiallyDesired: true, }).pipe(Effect.provide(harness.dependencies)); @@ -2101,11 +2047,11 @@ describe("EnvironmentSupervisor routes", () => { connectionRouteId( Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target, ), - ).toBe(secondLanTarget.connectionId); + ).toBe(SECOND_LAN_TARGET.connectionId); expect(attempted).toEqual([ LAN_TARGET.connectionId, connectionRouteId(RELAY_TARGET), - secondLanTarget.connectionId, + SECOND_LAN_TARGET.connectionId, ]); // The failed LAN session and the still-connecting relay session both close. expect(yield* Ref.get(harness.sessionCount)).toBe(3); @@ -2149,36 +2095,6 @@ describe("EnvironmentSupervisor routes", () => { }).pipe(Effect.provide(TestClock.layer())), ); - it.effect("keeps one session when LAN and relay become ready together", () => - Effect.gen(function* () { - const waiting = yield* Ref.make(0); - const bothWaiting = yield* Deferred.make(); - const ready = yield* Deferred.make(); - const harness = yield* makeHarness({ - checkRoute: (route) => - Effect.succeed(route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked"), - prepare: (_attempt, target) => Effect.succeed(preparedFor(target)), - ready: () => - Ref.updateAndGet(waiting, (count) => count + 1).pipe( - Effect.flatMap((count) => - count === 2 ? Deferred.succeed(bothWaiting, undefined) : Effect.void, - ), - Effect.andThen(Deferred.await(ready)), - ), - }); - const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { - initiallyDesired: true, - }).pipe(Effect.provide(harness.dependencies)); - - yield* TestClock.adjust("500 millis"); - yield* Deferred.await(bothWaiting); - yield* Deferred.succeed(ready, undefined); - yield* awaitState(supervisor.state, (state) => state.phase === "connected"); - expect(yield* Ref.get(harness.sessionCount)).toBe(2); - expect(yield* Ref.get(harness.releaseCount)).toBe(1); - }).pipe(Effect.provide(TestClock.layer())), - ); - it.effect("closes both racing routes when the user disconnects", () => Effect.gen(function* () { const lanWaiting = yield* Deferred.make(); From e9e231ef2f33929abcad6927891c182767322292 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Nov 2026 07:59:18 +0000 Subject: [PATCH 6/6] fix(client-runtime): an incompatible T3 Connect server no longer beats a compatible direct route Relay can reach a different server than a direct route with the same environment id (a preview app sharing the home directory). The speculative relay attempt now counts an unsupported result as a failed backup and reports it only when no direct route connects. Co-Authored-By: Claude Opus 5.5 --- docs/internals/remote.md | 4 +- .../client-runtime/src/connection/driver.ts | 14 ++-- .../src/connection/supervisor.test.ts | 75 +++++++++++-------- 3 files changed, 52 insertions(+), 41 deletions(-) diff --git a/docs/internals/remote.md b/docs/internals/remote.md index 57c416a0a3b12..18674a6abd1c5 100644 --- a/docs/internals/remote.md +++ b/docs/internals/remote.md @@ -41,7 +41,9 @@ server on every route. A direct route that answered the check but is still connecting when the check's head start ends starts T3 Connect instead of holding it back. T3 Connect then races that route and every direct route after it, so a failed direct route still moves on to the next one. The first to connect wins and -the others close. While connected over a later route the +the others close. An incompatible T3 Connect server does not end that race, because +relay can reach a different server than a direct route, such as a preview app +sharing the home directory; it is reported only when no direct route connects. While connected over a later route the [supervisor](../../packages/client-runtime/src/connection/supervisor.ts) preflights the earlier ones and replaces the session when one would connect. Preflight includes authorization so a route that answers but rejects this diff --git a/packages/client-runtime/src/connection/driver.ts b/packages/client-runtime/src/connection/driver.ts index f09ae0731ac9e..e0509af2a841c 100644 --- a/packages/client-runtime/src/connection/driver.ts +++ b/packages/client-runtime/src/connection/driver.ts @@ -129,7 +129,9 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") const fiber = yield* Effect.forkChild( attempt(relayRoute!, relayScope).pipe( Effect.flatMap((result) => - result._tag === "Success" || result.failure.reason === "unsupported" + // Relay can reach a different server than the direct routes, so an + // incompatible relay is a failed backup, not the end of the walk. + result._tag === "Success" ? Effect.succeed({ result, scope: relayScope }) : Effect.fail(result), ), @@ -178,12 +180,10 @@ export const connectOverRoutes = Effect.fn("ConnectionDriver.connectOverRoutes") if (result.failure.reason === "unsupported") return yield* result.failure; } if (relay !== undefined) { - const settled = yield* Fiber.join(relay.fiber).pipe(Effect.option); - if (Option.isSome(settled)) { - const { result } = settled.value; - if (result._tag === "Success") return result.success; - return yield* result.failure; - } + const settled = yield* Fiber.join(relay.fiber).pipe(Effect.result); + const result = settled._tag === "Success" ? settled.success.result : settled.failure; + if (result._tag === "Success") return result.success; + if (result.failure.reason === "unsupported") return yield* result.failure; } const late = yield* Effect.forEach(silent, ({ index }) => Fiber.join(checks[index]!), { concurrency: "unbounded", diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index b1c6e83584480..3f2ae32608121 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -2059,40 +2059,49 @@ describe("EnvironmentSupervisor routes", () => { }).pipe(Effect.provide(TestClock.layer())), ); - it.effect("keeps waiting for a slow LAN route when relay fails its identity check", () => - Effect.gen(function* () { - const lanWaiting = yield* Deferred.make(); - const attempted: string[] = []; - const harness = yield* makeHarness({ - checkRoute: (route) => - Effect.succeed(route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked"), - prepare: (_attempt, target) => { - attempted.push(target._tag); - return target._tag === "RelayConnectionTarget" - ? Effect.fail( - environmentMismatchError({ - expected: TARGET.environmentId, - actual: EnvironmentId.make("environment-2"), - }), - ) - : Effect.succeed(preparedFor(target)); - }, - ready: () => - Deferred.succeed(lanWaiting, undefined).pipe(Effect.andThen(Effect.sleep("3 seconds"))), - }); - const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { - initiallyDesired: true, - }).pipe(Effect.provide(harness.dependencies)); + it.effect.each(["fails its identity check", "reaches an unsupported server"] as const)( + "keeps waiting for a slow LAN route when relay %s", + (relayFailure) => + Effect.gen(function* () { + const lanWaiting = yield* Deferred.make(); + const attempted: string[] = []; + const harness = yield* makeHarness({ + checkRoute: (route) => + Effect.succeed( + route.target._tag === "BearerConnectionTarget" ? "answered" : "unchecked", + ), + prepare: (_attempt, target) => { + attempted.push(target._tag); + return target._tag === "RelayConnectionTarget" + ? Effect.fail( + relayFailure === "fails its identity check" + ? environmentMismatchError({ + expected: TARGET.environmentId, + actual: EnvironmentId.make("environment-2"), + }) + : new ConnectionBlockedError({ + reason: "unsupported", + detail: "This client requires a newer server.", + }), + ) + : Effect.succeed(preparedFor(target)); + }, + ready: () => + Deferred.succeed(lanWaiting, undefined).pipe(Effect.andThen(Effect.sleep("3 seconds"))), + }); + const supervisor = yield* EnvironmentSupervisor.make(LAN_THEN_RELAY_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); - yield* Deferred.await(lanWaiting); - yield* TestClock.adjust("3 seconds"); - yield* awaitState(supervisor.state, (state) => state.phase === "connected"); - expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target._tag).toBe( - "BearerConnectionTarget", - ); - expect(attempted).toEqual(["BearerConnectionTarget", "RelayConnectionTarget"]); - expect(yield* Ref.get(harness.releaseCount)).toBe(0); - }).pipe(Effect.provide(TestClock.layer())), + yield* Deferred.await(lanWaiting); + yield* TestClock.adjust("3 seconds"); + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).target._tag).toBe( + "BearerConnectionTarget", + ); + expect(attempted).toEqual(["BearerConnectionTarget", "RelayConnectionTarget"]); + expect(yield* Ref.get(harness.releaseCount)).toBe(0); + }).pipe(Effect.provide(TestClock.layer())), ); it.effect("closes both racing routes when the user disconnects", () =>