Skip to content

Revert and delete no longer hang when an actor's node is deleted - #2486

Merged
Michelle Au (msau42) merged 3 commits into
agent-substrate:mainfrom
eren23:fix/lost-node-terminate
Oct 20, 2026
Merged

Michelle Au (msau42) merged 3 commits into
agent-substrate:mainfrom
eren23:fix/lost-node-terminate

Conversation

@eren23

@eren23 Eren Akbulut (eren23) commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

After a node is deleted, revert can still hang for a CRASHED actor or a paused actor whose latest upload is PENDING. Delete can hang for any actor still bound to that node. #2883 already lets revert and resume forget an unreachable node when the actor has no worker assignment or crash and the latest local snapshot has a COMPLETED durable copy.

When no atelet is indexed on the assigned node, ensureAteletTerminated uses NodeGone to check the Node through the live Kubernetes API. It skips termination only on NotFound, after checking again that no atelet appeared. Existing nodes and lookup failures stay retryable. The API server gains get permission on nodes.

External volumes still go through DetachVolume: nil and NotFound succeed, other errors propagate. Kubernetes Node deletion does not fence a machine that is still running; resources on such a machine may remain.

Rebased onto main 872ab2077. The whole-workflow test comment now identifies its paused actor's PENDING upload. Validation:

  • Targeted revert/delete/detach and TestNodeGone tests pass with -race -count=3 -shuffle=on and Docker-backed database tests enabled.
  • The full race suite passes all 15 ateapi packages with tests. Overall it reports 10,601 passing test/subtest events, 8 skipped, and two failures in cmd/ateworker-microvm/internal/child: TestReapedWhenItDiesOnItsOwn and TestStop. Both report unsupported operation on macOS and reproduce from unchanged main 872ab2077; the non-Linux pidfd implementation returns errors.ErrUnsupported.
  • hack/verify-all.sh passes (using local Python 3.13 and clang-format 23.1.3).
  • A fresh independent review found no issues in the rebased patch. git diff --check passes.

Earlier kind runs exercised real worker deletion, paused/crashed revert and delete, and the kept-node control. Those live runs were not repeated for this rebase. Current volume tests use a mock plugin; no real CSI-driver validation is claimed.

Part of #2470. Resume without a completed latest upload remains separate.

AI assistance: Claude Code helped write the original code and tests. Codex clarified the fixture comment, rebased the branch, reviewed the integration, and ran validation.

@adrianludwin

Copy link
Copy Markdown
Collaborator

I'm not an ATE developer, I just happened to run into this issue over the weekend so I'm taking an interest in it!

My only thought about this PR is: it's a very targeted solution. It takes exactly one flow (deleting an actor) and solves it precisely - but should we consider something more comprehensive? I don't know what that might look like and the answer might be "no," but I think it's worth considering.

@eren23

Copy link
Copy Markdown
Contributor Author

I agree there’s a broader recovery-policy question here. This change handles the shared termination step used by both revert and delete, so those operations can finish when the assigned Node object is gone. A missing atelet on a node that still exists remains retryable.

A more comprehensive design would need to cover recovery and reassignment, fencing when a machine remains alive after its Node object is deleted, and orphan cleanup. I’d keep this fix focused while discussing that broader policy separately. Is there a particular recovery flow you think should be included here?

@adrianludwin

Copy link
Copy Markdown
Collaborator

No, it was more of a "code smell" kind of situation. Julian Gutierrez Oschmann (@juli4n) and/or Scott Funkenhauser (@sfunkenhauser) might have follow-up suggestions.

@igooch igooch left a comment

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.

Left a comment on the issue that Resume also gets stuck when a paused actor's node is gone. Could you change "Fixes #2470" to "Part of #2470" so the issue stays open for resume? I'd like to reuse NodeGone there in a follow-up.

Comment on lines +163 to +164
// The node left the cluster, and its workload went with it. Waiting
// for its atelet would block revert and delete forever.

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.

The PR description says a deleted Node object with the machine still running leaves atelet's state behind, so "its workload went with it" isn't always true. Could the comment state the trade instead? Something like:

// A deleted Node object counts as the node being gone for good. If the
// machine still runs, whatever atelet holds for the actor stays there.

// for its atelet would block revert and delete forever.
slog.WarnContext(ctx, "assigned node no longer exists, skipping atelet terminate request",
slog.String("node", nodeName), slog.Any("actor", actorRef))
return 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.

After this returns, revert runs ensureVolumesDetached and delete runs ensureVolumesDetachedForDelete, both with the same gone node name. For CSI that becomes ControllerUnpublishVolume on a node that no longer exists. The CSI spec says a driver should return OK there, but not every driver does. Did your kind run include an actor with external volumes? If not, an actor with volumes could still get stuck at the detach step.

