Repository navigation
feat(seinode): maintenance hold for nodeConfig nodes (spec 010, PLT-1390) - #595
Conversation
spec.dataResetGeneration requests one data reset per increase on a nodeConfig SeiNode; status.dataResetGeneration records the last value handled. CEL keeps the counter monotonic and nodeConfig-only. The planner builds the reset plan before any other Running-phase plan: observe-image, mark-not-ready, stop-seid, reset-data, record-data-reset, mark-ready. One sequence serves every case, because a configRef change is not observed as drift. mark-ready is now start-guarded: while a reset is pending, a plan's mark-ready and a MarkReady SeiNodeTask fail terminally and submit nothing. A plan built before the counter bump can no longer release seid onto old data; the planner rebuilds from the current spec. reset-data keeps priv_validator_state.json in place instead of deleting and rewriting it, so a crash mid-wipe cannot zero a validator's sign state. It refuses while a seid or seidb process runs in the pod, and when config.toml moves [priv-validator] state-file. DataResetInProgress reports ResetPending, ResetRunning, ResetComplete, and ResetFailed (sticky until a retry succeeds), with events. Refs: PLT-1342, spec PR #590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spec.maintenance.hold parks seid at the sidecar start gate with the pod alive, for seid rollback and chain-halt work through kubectl exec. status.maintenanceHold records the hold in effect; every plan that changes it ends with record-maintenance-hold. - Immediate: mark-not-ready, stop-seid. - AfterExit on a running seid: mark-not-ready only, so seid parks when it exits, for example at halt-height. - AfterExit on a parked seid: start once (start-seid-once, the new read-only sidecar task await-seid-start, mark-not-ready), so one commit can stage a recovery binary and halt-height and run to it. - Release: mark-ready. The start guard now also refuses mark-ready while a hold is set; the hold's own start-once step obeys only the reset half. While held, the reset plan and the init plan park instead of releasing, the update plan carries no mark-ready, and no readiness reapproval is built. The StatefulSet still applies, so a template change rolls the pod parked. MaintenanceInProgress reports NotHeld, HoldPending, Held, Armed, and NotApplicable, with MaintenanceHeld/Armed/Released events. Refs: PLT-1390, spec PR #590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- reset-data reads both sign-state spellings, CometBFT's [priv-validator] state-file and sei-config's [priv_validator] state_file, and refuses on a value it cannot read. The guard read only the first spelling, so the second could move the sign state under the wipe. - The reset plan submits reset-data-keep-sign-state, a wire type only a sidecar that keeps the sign state accepts. Against an older sidecar the submission fails and retries with seid held, instead of zeroing a validator's sign state when the controller deploys first. - A reset plan whose final mark-ready the start guard refused (the counter rose again, or a hold arrived) reads ResetComplete with "start deferred", not ResetFailed: the wipe ran and was recorded. Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The hold half of the start guard acts only on a Running node. A hold set while an init plan ran made the guard refuse that plan's mark-ready, and an init plan failure is terminal. Now seid starts once and the hold plan stops it when the node reaches Running. - await-seid-start fails after two minutes. Unbounded, a pod roll between start-seid-once and seid starting held the node's only plan slot forever; now the plan fails and is built again. Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
This adds spec.maintenance.hold, which parks seid at the sidecar start gate through hold/start-once/release plans, a hold-aware start guard and a new await-seid-start sidecar task. Two paths, both first raised by codex and both confirmed against the tree, can leave seid running while MaintenanceInProgress reports Held, which the API documents as "exec work on the data is safe"; no other scout readings were supplied.
Blocking
2 findings on the changed lines, as inline comments.
1 nit, not posted on the code
internal/planner/planner.go:311— A held init plan now carriesrecord-maintenance-hold, soclassifyPlanlabels itmaintenance-holdrather thaninitin the plan-duration metrics.
seidroid review · decision request_changes · session 1edca46d6e9a4c1baccab3d6484925cc · turn resp_claude_382e751c1f8157c81d1a8f3c7f2b0258 · item 29e7f56705f85536bdaf8163b364e5ca
Findings: 2 blocking | 0 non-blocking | 2 posted inline
Addresses seidroid's two blocking findings on #595. - start-seid-once now refuses unless the requested hold is still AfterExit. A start-once plan already running when the operator changed the hold back to Immediate no longer starts seid. - A hold in effect is level-triggered against the observed start gate: when the sidecar reports ready (gate open) under a hold, the planner rebuilds the hold plan. A failed start-once plan that left the gate open is closed again even though want == have. - MaintenanceInProgress reads HoldPending, never Held or Armed, while a hold plan runs or while the gate is open under a hold. - A held init plan keeps its "init" metric label (nit). Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses seidroid's suggestion and nit on #594. - An update plan whose final mark-ready the start guard refused now sets NodeUpdateInProgress=False/UpdateComplete with "start deferred". The roll landed; UpdateFailed would have stayed until the next update. - A reset plan deferred because the counter rose again reads True/ResetPending for the newer value, not ResetComplete. - A ResetRunning to ResetRunning move for a newer counter records a DataResetStarted event. Refs: PLT-1342 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
Since the last review, start-seid-once has its own guard (startOnceBlocked), which refuses unless the request is still AfterExit. A hold now goes HoldPending while a hold plan runs or the gate is observed open, and buildHoldPlan rebuilds a hold whose gate the probe reports open, so both earlier blockers are fixed (checked against the tree: the probe runs fresh before every plan build, and an unreachable sidecar is Unknown, not open). Codex's one new reading, that open-gate recovery skips the unequal want/have state, holds and is kept as a non-blocking suggestion; no other scout readings were supplied.
Non-blocking
1 finding on the changed lines, as inline comments.
1 nit, not posted on the code
internal/task/task.go:239— The registry comment saysstart-seid-onceis "start-guarded by resets only", butstartOnceBlockednow also refuses any request other thanAfterExit.
seidroid review · decision approve · session 1edca46d6e9a4c1baccab3d6484925cc · turn resp_claude_85a8aec48b4701707fd987704c40f2d9 · item 540c8df73b5a535b9175099e6b19daa6
Findings: 0 blocking | 1 non-blocking | 1 posted inline
Superseded: the latest review found nothing blocking in this change.
Addresses seidroid's follow-up suggestion and nit on #595. Open-gate recovery now covers any requested hold, not only want == have. When start-once fails after opening the gate, want stays AfterExit and the hold in effect stays Immediate; the planner now applies Immediate again first (mark-not-ready, stop-seid), so a crash-looping seid cannot keep the gate open across retries. The next plan retries start-once with the gate closed. The registry comment for start-seid-once now names both of its guards. Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
Since the last pass, buildHoldPlan closes a gate the probe reports open whenever a hold is both requested and in effect, even if the two differ: it re-applies the hold in effect first and retries start-once on the next plan, which fixes the open thread (stop-seid treats an already-stopped seid as success, and normal hold-from-none planning is unaffected); the stale registry comment in task.go is fixed too. Nothing blocks; codex found nothing this round and I found nothing new.
seidroid review · decision approve · session 1edca46d6e9a4c1baccab3d6484925cc · turn resp_claude_516ce0feefc7b14123fcacea56bd2c8f · item 322cd692d6f85fce9bd33d576d8df6bf
Findings: 0 blocking | 0 non-blocking | 0 posted inline
…t branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hold branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h HoldPending Found by the harbor e2e: the release plan runs before NotHeld, so the condition moves HoldPending -> NotHeld and the old Held/Armed-only check never recorded the event spec 010 Req 5.6 requires. Any True state before NotHeld now counts as a release. Refs: PLT-1390 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
|
@seidroid review |
PR SummaryHigh Risk Overview The start guard now blocks Spec 010 acceptance criteria are nudged for release+reset and condition wording; CRD/manifests and deepcopy follow the API changes, with broad planner/controller/sidecar tests. Reviewed by Cursor Bugbot for commit b4b8fa6. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Nothing material has changed since the last pass: the diff, the merge commit (1a76b1c), the PR description and the base standards are all identical, and the three earlier threads remain correctly resolved. Codex repeated both claims; I keep the first as the open blocker below (releasing a hold together with a data reset reports Held through the reset) and again drop the second, because spec 010 User Story 5 says a node created with AfterExit initializes parked and is then started once, which is what the code does.
Blocking
1 finding on the changed lines, as inline comments.
seidroid review · decision request_changes · session 1edca46d6e9a4c1baccab3d6484925cc · turn resp_claude_8dbde396c5b2e822e4376ac7007a2a00 · item 643a3cb6f9b1551387fce53792acb69f
Findings: 1 blocking | 0 non-blocking | 1 posted inline
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5c089bf. Configure here.
| } | ||
| setMaintenanceCondition(node, metav1.ConditionTrue, reason, message) | ||
| } | ||
| } |
There was a problem hiding this comment.
Hold condition lags after plan
Medium Severity
MaintenanceInProgress is written only at the start of reconcile, before the sidecar probe and before the hold plan can finish. After record-maintenance-hold commits, the condition can stay HoldPending—and even claim the start gate is still open—because it still reads the pre-plan SidecarReady value. MaintenanceHeld / MaintenanceArmed then wait for a later poll. Data reset avoids this with observeTerminalDataResetPlan; the hold path has no equivalent.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 5c089bf. Configure here.
A hold removed in the same commit that raises dataResetGeneration ran the
plain reset plan. That plan ends in mark-ready but never recorded the
release, so status.maintenanceHold stayed set and MaintenanceInProgress
read Held while the wipe ran and seid started.
- The reset plan for a node whose hold is in effect but no longer requested
ends mark-ready -> record-maintenance-hold(""). The plan now counts as a
hold plan, so the condition reads HoldPending while it runs.
- startDeferred reads only the tasks before mark-ready, so the trailing
record step keeps a refused start a deferred start, not ResetFailed.
- ResolveMaintenance reports a pending release as HoldPending, as the
ReasonHoldPending API comment already states.
- Spec 010 Req 4.2, 5.2, and 5.3 now say so.
Refs: PLT-1390
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
Since the last pass, a reset plan that also releases a hold still in effect now ends mark-ready → record-maintenance-hold("") (clearHoldAfterRelease), ResolveMaintenance reports any requested-vs-in-effect mismatch as HoldPending (a pending release included), and startDeferred handles the new trailing step; spec 010 Req 4.2 and 5.2–5.3 are updated to match, so the open blocker is fixed. Codex's one reading repeats its AfterExit-at-creation claim and is dropped again, because spec 010 User Story 5 says such a node initializes parked and is then started once, which is what the code does.
seidroid review · decision approve · session 1edca46d6e9a4c1baccab3d6484925cc · turn resp_claude_d3ae227ddd1516dbe15d6b6942084671 · item 845cc174ab135737803cdd5c2bec85fc
Findings: 0 blocking | 0 non-blocking | 0 posted inline
Superseded: the latest review found nothing blocking in this change.


