fix(storage): terminal apply states are immutable - #644
Draft
aparajon wants to merge 1 commit into
Draft
Conversation
There was a problem hiding this comment.
Pull request overview
This PR enforces “terminal apply state immutability” as a storage-layer safety invariant, preventing stale writers from moving an apply from a terminal verdict back into the active lifecycle, and hardens the stop-before-start normalization path with a CAS write to avoid clobbering concurrent state advances.
Changes:
- Add a storage guard in
Applies().Updatethat refuses terminal → active transitions with a distinctstorage.ErrApplyTerminalStateImmutable. - Disambiguate “0 rows affected” outcomes for guarded active-state updates by re-reading the row to distinguish missing vs terminal vs idempotent no-op.
- Update the stop-before-start handler to use
UpdateDerivedState(CAS) and add coverage for the concurrent-advance scenario.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/storage/storage.go | Documents new Update() behavior around terminal immutability. |
| pkg/storage/mysqlstore/applies.go | Implements terminal→active guard + zero-rows disambiguation helper. |
| pkg/storage/mysqlstore/applies_test.go | Adds/updates tests for terminal immutability and Update() missing-row behavior. |
| pkg/storage/errors.go | Introduces ErrApplyTerminalStateImmutable. |
| pkg/api/handlers_test.go | Extends staticApplyStore test double with UpdateDerivedState CAS semantics. |
| pkg/api/control_handlers.go | Uses UpdateDerivedState to CAS the stop-before-start normalization write. |
| pkg/api/control_handlers_test.go | Adds tests covering the CAS stop-before-start behavior and concurrent advance handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
aparajon
force-pushed
the
armand/terminal-state-guard
branch
from
July 18, 2026 17:25
739c1cd to
68a6f3d
Compare
aparajon
force-pushed
the
armand/terminal-state-guard
branch
from
August 7, 2026 14:32
68a6f3d to
c7aac95
Compare
Applies().Update was last-writer-wins: a caller holding a stale in-memory snapshot could write an active state over a terminal row, resurrecting a completed, failed, or stopped apply. Update now refuses terminal-to-active transitions in the WHERE clause and surfaces the refusal as a distinct ErrApplyTerminalStateImmutable, resolved from the ambiguous zero-rows result by a read-committed re-read of the row. Terminal-to-terminal writes (including same-state refreshes) stay allowed; a settled apply re-enters the active lifecycle only through the dedicated guarded transition of claiming a stopped apply. The stop-before-start normalization in the API layer now reloads the row and proceeds from the newer verdict instead of overwriting it, and guard errors surface the apply identifier rather than the internal row ID. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aparajon
force-pushed
the
armand/terminal-state-guard
branch
from
August 7, 2026 19:51
c7aac95 to
07eccbe
Compare
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
Applies().Updatewas last-writer-wins: any caller holding a stale in-memory snapshot could write an active state over a terminal row, silently resurrecting a completed, failed, or stopped apply. A resurrected apply re-enters driver claiming with no operator action behind it — the storage layer must refuse the transition regardless of which caller races.What it does
Updaterefuses terminal-to-active transitions in the WHERE clause itself: when the new state is active, the write matches only rows that are still active, so a concurrent terminal verdict can never be overwritten from a snapshot.ErrApplyTerminalStateImmutable, a missing row surfacesErrApplyNotFound, and a still-active row is a benign no-op.🤖 Generated with Claude Code