Skip to content

feat: run snapshot and restore on the restate Environment object with durable chain state - #377

Merged
andrewn6 merged 6 commits into
feat/restate-artifactsfrom
feat/restate-snapshot
Nov 15, 2026
Merged

andrewn6 merged 6 commits into
feat/restate-artifactsfrom
feat/restate-snapshot

Conversation

@andrewn6

@andrewn6 andrewn6 commented Nov 14, 2026 •

Copy link
Copy Markdown
Collaborator

Runs snapshot and restore on the restate Environment/{vm_id} object, moves the checkpoint chain onto that object's state, and hands snapshot gc deletions to restate. #2 of the restate stack (#346 -> #345 -> #349 -> #348 -> #342).

  • Environment/{vm_id}.Snapshot (cold, live, diff, shrunk): begin (checks, quota, lineage) -> Run checkpoint (keyed by feat: idempotency keys on host-agent mutations so durable steps can retry after a lost response #370, so a retry or replay gets the first checkpoint back) -> row creating -> Run list -> ready. a record that cannot be finished runs the compensation: the host artifact is deleted and the row marked error (snapshot.failed). the host agent picks snapshot ids, so the row is written right after the keyed checkpoint rather than before it; the journal plus the key give the same "no untracked artifact" guarantee without an agent api change.
  • Environment/{vm_id}.Restore: validate -> lookup -> restoring -> Run restore -> ready; a restart resumes it. a host restore that fails for good marks the row error.
  • refusals (not found, not running, invalid state, quota, gpu, unsupported) cross restate as their own terminal error codes (480+) and come back as the same fleet sentinels, so the api status codes do not change.
  • checkpoint loop: each tick is Environment/{vm_id}.Checkpoint. the chain (head, base, standby) lives in object state, the base is pinned in ArtifactPin (chain:<vm_id>) instead of memory, and fm.chains is only a mirror for the standby picker. the copy to the standby is a one-way Artifacts move that reports back to ChainMoved (new notify on the move job), so a long copy never holds the environment object and blocks a destroy. the tick still walks vms one at a time by waiting for each copy to settle.
  • a delta migrate (still in process until its own sub-issue) takes HoldChain (1h lease) instead of chain.mu; a busy chain makes it take a full seed. a destroy drops the chain (deferred while a copy or hold is in flight). a copy that never reports back is forgotten after 2h without deleting anything, which keeps feat: run artifact moves as a durable restate service with pull pins in durable state #376's unsettled-move rule.
  • retention gc now skips snapshots any pin holds (in process or ArtifactPin), and on restate sends CollectSnapshot (idempotency-keyed per marking), retried under the ENVIRONMENT policy before the row goes error.
  • fork and migrate seed snapshots stay in process (snapshotInProcess): they are ported later, and a handler on the same object must not call it.
  • FUSE_RESTATE_MODE=off keeps the in-process paths; the snapshot/restore code is split into the same step methods both modes use.

No api surface change (fields, states, status codes), so no sdk/openapi changes. Docs: snapshots concept page, orchestrator-deploy (retry policy row, stale "nothing runs on it yet"), restate runbook.

verified locally

  • gofmt -l ., go build ./..., go vet ./..., golangci-lint v2.12.2 (0 issues), go test ./... -count=1, -race on durable/orchestrator; live/checkpoint/snapshot unit tests.
  • durable integration tests against a local restate-server 1.6.2: snapshot with retried checkpoints, refusal keeps its sentinel, compensation when ready never writes, endpoint restart between the host checkpoint and the row write (the restarted endpoint records the first checkpoint, no second one), restore with retries and a permanent refusal, endpoint restart mid-restore (completes), gc retries then marks error, chain full -> diff with the base pin moving, destroy retiring the chain, hold keeping the tick off, a failed merge retiring a diff chain.
  • docs bun run build.

not run

  • the phantom e2e: kill -9 between host checkpoint and row write, kill -9 during restore, live-snapshot-agent.sh 11/11 with mode self.
  • the 2h lapse of a copy that never reports back has no test.

Stacked on #376.

Closes #345

RetriggerView in GreptileConfidence Score: 3/5

Fix overlapping migration holds and deletion of newly pinned snapshots before merging.

Fix All in Claude CodeFindings

  1. P1 Migrations share the same hold ▶
  2. P1 Cleanup deletes a pinned source ▶
  3. P2 Deferred cleanup never wakes up ▶

Summary

Moves snapshots, restores, checkpoint chains, and retention cleanup onto Restate.

  • Snapshots and restores resume on each VM after an orchestrator restart.
  • Checkpoint chains stay with each VM and survive orchestrator restarts.
  • Snapshot cleanup skips pinned artifacts and retries host deletions through Restate.

Diagram

sequenceDiagram
  participant Caller
  participant Environment
  participant Host
  participant Artifacts
  participant ArtifactPin
  Caller->>Environment: Snapshot or Checkpoint
  Environment->>Host: Keyed checkpoint
  Host-->>Environment: Checkpoint ID
  Environment->>Environment: Save snapshot record
  opt Background checkpoint
    Environment->>Artifacts: Send Move
    Artifacts->>ArtifactPin: Pin source and destination
    Artifacts->>Host: Copy checkpoint
    Artifacts-->>Environment: ChainMoved
    Environment->>ArtifactPin: Pin chain base
    Environment->>Environment: Save chain
  end
Loading

Reviews (1) · Last reviewed commit: "docs: snapshot, restore and checkpoint c..." · Reviewed by Greptile

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
fuse 179dc8b Commit Preview URL

Branch Preview URL
Nov 15 2026, 06:15 AM

Comment thread internal/durable/checkpoint.go Outdated
Comment on lines +358 to +365
if st.Pending != nil || st.Dropped {
return orchestrator.ChainHold{Busy: true}, nil
}
now, err := nowMillis(ctx)
if err != nil {
return orchestrator.ChainHold{}, err
}
st.HeldUntil = now + lease.Milliseconds()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Migrations share the same hold

HoldChain grants another hold while HeldUntil is still in the future. Two concurrent live migrations can receive the same head and base. The durable branch no longer takes the shared chain.mu, so both can take competing diffs or consume the same base, causing migration to fail. Either caller can also release the other's hold and let checkpoints resume.

Reject an active hold, and tie each release to the hold that acquired it.

Fix in Claude Code

Comment on lines +156 to +160
if !found || record.State != SnapshotStateDeleting {
return nil
}
if err := fm.deleteSnapshotArtifact(ctx, record); err != nil && !IsNotFound(err) {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Cleanup deletes a pinned source

SnapshotCollect checks only that the row is deleting, not whether a move has since pinned it. The queued deletion can wait behind another environment operation while an already-planned move acquires its pin and starts reading. The collector then deletes the source despite that active pin, breaking the copy.

Check pins again when collection runs, and make deletion and new moves agree on whether the snapshot is still available.

Fix in Claude Code

Comment on lines +344 to +347
if st.Pending != nil || st.HeldUntil > now {
st.Dropped = true
restate.Set(ctx, chainKey, st)
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Deferred cleanup never wakes up

If a VM is destroyed during a migration hold and the process dies before ReleaseChain, this branch leaves Dropped set after the hold expires. The gone-VM sweep sends retirement once and removes its mirror, while checkpointTick returns immediately for dropped chains. A pending copy that never reports back has the same problem.

Schedule a durable follow-up for the deadline. Without it, destroyed VMs can leave their standby copies permanently pinned and using disk.

Fix in Claude Code

@andrewn6
andrewn6 force-pushed the feat/restate-snapshot branch from 988de82 to 179dc8b Compare November 15, 2026 06:14
@andrewn6
andrewn6 merged commit dbfa130 into main Nov 15, 2026
17 checks passed
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.

[restate] snapshot and restore on the Environment object

1 participant