refactor(controller): collapse fork reconcile status round-trips off the hot path#858
refactor(controller): collapse fork reconcile status round-trips off the hot path#858stubbi wants to merge 1 commit into
Conversation
…the hot path The husk fork reconcile persisted ForkSnapshotTaken in its own r.Status().Update before the child loop, so a happy single-pass fork spent one apiserver+etcd round-trip the client waits on purely to record intermediate progress. Fold that flag into the status writes the pass already makes (the per-spawn co-located child record, or the unconditional pass-boundary write), so a single-replica co-located fork drops from 3 status round-trips to 2 and a new-pod snapshot pass drops from 2 to 1. The crash-recovery invariant is preserved: ForkSnapshotTaken is still durable before any child is ACTIVATED from the snapshot, so a crash can never re-snapshot (re-pause) the source under a child already restored from an earlier snapshot. This block only runs on the pass that first takes the snapshot, where no child is yet recorded; the new-pod path activates children only in a later pass after the pass-boundary write persisted the flag, and the co-location path persists the flag atomically with the first recorded child. The load-bearing per-spawn cross-fork occupancy write and the final durable status write are kept unchanged. An in-package unit test drives reconcileHuskFork through a status-write counting client and asserts the happy path reaches Ready with exactly 2 status writes, the snapshot flag and timing anchors stay persisted, and the snapshot is taken once. Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
📝 WalkthroughWalkthroughThis PR changes status write behavior in ChangesFork Reconcile Status Round-trip
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Test as TestHuskForkHappyPathStatusRoundtrips
participant Reconciler as SandboxReconciler
participant Client as countingStatusWriter
Test->>Reconciler: reconcileHuskFork(fork)
Reconciler->>Reconciler: forkSnapshot() stub called
Reconciler->>Reconciler: set ForkSnapshotTaken=true (in-memory)
Reconciler->>Reconciler: spawnVM() stub called
Reconciler->>Client: Status().Update (record child)
Reconciler->>Client: Status().Update (pass boundary, persists ForkSnapshotTaken)
Test->>Client: assert exactly 2 Status().Update calls
Test->>Reconciler: assert Ready, ForkSnapshotTaken, timing fields
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/controller/fork_reconcile_roundtrips_test.go`:
- Around line 50-54: The delegated status update in countingStatusWriter.Update
returns the raw error from SubResourceWriter.Update without context. Update the
Update method to wrap that returned error with fmt.Errorf using a descriptive
message and %w, so failures from the delegated status write are easier to trace
while keeping the existing counting logic intact.
In `@internal/controller/sandboxfork_controller.go`:
- Around line 759-785: `ForkSnapshotTaken` is not guaranteed durable before
co-located child activation, because `spawnForkChildInSourcePod` can activate a
child before the per-spawn `Status().Update` commits. Update the
`SandboxForkController` flow so the flag is persisted before the first
co-located spawn/activation, or refactor `spawnForkChildInSourcePod` into a
pre-activation step plus a durable status write before activation proceeds. Keep
the existing invariant around `ForkSnapshotTaken` and ensure the first
co-located child cannot become active until that flag is confirmed written.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 89914299-d5a8-46fe-9ee9-ff0af46f37e6
📒 Files selected for processing (2)
internal/controller/fork_reconcile_roundtrips_test.gointernal/controller/sandboxfork_controller.go
|
Closing: CodeRabbit's Critical finding is correct and decisive. The ForkSnapshotTaken standalone Status().Update is load-bearing on the CO-LOCATION path (the prod path): spawnForkChildInSourcePod activates the child BEFORE the per-spawn status write at line 938, so removing the standalone durable write opens a crash window where an activated child exists while ForkSnapshotTaken is not durable, letting a later pass re-snapshot/re-fork the source and split the fork point. Making it safe means keeping that durable write, which reverts the optimization. The saving was one round-trip (~5-15ms), which the PR itself noted is within the fork's 432-551ms prod variance. So: the reconcile status round-trips are load-bearing, there is no safe wall-clock win here, and correctness wins. Recorded as a finding: the prod fork's orchestration cost is real and necessary, not fat to trim. |
|
Superseded by #859. GitHub's PR head pointer for this PR got stuck on an old commit (c88d7b2) after a force-push while the branch ref correctly advanced, so CI stopped re-triggering here and the PR could not be reopened. #859 tracks the same branch (with the CodeRabbit co-location crash-coherence fix applied) from a clean head. Closing this in favor of #859. |
Thinking Path
Linked Issues or Issue Description
No tracked issue. Problem: the husk fork reconcile persisted
ForkSnapshotTakenin its ownr.Status().Updatebefore the child loop on every path, adding an apiserver plus etcd round-trip the fork client waits on purely to record intermediate progress. On the new-pod path that write is redundant with the pass-boundary write.What Changed
reconcileHuskFork: the standaloneForkSnapshotTakenwrite is no longer unconditional. It is folded into the pass-boundary write on the new-pod path (where the child pods are only CREATED in the snapshot pass and activated a later pass), and KEPT on the co-location path (where the child is ACTIVATED in the same pass before its per-spawn write commits, so the flag must be durable first). The write now fires only whenspawnInSourcePod && coLocationBudgetRemaining > 0, preserving the exact pre-change ordering on that path.fork_reconcile_roundtrips_test.go) drivingreconcileHuskForkthrough a status-write counting client:TestHuskForkNewPodPathFoldsSnapshotWrite: the new-pod snapshot pass writes status EXACTLY ONCE (was twice), the flag and timing anchors stay durable, the snapshot is taken once, and the fork reaches Ready on the next pass.TestHuskForkCoLocatedSnapshotDurableBeforeActivation:ForkSnapshotTakenis durable in the store at the FIRST co-locatedspawn-vmactivation (the crash-coherence guard), and the fork reaches Ready.Verification
go build ./...go test ./internal/controller/(full envtest suite green on the pre-image; the two new tests pass; setup-envtest 1.31)TestHuskForkNewPodPathFoldsSnapshotWriteandTestHuskForkCoLocatedSnapshotDurableBeforeActivationpassgolangci-lint run --timeout=5m ./internal/controller/cleanGOOS=linux golangci-lint run --timeout=5m ./internal/controller/cleanRisks
Low risk. The change is confined to K8s status bookkeeping; it does not touch the husk RPC calls (ForkSnapshot / SpawnVM / Activate), fork correctness, isolation, or the leak-prevention paths. The co-location (hot) path keeps its pre-activation flag write, so its crash coherence is byte-for-byte the pre-change behavior; the only behavioral change is on the new-pod path, where no child is activated in the snapshot pass so the deferred flag is always durable before any later-pass activation. Status.Phase transitions and observed generation semantics for watchers are unchanged; fewer status writes reduce, not increase, conflict pressure. The saving is one round-trip on the new-pod snapshot pass (not the co-located hot path), so no wall-clock number is claimed; the operator measures on prod.
Model Used
Claude Opus 4.8 (claude-opus-4-8), extended thinking. Agent-assisted implementation and verification.
Checklist