Skip to content

Enforce minimum snapshot fidelity when resuming actors and report restored fidelity - #2818

Closed
Zoe Zhao (zoez7) wants to merge 2 commits into
agent-substrate:mainfrom
zoez7:minimum-fidelity
Closed

Zoe Zhao (zoez7) wants to merge 2 commits into
agent-substrate:mainfrom
zoez7:minimum-fidelity

Conversation

@zoez7

@zoez7 Zoe Zhao (zoez7) commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

The PR title becomes the release note. Write it for users: what changed for them, not how the code changed.

Add minimum_fidelity to SnapshotConfig on ActorTemplate (defaulting to SNAPSHOT_FIDELITY_VOLUMES and validated to be <= preferred_fidelity) and fidelity to ResumeActorResponse:

I think the current semantics have some problems, see https://github.com/zoez7/substrate/blob/d56054bb0bfccf364f64fc63884546186c1a6af4/minimum-fidelity-semantics.md#proposal

Fixes #2102

  • Tests pass
  • Appropriate changes to documentation are included in the PR

…tored fidelity

Add `minimum_fidelity` to `SnapshotConfig` on `ActorTemplate` (defaulting to
`SNAPSHOT_FIDELITY_VOLUMES` and validated to be `<= preferred_fidelity`) and
`fidelity` to `ResumeActorResponse`.

Previously, when a snapshot restore failed or was unavailable on the target
worker, `ResumeActor` always degraded to a cold boot (`SNAPSHOT_FIDELITY_VOLUMES`)
without letting templates require higher restore fidelity or telling callers
which fidelity was actually restored.

Now `ResumeActor`:
- Prechecks the best achievable restore fidelity before creating volumes or
  assigning a worker, and rejects runtime placement fallback when the fallback
  fidelity would drop below `minimum_fidelity`.
- Checks the actual restore fidelity returned by `atelet` and transitions the
  actor to `ACTOR_STATE_CRASHED` (returning `FailedPrecondition`) if the
  effective restore fidelity is below `minimum_fidelity`.
- Reports the effective restore fidelity on `ResumeActorResponse.fidelity`
  (`SNAPSHOT_FIDELITY_UNSPECIFIED` when the actor was already `RUNNING`).
A template-level restore floor cannot be honored when memory loss is
unavoidable (template replaced, runtime upgraded, memory layer not
captured), so a Resume refused for those reasons never succeeds and
leaves the actor stuck. The note proposes dropping the template field,
keeping ResumeActorResponse.fidelity, and moving any wait-rather-than-
downgrade choice to the Resume request.
@bowei Bowei Du (bowei) added area/api User-facing API changes area/api-machinery area/storage kind/feature An enhancement / feature request or implementation labels Oct 19, 2026
Benjamin Elder (BenTheElder) pushed a commit to BenTheElder/substrate that referenced this pull request Oct 20, 2026
…nt-substrate#2881)

A suspended actor with a local snapshot is pinned to its node. While
that node was full, its resume failed with `ResourceExhausted` even when
other nodes had room, until atelet detached the actor on its own after
`--snapshot-detach-delay`. Resume now detaches the actor itself, with
`DetachActor` from agent-substrate#2852, and restores it on another node.

Part of agent-substrate#589. This covers a full node. A node that is gone, the
scale-down case, is still open.

#### How the move works

Assignment still runs pinned to the node first. When it finds no room
there, and a worker on another node has room, resume:

1. Calls `DetachActor` on the pinned node. atelet uploads the latest
snapshot and deletes the local copies.
2. Records the detach, which marks the upload `COMPLETED` and clears the
locality and `assigned_node`.
3. Assigns a worker on any node and restores from the durable snapshot.

`DetachActor` is synchronous. If the snapshot isn't durable yet, the
resume waits for atelet to upload it.

#### What the restore keeps

On a worker of the same runtime, the restore keeps the snapshot's
fidelity. On a worker of another runtime, a MEMORY snapshot restores at
ROOTFS, as for any durable-only actor since agent-substrate#2880. Process memory is
lost, but rootfs writes and volumes are kept. ROOTFS and VOLUMES
snapshots keep their fidelity on any worker of the same sandbox class. A
template can't yet refuse the downgrade; agent-substrate#2818 would add that.

#### When the actor doesn't move

- No worker anywhere has room. The resume returns `ResourceExhausted` as
before, without calling atelet.
- The detach fails. The resume fails, and the actor stays SUSPENDED on
its node, not crashed, since it can still resume there.
- The node is gone. `DetachActor` can't reach atelet, so the resume
returns `Unavailable` and the actor stays pinned.

```mermaid
sequenceDiagram
    participant C as client
    participant A as ate-api-server
    participant L1 as atelet on node1 (full)
    participant O as object storage
    participant L2 as atelet on node2
    C->>A: ResumeActor
    A->>A: assign on node1: no room
    A->>A: node2 has room
    A->>L1: DetachActor
    L1->>O: upload latest snapshot
    L1->>L1: delete local copies
    L1-->>A: state with latest_durable only
    A->>A: mark COMPLETED, clear locality and assigned_node
    A->>A: reload restore source, assign on node2
    A->>L2: Restore(durable snapshot)
    L2->>O: download
    L2-->>A: ok
    A-->>C: RUNNING on node2
```

#### Design choices

- **Any runtime counts as room.**
- Memory is an optimization for GA, so a worker that can't restore the
snapshot's process memory still triggers the move, and the actor
restores there at ROOTFS.
  - Assignment still prefers a worker that keeps it.
- **No claim before the detach.**
- Claiming a worker first would leave the claim and the snapshot record
disagreeing if ate-api-server crashed before the detach.
  - In this order, every step ends in a state resume already handles.
- Two actors detached for one free place race for it, and the loser has
paid an upload, but it ends durable-only and its next resume schedules
it onto any node without another detach.

#### Also worth knowing

- A concurrent update fails the record write with `Aborted`, and the
next resume detaches again; atelet returns the same state. If the node
has room by then, the resume restores there from the upload instead, and
the record keeps the upload `PENDING`, because the detach cancelled
atelet's report.
- `DetachActor` errors map as the tag workflow maps `PromoteSnapshot`
errors, plus atelet's `Aborted` passed through.
- A resume that detaches records two scheduler samples, `no_capacity`
then `assigned`. A dashboard that reads `no_capacity` as a failed resume
will count these.
- No API changes.
- `TestResumeActor_RelocatesAfterDetach` checked `ResourceExhausted` at
a point where node2 already had room, which now detaches. The check
moved to before node2 exists.
- The functional fake's `DetachActor` returns the named actor's last
checkpointed snapshot, not the last of any actor.
@dims

Copy link
Copy Markdown
Collaborator

#2102 has closed, do we still need this?

@zoez7

Zoe Zhao (zoez7) commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

No, I removed this from #2102 (comment) scope

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 area/storage kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ActorTemplate: replace on_pause/on_commit/on_resume with capture_scope and minimum_scope

3 participants