Repository navigation
fix(server): preserve the crash recovery budget across restarts #25603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6d528a0
21c01ef
45acfbc
e112e6f
7e0a53e
2432ccb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ import * as Context from "effect/Context"; | |
| import * as DateTime from "effect/DateTime"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Layer from "effect/Layer"; | ||
| import * as Option from "effect/Option"; | ||
| import * as Schema from "effect/Schema"; | ||
|
|
||
| import * as EffectOutbox from "./EffectOutbox.ts"; | ||
|
|
@@ -839,13 +840,39 @@ export const make = Effect.gen(function* () { | |
| now, | ||
| ) | ||
| : undefined; | ||
| let processLossCount = 1; | ||
| // Normal upgrades must not exhaust an unfinished turn's crash allowance. | ||
| if ( | ||
| continuationRun?.restartContinuationOfRunId !== undefined && | ||
| continuationRun.status !== "completed" && | ||
| continuationRun.status !== "waiting" | ||
| ) { | ||
| const previous = yield* outbox | ||
| .get(`effect:restart-continuation:${continuationRun.restartContinuationOfRunId}`) | ||
| .pipe( | ||
| Effect.mapError( | ||
| (cause) => | ||
| new ProviderRuntimeRecoveryError({ | ||
| operation: "reconcile", | ||
| threadId: projection.thread.id, | ||
| cause, | ||
| }), | ||
| ), | ||
| ); | ||
| if (Option.isSome(previous) && previous.value.request.type === "provider-runtime.continue") | ||
| processLossCount += previous.value.request.processLossCount ?? 0; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the legacy chain guard when the predecessor budget is unknown. When an unplanned restart finds a running continuation whose predecessor intent has no The count budget still limits the result. Counts 2 and 3 continue, and count 4 is declined. The impact is at most three extra recoveries for each affected chain, and only when the restart lands on a running third continuation. Leave the count undefined when the predecessor budget is unknown. The legacy guard then applies to that chain, as it did before this change. Suggested fix
|
||
| } | ||
| const effects: Array<EffectOutbox.PendingOrchestrationEffectV2> = continuationRun | ||
| ? [ | ||
| { | ||
| id: `effect:restart-continuation:${continuationRun.id}`, | ||
| commandId, | ||
| threadId: projection.thread.id, | ||
| request: { type: "provider-runtime.continue", sourceRunId: continuationRun.id }, | ||
| request: { | ||
| type: "provider-runtime.continue", | ||
| sourceRunId: continuationRun.id, | ||
| processLossCount, | ||
| }, | ||
| }, | ||
| ] | ||
| : []; | ||
|
|
@@ -1020,7 +1047,11 @@ export const make = Effect.gen(function* () { | |
| id: `effect:restart-continuation:${run.id}`, | ||
| commandId, | ||
| threadId, | ||
| request: { type: "provider-runtime.continue", sourceRunId: run.id }, | ||
| request: { | ||
| type: "provider-runtime.continue", | ||
| sourceRunId: run.id, | ||
| processLossCount: 0, | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the predecessor's crash budget until the failed-start retry reads it.
When a continuation run commits
failed, its predecessor intent is no longer protected frompruneSettled. The protection query counts only live statuses, andfailedis not one of them. The retry lookup atOrchestrator.ts:3258runs later, in the run-update consumer, outside the transaction that committed the failure. The prune worker does not take the thread lock. If the predecessor'scompleted_atis older than the seven-day retention, a prune batch can delete it in that gap. The lookup then returns none, and the retry writes an intent withoutprocessLossCount. That drops the cumulative count.This needs a continuation that stays live for more than seven days before it fails, and a prune run that lands in the gap. The likelihood is low, but no ordering guarantee in the code rules it out.
Keep the predecessor protected while a failed continuation's retry is still pending. The fix belongs in the prune predicate or in how the count reaches the retry. Verify the chosen approach against the delivery path before merging.