Skip to content

feat(api): introduce RevertActor lifecycle RPC - #1675

Merged
Zoe Zhao (zoez7) merged 18 commits into
agent-substrate:mainfrom
shrutiyam-glitch:revertrpc_1
Sep 18, 2026
Merged

Zoe Zhao (zoez7) merged 18 commits into
agent-substrate:mainfrom
shrutiyam-glitch:revertrpc_1

Conversation

@shrutiyam-glitch

@shrutiyam-glitch shrutiyam-glitch commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces the RevertActor RPC for actor lifecycle management. It includes the API definition, the corresponding workflow execution logic, observability metrics, and updates to the authorization model to support reverting actors.

Fixes #1556
Docs updated in #1711

Commit-wise Changes

1. Add RevertActor RPC (1145d99)

  • Introduces the new RevertActor RPC to the API definitions.
  • Updates the corresponding protobuf bindings (affecting files like ateapi.pb.go and ateapi_pb2.py).

2. Add the revertActor workflow and observability metrics (dcb1fae)

  • Implements the core revertActor workflow logic, designed to be idempotent and re-enterable. It progresses through the following steps:
    • Mark Reverting: Validates that the actor is in a revertable state (RUNNING, PAUSED, or CRASHED) and transitions its state to REVERTING.
    • Discard Worker: Safely tears down the execution environment by terminating the workload, detaching volumes, and releasing the assigned worker.
    • Collect In-Progress Snapshot: Cleans up external object storage by deleting any objects a previous suspend operation was partway through writing.
    • Finalize: Commits the actor to SUSPENDED and strips all node-local and in-progress state pointers (clearing WorkerAssignment, LocalSnapshotInfo, etc.), returning the actor to its untouched external snapshot.
  • Instruments the workflow with lifecycle operation metrics (e.g., updating ate.actor.lifecycle.operation.duration to track revert operations).
  • Note/TODO: Currently, when reverting a paused actor, the workflow drops the pointer to the node-local state but does not actually prune the local checkpoint bytes from the node (this is tracked in DeleteActor never cleans the actor's node directory #641).

3. Add can_revert to the authorization model (2fc53ae)

  • Adds the can_revert permission to the auth model, mirroring the shape of can_suspend (editor tier of the parent atespace, plus a direct grant so a machine identity can revert the actor it drives without holding an atespace role).

4. Serve RevertActor and add the CLI verb (f04fab4)

  • Wires the Control.RevertActor service method to the workflow (replacing the generated stub that previously answered Unimplemented).
  • Adds the "ate revert actor" CLI command, making the feature usable end-to-end.
  • Implements Terminate for the fake atelet. This was necessary because reverting an actor from the RUNNING state is the first path to reach this call in functional tests (previously, delete tests skipped this step as they ran against actors with no worker assignment).

5. Add a manual verify script for RevertActor (8022db3)

  • Adds a script to manually exercise RevertActor against a real control plane, since unit and functional tests only run against a fake atelet.
  • Tests reverting from CRASHED, RUNNING, and PAUSED states, and verifies that attempting to revert a SUSPENDED actor is properly rejected.
  • Simulates a crash by deleting the worker pod the actor runs on to verify the workflow can handle the absence of a worker to terminate.
  • Tests pass
  • Appropriate changes to documentation are included in the PR

RevertActor is a new actor lifecycle RPC, so the model needs a permission for it. Give it the same shape as can_suspend: the editor tier of the parent atespace, plus a direct grant so a machine identity can revert the actor it drives without holding an atespace role.

Revert is the recovery counterpart of suspend and discards execution the same way, so anyone trusted to suspend an actor is trusted to revert it. Viewers and cluster viewers are asserted false, matching the other data plane verbs.

Nothing in Go reads these relations yet. This is the model and its test suite only, as with can_suspend and can_resume.
The workflow existed but nothing reached it: Control.RevertActor was still
the generated stub answering Unimplemented. Wire the service method to the
workflow and add "ate revert actor", so the feature is usable end to end.

The fake atelet gained a Terminate implementation. It had none, because no
functional test had ever reached that call: delete is the only other caller
and every delete test runs against an actor with no worker assignment, which
skips the step. Revert from RUNNING is the first path that gets there, and
without the method the embedded UnimplementedAteomHerderServer answers
Unimplemented, which revert treats as a real failure.
@shrutiyam-glitch shrutiyam-glitch changed the title Revertrpc 1 feat(api): introduce RevertActor lifecycle RPC Sep 16, 2026
RevertActor's unit and functional tests run against a fake atelet, so
nothing exercises the verb against a real control plane. This script
reverts from CRASHED, RUNNING and PAUSED, and checks that a SUSPENDED
actor is rejected. The crash is produced by deleting the worker pod the
actor runs on, which is the case that leaves revert with no worker to
terminate through.

It needs a live cluster and an existing template, so it is not wired into
make verify.
Comment thread cmd/ateapi/internal/controlapi/functionaltest/actor_test.go
Comment thread hack/verify-revert-actor.sh Outdated
Add TestRevertActor_FromPaused and TestRevertActor_FromCrashed to
verify RevertActor recovers paused and crashed actors to SUSPENDED at
their last external snapshot and allows subsequent resumes
Comment thread cmd/ateapi/internal/controlapi/workflow_revert.go Outdated
}

