Repository navigation
feat(seinode): pace pod-template drift with a per-namespace roll budget (spec 012) - #605
Conversation
…et (spec 012) A pod-template drift (seid image, cell sidecar image, isolation) made every affected SeiNode build its update plan in the same reconcile, so a cell sidecar bump restarted every node pod in the cell at once. On 2026-10-07 that paused arctic-1 in each cell and restarted every pacific-1 RPC node in prod together. The controller config gains rollout.driftUpdateBudgetPercent. Above 0, a namespace gets max(1, N*percent/100) roll slots (N = Running, unpaused SeiNodes). Nodes already updating hold the first slots; drifted nodes follow in name order, so every node computes the same order and a stale cache delays a roll rather than overfilling the slots. - ResolvePlan drops a drift update plan that holds no slot and reports NodeUpdateInProgress=False/UpdateDeferred, naming the slot holders. - A nodeConfig node's StatefulSet is RollingUpdate, so the reconciler also skips its StatefulSet apply while the drift waits. - Resets, holds, config updates, init plans, and resizes are not paced. - 0 or unset keeps today's behavior. Refs: PLT-1399 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
PR SummaryMedium Risk Overview Operators set Plan gate: Template gate: For Spec 012 documents the feature; planner, reconciler, and platform load tests cover slots, deferral, pinning, and config validation. Reviewed by Cursor Bugbot for commit 372b45c. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Adds a per-namespace budget for pod-template drift updates, enforced in two places: a plan gate in ResolvePlan, and a StatefulSet-apply gate for nodeConfig nodes. The template gate blocks the replicas=0 apply that spec.paused depends on, so pausing a drifted nodeConfig node does nothing while the budget is full; that breaks an existing contract and decides the verdict. Of codex's two findings I kept both, as suggestions: the fallback that skips waiting nodes (line 272) and the bypass while another plan is active (line 292). No other reader contributed.
Blocking
1 finding on the changed lines, as inline comments.
Non-blocking
3 findings on the changed lines, as inline comments.
seidroid review · decision request_changes · session 564dbb6bb94f48c89d8fc91df71f21be · turn resp_claude_c6c026935ebc1b656624ed4bc0245d52 · item 99749fc78fa25116b5e551facda80019
Findings: 1 blocking | 3 non-blocking | 4 posted inline
…pping its apply Review on #605: - Blocker: skipping the StatefulSet apply also skipped Replicas=0, so pausing a drifted nodeConfig node did nothing while the budget was full, and an unpause stayed at zero until a slot. The reconciler now renders from a copy pinned to the running image, sidecar image, and isolation; every other field applies, so pause, resize, and config refs work. A paused node is not pinned. - The fallback for a node the list does not show as drifted counts the whole order, waiting nodes included, so it cannot overfill. - The budget list is an uncached read (mgr.GetAPIReader). With one reconcile at a time, each decision sees every earlier node's persisted status, so a stale cache cannot let a newcomer pass a holder. - Pinning stays off while a plan is active or a reset or hold change is pending: observe-image stamps the spec image, so a plan run on a pinned template would record an image the pod does not run. Spec 012 records the resulting leak under Known limits. Refs: PLT-1399 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
Since my last review, the template gate no longer skips the StatefulSet apply. It renders from a copy pinned to the running images, so spec.paused and other fields still apply, and paused nodes are never pinned. The fallback now counts waiting nodes, the budget list reads uncached while the node controller reconciles one node at a time, and the plan-active leak is documented in Known limits. I checked all four threads against the code and each is addressed, with nothing new that blocks; codex found nothing, which agrees with my reading.
seidroid review · decision approve · session 564dbb6bb94f48c89d8fc91df71f21be · turn resp_claude_63605e0b752ca3d6d24e5a0926dd917d · item 3fd5a80aae435e44816fcba76fd4842a
Findings: 0 blocking | 0 non-blocking | 0 posted inline
Superseded: the latest review found nothing blocking in this change.
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 af57277. Configure here.
Bugbot on #605: imageDrifted treats an empty status.currentImage as drift, but pinnedToRunning pinned the image only from status. A Running nodeConfig node whose image was never observed counted as waiting while its drifted template still applied. The pin now takes each running image from status, or, where status has not observed it, from the live StatefulSet's pod template (noderesource.TemplateImages). With no running image anywhere, the node is not pinned and the plan gate alone defers the drift plan. Refs: PLT-1399 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@seidroid review |
There was a problem hiding this comment.
Since my last review, the only change is in pinnedToRunning: when status has not yet recorded a running image, it now takes that image from the live StatefulSet's template (via the new noderesource.TemplateImages), and it leaves the node unpinned when no running seid image is known. That is correct, because the unpinned case has no pod to roll and the plan gate still defers the plan; my four earlier threads stay addressed, and codex again found nothing, which matches my reading.
seidroid review · decision approve · session 564dbb6bb94f48c89d8fc91df71f21be · turn resp_claude_ca4aa634be81f796c62b2a1439f30ef2 · item dd7b616b8afe5a5499e36a4ae26342ae
Findings: 0 blocking | 0 non-blocking | 0 posted inline

Summary
A pod-template drift (seid image, cell sidecar image, isolation) makes every affected SeiNode build its update plan in the same reconcile. A cell sidecar bump therefore restarts every node pod in the cell at once. On 2026-10-07 this paused arctic-1 for about 30 seconds in each cell, and every pacific-1 RPC node in
prodrestarted together. This PR adds a per-namespace drift-roll budget (spec 012, PLT-1399).Design
rollout.driftUpdateBudgetPercentin the controller config file. 0 or unset keeps today's behavior. Values outside 0..100 fail startup validation.max(1, N*percent/100)slots, where N counts its Running, unpaused SeiNodes.NodeUpdateInProgress=True(sorted by name), then drifted nodes waiting for a slot (sorted by name). A node holds a slot when its position is below the slot count. Every node computes the same order from the same cache, so a stale read delays a roll; it never overfills the slots.ResolvePlan, a drift update plan with no slot is dropped. The node reportsNodeUpdateInProgress=False/UpdateDeferredwith the slot holders, and it retries on the 30-second status poll.RollingUpdate, so the reconciler also skips the StatefulSet apply while the drift waits. The gate applies only when no plan is active and no reset or hold change is pending. It mirrors the existingholdForWorkflowskip.Known limit
A drift update stuck in progress keeps its slot, so the namespace's further drift waits until an operator clears it. The
UpdateDeferredmessage names the holder.Verification
UpdateDeferred. It fails if the controller gate is removed. The plan-gate test likewise fails without the gate.gofmt,go vet,golangci-lint --new-from-merge-base, andgo test ./...(20 packages) are clean.make manifests generateleaves no diff.🤖 Generated with Claude Code