fix(tern): sequential operation-scoped drives leave the parent apply to the projection - #1074
Conversation
…to the projection A multi-operation drive holds only its operation lease; the parent applies row is owned by the operator's rollout projection, and storage fails a drive's direct parent write closed. The grouped resume path already honors this, but the sequential path wrote the parent at four points — the mark-running before task work, the completed write when a re-plan finds no remaining work, terminal settlement in finalizeSequentialApply, and the fail/retryable helpers — so an operation-scoped sequential resume aborted on the refused write before driving any task, and the operator re-claimed and re-refused the operation on every tick while the schema change never progressed. The engine terminal-truth reconcile at the head of every resume had the same refused-write abort when a pending stop or cancel raced an already-terminal engine outcome. The sequential path now mirrors the grouped path: under an operation-only lease the drive persists its task outcomes (including cancelling queued tasks behind a failure) and exits, leaving the parent state, control-request resolution, apply-level metrics, and the terminal summary to the operator's projection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes operation-scoped drives for multi-operation applies by making the sequential resume path projection-aware: under an operation-only lease, the drive now settles only its own tasks and avoids parent applies writes that storage correctly rejects, leaving parent-state updates and terminal UX to the operator’s rollout projection.
Changes:
- Skip parent apply state updates and parent control-request completion during sequential resume when running under an operation-only lease.
- Ensure terminal-truth reconciliation adopts engine outcomes onto tasks without writing parent terminal state under an operation-only lease.
- Add integration coverage that drives an operation-scoped sequential resume through success, “no remaining work” re-plan, and engine rejection while asserting the parent apply row remains untouched.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/tern/local_control_resume.go | Makes sequential resume and recovery paths suppress parent apply writes under operation-only leases. |
| pkg/tern/local_control_reconcile.go | Suppresses parent terminal writes and pending-command completion during terminal-truth adoption for operation-scoped drives. |
| pkg/tern/local_control_multiop_resume_integration_test.go | Adds integration tests validating operation-scoped sequential resume settles tasks without mutating the parent apply row. |
| pkg/tern/local_apply_sequential.go | Updates sequential finalization to settle/cancel tasks only (no parent apply writes) when operation-scoped. |
| pkg/tern/local_apply_failure.go | Updates failure/retryable paths to settle tasks only (no parent apply writes/metrics/log stream) when operation-scoped. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…taches A deployment-keyed apply is the shared container for one keyed generation of sibling dispatches, but a fast sibling completing first terminalized it and sealed it against every slower sibling and the group finalizer, so only ~one shard of a sharded schema change converged per dispatch attempt. AttachOperationWithTasks now reopens a COMPLETED apply under the target lock: it re-runs the one-active-apply-per-target overlap check (completion released the reservation) and moves the apply back to running with its completion cleared. Failed, cancelled, and reverted applies still refuse with ErrApplyNotActive — those need operator reconciliation. The client surfaces a reopen with an info log, a reopened_completed attach metric, and an active-applies gauge increment for the terminal-to-active transition the operator's projection cannot observe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation attaches" This reverts commit 4512ce0.
…cleanly A grouped-mode engine failure under an operation-only lease durably settles the operation's tasks as failed; the drive returning that error made the claim loop read the drive as transiently failed and leave the already-settled operation claimable on every poll. The drive now returns nil so the claim loop persists the operation row from its failed tasks immediately and the projection settles the parent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Review findings - created by Kiran's code review agent - for schemabot/pull/1074, 62c516a. Verdict: 7 findings — 2 blocking (unconditional parent-apply writes still reachable under an operation-lease-only ctx), 3 non-blocking, 2 general suggestions. Blocking
Non-blocking
General suggestions
The one thing that could have broken, verifiedThe riskiest mechanism is Verified correct
This review was generated by Claude Code (claude-sonnet-5). |
…ration lease Three control paths could still write the parent applies row from an operation-lease-only drive, where storage refuses the write and the refusal turns settled work into a drive error the claim loop re-runs forever: consuming a pending start on a waiting_for_deploy parent, settling a stop/cancel the engine rejected as already completed, and marking the parent cutting_over before dispatching a deferred cutover. Each now settles the work the drive owns and leaves the parent row to the operator's projection. The sequential finalizer also adopts the settled outcome into the in-memory apply under an operation lease, so the drive's own logs report what the operation settled to instead of the projection's stale running state, and the suppressed failure paths no longer log the same settle twice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 All seven findings addressed in fdccef5:
This reply was written by Claude Code (Claude Fable 5). |
Why this matters
When one schema change targets multiple deployments, SchemaBot creates a single apply with one operation per deployment and drives each operation separately. A driver working on one operation owns only that operation — the shared parent apply row belongs to the operator, which watches all the operations and derives the parent's state from their outcomes. Storage enforces this ownership: a driver holding only an operation lease gets refused if it tries to write the parent row directly.
The grouped resume path respects that rule. The sequential resume path did not: the first thing it did on every drive was write "running" onto the parent row. Storage refused the write, the drive gave up before starting any task work, and the operator re-claimed the operation on the next tick and failed the exact same way. The schema change made no progress, forever:
What it does
The sequential path now behaves like the grouped path. A driver holding only an operation lease:
Integration tests drive one operation of a two-operation apply through three endings — success, nothing left to do after a re-plan, and an engine failure — and verify the tasks settle while the parent row is never written by the drive.
How it moves us toward the northstar
The northstar puts one data-plane apply behind each deployment, so several operations sharing one parent apply is the normal shape of every multi-deployment schema change — not an edge case. This closes the last drive path that could not make progress under the operator-owned parent model.
Opened by Claude (Fable 5).