// A node that left the cluster takes its workload with it, so revert and
// delete must not wait for its atelet. A node that is still there may get its
// atelet back, and a failed lookup proves nothing, so both stay retryable.
func TestEnsureAteletTerminated_NodeGone(t *testing.T) {

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.

This tests the helper on its own. The bug in #2470 is that the workflows never finish, so could we add a test at the workflow level as well? For example, a PAUSED actor whose node is gone reverts to SUSPENDED with assigned_node cleared, and delete --any-state removes it. That would catch a later step that dials the node again, and it would keep the fix if ensureAteletTerminated gets restructured.

@eren23

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I rebased on main, changed "Fixes" to "Part of #2470", and took your wording for the comment.

No, my kind run had no actor with external volumes. Revert and delete still call the plugin's DetachVolume with the gone node's name. A nil or NotFound result lets the step finish. Any other error fails it. Revert then stays REVERTING. Delete stays DELETING and skips the worker release, but it still runs the volume delete and the snapshot cleanup. I kept the detach because a deleted Node object with a running machine can still hold the disk. Keeping it does not fence that disk, but skipping it would make that case worse.

I added a test that runs the whole revert and delete for a paused and a crashed actor. It covers a gone node and a kept node, and a volume whose detach returns nil, NotFound or another error. The volume cases use a mock plugin, not a real CSI driver. Without the fix, every gone node case fails.

Reusing NodeGone for resume sounds good to me. I'm happy to help with that follow-up.

@igooch igooch left a comment

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.

#2883 merged today and narrows what this PR fixes. On main, revert already forgets an unreachable node when canDetachWithoutAtelet holds. That needs no worker assignment, no crash, and a COMPLETED durable copy of the latest local snapshot. detachFromFullNode does the same for resume. This PR still fixes the cases that check excludes. Those are a CRASHED actor, a paused actor whose upload never completed, and every delete. Could the first paragraph of the description say that, so it matches main?

The branch is also behind main, and #2881 and #2883 both touched workflow_revert.go.

Once rebased, this looks good to me.

Comment on lines +141 to +143
// atelet (#2470). If the Node object is gone, both finish, and the revert
// clears assigned_node. If it still exists, both fail with ErrNoAteletOnNode
// and leave the actor REVERTING or DELETING for a retry. A volume detach on

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.

The paused actor here has a PENDING upload. Could the comment say so? On main, a paused actor with a COMPLETED upload passes canDetachWithoutAtelet, so revert finishes on a kept node too.

Since agent-substrate#2222, revert and delete call atelet Terminate whenever the actor
has an AssignedNode. A PAUSED or CRASHED actor keeps that node. When
the node left the cluster, the dial fails with ErrNoAteletOnNode on
every try, so the actor stays in REVERTING or DELETING, and even
delete --any-state cannot remove it.

When the dial finds no atelet, ensureAteletTerminated now asks the
dialer's new NodeGone whether the node still exists. NodeGone reads the
node live from the API server, not from an informer, so a stale cache
cannot skip a terminate. It reports the node as gone only on NotFound
and when no atelet pod appeared meanwhile. A deleted node counts as
nothing left to terminate. A node that exists and a failed lookup keep
the current retry, and the lookup error is added to the returned error.

ate-api-server gets get on nodes.

Fixes agent-substrate#2470
Run the whole revert and delete workflows for a paused and a crashed
actor whose node has no atelet, with the Node object gone and kept, and
with an external volume whose detach on the gone node returns OK,
NotFound or another error. Also reword the comment on the skipped
terminate: a deleted Node object counts as gone for good, and if the
machine still runs, what atelet holds for the actor stays there.
@msau42
Michelle Au (msau42) added this pull request to the merge queue Oct 20, 2026
Merged via the queue into agent-substrate:main with commit 94c91b2 Oct 20, 2026
15 checks passed
Tom Gamble (gambtho) pushed a commit to gambtho/substrate that referenced this pull request Oct 21, 2026
…#3010)

agent-substrate#2883 lets resume forget an unreachable node when a paused actor's
latest snapshot is durable. When that upload is still PENDING, the actor
stays pinned. If the node was deleted, every resume returns
`Unavailable`, and nothing says the snapshot is lost.

Now, when resume finds no atelet on the pinned node and `NodeGone`
reports the Node object deleted, it reads the actor again. If the upload
was recorded during the lookup, it detaches as usual. Otherwise it
crashes the actor. The crash keeps `assigned_node` and the PENDING
snapshot. `RevertActor` then skips the terminate, deletes the pending
objects, and returns the actor to its last completed durable snapshot. A
node that still exists, or a failed Node lookup, keeps the current
retry.

There are two known limits. The check runs only after resume finds room
on another node, so with no room anywhere resume still returns
`RESOURCE_EXHAUSTED`. And a Node object deleted while the machine keeps
running crashes the actor even though its checkpoint is still on that
machine. That's the same trade agent-substrate#2486 makes.

A PENDING copy may already be fully uploaded if atelet died before
reporting it. Resume still crashes the actor, and revert deletes that
copy. Recovering it needs ate-api-server to check for the manifest,
which is a follow-up.

No proto changes. `ResumeActor` can now crash the actor and return
`FAILED_PRECONDITION` in this case, and `docs/api-guide.md` says so.

Part of agent-substrate#2470

Supersedes agent-substrate#3005. Its "upload completes during lookup" regression case
is adapted here.

AI use: Claude Code helped write the change and tests. I reviewed them.

- [ ] Tests pass
- [x] Docs updated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants