Skip to content

refactor(controller): collapse fork reconcile status round-trips off the hot path#859

Open
stubbi wants to merge 2 commits into
mainfrom
perf/fork-reconcile-fewer-roundtrips
Open

refactor(controller): collapse fork reconcile status round-trips off the hot path#859
stubbi wants to merge 2 commits into
mainfrom
perf/fork-reconcile-fewer-roundtrips

Conversation

@stubbi

@stubbi stubbi commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Mitos boots Firecracker microVMs and forks them via copy-on-write snapshots, exposed through CRDs; the hosted fork runs through the husk-pod reconcile in internal/controller/sandboxfork_controller.go.
  • The measured prod fork is noisy (432 to 551 ms), yet an in-process fork-bench shows the fork ENGINE is only ~75 ms, so most of the wall clock is orchestration and client RTT, not the engine.
  • The husk fork reconcile persisted ForkSnapshotTaken in its OWN r.Status().Update before the child loop, on every path. Each status write is an apiserver plus etcd round-trip the client waits on, and this one records only intermediate progress.
  • On the NEW-POD path that write is redundant: no child is activated in the snapshot pass, so the flag can ride the unconditional pass-boundary write the pass already makes. On the CO-LOCATION path the child is activated in the SAME pass before the per-spawn write commits, so the flag must stay durable before that first activation, else a crash could re-snapshot under an already-restored child and split a multi-replica fork point.
  • This pull request folds the standalone write into the pass-boundary write on the new-pod path only, and keeps it on the co-location path exactly where it was, made conditional on the pass actually co-locating a child.
  • The benefit is one fewer apiserver round-trip (and one fewer self-triggered reconcile) on the new-pod snapshot pass, with the crash-coherence invariant preserved and no change to the husk RPC calls, fork correctness, or isolation.

Linked Issues or Issue Description

No tracked issue. Problem: the husk fork reconcile persisted ForkSnapshotTaken in its own r.Status().Update before 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 standalone ForkSnapshotTaken write 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 when spawnInSourcePod && coLocationBudgetRemaining > 0, preserving the exact pre-change ordering on that path.
  • Documented the load-bearing reason inline so a future edit does not re-collapse the co-location write.
  • Added an in-package unit test (fork_reconcile_roundtrips_test.go) driving reconcileHuskFork through 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: ForkSnapshotTaken is durable in the store at the FIRST co-located spawn-vm activation (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)
  • TestHuskForkNewPodPathFoldsSnapshotWrite and TestHuskForkCoLocatedSnapshotDurableBeforeActivation pass
  • golangci-lint run --timeout=5m ./internal/controller/ clean
  • GOOS=linux golangci-lint run --timeout=5m ./internal/controller/ clean

