fix(tern): transient storage errors no longer terminalize in-flight applies#642
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens Tern apply recovery by ensuring transient storage failures don’t incorrectly terminalize in-flight applies (which can orphan engine-side work like checkpointed copies or live Vitess deploy requests), aligning storage-error handling with the intended “fail closed but retryable” operational model.
Changes:
- Split recovery plan-load handling so storage read errors return an error (release claim / retry later) while only a confirmed-missing plan row terminalizes the apply.
- After a grouped Vitess apply is accepted, treat failures to persist engine resume state as retryable (
failed_retryable) instead of terminal failure, preventing orphaned deploy requests. - Add targeted tests covering both the transient plan-load error path and the post-accept resume-state save-failure path.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/tern/local_control_resume.go | Separates plan-load storage errors from confirmed-missing plan rows during recovery, returning errors for transient failures. |
| pkg/tern/local_control_resume_test.go | Adds recovery tests verifying storage errors remain recoverable and missing plans terminalize with observer notification. |
| pkg/tern/local_client_test.go | Extends test storage with Plans() and adds a grouped-apply test asserting post-accept save failures pause as failed_retryable. |
| pkg/tern/local_apply_grouped.go | Switches post-accept resume-state save failures to failed_retryable for Vitess grouped applies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
aparajon
marked this pull request as ready for review
July 13, 2026 17:28
aparajon
requested review from
Kiran01bm,
eeSeeGee,
jayjanssen and
morgo
as code owners
July 13, 2026 17:28
…pplies Two recovery-adjacent paths converted a transient storage failure into a permanently failed apply while the engine-side work was untouched: - Crash recovery conflated a storage error on the plan load with a genuinely missing plan row and marked the apply failed. The two cases are now split: a read failure exits the recovery attempt with an error so the claim is released and a later attempt retries against intact storage; only a confirmed-missing plan row terminalizes the apply. - After the engine accepted a grouped Vitess apply (deploy request live on the provider), a failed resume-state save permanently failed the stored apply and orphaned the in-flight deploy request. The save failure now pauses the apply as failed_retryable — matching the identical save failure during progress polling — so the resume path reattaches to the in-flight work via its persisted state or the schema change context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Routing the confirmed-missing plan path through the shared terminalization helper keeps the terminal invariants consistent: CompletedAt/UpdatedAt are recorded, in-flight tasks fail with the same operator-facing reason, the active-applies gauge is decremented, and a reload prevents clobbering an apply that a concurrent Stop() already settled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on races When failApplyWithTasks or markApplyRetryableWithTasks finds the apply already settled by a concurrent actor (e.g. a raced Stop()), adopt the stored terminal state into the in-memory apply instead of leaving it stale. Callers that notify the terminal observer afterwards now deliver the settled verdict rather than the pre-failure in-flight state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aparajon
force-pushed
the
armand/transient-errors-not-terminal
branch
from
July 18, 2026 13:41
b0be0db to
023dbc5
Compare
morgo
approved these changes
Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this matters
SchemaBot's own storage database can be briefly unreachable — a credential rotation, a failover, a network blip. Two recovery paths treated that brief outage as if the schema change itself had failed, and permanently marked the apply failed even though the real work (a checkpointed copy on the target database, or a live PlanetScale deploy request) was still running fine. The user sees "failed" on their PR while the change actually completes — or keeps running with nothing tracking it.
What it does
1. Crash recovery no longer fails an apply just because it couldn't read the plan.
When recovery picks up an apply, it loads the plan to rebuild the reviewed DDL. Previously "the read errored" and "the plan row doesn't exist" were handled identically: mark the apply failed. Now they're distinguished:
2. A running Vitess apply is no longer abandoned over a bookkeeping write.
Once PlanetScale accepts the apply, the deploy request is live on the provider. If the follow-up write that saves the resume state (the record that lets a restart reattach) fails, the apply used to be marked failed — orphaning the live deploy. Now it pauses as
failed_retryable, and the retry reattaches to the existing deploy request instead of abandoning it. This matches how the same save failure was already handled during progress polling.3. If a Stop() lands at the same moment as a failure, the reported outcome is the Stop.
The failure helpers already refused to overwrite a concurrently-settled terminal state in storage, but they still notified the PR check with the stale in-flight state they were holding. Now they adopt the settled state first, so the check reports what actually happened (e.g.
stopped, not a leftoverrunning).Before / after
🤖 Generated with Claude Code