authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-04 01:07:49-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 01:07:49-04:00
log39abb9d1d6ae6501c573b9dc0cb9c28e2f75659c
tree0422bfb6670142a9f32f69e12e781e06ae756af4
parent57832803eed4c87c462de92892777a0934019721
signature Signed by PGP key B5690EEEBB952194

fix(connect): refresh authorization without disconnecting (#9582)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

2 files changed, 414 insertions(+), 62 deletions(-)

packages/client-runtime/src/connection/supervisor.test.ts+155-12
......@@ -1097,40 +1097,183 @@ describe("EnvironmentSupervisor", () => {
10971097 }),
10981098 );
10991099
1100 it.effect("renews a relay connection before its DPoP access token expires", () =>
1100 it.effect("hands off relay authorization after the replacement session is ready", () =>
11011101 Effect.gen(function* () {
11021102 const tokenLifetimeMs = DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS * 2;
1103 const replacementStarted = yield* Deferred.make<void>();
1104 const releaseReplacement = yield* Deferred.make<void>();
11031105 const harness = yield* makeHarness({
11041106 prepare: (attempt) =>
1105 Effect.succeed({
1106 ...PREPARED_CONNECTION,
1107 target: RELAY_TARGET,
1108 httpAuthorization: {
1109 _tag: "Dpop",
1110 accessToken: `access-token-${attempt}`,
1111 expiresAtEpochMs: tokenLifetimeMs * attempt,
1112 },
1113 }),
1107 attempt === 2
1108 ? Effect.fail(transient("Authorization refresh failed."))
1109 : Effect.succeed({
1110 ...PREPARED_CONNECTION,
1111 target: RELAY_TARGET,
1112 httpAuthorization: {
1113 _tag: "Dpop",
1114 accessToken: `access-token-${attempt}`,
1115 expiresAtEpochMs: tokenLifetimeMs * attempt,
1116 },
1117 }),
1118 ready: (attempt) =>
1119 attempt === 2
1120 ? Deferred.succeed(replacementStarted, undefined).pipe(
1121 Effect.andThen(Deferred.await(releaseReplacement)),
1122 )
1123 : Effect.void,
11141124 });
11151125 const supervisor = yield* EnvironmentSupervisor.make(RELAY_ENTRY, {
11161126 initiallyDesired: true,
11171127 }).pipe(Effect.provide(harness.dependencies));
11181128
11191129 yield* awaitState(supervisor.state, (state) => state.phase === "connected");
1130 const firstSession = Option.getOrThrow(yield* SubscriptionRef.get(supervisor.session));
1131 const firstPrepared = Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared));
11201132 yield* TestClock.adjust(DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS - 1);
11211133 expect(yield* Ref.get(harness.sessionCount)).toBe(1);
11221134
11231135 yield* TestClock.adjust(1);
1136 expect(yield* Ref.get(harness.prepareCount)).toBe(2);
1137 expect(yield* Ref.get(harness.sessionCount)).toBe(1);
1138 expect(yield* Ref.get(harness.releaseCount)).toBe(0);
1139 yield* Effect.yieldNow;
1140
1141 yield* TestClock.adjust("2999 millis");
1142 expect(yield* Ref.get(harness.prepareCount)).toBe(2);
1143 yield* TestClock.adjust("1 milli");
1144 yield* Deferred.await(replacementStarted);
1145
1146 expect(yield* Ref.get(harness.prepareCount)).toBe(3);
1147 expect(yield* Ref.get(harness.sessionCount)).toBe(2);
1148 expect(yield* Ref.get(harness.releaseCount)).toBe(0);
1149 expect(yield* SubscriptionRef.get(supervisor.state)).toMatchObject({
1150 phase: "connected",
1151 generation: 1,
1152 });
1153 expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.session))).toBe(firstSession);
1154 expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared))).toBe(
1155 firstPrepared,
1156 );
1157
1158 yield* Deferred.succeed(releaseReplacement, undefined);
11241159 yield* awaitState(
11251160 supervisor.state,
11261161 (state) => state.phase === "connected" && state.generation === 2,
11271162 );
11281163
1129 expect(yield* Ref.get(harness.sessionCount)).toBe(2);
11301164 expect(yield* Ref.get(harness.releaseCount)).toBe(1);
1165 expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.session))).not.toBe(
1166 firstSession,
1167 );
11311168 expect(
11321169 Option.getOrThrow(yield* SubscriptionRef.get(supervisor.prepared)).httpAuthorization,
1133 ).toMatchObject({ accessToken: "access-token-2" });
1170 ).toMatchObject({ accessToken: "access-token-3" });
1171 }).pipe(Effect.provide(TestClock.layer())),
1172 );
1173
1174 it.effect("cleans up timed-out replacements and an interrupted retry", () =>
1175 Effect.gen(function* () {
1176 const tokenLifetimeMs = DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS * 2;
1177 const replacementStarted = yield* Deferred.make<void>();
1178 const retryStarted = yield* Deferred.make<void>();
1179 const harness = yield* makeHarness({
1180 prepare: (attempt) =>
1181 Effect.succeed({
1182 ...PREPARED_CONNECTION,
1183 target: RELAY_TARGET,
1184 httpAuthorization: {
1185 _tag: "Dpop",
1186 accessToken: `access-token-${attempt}`,
1187 expiresAtEpochMs: tokenLifetimeMs * attempt,
1188 },
1189 }),
1190 ready: (attempt) => {
1191 if (attempt === 2) {
1192 return Deferred.succeed(replacementStarted, undefined).pipe(
1193 Effect.andThen(Effect.never),
1194 );
1195 }
1196 if (attempt === 3) {
1197 return Deferred.succeed(retryStarted, undefined).pipe(Effect.andThen(Effect.never));
1198 }
1199 return Effect.void;
1200 },
1201 });
1202 const supervisor = yield* EnvironmentSupervisor.make(RELAY_ENTRY, {
1203 initiallyDesired: true,
1204 }).pipe(Effect.provide(harness.dependencies));
1205
1206 yield* awaitState(supervisor.state, (state) => state.phase === "connected");
1207 const firstSession = Option.getOrThrow(yield* SubscriptionRef.get(supervisor.session));
1208 yield* TestClock.adjust(DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS);
1209 yield* Deferred.await(replacementStarted);
1210
1211 expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.session))).toBe(firstSession);
1212 expect(yield* Ref.get(harness.releaseCount)).toBe(0);
1213
1214 yield* TestClock.adjust("15 seconds");
1215 yield* Effect.yieldNow;
1216 expect(yield* Ref.get(harness.releaseCount)).toBe(1);
1217 expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.session))).toBe(firstSession);
1218 expect(yield* SubscriptionRef.get(supervisor.state)).toMatchObject({
1219 phase: "connected",
1220 generation: 1,
1221 });
1222
1223 yield* TestClock.adjust("3 seconds");
1224 yield* Deferred.await(retryStarted);
1225 expect(yield* Ref.get(harness.sessionCount)).toBe(3);
1226 expect(yield* Ref.get(harness.releaseCount)).toBe(1);
1227 expect(Option.getOrThrow(yield* SubscriptionRef.get(supervisor.session))).toBe(firstSession);
1228
1229 yield* supervisor.disconnect;
1230 yield* awaitState(supervisor.state, (state) => state.phase === "available");
1231
1232 expect(yield* Ref.get(harness.releaseCount)).toBe(3);
1233 expect(Option.isNone(yield* SubscriptionRef.get(supervisor.session))).toBe(true);
1234 expect(Option.isNone(yield* SubscriptionRef.get(supervisor.prepared))).toBe(true);
1235 }).pipe(Effect.provide(TestClock.layer())),
1236 );
1237
1238 it.effect("does not keep a relay session past its authorization expiry", () =>
1239 Effect.gen(function* () {
1240 const tokenLifetimeMs = DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS * 2;
1241 const harness = yield* makeHarness({
1242 prepare: (attempt) =>
1243 attempt === 1
1244 ? Effect.succeed({
1245 ...PREPARED_CONNECTION,
1246 target: RELAY_TARGET,
1247 httpAuthorization: {
1248 _tag: "Dpop",
1249 accessToken: "access-token-1",
1250 expiresAtEpochMs: tokenLifetimeMs,
1251 },
1252 })
1253 : Effect.fail(transient("Authorization refresh failed.")),
1254 });
1255 const supervisor = yield* EnvironmentSupervisor.make(RELAY_ENTRY, {
1256 initiallyDesired: true,
1257 }).pipe(Effect.provide(harness.dependencies));
1258
1259 yield* awaitState(supervisor.state, (state) => state.phase === "connected");
1260 yield* TestClock.adjust(DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS);
1261 yield* Effect.yieldNow;
1262 for (const delay of [3_000, 4_000, 8_000, 16_000, 16_000, 13_000]) {
1263 yield* TestClock.adjust(delay);
1264 yield* Effect.yieldNow;
1265 }
1266
1267 yield* awaitState(supervisor.state, (state) => state.phase === "backoff");
1268
1269 expect(yield* Ref.get(harness.sessionCount)).toBe(1);
1270 expect(yield* Ref.get(harness.releaseCount)).toBe(1);
1271 expect(yield* SubscriptionRef.get(supervisor.state)).toMatchObject({
1272 phase: "backoff",
1273 generation: 1,
1274 });
1275 expect(Option.isNone(yield* SubscriptionRef.get(supervisor.session))).toBe(true);
1276 expect(Option.isNone(yield* SubscriptionRef.get(supervisor.prepared))).toBe(true);
11341277 }).pipe(Effect.provide(TestClock.layer())),
11351278 );
11361279
packages/client-runtime/src/connection/supervisor.ts+259-50
......@@ -60,16 +60,24 @@ interface TracedAttemptFailure {
6060 readonly attemptSpan: Option.Option<Tracer.Span>;
6161}
6262
63interface ScopedConnection {
64 readonly attemptSpan: Option.Option<Tracer.Span>;
65 readonly lease: ConnectionDriver.EnvironmentConnectionLease;
66 readonly scope: Scope.Closeable;
67}
68
6369type AttemptOutcome =
6470 | {
6571 readonly _tag: "Interrupted";
6672 readonly established: boolean;
73 readonly generation: number;
6774 readonly stable: boolean;
6875 readonly resetRetry: boolean;
6976 }
7077 | {
7178 readonly _tag: "Failure";
7279 readonly established: boolean;
80 readonly generation: number;
7381 readonly stable: boolean;
7482 readonly failure: TracedAttemptFailure;
7583 };
......@@ -77,17 +85,26 @@ type AttemptOutcome =
7785type EstablishmentEvent =
7886 | {
7987 readonly _tag: "Completed";
80 readonly exit: Exit.Exit<
81 {
82 readonly attemptSpan: Option.Option<Tracer.Span>;
83 readonly lease: ConnectionDriver.EnvironmentConnectionLease;
84 },
85 TracedAttemptFailure
86 >;
88 readonly exit: Exit.Exit<ScopedConnection, TracedAttemptFailure>;
8789 }
8890 | { readonly _tag: "Interrupted"; readonly resetRetry: boolean }
8991 | { readonly _tag: "TimedOut" };
9092
93type ReplacementPreparationEvent =
94 | { readonly _tag: "Completed"; readonly exit: Exit.Exit<ScopedConnection, TracedAttemptFailure> }
95 | { readonly _tag: "TimedOut" }
96 | { readonly _tag: "AuthorizationExpired" };
97
98type ConnectedLeaseEvent =
99 | {
100 readonly _tag: "ActiveCompleted";
101 readonly exit: Exit.Exit<boolean, TracedAttemptFailure>;
102 }
103 | {
104 readonly _tag: "ReplacementCompleted";
105 readonly exit: Exit.Exit<Option.Option<ScopedConnection>, TracedAttemptFailure>;
106 };
107
91108function exitUnlessInterrupted<A, E, R>(
92109 effect: Effect.Effect<A, E, R>,
93110): Effect.Effect<Exit.Exit<A, E>, never, R> {
......@@ -168,16 +185,18 @@ function failureFromExit<A>(
168185 target: ConnectionTarget,
169186 exit: Exit.Exit<A, TracedAttemptFailure>,
170187 established: boolean,
188 generation: number,
171189 stable: boolean,
172190): AttemptOutcome {
173191 if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) {
174 return { _tag: "Interrupted", established, stable, resetRetry: false };
192 return { _tag: "Interrupted", established, generation, stable, resetRetry: false };
175193 }
176194 const typedFailure = exit.cause.reasons.find(Cause.isFailReason);
177195 if (typedFailure) {
178196 return {
179197 _tag: "Failure",
180198 established,
199 generation,
181200 stable,
182201 failure: typedFailure.error,
183202 };
......@@ -185,6 +204,7 @@ function failureFromExit<A>(
185204 return {
186205 _tag: "Failure",
187206 established,
207 generation,
188208 stable,
189209 failure: {
190210 error: new ConnectionTransientError({
......@@ -289,9 +309,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
289309 attempt: number,
290310 generation: number,
291311 lastFailure: ConnectionAttemptError | null,
312 publishProgress: boolean,
292313 ) {
293314 return yield* driver.connect(entry, (progress) =>
294 reportProgress(attempt, generation, lastFailure, progress),
315 publishProgress ? reportProgress(attempt, generation, lastFailure, progress) : Effect.void,
295316 );
296317 });
297318
......@@ -342,16 +363,17 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
342363 generation: number,
343364 lastFailure: ConnectionAttemptError | null,
344365 pendingRetry: Option.Option<PendingRetryTrace>,
366 publishProgress: boolean,
345367 ) {
346368 if (target._tag === "RelayConnectionTarget") {
347369 return yield* traceRelayEstablishment(
348 establishConnection(attempt, generation, lastFailure),
370 establishConnection(attempt, generation, lastFailure, publishProgress),
349371 attempt,
350372 generation,
351373 pendingRetry,
352374 );
353375 }
354 return yield* establishConnection(attempt, generation, lastFailure).pipe(
376 return yield* establishConnection(attempt, generation, lastFailure, publishProgress).pipe(
355377 Effect.map((lease) => ({
356378 attemptSpan: Option.none<Tracer.Span>(),
357379 lease,
......@@ -363,6 +385,37 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
363385 );
364386 });
365387
388 const forkScopedTracedConnection = Effect.fnUntraced(function* (
389 attempt: number,
390 generation: number,
391 lastFailure: ConnectionAttemptError | null,
392 pendingRetry: Option.Option<PendingRetryTrace>,
393 publishProgress: boolean,
394 ) {
395 const parentScope = yield* Scope.Scope;
396 return yield* Effect.uninterruptibleMask((restore) =>
397 Effect.gen(function* () {
398 const connectionScope = yield* Scope.fork(parentScope, "sequential");
399 const fiber = yield* restore(
400 establishTracedConnection(
401 attempt,
402 generation,
403 lastFailure,
404 pendingRetry,
405 publishProgress,
406 ).pipe(
407 Scope.provide(connectionScope),
408 Effect.map(
409 (established) =>
410 ({ ...established, scope: connectionScope }) satisfies ScopedConnection,
411 ),
412 ),
413 ).pipe(Effect.forkChild);
414 return { fiber, scope: connectionScope };
415 }),
416 );
417 });
418
366419 const waitForEstablishmentInterrupt = Effect.fnUntraced(function* () {
367420 for (;;) {
368421 const next = yield* Queue.take(signals);
......@@ -484,32 +537,136 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
484537 }
485538 });
486539
487 const waitForAuthorizationRefresh = Effect.fnUntraced(function* (
540 const waitForAuthorizationDeadline = Effect.fnUntraced(function* (
488541 preparedConnection: PreparedConnection,
542 skewMs: number,
489543 ) {
490544 const authorization = preparedConnection.httpAuthorization;
491545 if (authorization?._tag !== "Dpop") {
492546 return yield* Effect.never;
493547 }
494548 const now = yield* Clock.currentTimeMillis;
495 yield* Effect.sleep(
496 Math.max(0, authorization.expiresAtEpochMs - now - DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS),
549 yield* Effect.sleep(Math.max(0, authorization.expiresAtEpochMs - now - skewMs));
550 });
551
552 const waitForActiveCompletion = Effect.fnUntraced(function* (active: ScopedConnection) {
553 const exit = yield* exitUnlessInterrupted(
554 Effect.raceAllFirst([
555 active.lease.session.closed.pipe(
556 Effect.mapError((error): TracedAttemptFailure => ({
557 error,
558 attemptSpan: active.attemptSpan,
559 })),
560 ),
561 monitorConnectedLease(active.lease).pipe(
562 Effect.mapError((error): TracedAttemptFailure => ({
563 error,
564 attemptSpan: active.attemptSpan,
565 })),
566 ),
567 ]),
497568 );
498 yield* Effect.logDebug("Refreshing the environment connection before its DPoP token expires.");
499 return true;
569 return { _tag: "ActiveCompleted", exit } satisfies ConnectedLeaseEvent;
570 });
571
572 const prepareReplacement = Effect.fnUntraced(function* (
573 active: ScopedConnection,
574 generation: number,
575 ) {
576 yield* waitForAuthorizationDeadline(active.lease.prepared, DPOP_ACCESS_TOKEN_REFRESH_SKEW_MS);
577 yield* Effect.logDebug(
578 "Preparing a replacement environment connection before its DPoP token expires.",
579 );
580
581 let failureCount = 0;
582 for (;;) {
583 const candidate = yield* forkScopedTracedConnection(
584 failureCount + 1,
585 generation,
586 null,
587 Option.none(),
588 false,
589 );
590 const replacement = yield* Effect.raceAllFirst([
591 Fiber.await(candidate.fiber).pipe(
592 Effect.map((exit): ReplacementPreparationEvent => ({ _tag: "Completed", exit })),
593 ),
594 waitForAuthorizationDeadline(active.lease.prepared, 0).pipe(
595 Effect.as<ReplacementPreparationEvent>({ _tag: "AuthorizationExpired" }),
596 ),
597 Effect.sleep(CONNECTION_ESTABLISHMENT_TIMEOUT).pipe(
598 Effect.as<ReplacementPreparationEvent>({ _tag: "TimedOut" }),
599 ),
600 ]);
601
602 if (replacement._tag !== "Completed") {
603 yield* Fiber.interrupt(candidate.fiber);
604 yield* Fiber.await(candidate.fiber);
605 yield* Scope.close(candidate.scope, Exit.void).pipe(Effect.ignore);
606 } else if (Exit.isFailure(replacement.exit)) {
607 yield* Scope.close(candidate.scope, Exit.void).pipe(Effect.ignore);
608 }
609 if (replacement._tag === "AuthorizationExpired") {
610 return Option.none<ScopedConnection>();
611 }
612 if (replacement._tag === "Completed" && Exit.isSuccess(replacement.exit)) {
613 return Option.some(replacement.exit.value);
614 }
615
616 let replacementError: ConnectionTransientError;
617 if (replacement._tag === "Completed") {
618 if (Exit.isSuccess(replacement.exit)) {
619 return yield* Effect.die("A successful replacement was not installed.");
620 }
621 const failure = Cause.findErrorOption(replacement.exit.cause);
622 if (Option.isNone(failure) || failure.value.error._tag === "ConnectionBlockedError") {
623 return yield* Effect.failCause(replacement.exit.cause);
624 }
625 replacementError = failure.value.error;
626 } else {
627 replacementError = new ConnectionTransientError({
628 reason: "timeout",
629 detail: `${target.label} did not respond during connection setup.`,
630 });
631 }
632
633 const retryDelay = retryDelayMs(failureCount);
634 failureCount += 1;
635 yield* Effect.logWarning(
636 "Could not prepare a replacement environment connection; keeping the active connection.",
637 ).pipe(
638 Effect.annotateLogs({
639 "authorization.refresh.retry_delay_ms": retryDelay,
640 ...safeErrorLogAttributes(replacementError),
641 }),
642 );
643 const retryBeforeExpiry = yield* Effect.raceFirst(
644 Effect.sleep(retryDelay).pipe(Effect.as(true)),
645 waitForAuthorizationDeadline(active.lease.prepared, 0).pipe(Effect.as(false)),
646 );
647 if (!retryBeforeExpiry) {
648 return Option.none<ScopedConnection>();
649 }
650 }
500651 });
501652
502653 const runAttempt = Effect.fnUntraced(function* (
503654 attempt: number,
504 generation: number,
655 previousGeneration: number,
505656 lastFailure: ConnectionAttemptError | null,
506657 pendingRetry: Option.Option<PendingRetryTrace>,
507658 ) {
659 const initialGeneration = previousGeneration + 1;
508660 yield* SubscriptionRef.set(prepared, Option.none());
661 const initial = yield* forkScopedTracedConnection(
662 attempt,
663 initialGeneration,
664 lastFailure,
665 pendingRetry,
666 true,
667 );
509668 const establishment = yield* Effect.raceAllFirst([
510 exitUnlessInterrupted(
511 establishTracedConnection(attempt, generation, lastFailure, pendingRetry),
512 ).pipe(
669 Fiber.await(initial.fiber).pipe(
513670 Effect.map((exit): EstablishmentEvent => ({
514671 _tag: "Completed",
515672 exit,
......@@ -526,10 +683,18 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
526683 ),
527684 ]);
528685
686 if (establishment._tag !== "Completed") {
687 yield* Fiber.interrupt(initial.fiber);
688 yield* Fiber.await(initial.fiber);
689 yield* Scope.close(initial.scope, Exit.void).pipe(Effect.ignore);
690 } else if (Exit.isFailure(establishment.exit)) {
691 yield* Scope.close(initial.scope, Exit.void).pipe(Effect.ignore);
692 }
529693 if (establishment._tag === "Interrupted") {
530694 return {
531695 _tag: "Interrupted",
532696 established: false,
697 generation: previousGeneration,
533698 stable: false,
534699 resetRetry: establishment.resetRetry,
535700 } satisfies AttemptOutcome;
......@@ -538,6 +703,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
538703 return {
539704 _tag: "Failure",
540705 established: false,
706 generation: previousGeneration,
541707 stable: false,
542708 failure: {
543709 error: new ConnectionTransientError({
......@@ -552,7 +718,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
552718 const isUnexpectedDefect =
553719 !Cause.hasInterruptsOnly(establishment.exit.cause) &&
554720 !establishment.exit.cause.reasons.some(Cause.isFailReason);
555 const outcome = failureFromExit(target, establishment.exit, false, false);
721 const outcome = failureFromExit(target, establishment.exit, false, previousGeneration, false);
556722 if (isUnexpectedDefect) {
557723 const defect = establishment.exit.cause.reasons.find(Cause.isDieReason)?.defect;
558724 yield* Effect.logError("Connection attempt failed with an unexpected defect.").pipe(
......@@ -567,18 +733,20 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
567733 return outcome;
568734 }
569735
570 const active = establishment.exit.value;
571736 const currentIntent = yield* Ref.get(intent);
572737 if (!currentIntent.desired || currentIntent.network === "offline") {
573738 return {
574739 _tag: "Interrupted",
575740 established: false,
741 generation: previousGeneration,
576742 stable: false,
577743 resetRetry: false,
578744 } satisfies AttemptOutcome;
579745 }
580746
581747 const connectedAt = yield* Clock.currentTimeMillis;
748 let active = establishment.exit.value;
749 let activeGeneration = initialGeneration;
582750 yield* SubscriptionRef.set(prepared, Option.some(active.lease.prepared));
583751 yield* SubscriptionRef.set(session, Option.some(active.lease.session));
584752 yield* setState({
......@@ -587,36 +755,78 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
587755 phase: "connected",
588756 stage: null,
589757 attempt,
590 generation,
758 generation: activeGeneration,
591759 lastFailure: null,
592760 retryAt: null,
593761 });
594762
595 const connectedExit = yield* Effect.raceAllFirst([
596 active.lease.session.closed.pipe(
597 Effect.mapError((error): TracedAttemptFailure => ({
598 error,
599 attemptSpan: active.attemptSpan,
600 })),
601 ),
602 monitorConnectedLease(active.lease).pipe(
603 Effect.mapError((error): TracedAttemptFailure => ({
604 error,
605 attemptSpan: active.attemptSpan,
606 })),
607 ),
608 waitForAuthorizationRefresh(active.lease.prepared),
609 ]).pipe(exitUnlessInterrupted);
610 const connectedForMs = (yield* Clock.currentTimeMillis) - connectedAt;
611 if (Exit.isSuccess(connectedExit)) {
612 return {
613 _tag: "Interrupted",
614 established: true,
615 stable: connectedForMs >= BACKOFF_RESET_AFTER_MS,
616 resetRetry: connectedExit.value,
617 } satisfies AttemptOutcome;
763 for (;;) {
764 const connectedEvent = yield* Effect.raceAllFirst([
765 waitForActiveCompletion(active),
766 exitUnlessInterrupted(prepareReplacement(active, activeGeneration + 1)).pipe(
767 Effect.map((exit): ConnectedLeaseEvent => ({ _tag: "ReplacementCompleted", exit })),
768 ),
769 ]);
770 const stable = (yield* Clock.currentTimeMillis) - connectedAt >= BACKOFF_RESET_AFTER_MS;
771 if (connectedEvent._tag === "ActiveCompleted") {
772 if (Exit.isSuccess(connectedEvent.exit)) {
773 return {
774 _tag: "Interrupted",
775 established: true,
776 generation: activeGeneration,
777 stable,
778 resetRetry: connectedEvent.exit.value,
779 } satisfies AttemptOutcome;
780 }
781 return failureFromExit(target, connectedEvent.exit, true, activeGeneration, stable);
782 }
783 if (Exit.isFailure(connectedEvent.exit)) {
784 return failureFromExit(target, connectedEvent.exit, true, activeGeneration, stable);
785 }
786 if (Option.isNone(connectedEvent.exit.value)) {
787 return {
788 _tag: "Interrupted",
789 established: true,
790 generation: activeGeneration,
791 stable,
792 resetRetry: true,
793 } satisfies AttemptOutcome;
794 }
795
796 const candidate = connectedEvent.exit.value.value;
797 const replacementIntent = yield* Ref.get(intent);
798 if (!replacementIntent.desired || replacementIntent.network === "offline") {
799 yield* Scope.close(candidate.scope, Exit.void).pipe(Effect.ignore);
800 return {
801 _tag: "Interrupted",
802 established: true,
803 generation: activeGeneration,
804 stable,
805 resetRetry: false,
806 } satisfies AttemptOutcome;
807 }
808
809 const previous = active;
810 yield* Effect.uninterruptible(
811 Effect.gen(function* () {
812 active = candidate;
813 activeGeneration += 1;
814 yield* SubscriptionRef.set(prepared, Option.some(active.lease.prepared));
815 yield* SubscriptionRef.set(session, Option.some(active.lease.session));
816 yield* setState({
817 desired: true,
818 network: replacementIntent.network,
819 phase: "connected",
820 stage: null,
821 attempt: 1,
822 generation: activeGeneration,
823 lastFailure: null,
824 retryAt: null,
825 });
826 yield* Scope.close(previous.scope, Exit.void).pipe(Effect.ignore);
827 }),
828 );
618829 }
619 return failureFromExit(target, connectedExit, true, connectedForMs >= BACKOFF_RESET_AFTER_MS);
620830 }, Effect.ensuring(clearLease));
621831
622832 const waitForRetrySignal = Effect.fnUntraced(function* (delayMs: number) {
......@@ -681,15 +891,14 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
681891 }
682892
683893 const attempt = failureCount + 1;
684 const nextGeneration = generation + 1;
685894 const outcome: AttemptOutcome = yield* Effect.scoped(
686 runAttempt(attempt, nextGeneration, latestFailure, pendingRetry),
895 runAttempt(attempt, generation, latestFailure, pendingRetry),
687896 );
688897 // Consumed on every iteration so a stale marker can never leak into a
689898 // later, unrelated failure.
690899 const failedWakeProbe = yield* Ref.getAndSet(wakeProbeFailed, false);
900 generation = outcome.generation;
691901 if (outcome.established) {
692 generation = nextGeneration;
693902 if (outcome.stable) {
694903 resetRetryLadder();
695904 latestFailure = null;