Risks

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

  • PR title is a conventional commit (refactor; note: pr-policy allows feat, fix, docs, ci, chore, refactor, test only, so refactor is used rather than perf)
  • Thinking Path traces from project context to this change
  • Model Used is filled in (with version and capability details)
  • Tests added for behavior changes, in the same commit (TDD)
  • Docs updated in the same PR (no doc surface changed; internal bookkeeping only)
  • Threat-model delta (docs/threat-model.md) included if the security surface moved (security surface unchanged)
  • Benchmark run (bench/) included if the hot path was touched (saving is one round-trip on the non-hot new-pod path; no bench number claimed)
  • No em or en dashes introduced anywhere
  • Secret values never logged, in errors, in condition messages, or on host paths
  • No internal/instance-local references (only public #NNN / mitos-run/mitos URLs)
  • Every commit carries a Signed-off-by trailer (git commit -s)

Summary by CodeRabbit

  • Bug Fixes
    • Improved fork startup reliability so snapshot status is saved at the right time during reconciliation.
    • Fixed duplicate snapshot handling, reducing unnecessary repeat writes and ensuring snapshot state stays consistent across passes.
    • Strengthened co-located fork activation so readiness information is available before the fork is brought online.

…the hot path

The husk fork reconcile persisted ForkSnapshotTaken in its OWN r.Status().Update
before the child loop, on every path, so a fork spent an apiserver plus etcd
round-trip the client waits on purely to record intermediate progress.

Fold that standalone write into the unconditional pass-boundary write on the
NEW-POD path, where it is redundant: no child is ACTIVATED in the snapshot pass
there (the child pods are only CREATED, and activated a LATER pass after the
pass-boundary write has already persisted the flag), so the snapshot pass drops
from 2 status round-trips to 1.

The CO-LOCATION write is KEPT because it is load-bearing: spawnForkChildInSourcePod
ACTIVATES the child inside the source pod in the same pass, before its per-spawn
status write commits, so ForkSnapshotTaken must be durable BEFORE that first
activation. Folding it there would let a failed per-spawn write or a crash in that
window leave an active child while the flag is not durable, and a later pass would
re-snapshot under an already-restored child and split a multi-replica fork into an
incoherent point. The write is now made only when the pass will actually co-locate
a child (spawnInSourcePod and remaining budget), preserving the exact pre-change
ordering on that path.

Tests (in-package, via a status-write counting client):
  - the new-pod snapshot pass writes status exactly once and still reaches Ready
    with the flag and timing anchors durable and the snapshot taken once;
  - the co-located path has ForkSnapshotTaken durable in the store at the first
    spawn-vm activation and still reaches Ready (the crash-coherence guard).

Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23a11120-e272-4962-a791-1137d56eb440

📥 Commits

Reviewing files that changed from the base of the PR and between 9117eda and 971ab0e.

📒 Files selected for processing (2)
  • internal/controller/fork_reconcile_roundtrips_test.go
  • internal/controller/sandboxfork_controller.go

📝 Walkthrough

Walkthrough

Changes reconcileHuskFork's handling of the ForkSnapshotTaken status flag from an unconditional Status().Update immediately after a successful snapshot to a conditional update triggered only when co-location activation will occur in the current pass. Adds unit tests verifying status-write counts and durability across reconcile passes.

Changes

Fork Status Write Consolidation

Layer / File(s) Summary
Conditional ForkSnapshotTaken persistence
internal/controller/sandboxfork_controller.go
ForkSnapshotTaken is set to true after a successful snapshot, but the status write is now conditional on spawnInSourcePod && coLocationBudgetRemaining > 0, rather than always executing immediately.
Status-write round-trip tests
internal/controller/fork_reconcile_roundtrips_test.go
Adds statusUpdateCounter/countingStatusWriter and helper functions, plus TestHuskForkNewPodPathFoldsSnapshotWrite and TestHuskForkCoLocatedSnapshotDurableBeforeActivation to verify status-write counts and ForkSnapshotTaken durability across NEW-POD and CO-LOCATION reconcile paths.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant reconcileHuskFork
  participant StatusWriter
  participant spawnVM

  Test->>reconcileHuskFork: pass 1 (NEW-POD, snapshot succeeds)
  reconcileHuskFork->>reconcileHuskFork: set ForkSnapshotTaken=true
  reconcileHuskFork->>StatusWriter: Status().Update (pass-boundary write)
  Test->>reconcileHuskFork: pass 2 (child ready)
  reconcileHuskFork->>StatusWriter: Status().Update (single additional write)

  Test->>reconcileHuskFork: reconcile (CO-LOCATION path)
  reconcileHuskFork->>reconcileHuskFork: set ForkSnapshotTaken=true
  alt spawnInSourcePod and budget remaining
    reconcileHuskFork->>StatusWriter: Status().Update (durable before activation)
    reconcileHuskFork->>spawnVM: spawn co-located child
  else
    reconcileHuskFork->>StatusWriter: deferred pass-boundary Status().Update
  end
Loading

Possibly related PRs

  • mitos-run/mitos#803: Both PRs modify the reconcileHuskFork co-location/multi-VM routing path, with this PR changing ForkSnapshotTaken persistence timing during the activation window that PR #803 implements.
  • mitos-run/mitos#804: Both PRs modify reconcileHuskFork's co-location budget logic (coLocationBudget/coLocationBudgetRemaining) affecting when ForkSnapshotTaken is persisted.
  • mitos-run/mitos#812: Both PRs target the multi-VM co-located fork path, with this PR ensuring snapshot durability before activation which #812 relies on for restoring co-located children.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is a clear conventional commit that summarizes the main controller change and its intent.
Description check ✅ Passed The description follows the template with Thinking Path, issue context, changes, verification, risks, model used, and checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/fork-reconcile-fewer-roundtrips

Comment @coderabbitai help to get the list of available commands.

@stubbi

stubbi commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

CI status: all checks this diff can affect are green (go-test, go-lint, python-test, sdk-conformance/examples, typescript-sdk, docker-build, CodeQL, CodeRabbit, dco, title/body/no-dashes).

firecracker-test is RED, but it is a pre-existing failure on main unrelated to this change:

Nothing in this PR needs to change for firecracker-test; it will go green once the pre-existing internal/husk KVM prewarm issue is resolved on main.

@stubbi

stubbi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Review verdict: HOLD, and honestly the simplest outcome is closing this one.

The fold breaks the crash-coherence invariant the standalone write existed for, on the new-pod recovery path. Trace: pass 1 takes snapshot S1, sets ForkSnapshotTaken in memory only, creates child pods, then the end-of-pass status write fails (status conflicts on the Sandbox CR are realistic) or the controller crashes or loses leadership. The flag is not durable but child pods now exist. Pass 2 sees !ForkSnapshotTaken and re-snapshots the source (re-pausing a pauseSource=false source a second time and overwriting mem/vmstate), and a pass-1 child can be Running+Ready by then, so it activates in the same pass as the re-snapshot: the exact configuration the hazard comment at sandboxfork_controller.go:814-821 exists to prevent. With a second boundary failure you get children active on different snapshot generations. The pre-change code was immune by construction: the flag was durable before any child pod existed. The new tests cover only the clean pass, not this recovery window.

The trade also buys little: the saved write is on the cold new-pod path, and the #868 measurements show the client-visible latency lives elsewhere. On top of that the branch conflicts with main exactly in the rewritten hunk (the #860 per-stage instrumentation wraps this very write, and #874's RequireMemFile landed in the same block), and the govulncheck red is just the pre-#862 toolchain on the branch.

To make it mergeable it would need: the invariant restored (refuse to re-snapshot when child pods already exist, or only fold when no child artifacts precede the durable flag), a failed-final-write recovery test, and a rebase re-woven around #860/#874. Given the first point likely collapses it back to the status quo, recommend closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant