Skip to content

fix(controller): default n>1 REST spawns to per-child netns (fan-out unblocked) - #2

Merged
keeganmccallum merged 6 commits into
urun/phase0-forkfrom
keeganmccallum/s3-01-forkd-fanout
Oct 10, 2026
Merged

keeganmccallum merged 6 commits into
urun/phase0-forkfrom
keeganmccallum/s3-01-forkd-fanout

Conversation

@keeganmccallum

@keeganmccallum keeganmccallum commented Oct 10, 2026 •

Copy link
Copy Markdown

What

POST /v1/sandboxes with n>1 and no explicit per_child_netns previously 503'd ('shared-TAP spawns with n>1 are not supported') — the shared forkd-tap0 cannot back a concurrent multi-restore batch, blocking all daemon-side fan-out (s2-03 receipt: 'REST n>1 fails - shared tap busy', reproduced live on forkd-p0-1).

per_child_netns becomes Option<bool>: absent ⇒ controller picks (n=1 → shared tap, n>1 → per-child netns); explicit false with n>1 keeps the 503. n=1 behavior and the workspace path unchanged.

Follow-on (717846a): the clock-sync agent poll burned 10 s/child on agent-less (E2B-shaped envd) snapshots — measured 10.9 s for a 60 ms restore. ForkOpts gains clock_sync_timeout_secs; daemon spawn paths use 2 s. 94db790 pins the 10 s default + test-gates the legacy const.

Root cause (live repro on forkd-p0-1, stock v0.5.3 controller)

  • n>1 shared-tap: tap name frozen in the snapshot vmstate (strings vmstate shows forkd-tap0) → every child re-opens ONE host tap → second restore EBUSY (os error 16) → 500.
  • Concurrent same-tag spawns on stock also collide on work_dir (forkd-daemon-{tag}-o{offset}, check-then-act pick_netns_offset): two /snapshot/load PUTs landed in ONE firecracker 5 ms apart; loser got The requested operation is not supported after starting the microVM. Our fork's fix(controller): bound netns allocator to provisioned namespaces deeplethe/forkd#282 NetnsAllocator (in this base) fixes that half.

Proof (lane s3-01, forkd-p0-1 c3-standard-8)

  • Red-first: create_sandbox_n_gt_1_defaults_to_per_child_netns FAILED on parent 2e3f9ea with the exact production 503; green on the fix. create_sandbox_explicit_shared_tap_n_gt_1_still_503 pins the explicit-false 503. 94db790 adds the clock-sync default pin.
  • Idle-node walls on the fixed binary: n=2 57 ms, n=10 119 ms, n=100 **901 ms (100/100 alive, netns 2–101), teardown ~15–20 ms/child — ≈9 ms/child REST fan-out (≈3× faster than CLI fork-100's 310 ms).
  • Fix proven live on the released artifact: tag v0.5.3-urun.2 → 1093c62; release run 38026932175 green (tarball sha256 bc469592… = SHA256SUMS, verified on laptop AND node); running daemon's /proc/<pid>/exe byte-identical to the release controller (sha256 f77b2354…, checked across 3 daemon restarts). Live on that binary: n=2 absent-flag → 201 (forkd-child-1/2); n=10 absent-flag → 201, 10 children netns 3–12 disjoint; both batches cleaned 204. Node left at 0 live sandboxes.
  • Suites on head 94db790: 226 passed, 0 failed (controller 111+8, vmm 50, cli 57); fmt clean; no new clippy warnings. cargo test --workspace failures in experiments/ PoC crates pre-exist on HEAD.

Not in this PR (filed to owners)

  • publish-pypi on v0.5.3-urun.2 fails invalid-publisher: PyPI trusted-publisher still registered to deeplethe, needs PyPI-side registration for urun-sh (same failure on urun.1).
  • Intermittent n=10 re-point child rootfs … connect ENOENT/ECONNREFUSED under daemon-restart contention: feat(vmm): give every restored child its own rootfs backing deeplethe/forkd#321's fail-closed guard (base phase0-fork feature), for s3-02/s3-03 to re-test on a stable daemon + production rootfs.

Merge: coordinator only (never self-merge). 717846a (clock-sync cap) is post-release: verify live on the node during lanes 02/03's re-test, then cut -urun.3 if desired.

…unblocked)

POST /v1/sandboxes with n>1 and no explicit per_child_netns previously
503'd ('shared-TAP spawns with n>1 are not supported') — the shared
forkd-tap0 cannot back a concurrent multi-restore batch, blocking all
daemon-side fan-out (s2-03 receipt: 'REST n>1 fails - shared tap busy').

per_child_netns is now Option<bool>: absent => controller picks
(n=1 shared tap, n>1 per-child netns); explicit false with n>1 keeps
the 503. CLI flag defaults, n=1 behavior, and the workspace path are
unchanged.

Red-first: create_sandbox_n_gt_1_defaults_to_per_child_netns failed on
the old behavior with the exact production 503; green after the fix.
Explicit-false test pins the 503 stays.
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Team
  • Run ID: e74be8e9-cf23-44b4-afb6-e07ab01829ed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

… -D warnings

The inner 'use std::os::unix::fs::MetadataExt' at the diff-snapshot
site makes the top-level import dead; Linux CI (cargo clippy
--all-targets --all-features -D warnings) fails on it while macOS
builds skip the cfg(target_os = "linux") import entirely, so it
slipped through. Blocking release CI for v0.5.3-urun.2.
…T create)

sync_guest_clocks blocked POST /v1/sandboxes on a 10s/child guest-agent
poll. Agent-less snapshots (E2B-shaped envd images, no forkd-agent on
:8888) burned the full budget every spawn: measured 10s on n=1 and
20-26s on n=2 on forkd-p0-1 while the restore itself took ~60ms.

ForkOpts gains clock_sync_timeout_secs (0 = historical 10s default).
The daemon's sandbox-create and workspace-spawn paths use 2s: a
restored forkd-agent answers in well under a second, so real sync
still lands; agent-less snapshots cap their penalty at 2s instead of
10s. Clock sync stays best-effort non-fatal (unchanged semantics).
CLI paths keep the 10s default; sync_guest_clocks=false paths are
untouched.
717846a threaded the agent-wait budget through ForkOpts but left the
old const as a non-test definition — dead in lib builds (clippy
noise, same class as d7914dc) — and shipped no guard pinning the
default. Adds fork_opts_clock_sync_default_is_historical_10s (default
stays 10s so CLI/restore keep their budget; 0 falls back, never a
zero-second poll) and cfg(test)-gates the legacy const.
@keeganmccallum
keeganmccallum merged commit 489a6f2 into urun/phase0-fork Oct 10, 2026
1 check passed
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