Skip to content

buildkit: rebind daemon to the current process on miren restart#885

Merged
evanphx merged 1 commit into
mainfrom
mir-1303-buildkitd-stale-process-reuse-after-miren-restart
Jul 6, 2026
Merged

buildkit: rebind daemon to the current process on miren restart#885
evanphx merged 1 commit into
mainfrom
mir-1303-buildkitd-stale-process-reuse-after-miren-restart

Conversation

@evanphx

@evanphx evanphx commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem (MIR-1303)

buildkitd runs as a containerd-managed container. On a clean miren shutdown, the first SIGTERM cancels the server context (cli/commands/global.go), which unwinds Server() and runs the deferred buildkitComponent.Stop() (cli/commands/server.go). Stop() SIGTERMs buildkitd and deletes the container, so the next boot's LoadContainer returns NotFound and creates a fresh daemon. That is the normal, working restart path — the one exercised by systemctl restart, deploy-driven bounces, and make dev-server-restart.

KillMode=process in the systemd unit changes this on an ungraceful exit. If miren dies without its Stop() defer completing — a crash / panic / OOM-kill, a second SIGTERM (which hits os.Exit(130) and bypasses defers), or a graceful stop that exceeds TimeoutStopSec=90s and gets SIGKILLed — KillMode=process kills only miren's own process and leaves buildkitd (in containerd's cgroup) orphaned and still running.

On the next boot, restartExistingContainer() finds that still-running task, sets c.running = true, and reuses it — only dialing the socket to confirm readiness. It never restarts the buildkitd process. But the reused daemon can't serve the new miren process: buildkit builds use a client-side session (the daemon streams local build context back from the client over a hijacked gRPC stream), and that session state was bound to the miren process that exited. The new process's solves fail with NotFound: no such job <ref>. This is silent — no error, no log — and every build fails until buildkitd is manually killed and miren restarted.

It hit the luanchat cluster on 2026-06-30: all builds failed for ~2.5 hours, and buildkitd had been up ~3 days — i.e. it had survived across the restart via the ungraceful path above.

So the failure is rare but fatal: it needs an ungraceful exit to orphan a live daemon, which is why it went unnoticed for so long even though miren has been restarted many times — the vast majority of those restarts took the fresh-daemon path. But once it happens, every build fails until manual intervention.

Fix

Handle the orphaned daemon at the layer that owns buildkit's lifecycle — the containerd component, on start. When Start() finds an existing container, reach the same known-good state a fresh boot produces: a buildkitd task freshly bound to the current process.

  • restartExistingContainer() now always force-stops any existing task and starts a fresh one bound to the current process (the primary fix). It no longer reuses a running task. This evicts an orphaned daemon on the next start regardless of how miren exited.
  • stopTask() no longer returns early when SIGTERM fails, so task.Delete() always runs and can't leak a task that would block the next NewTask. The delete context keeps the containerd namespace while detaching from the parent's cancellation.
  • Exit-monitor goroutine (via task.Wait(), established before Start()) clears c.running when buildkitd dies unexpectedly, so IsRunning()/Client() reflect reality. Stop() cancels it and now tears down the container even after such a death.
  • bkc.Info() pre-flight is now fatal — a dead/stale daemon fails the build immediately instead of proceeding into a confusing downstream error.
  • BuildKitProvider interface in servers/build lets builds be faked in unit tests (with a typed-nil guard preserving the BuildKit == nil check).

Testing

  • hack/it components/buildkit — 10 tests pass, including new TestBuildkitRestart subtests that prove the buildkitd PID actually changes across a simulated restart (old task evicted, new one created and serving), that IsRunning() flips false on an out-of-band daemon death, and that Stop() cleans up the container even after a death.
  • go test ./servers/build/ — new fake-provider unit test for the connect-error fast-fail; full package green.
  • golangci-lint / gofmt / go vet clean.

@evanphx evanphx requested a review from a team as a code owner July 6, 2026 18:12
@coderabbitai

coderabbitai Bot commented Jul 6, 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: CHILL

Plan: Pro

Run ID: 0be547e6-f125-4419-b6e7-0bcd4e6bb6e6

📥 Commits

Reviewing files that changed from the base of the PR and between 6bf18d7d5ae24c0ec750e677c2d5989a81ba50b4 and ef7097e.

📒 Files selected for processing (5)
  • components/buildkit/buildkit.go
  • components/buildkit/buildkit_test.go
  • servers/build/build.go
  • servers/build/build_saga_buildkit.go
  • servers/build/build_saga_buildkit_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • servers/build/build.go
  • servers/build/build_saga_buildkit.go
  • components/buildkit/buildkit_test.go
  • components/buildkit/buildkit.go
  • servers/build/build_saga_buildkit_test.go

📝 Walkthrough

Walkthrough

Changes

components/buildkit/buildkit.go now tracks a monitor cancel function, starts BuildKit tasks through a helper that wires exit monitoring before marking the component running, and updates stop/restart paths to clear stale tasks and tolerate unexpected exits. servers/build/build.go introduces a BuildKitProvider interface and stores it in Builder. servers/build/build_saga_buildkit.go now fails builds when BuildKit Info calls fail. New tests cover restart, exit monitoring, and BuildKit client failure cases.

Compact metadata

Type Enhancement, Bug fix
Scope components/buildkit, servers/build
Estimated effort High

Related issues: None referenced.

Related PRs: None referenced.

Suggested labels: buildkit, reliability, testing

Suggested reviewers: None specified.