// TODO: Check if local checkpoints need to be pruned #641
// w.ensureLocalCheckpointsPruned(leaseCtx, actor)

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 local checkpoints should be pruned by ensureWorkerDiscarded -> ensureAteletTerminated above. is that not the case?

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.

Currently, ensureAteletTerminated runs pruneLocalCheckpoints(actorUID) only for RUNNING actors

For PAUSED actors, worker_assignment is nil (

if assignment == nil {
slog.InfoContext(ctx, "actor has no worker assignment, skipping atlet terminate request", slog.Any("actor", actorRef))
return nil
}

), so ensureAteletTerminated skips.
so, I guessed it will be a separate step itself and get called from deleteActor flow and revertActor flow.

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.

I see, we should be able to call pruneLocalCheckpoints for paused actors as well, after #1742 is submitted, we can do it in a follow up PR.

return nil, nil, fmt.Errorf("while fetching actor: %w", err)
}

actorTemplate, err := resolveActorTemplate(ctx, w.store, actor)

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.

I think there is an issue with the sequence:

  1. suspended actor on actor template v1 , take a full snapshot, actor.status.current_actor_template_uid == v1
  2. UpdateActor to v2, ResumeActor, now current_actor_template_uid updated to v2
  3. Call RevertActor, now the snapshot is reverted to the one taken on v1, but the actor's current_actor_template_uid is still v2.

Can we move the actor_template_uid as a field of ExternalSnapshot, so the revert will take effect together?

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.

handled this case in a follow up PR - #1713

…iscarded

Align function, trace span, and test names with revert semantics of
discarding rather than collecting in-progress snapshots, and clarify
local checkpoint pruning behavior during revert.
The in-progress snapshot is now recorded as a full URI rather than a
name derived from the actor's template, so revert reads the URI
directly and no longer needs the template to find the objects.
Check that the URI is owned by the actor before deleting its prefix.
Derivation used to make that structural; a stored URI can name anything,
and this step deletes what it is pointed at.
Adding RevertActor left both unupdated.

releaseBoundActor labelled the crash metric with the unknown operation,
hiding that a revert was in flight when the worker pod went away.

The golden snapshot reconciler fell through to the default branch and
requeued forever instead of failing the template. Nothing in the golden
flow reverts its actor, so that state means someone else interfered.
@shrutiyam-glitch

Copy link
Copy Markdown
Collaborator Author

The tests are failing because of atelet panic - documented here - #1710.
Temporary fix for this - #1684

@SHRUTI6991

Copy link
Copy Markdown
Collaborator

The FGA part looks good to me.

@bowei Bowei Du (bowei) added kind/feature An enhancement / feature request or implementation area/api User-facing API changes area/api-machinery labels Sep 17, 2026
}

// TODO: Check if local checkpoints need to be pruned #641
// w.ensureLocalCheckpointsPruned(leaseCtx, actor)

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.

I see, we should be able to call pruneLocalCheckpoints for paused actors as well, after #1742 is submitted, we can do it in a follow up PR.

ctx, done := stepSpan(ctx, "FinalizeReverted")
defer func() { err = done(err) }()

latestActor, err := w.store.GetActor(ctx, actorRef)

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.

check the latestActor is in Reverting state first?

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.

done in 30870b7

ctx, done := stepSpan(ctx, "DiscardWorker")
defer func() { err = done(err) }()

if actor.GetStatus().GetWorkerAssignment() != 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.

We should check if the worker is still assigned to the same Actor.

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.

done in eaf34c5

}
// A suspend records the in-progress URI under the actor's own prefix
// before atelet writes the first object, so a URI owned by anything else
// is a corrupted record: deleting it would collect another actor's data.

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.

Suggested change
// is a corrupted record: deleting it would collect another actor's data.
// is a corrupted record.

It shouldn't be possible that another actor owns this snapshot.

Comment thread cmd/ateapi/internal/controlapi/workflow_revert.go
@zoez7
Zoe Zhao (zoez7) added this pull request to the merge queue Sep 18, 2026
Merged via the queue into agent-substrate:main with commit 944abe3 Sep 18, 2026
11 checks passed
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/api-machinery kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Need a way to recover an actor from CRASHED state

4 participants