Skip to content

report actors whose sandbox dies on its own as crashed - #2695

Merged
Scott Funkenhauser (sfunkenhauser) merged 15 commits into
agent-substrate:mainfrom
BenTheElder:actor-crash-report
Oct 19, 2026
Merged

Scott Funkenhauser (sfunkenhauser) merged 15 commits into
agent-substrate:mainfrom
BenTheElder:actor-crash-report

Conversation

@BenTheElder

@BenTheElder Benjamin Elder (BenTheElder) commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #2626

The control plane only found out a sandbox died when the next operation on the actor failed, which for an idle actor could be never.

  • New WorkerService.ReportActorCrashed (and the atelet AteomSupport mirror), authorized like RequestActorSuspend. A RUNNING actor is crashed under its lease, anything else is refused since the operation in flight owns it.
  • ateworker-microvm reaps cloud-hypervisor / virtiofsd as soon as they exit, and ateworker-gvisor watches the runsc sandbox. If one dies while ateworker isn't stopping it, ateworker tears the actor down itself (the crash clears the assignment, so nothing else would), writes its final usage record, and reports it
  • Exits are watched through pidfds on the netpoller, so watching doesn't cost a thread per process
  • The gVisor sandbox is pinned right after runsc create, which closes the pid reuse gap Scott Funkenhauser (@sfunkenhauser) pointed out on run ateom under minimal init #2626
  • Not covered yet: a workload exiting while its sandbox stays up, and a micro-VM guest panic, which reboots the guest without cloud-hypervisor exiting. Both need their own design ...

Tested with a new e2e test, TestSandboxCrashIsReported (kills the sandbox from an ephemeral container, expects CRASHED, then revert + resume), plus both full lanes on GKE with no false crash reports.

  • Tests pass
  • Appropriate changes to documentation are included in the PR (none needed)

This PR was AI assisted. I have tested everything on GKE and reviewed the changes.

Comment thread cmd/ateworker-gvisor/main.go Resolved
Comment thread cmd/ateworker-gvisor/main.go Outdated
if sandbox == nil {
slog.WarnContext(ctx, "Cannot watch the sandbox exit after checkpoint: it was never pinned")
}
err := rcmd.cmdCheckpoint(ctx, ocispec.PauseContainer, checkpointPath, durablePaths)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A failed checkpoint (849) resets stopping (854), so a sandbox that checkpoint still killed is reported as a crash. Intended?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

this is intended, the actor is dead here?

Comment thread internal/pidfd/pidfd.go Resolved
Comment thread internal/pidfd/pidfd.go
// A process already gone yields a closed channel.
func Exited(pid int) (<-chan struct{}, error) {
done := make(chan struct{})
fd, err := unix.PidfdOpen(pid, unix.PIDFD_NONBLOCK)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PIDFD_NONBLOCK needs kernel 5.10+. Document the minimum?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

On older kernels it now opens the pidfd without the flag and uses the blocking-poll fallback. The package doc says Linux 5.3+ is required, and that before 5.10 each watch holds its own thread.

I think we might actually require 6+ in some other places but I don't think we needed a hard requirement here. It's an optimization we really want but we keep it onsufficiently new kernels.

Comment thread internal/e2e/sandboxkill.go Resolved
if state := actor.GetStatus().GetState(); state != ateapipb.ActorState_ACTOR_STATE_RUNNING {
return nil, apierror.FailedPrecondition("actor %s is %s, not RUNNING", actorRef, state)
}
if err := crashActor(leaseCtx, w.store, actorRef, ateattr.OperationUnknown, message); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Counted as operation unknown, same bucket as every other crash. Own value or a reason label?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, need to think on that one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think this thread may need further cleanup beyond the scope of the PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

cc Jeff Luo (@JeffLuoo) Krisztian F (@krisztianfekete) , I think we might want to add reason but that will require updating more places and this PR is already pretty big, I think I'll defer it and we'll chat on slack about how we want to handle these generally?

@BenTheElder
Benjamin Elder (BenTheElder) force-pushed the actor-crash-report branch 3 times, most recently from 095c324 to c612ebd Compare October 16, 2026 17:33
}
reportCtx, cancel := context.WithTimeout(ctx, crashReportTimeout)
defer cancel()
if err := s.crashReporter.ReportCrashed(reportCtx, attribution, message); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If the ReportCrashed fails, we can have an actor still RUNNING and worker capacity being consumed. Maybe instead of having 1-off crash detection, we need some kind of status reporting (worker reports all currently running actors) and reconciliation?

I don't really have a good suggestion here, and this change is a step in the right direction so I don't think we need to block on this, but maybe file a gh issue to track the gap?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

SGTM, we can follow-up. I think we are trying to balance chattiness with accuracy but we should have some eventual reconciliation ...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread cmd/ateworker-gvisor/hosted.go Resolved
…y exit

under the pid1 init a VMM that died on its own stayed a zombie until the actor was torn down, which may never happen
the control plane drops the worker assignment when it crashes the actor, so nothing would tear it down otherwise
the sandbox is watched through a pidfd, so the init reaping it doesn't hide the exit
the checkpoint wait and the crash watcher used to read the pidfile later, when a dead sandbox's pid could already be reused
a failed checkpoint can leave the actor RUNNING, and the watcher stayed off for the rest of the activation
…e fd

a read error used to look like the process exiting. this also covers kernels before 5.10, which lack PIDFD_NONBLOCK
if client != nil {
tShutdown := time.Now()
shutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
if err := client.Shutdown(shutCtx); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

qq: is this client.Shutdown necessary here (since we call kill ch+virtiofsd processes later)?

@BenTheElder Benjamin Elder (BenTheElder) Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Probably not, and it might even be wasting time, but it predates this PR, I think we should handle it in a follow-up.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sounds good, sent a small cleanup PR: #2904

Merged via the queue into agent-substrate:main with commit d4204fd Oct 19, 2026
15 checks passed
@BenTheElder
Benjamin Elder (BenTheElder) deleted the actor-crash-report branch October 19, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api User-facing API changes area/node area/reliability kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ateom-gvisor: runsc delete fails after most checkpoints because cleanup runs before the sentry is reaped

4 participants