Skip to content

Commit 145fba7

Browse files
authored
fix(client-runtime): a dropped session no longer reads as a failed connection (#18469)
1 parent 73ed549 commit 145fba7

2 files changed

Lines changed: 79 additions & 4 deletions

File tree

‎packages/client-runtime/src/connection/supervisor.test.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
type SupervisorConnectionState,
3737
} from "./model.ts";
3838
import * as RpcSession from "../rpc/session.ts";
39+
import { presentConnectionState } from "./presentation.ts";
3940
import * as EnvironmentSupervisor from "./supervisor.ts";
4041
import * as ConnectionWakeups from "./wakeups.ts";
4142
import { connectionRouteId, entryWithRoutes, isLearned, mergeLearnedRoutes } from "./routes.ts";
@@ -985,6 +986,73 @@ describe("EnvironmentSupervisor", () => {
985986
}).pipe(Effect.provide(TestClock.layer())),
986987
);
987988

989+
it.effect("does not report a session lost while suspended as a failed connection", () =>
990+
Effect.gen(function* () {
991+
const allowReconnect = yield* Deferred.make<void>();
992+
const harness = yield* makeHarness({
993+
prepare: (attempt) =>
994+
attempt === 2
995+
? Deferred.await(allowReconnect).pipe(Effect.as(PREPARED_CONNECTION))
996+
: Effect.succeed(PREPARED_CONNECTION),
997+
});
998+
const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, {
999+
initiallyDesired: true,
1000+
}).pipe(Effect.provide(harness.dependencies));
1001+
1002+
yield* awaitState(supervisor.state, (state) => state.phase === "connected");
1003+
// The OS killed the socket while the app was suspended; its close arrives
1004+
// before the app reports it is active again.
1005+
yield* harness.closeLatestSession();
1006+
const waiting = yield* awaitState(supervisor.state, (state) => state.phase === "backoff");
1007+
expect(presentConnectionState(waiting)).toEqual({
1008+
phase: "reconnecting",
1009+
error: null,
1010+
traceId: null,
1011+
});
1012+
1013+
yield* harness.wake("application-active");
1014+
const replacing = yield* awaitState(
1015+
supervisor.state,
1016+
(state) => state.phase === "connecting" && state.stage === "preparing",
1017+
);
1018+
expect(presentConnectionState(replacing).error).toBeNull();
1019+
1020+
yield* Deferred.succeed(allowReconnect, undefined);
1021+
yield* awaitState(
1022+
supervisor.state,
1023+
(state) => state.phase === "connected" && state.generation === 2,
1024+
);
1025+
}).pipe(Effect.provide(TestClock.layer())),
1026+
);
1027+
1028+
it.effect("does not show an earlier setup failure while replacing a short session", () =>
1029+
Effect.gen(function* () {
1030+
const harness = yield* makeHarness({
1031+
prepare: (attempt) =>
1032+
attempt === 1
1033+
? Effect.fail(transient("Relay connection timed out."))
1034+
: attempt === 3
1035+
? Effect.never
1036+
: Effect.succeed(PREPARED_CONNECTION),
1037+
});
1038+
const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, {
1039+
initiallyDesired: true,
1040+
}).pipe(Effect.provide(harness.dependencies));
1041+
1042+
yield* awaitState(supervisor.state, (state) => state.phase === "backoff");
1043+
yield* TestClock.adjust("3 seconds");
1044+
yield* awaitState(supervisor.state, (state) => state.phase === "connected");
1045+
// A long resume replaces the session before it counts as stable.
1046+
yield* harness.wake("application-active-reconnect");
1047+
const replacing = yield* awaitState(
1048+
supervisor.state,
1049+
(state) => state.phase === "connecting" && state.generation === 2,
1050+
);
1051+
1052+
expect(presentConnectionState(replacing).error).toBeNull();
1053+
}).pipe(Effect.provide(TestClock.layer())),
1054+
);
1055+
9881056
it.effect("keeps escalating backoff when a newly opened session flaps", () =>
9891057
Effect.gen(function* () {
9901058
const harness = yield* makeHarness();
@@ -1245,6 +1313,7 @@ describe("EnvironmentSupervisor", () => {
12451313
(state) => state.phase === "connecting",
12461314
);
12471315
expect(reconnecting.attempt).toBe(1);
1316+
expect(reconnecting.lastFailure).toBeNull();
12481317
expect(Option.isNone(yield* SubscriptionRef.get(supervisor.session))).toBe(true);
12491318

12501319
yield* Deferred.succeed(allowReconnect, undefined);

‎packages/client-runtime/src/connection/supervisor.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -885,9 +885,13 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
885885
const failedProbe = yield* Ref.getAndSet(probeUnanswered, false);
886886
if (outcome.established) {
887887
generation = nextGeneration;
888+
// A session came up, so no earlier failure describes the connection.
889+
// Losing a live session is not a failed connection either, for example
890+
// a socket the OS killed while the app was suspended: its replacement
891+
// reports no failure until it fails itself.
892+
latestFailure = null;
888893
if (outcome.stable) {
889894
resetRetryLadder();
890-
latestFailure = null;
891895
}
892896
}
893897
if (outcome._tag === "Interrupted") {
@@ -900,7 +904,9 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
900904

901905
const attemptSpan: Option.Option<Tracer.Span> = outcome.failure.attemptSpan;
902906
const error: ConnectionAttemptError = outcome.failure.error;
903-
latestFailure = error;
907+
if (!outcome.established) {
908+
latestFailure = error;
909+
}
904910
if (error._tag === "ConnectionBlockedError") {
905911
const blockedIntent = yield* Ref.get(intent);
906912
yield* setState({
@@ -922,7 +928,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
922928

923929
if (failedProbe) {
924930
resetRetryLadder();
925-
yield* setState(connectingState(yield* Ref.get(intent), generation, 1, error));
931+
yield* setState(connectingState(yield* Ref.get(intent), generation, 1, latestFailure));
926932
continue;
927933
}
928934

@@ -942,7 +948,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
942948
stage: null,
943949
attempt,
944950
generation,
945-
lastFailure: error,
951+
lastFailure: latestFailure,
946952
retryAt: (yield* Clock.currentTimeMillis) + delayMs,
947953
});
948954
const applicationActivated = yield* waitForRetrySignal(delayMs);

0 commit comments

Comments
 (0)