fix(operator): choose the drive mode from the generation manifest, not the attached row count - #1101
Conversation
…t the attached row count A deployment-keyed apply receives its operations one dispatch at a time, so the first-attached operation can be claimed while the generation manifest still expects siblings. The claim loop chose the drive mode from the attached row count alone, so that lone operation took the legacy parent-lease drive, whose finalizer terminalizes the parent apply when its own work settles — and every later sibling dispatch was then refused against the terminal apply, failing the rest of the generation one retry at a time. The drive-mode decision now consults the manifest: an apply whose manifest declares unattached operation keys drives under the operation lease only, so the parent applies row is moved solely by the operator's projection CAS, which already holds the whole-generation verdict until every declared key has attached and finished. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes an operator race for deployment-keyed applies by choosing the drive mode based on the apply’s generation manifest (declared expected operation keys), rather than the currently attached apply_operation row count, ensuring the parent apply cannot be terminalized while sibling operations are still expected to arrive.
Changes:
- Add
Apply.ManifestDeclaresUnattachedWorkto express “manifest expects siblings that haven’t attached yet.” - Update the operator claim/drive path to consult the parent apply’s manifest before selecting the legacy parent-lease vs operation-lease drive mode.
- Add an integration test that reproduces the “first op finishes before siblings attach” race and verifies the apply remains open until all manifest keys attach and finish.
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/storage/types.go | Adds a named predicate for detecting manifest-declared but unattached work. |
| pkg/storage/types_test.go | Adds unit coverage for the new manifest predicate behavior. |
| pkg/api/operator.go | Updates drive-mode selection to consult the generation manifest before choosing the legacy path. |
| pkg/api/operator_manifest_drive_integration_test.go | Adds an end-to-end integration test exercising late sibling attachment and whole-generation completion behavior. |
| pkg/api/handlers_test.go | Fixes the fake apply store to materialize the returned apply ID so reads by ID can succeed. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Compute the missing manifest keys once, log their count instead of the full list (a large generation would bloat the entry), and carry the claim's triage fields: lease_owner, operation_key, and the operation's own deployment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Review findings - created by Kiran's code review agent - for schemabot/pull/1101, ac9a5ea. Verdict: 8 findings — 0 blocking, 6 non-blocking, 2 suggestions. The drive-mode truth table is sound and the no-manifest legacy path is provably unregressed; the weak spots are the PR body's claims and the observability around the new hold. Non-blocking1. The PR body claims a named predicate that does not exist. "Adds 2. The new test proves a mechanism proxy, not the harm the PR claims to fix. Reverting the whole 3. Every healthy keyed rollout now trips the stranded-parent repair arm once a minute. With the lone operation completed and the sibling not yet dispatched, the parent dwells RUNNING with all attached rows terminal and a stale 4. The 5. Nothing bounds the manifest hold. If a declared sibling never attaches, the projection rewrites every verdict back to RUNNING forever; the operator's only expiry pass is 6. Banned comment form. operator_manifest_drive_integration_test.go:25 closes with "Without the manifest-aware mode decision the first drive would terminalize the parent…" — the exact shape AGENTS.md bans twice (§ No bug references in code: " General suggestions7. The parent apply is loaded, used once, and thrown away. operator.go:763's 8. The new Info log can't answer "which shard hasn't dispatched?". operator.go:785 logs The one thing that could have broken, verifiedWhether the new branch regresses the drive mode for ordinary non-keyed applies — the legacy parent-lease path carries the engine's parent writes, the per-driver terminal observer and the check-gate summary, so silently diverting it would break every single-deployment apply. Proved safe by enumeration plus mutation: the legacy path is reachable only when Verified correct
This review was generated by Claude Code (claude-opus-5). |
…e ownership A deployment-keyed apply whose attached operations have all settled matches the stranded-parent repair claim while its generation manifest still expects siblings. The projection result now marks that hold, so the repair arm logs it as a healthy rollout awaiting dispatches instead of warning that the target is blocked, and releases the driver tick to claim real operation work. The projection's active-applies gauge ownership now follows the drive mode via projectionOwnsActiveAppliesGauge: operation-lease-only drives suppress the parent-level gauge, so the projection releases it for multi-operation rollouts and for keyed applies whose manifest still expects unattached operations, including a lone attached operation that fails the generation permanently. The manifest drive-mode integration test now probes that a direct parent applies write is refused under the operation-only lease, so it fails on the real harm (a prematurely terminal parent and a refused sibling attach) rather than a drive-count proxy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Thanks for the thorough review — all eight findings triaged; four landed as code/body fixes in 9b72312 (new commits only, since the PR is approved), and
This comment was written by Claude Code (Fable 5) on Armand's behalf. |
…ew-drift-rollup * origin/main: (357 commits) fix(github): render each lint violation as its own bullet in unsafe-change comments (#1105) feat(engine): disclose at plan time whether an apply continues or discards a copy (#1087) fix(operator): choose the drive mode from the generation manifest, not the attached row count (#1101) feat(tern): one deployment correlates to exactly one remote apply (#1060) fix(github): record the passing check when an apply plan finds no changes (#1099) feat(spirit): detect an unfinished row copy and log what the apply will do to it (#1048) docs: reserve metrics for signals worth alerting on (#1089) feat(cli): browse stored plan history with the list-plans command (#1083) feat(cli): render status sources as OSC 8 hyperlinks on interactive terminals (#1097) feat(github): show VSchema changes in sharded apply comments (#1096) test(webhook): PostgreSQL failure-matrix row — declined stop is terminal, apply completes (#1098) feat(observability): log the delivery GUID when a goroutine panics (#1092) test(webhook): pin apply-confirm lock-path dispositions (#1091) fix(api): type terminal rollback validation errors (#1090) build(deps): pin pg-sprite to released v0.1.0 (#1093) feat(cli): show apply provenance as a clickable source in status output (#1086) fix(github): give sharded applies a real terminal summary comment (#1085) fix(vitess): gate stored-plan applies on recorded VSchema deletions and mutations (#1084) webhook: PostgreSQL failure-matrix rows — restart survival and permanent privilege refusal (#1079) fix(tern): complete a deployment-keyed apply only when its generation manifest is satisfied (#1076) ... # Conflicts: # pkg/webhook/plan.go # pkg/webhook/templates/plan.go
Why this matters
A deployment-keyed apply receives its operations one dispatch at a time, and the first dispatch records the generation manifest — every operation key the dispatcher will send. The claim loop, however, chose the drive mode from the attached row count alone. A keyed apply with one attached operation looked single-operation, took the legacy parent-lease drive, and that drive's finalizer terminalized the parent apply the moment its own work settled. Every later sibling dispatch was then refused against the terminal apply, failing the rest of the generation one retry at a time. A generation whose first operation is a sub-second
CREATE TABLEloses this race deterministically: the whole fan-out degrades to one table per apply attempt.Before — the drive mode races the fan-out; the first drive closes the apply:
After — the manifest picks the drive mode; the projection owns the verdict:
What it does
Apply.MissingExpectedOperationKeys: an apply whose manifest declares operation keys with no attached row drives under the operation lease only, so the parent applies row is moved solely by the operator's projection CAS — which already holds the whole-generation verdict until every declared key has attached and finished. Applies without a manifest, fully attached manifests, and single-key retry dispatches keep the legacy parent-lease drive byte-for-byte.projectionOwnsActiveAppliesGauge) replaces the attached-row-count check.How it moves us toward the northstar
Large declarative schema changes fan out as one keyed generation per deployment; the generation's outcome must be decided by what the dispatcher declared, not by which operation happened to finish first. This makes the manifest the drive-mode authority end to end — creation refuses undeclared keys, the projection gates the verdict, and now the claim path can no longer bypass either.
Opened by Claude (Fable 5).