| ... | ... | @@ -60,16 +60,24 @@ interface TracedAttemptFailure { |
| 60 | 60 | readonly attemptSpan: Option.Option<Tracer.Span>; |
| 61 | 61 | } |
| 62 | 62 | |
| 63 | interface ScopedConnection { |
| 64 | readonly attemptSpan: Option.Option<Tracer.Span>; |
| 65 | readonly lease: ConnectionDriver.EnvironmentConnectionLease; |
| 66 | readonly scope: Scope.Closeable; |
| 67 | } |
| 68 | |
| 63 | 69 | type AttemptOutcome = |
| 64 | 70 | | { |
| 65 | 71 | readonly _tag: "Interrupted"; |
| 66 | 72 | readonly established: boolean; |
| 73 | readonly generation: number; |
| 67 | 74 | readonly stable: boolean; |
| 68 | 75 | readonly resetRetry: boolean; |
| 69 | 76 | } |
| 70 | 77 | | { |
| 71 | 78 | readonly _tag: "Failure"; |
| 72 | 79 | readonly established: boolean; |
| 80 | readonly generation: number; |
| 73 | 81 | readonly stable: boolean; |
| 74 | 82 | readonly failure: TracedAttemptFailure; |
| 75 | 83 | }; |
| ... | ... | @@ -77,17 +85,26 @@ type AttemptOutcome = |
| 77 | 85 | type EstablishmentEvent = |
| 78 | 86 | | { |
| 79 | 87 | 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>; |
| 87 | 89 | } |
| 88 | 90 | | { readonly _tag: "Interrupted"; readonly resetRetry: boolean } |
| 89 | 91 | | { readonly _tag: "TimedOut" }; |
| 90 | 92 | |
| 93 | type ReplacementPreparationEvent = |
| 94 | | { readonly _tag: "Completed"; readonly exit: Exit.Exit<ScopedConnection, TracedAttemptFailure> } |
| 95 | | { readonly _tag: "TimedOut" } |
| 96 | | { readonly _tag: "AuthorizationExpired" }; |
| 97 | |
| 98 | type 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 | |
| 91 | 108 | function exitUnlessInterrupted<A, E, R>( |
| 92 | 109 | effect: Effect.Effect<A, E, R>, |
| 93 | 110 | ): Effect.Effect<Exit.Exit<A, E>, never, R> { |
| ... | ... | @@ -168,16 +185,18 @@ function failureFromExit<A>( |
| 168 | 185 | target: ConnectionTarget, |
| 169 | 186 | exit: Exit.Exit<A, TracedAttemptFailure>, |
| 170 | 187 | established: boolean, |
| 188 | generation: number, |
| 171 | 189 | stable: boolean, |
| 172 | 190 | ): AttemptOutcome { |
| 173 | 191 | 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 }; |
| 175 | 193 | } |
| 176 | 194 | const typedFailure = exit.cause.reasons.find(Cause.isFailReason); |
| 177 | 195 | if (typedFailure) { |
| 178 | 196 | return { |
| 179 | 197 | _tag: "Failure", |
| 180 | 198 | established, |
| 199 | generation, |
| 181 | 200 | stable, |
| 182 | 201 | failure: typedFailure.error, |
| 183 | 202 | }; |
| ... | ... | @@ -185,6 +204,7 @@ function failureFromExit<A>( |
| 185 | 204 | return { |
| 186 | 205 | _tag: "Failure", |
| 187 | 206 | established, |
| 207 | generation, |
| 188 | 208 | stable, |
| 189 | 209 | failure: { |
| 190 | 210 | error: new ConnectionTransientError({ |
| ... | ... | @@ -289,9 +309,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 289 | 309 | attempt: number, |
| 290 | 310 | generation: number, |
| 291 | 311 | lastFailure: ConnectionAttemptError | null, |
| 312 | publishProgress: boolean, |
| 292 | 313 | ) { |
| 293 | 314 | return yield* driver.connect(entry, (progress) => |
| 294 | | reportProgress(attempt, generation, lastFailure, progress), |
| 315 | publishProgress ? reportProgress(attempt, generation, lastFailure, progress) : Effect.void, |
| 295 | 316 | ); |
| 296 | 317 | }); |
| 297 | 318 | |
| ... | ... | @@ -342,16 +363,17 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 342 | 363 | generation: number, |
| 343 | 364 | lastFailure: ConnectionAttemptError | null, |
| 344 | 365 | pendingRetry: Option.Option<PendingRetryTrace>, |
| 366 | publishProgress: boolean, |
| 345 | 367 | ) { |
| 346 | 368 | if (target._tag === "RelayConnectionTarget") { |
| 347 | 369 | return yield* traceRelayEstablishment( |
| 348 | | establishConnection(attempt, generation, lastFailure), |
| 370 | establishConnection(attempt, generation, lastFailure, publishProgress), |
| 349 | 371 | attempt, |
| 350 | 372 | generation, |
| 351 | 373 | pendingRetry, |
| 352 | 374 | ); |
| 353 | 375 | } |
| 354 | | return yield* establishConnection(attempt, generation, lastFailure).pipe( |
| 376 | return yield* establishConnection(attempt, generation, lastFailure, publishProgress).pipe( |
| 355 | 377 | Effect.map((lease) => ({ |
| 356 | 378 | attemptSpan: Option.none<Tracer.Span>(), |
| 357 | 379 | lease, |
| ... | ... | @@ -363,6 +385,37 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 363 | 385 | ); |
| 364 | 386 | }); |
| 365 | 387 | |
| 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 | |
| 366 | 419 | const waitForEstablishmentInterrupt = Effect.fnUntraced(function* () { |
| 367 | 420 | for (;;) { |
| 368 | 421 | const next = yield* Queue.take(signals); |
| ... | ... | @@ -484,32 +537,136 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 484 | 537 | } |
| 485 | 538 | }); |
| 486 | 539 | |
| 487 | | const waitForAuthorizationRefresh = Effect.fnUntraced(function* ( |
| 540 | const waitForAuthorizationDeadline = Effect.fnUntraced(function* ( |
| 488 | 541 | preparedConnection: PreparedConnection, |
| 542 | skewMs: number, |
| 489 | 543 | ) { |
| 490 | 544 | const authorization = preparedConnection.httpAuthorization; |
| 491 | 545 | if (authorization?._tag !== "Dpop") { |
| 492 | 546 | return yield* Effect.never; |
| 493 | 547 | } |
| 494 | 548 | 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 | ]), |
| 497 | 568 | ); |
| 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 | } |
| 500 | 651 | }); |
| 501 | 652 | |
| 502 | 653 | const runAttempt = Effect.fnUntraced(function* ( |
| 503 | 654 | attempt: number, |
| 504 | | generation: number, |
| 655 | previousGeneration: number, |
| 505 | 656 | lastFailure: ConnectionAttemptError | null, |
| 506 | 657 | pendingRetry: Option.Option<PendingRetryTrace>, |
| 507 | 658 | ) { |
| 659 | const initialGeneration = previousGeneration + 1; |
| 508 | 660 | yield* SubscriptionRef.set(prepared, Option.none()); |
| 661 | const initial = yield* forkScopedTracedConnection( |
| 662 | attempt, |
| 663 | initialGeneration, |
| 664 | lastFailure, |
| 665 | pendingRetry, |
| 666 | true, |
| 667 | ); |
| 509 | 668 | const establishment = yield* Effect.raceAllFirst([ |
| 510 | | exitUnlessInterrupted( |
| 511 | | establishTracedConnection(attempt, generation, lastFailure, pendingRetry), |
| 512 | | ).pipe( |
| 669 | Fiber.await(initial.fiber).pipe( |
| 513 | 670 | Effect.map((exit): EstablishmentEvent => ({ |
| 514 | 671 | _tag: "Completed", |
| 515 | 672 | exit, |
| ... | ... | @@ -526,10 +683,18 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 526 | 683 | ), |
| 527 | 684 | ]); |
| 528 | 685 | |
| 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 | } |
| 529 | 693 | if (establishment._tag === "Interrupted") { |
| 530 | 694 | return { |
| 531 | 695 | _tag: "Interrupted", |
| 532 | 696 | established: false, |
| 697 | generation: previousGeneration, |
| 533 | 698 | stable: false, |
| 534 | 699 | resetRetry: establishment.resetRetry, |
| 535 | 700 | } satisfies AttemptOutcome; |
| ... | ... | @@ -538,6 +703,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 538 | 703 | return { |
| 539 | 704 | _tag: "Failure", |
| 540 | 705 | established: false, |
| 706 | generation: previousGeneration, |
| 541 | 707 | stable: false, |
| 542 | 708 | failure: { |
| 543 | 709 | error: new ConnectionTransientError({ |
| ... | ... | @@ -552,7 +718,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 552 | 718 | const isUnexpectedDefect = |
| 553 | 719 | !Cause.hasInterruptsOnly(establishment.exit.cause) && |
| 554 | 720 | !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); |
| 556 | 722 | if (isUnexpectedDefect) { |
| 557 | 723 | const defect = establishment.exit.cause.reasons.find(Cause.isDieReason)?.defect; |
| 558 | 724 | yield* Effect.logError("Connection attempt failed with an unexpected defect.").pipe( |
| ... | ... | @@ -567,18 +733,20 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 567 | 733 | return outcome; |
| 568 | 734 | } |
| 569 | 735 | |
| 570 | | const active = establishment.exit.value; |
| 571 | 736 | const currentIntent = yield* Ref.get(intent); |
| 572 | 737 | if (!currentIntent.desired || currentIntent.network === "offline") { |
| 573 | 738 | return { |
| 574 | 739 | _tag: "Interrupted", |
| 575 | 740 | established: false, |
| 741 | generation: previousGeneration, |
| 576 | 742 | stable: false, |
| 577 | 743 | resetRetry: false, |
| 578 | 744 | } satisfies AttemptOutcome; |
| 579 | 745 | } |
| 580 | 746 | |
| 581 | 747 | const connectedAt = yield* Clock.currentTimeMillis; |
| 748 | let active = establishment.exit.value; |
| 749 | let activeGeneration = initialGeneration; |
| 582 | 750 | yield* SubscriptionRef.set(prepared, Option.some(active.lease.prepared)); |
| 583 | 751 | yield* SubscriptionRef.set(session, Option.some(active.lease.session)); |
| 584 | 752 | yield* setState({ |
| ... | ... | @@ -587,36 +755,78 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 587 | 755 | phase: "connected", |
| 588 | 756 | stage: null, |
| 589 | 757 | attempt, |
| 590 | | generation, |
| 758 | generation: activeGeneration, |
| 591 | 759 | lastFailure: null, |
| 592 | 760 | retryAt: null, |
| 593 | 761 | }); |
| 594 | 762 | |
| 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 | ); |
| 618 | 829 | } |
| 619 | | return failureFromExit(target, connectedExit, true, connectedForMs >= BACKOFF_RESET_AFTER_MS); |
| 620 | 830 | }, Effect.ensuring(clearLease)); |
| 621 | 831 | |
| 622 | 832 | const waitForRetrySignal = Effect.fnUntraced(function* (delayMs: number) { |
| ... | ... | @@ -681,15 +891,14 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 681 | 891 | } |
| 682 | 892 | |
| 683 | 893 | const attempt = failureCount + 1; |
| 684 | | const nextGeneration = generation + 1; |
| 685 | 894 | const outcome: AttemptOutcome = yield* Effect.scoped( |
| 686 | | runAttempt(attempt, nextGeneration, latestFailure, pendingRetry), |
| 895 | runAttempt(attempt, generation, latestFailure, pendingRetry), |
| 687 | 896 | ); |
| 688 | 897 | // Consumed on every iteration so a stale marker can never leak into a |
| 689 | 898 | // later, unrelated failure. |
| 690 | 899 | const failedWakeProbe = yield* Ref.getAndSet(wakeProbeFailed, false); |
| 900 | generation = outcome.generation; |
| 691 | 901 | if (outcome.established) { |
| 692 | | generation = nextGeneration; |
| 693 | 902 | if (outcome.stable) { |
| 694 | 903 | resetRetryLadder(); |
| 695 | 904 | latestFailure = null; |