Skip to content

fix(cloudhypervisor): stop create race spawning duplicate VMM - #1150

Merged
richardcase merged 2 commits into
mainfrom
fix/999-cloudhypervisor-create-race
Jul 19, 2026
Merged

richardcase merged 2 commits into
mainfrom
fix/999-cloudhypervisor-create-race

Conversation

@richardcase

Copy link
Copy Markdown
Member

Fixes #999

Problem

With provider: cloudhypervisor, VM creates intermittently (~1 in 3, worse under nested virt/load) never reach CREATED; cloudhypervisor.stderr fills with API socket "…/cloudhypervisor.sock" is already in use by another running instance. The winning guest boots and networks fine — it's a control-plane reconcile bug, not guest/boot.

Root cause (traced in #999 (comment)):

  • startCloudHypervisor does cmd.Start() and returns before CH binds its API socket or finishes boot.
  • The reconcile loop re-invokes the create step's ShouldDo() (gated on State() == Pending) right after Do().
  • CH State() returned Pending during two windows while the process was already alive: pre-socket-bind (!sockExists), and boot (Info() == "Created", long under nested virt).
  • Either window → the gate re-fires Create() → a second cloud-hypervisor on the same --api-socket (Address in use / tap Resource busy), and ensureState() unlinked the socket out from under the first healthy process → permanent orphan → retries exhaust MaximumRetry → FailedState.

Firecracker is immune because its State() treats "pidfile present + process alive" as Running.

Fix

The double-spawn requires State() to return exactly (Pending, nil). So: once our pidfile process is alive, State() never returns Pending (mirrors firecracker), keeping Info() for richer reporting:

situation (process alive) result
socket not yet bound Running
Info() transport error Running (trust liveness, log warn)
Info = Running / Created Running
Info = Shutdown / other Unknown + err (post-boot only, safe — Unknown ≠ Pending)

This closes both race windows.

Belt-and-suspenders: Create() early-returns when the pidfile PID is a live process, so a stray re-create can neither double-spawn nor orphan the socket. Also fixes two pre-existing wrong-error-variable typos (startErr, delErr).

Tests

New provider_test.go — 7 State() cases (real short-lived process for liveness, fake CH HTTP-over-unix server for Info states) covering both race windows and the Info mapping. New create_test.go — the Create() guard leaves a live VM's socket intact.

go build ./..., go vet, and go test ./core/... ./infrastructure/... (159 passed) all green. End-to-end repro under nested KVM still needs a real host.

🤖 Generated with Claude Code

CH State() returned Pending while the cloud-hypervisor process was already
alive: during the pre-socket-bind window and while Info() reports "Created"
(guest still booting, long under nested virt). The reconcile create-gate
(ShouldDo == Pending) then re-fired Create(), spawning a second
cloud-hypervisor on the same --api-socket ("Address in use" / tap
"Resource busy") and ensureState() unlinked the socket from under the first
healthy process, orphaning it until retries exhausted -> never CREATED.

The double-spawn requires State() to return exactly (Pending, nil). Fix:
once our pidfile process is alive, State() never returns Pending, mirroring
the firecracker provider:
  - socket not yet bound        -> Running
  - Info() transport error      -> Running (trust liveness, log warn)
  - Info() Running/Created      -> Running
  - Info() Shutdown/other       -> Unknown+err (post-boot only, safe)

Belt-and-suspenders: Create() early-returns when the pidfile PID is a live
process, so a stray re-create can neither double-spawn nor orphan the socket.
Also fixes two wrong-error-variable typos (startErr, delErr).

Adds provider/create tests covering both race windows.

Fixes #999

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 19, 2026 14:36
@netlify

netlify Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for flintlock-docs canceled.

Name Link
🔨 Latest commit 37a7703
🔍 Latest deploy log https://app.netlify.com/projects/flintlock-docs/deploys/6a5d0043c611fb00084e7a6a

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an intermittent Cloud Hypervisor create/reconcile race in flintlock where the create step could re-run while the VMM process was already alive, causing duplicate cloud-hypervisor instances to contend for the same API socket and leaving the “winning” process orphaned.

Changes:

  • Adjust cloudhypervisor provider State() so that once the pidfile exists and the process is alive, it never reports Pending (mapping socket-not-bound and transient Info() failures to Running, and treating Created as Running).
  • Add a belt-and-suspenders guard in Create() to no-op when the pidfile points to a live process, preventing double-spawn and avoiding socket deletion in ensureState().
  • Add focused unit tests covering the new State() mappings and the Create() guard behavior; also fix two wrong error-variable usages.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
infrastructure/microvm/cloudhypervisor/provider.go Prevents State() from returning Pending once the VMM pid is alive, closing the duplicate-spawn windows.
infrastructure/microvm/cloudhypervisor/create.go Adds a live-process guard to skip re-create and fixes error-variable typos.
infrastructure/microvm/cloudhypervisor/provider_test.go Adds State() test matrix covering pid/socket/Info() permutations and CH state mapping.
infrastructure/microvm/cloudhypervisor/create_test.go Verifies Create() does not delete an existing socket when a live VMM is already running.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +44
// Never re-create over a live cloud-hypervisor. The State()-based create guard
// should already prevent this, but this is a belt-and-suspenders check against a
// double-spawn (Address in use) or orphaning a running process by deleting its
// socket in ensureState.
if pidExists, _ := afero.Exists(p.fs, vmState.PIDPath()); pidExists {
if pid, perr := vmState.PID(); perr == nil {
if live, _ := process.Exists(pid); live {
logger.Debug("cloud-hypervisor already running for this microvm, skipping create")

return nil
}
}
}
The belt-and-suspenders create guard swallowed errors from afero.Exists,
vmState.PID() and process.Exists. A transient FS error would fall through
into ensureState(), which deletes an existing socket and risks orphaning a
live VMM. Return early with context on each, matching State() and Delete().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@richardcase
richardcase merged commit b951cf5 into main Jul 19, 2026
11 checks passed
@richardcase
richardcase deleted the fix/999-cloudhypervisor-create-race branch July 19, 2026 17:12
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.

cloudhypervisor: VM creation errors with "Address in use"

2 participants