Poem
A task begins, its watcher set,
so exits never go unmet.
The build asks first if daemon breathes,
and stops the run if error seethes.
When restarts come, the stale is gone,
and running state keeps pace with dawn.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
servers/build/build_saga_buildkit_test.go (1)

22-33: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't exercise the newly-added bkc.Info() health check.

fakeBuildKitProvider.Client always returns nil, f.clientErr, so runBuildkitBuild short-circuits at the pre-existing b.BuildKit.Client(ctx) error check (build_saga_buildkit.go lines 142-147) and never reaches the new pre-flight bkc.Info(ctx) health check (lines 150-161) that this PR actually introduces. The test validates real but pre-existing behavior, leaving the PR's core new safeguard (detecting a stale-but-reachable daemon) untested.

Consider adding a case where Client() succeeds (e.g., a client dialed against a non-listening/bogus unix socket) so Info() itself fails, actually covering the new code path.

Also applies to: 37-55

🤖 Prompt for 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.

In `@servers/build/build_saga_buildkit_test.go` around lines 22 - 33, The current
test setup only exercises the existing BuildKit client creation failure path and
never reaches the new bkc.Info() pre-flight health check in runBuildkitBuild.
Update fakeBuildKitProvider (or add a new variant) so Client() can succeed while
Info() fails, allowing the test to cover the stale-but-reachable daemon path
introduced by the new health check. Use the runBuildkitBuild and
fakeBuildKitProvider symbols to locate the affected test cases and add coverage
for the Info() failure branch.
🤖 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 `@components/buildkit/buildkit.go`:
- Around line 177-189: Reset c.container to nil whenever task setup fails after
assigning the container, so later Stop() calls don’t act on a deleted container.
Update the failure paths around container.NewTask and c.startTaskAndMonitor to
clear c.container before returning after container.Delete(...), and apply the
same cleanup pattern in restartExistingContainer where the container is deleted
on startup failure.

---

Nitpick comments:
In `@servers/build/build_saga_buildkit_test.go`:
- Around line 22-33: The current test setup only exercises the existing BuildKit
client creation failure path and never reaches the new bkc.Info() pre-flight
health check in runBuildkitBuild. Update fakeBuildKitProvider (or add a new
variant) so Client() can succeed while Info() fails, allowing the test to cover
the stale-but-reachable daemon path introduced by the new health check. Use the
runBuildkitBuild and fakeBuildKitProvider symbols to locate the affected test
cases and add coverage for the Info() failure branch.
🪄 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: CHILL

Plan: Pro

Run ID: 0397d09d-a9ff-42fe-86d0-e44413bbe841

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9b37b and b6581c796aaaead39097749386d452b68c844858.

📒 Files selected for processing (6)
  • cli/commands/server_install.go
  • components/buildkit/buildkit.go
  • components/buildkit/buildkit_test.go
  • servers/build/build.go
  • servers/build/build_saga_buildkit.go
  • servers/build/build_saga_buildkit_test.go

Comment thread components/buildkit/buildkit.go
@evanphx evanphx force-pushed the mir-1303-buildkitd-stale-process-reuse-after-miren-restart branch from b6581c7 to 6bf18d7 Compare July 6, 2026 18:39

@phinze phinze 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.

LGTM! Good catch and good fix 🛠️

Comment thread servers/build/build.go
@@ -95,14 +105,14 @@ type Builder struct {

// BuildKit is the persistent BuildKit component for container image builds.
// When set, uses the shared daemon instead of launching ephemeral sandboxes.
BuildKit *buildkit.Component
BuildKit BuildKitProvider

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.

👏 Love the seam

buildkitd runs as a containerd-managed container. On a clean miren
shutdown, Stop() SIGTERMs it and deletes the container, so the next boot
creates a fresh daemon. But KillMode=process means an ungraceful miren
exit (crash, OOM, a second SIGTERM that hits os.Exit, or a stop that
overruns TimeoutStopSec and gets SIGKILLed) leaves buildkitd orphaned and
still running.

On the next boot restartExistingContainer() found that running task and
reused it, only dialing the socket to confirm readiness. buildkitd's
in-process session server was bound to the miren process that launched it,
so the reused daemon can't serve the new process: builds fail with
"NotFound: no such job". This is silent and every build fails until
buildkitd is manually killed and miren restarted. Rare (needs an
ungraceful exit) but fatal when it happens.

Fixes MIR-1303:

- restartExistingContainer() now always force-stops any existing task and
  creates a fresh one bound to the current process.
- stopTask() no longer returns early when SIGTERM fails, so task.Delete()
  always runs and can't leak a task that blocks the next NewTask. The
  delete context keeps the containerd namespace while detaching from the
  parent's cancellation.
- A task exit-monitor goroutine via task.Wait() clears c.running when
  buildkitd dies unexpectedly, so IsRunning()/Client() reflect reality;
  Stop() cancels it and tears down the container even after such a death.
- The build pre-flight bkc.Info() check is fatal, failing a build fast on
  a dead/stale daemon instead of proceeding into a confusing error.
- servers/build gains a BuildKitProvider interface so builds can be faked
  in unit tests; adds unit + integration-gated component tests.
@evanphx evanphx force-pushed the mir-1303-buildkitd-stale-process-reuse-after-miren-restart branch from 6bf18d7 to ef7097e Compare July 6, 2026 19:14
@evanphx evanphx merged commit f5adf99 into main Jul 6, 2026
17 checks passed
@evanphx evanphx deleted the mir-1303-buildkitd-stale-process-reuse-after-miren-restart branch July 6, 2026 20:01
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.

2 participants