Summary
This PR implements spec 010 (#590). It stacks on #594 (spec 009), because it extends that PR's start guard.
spec.maintenance.holdparks seid at the sidecar start gate. The pod stays alive and the data volume stays mounted, so an operator can runseid rollbackand chain-halt diagnostics throughkubectl exec -c seid.status.maintenanceHoldrecords the hold in effect. Every plan that changes it ends withrecord-maintenance-hold.Immediate← none /AfterExitmark-not-ready→stop-seid→ record(Immediate)AfterExit← nonemark-not-ready→ record(AfterExit): seid runs until it exits, then parksAfterExit←Immediate(start once)observe-image→start-seid-once→await-seid-start→mark-not-ready→ record(AfterExit)observe-image→mark-ready→ record(none)Start once supports coordinated chain recovery with one commit per validator. The commit carries the recovery image, a ConfigMap with
halt-height, and the change fromImmediatetoAfterExit. The pod rolls, seid starts once, and seid parks when it exits at the halt height.seid startappears. On atlantic-2 and pacific-1, seid needs minutes to load state, so it cannot commit a block first.Details
internal/task/start_guard.go). While a hold is set,mark-readyfails as terminal on every path, including theMarkReadySeiNodeTask.start-seid-oncesubmits the same sidecarmark-ready, but only a pending reset blocks it.mark-readybecomes record(Immediate).mark-ready.staticConfigPlanner.buildRunningPlan): reset, then hold change, then template drift, then reapproval.await-seid-startis read-only. It polls/procevery 250 ms forseid start.start-seid-onceandrecord-maintenance-hold.MaintenanceInProgressis always present. Its reasons areNotApplicable,NotHeld,HoldPending,Held, andArmed.MaintenanceHeld,MaintenanceArmed, orMaintenanceReleased.Not in this PR (spec 010 "Out of scope")
pods/execRBAC. The platform draft is not pushed yet, because the RBAC piece needs your decision.Needs sign-off
The API fields are listed in #590:
spec.maintenance.hold, an enum ofImmediateandAfterExitstatus.maintenanceHoldMaintenanceInProgressconditionawait-seid-start. Wire types are a published contract.Tests (each names its spec requirement)
internal/controller/node/envtest/maintenance_validation_test.goTestHoldPlans(6 transitions)TestHold_StaleUpdatePlanCannotReleaseGate,TestStartGuard_Hold,TestReconcile_MarkReady_RefusedWhileHeldTestHold_StartOncePassesGuard,TestAwaitSeidStart_*TestHoldInEffect_NoPlanNoReapproval,TestHold_UpdatePlanCarriesNoMarkReadyTestHold_InitPlanParksTestHold_ResetParks,TestRelease_WithResetPendingTestResolveMaintenanceSC-006 and SC-007 (harbor: the hold survives a pod delete and a controller restart;
AfterExitathalt-height) are judgement checks. They are not run yet.Independent review: fixes in bc05b03
mark-ready. An init plan failure is terminal, so the node wentFailed.Runningnode. seid starts once, then the hold plan stops it (TestHold_SetMidInitDoesNotFailNode).await-seid-starthad no time limit. A pod roll betweenstart-seid-onceand seid starting held the plan slot forever.TestAwaitSeidStart_FailsAfterTimeout).This branch merges #594's fixes (b24613f). It uses a merge commit, not a rebase, so nothing was force-pushed.
Deploy and rollback order
await-seid-start.UnknownTaskTypeError.Verification
All commands ran with
env -u GOROOT GOTOOLCHAIN=go1.26.0.gofmt -l(3 modules)go vet ./...(3 modules)golangci-lintv2.12.1--new-from-merge-base=origin/main0 issues.in each modulemake testmake tidy-check,make verify-generated,make buildRefs: PLT-1390, PLT-1389
🤖 Generated with Claude Code