Skip to content

atelet: prune local pause snapshots - #705

Open
NekoPunch (orangeCatDeveloper) wants to merge 1 commit into
agent-substrate:mainfrom
orangeCatDeveloper:issue-668-local-snapshot-gc
Open

atelet: prune local pause snapshots#705
NekoPunch (orangeCatDeveloper) wants to merge 1 commit into
agent-substrate:mainfrom
orangeCatDeveloper:issue-668-local-snapshot-gc

Conversation

@orangeCatDeveloper

Copy link
Copy Markdown
Contributor

Summary

atelet never deletes local pause snapshots, so they accumulate until the node disk fills (#668). The control plane only ever references the latest one (LocalSnapshotInfo is single-valued), so older directories are dead weight by design. Pause now prunes everything except the snapshot it just wrote; suspend removes them all, since the durable snapshot supersedes local state. Pruning is best-effort: a failed delete is logged and retried by the next prune, never failing the checkpoint.

Snapshot prefixes are now validated as single path segments at the Checkpoint/Restore RPC boundary (shared ValidateLocalSnapshotPrefix). Previously only checked non-empty: a nested prefix like pause/2 would write nested but be pruned as its top-level directory, deleting the fresh snapshot; .. could escape the actor's directory entirely.

Fixes #668

Test plan

  • Unit: prune keep/remove-all/missing-dir cases; validator table (nested, traversal, absolute, backslash); Checkpoint + Restore request rejection cases
  • Live on kind: pause×2 leaves exactly one snapshot directory (the newest, verified by timestamp); suspend removes the directory; actor resumes to RUNNING afterwards

Pause snapshots accumulated until the node disk filled; the control
plane only ever references the latest one. Pause now keeps just the
snapshot it wrote, suspend removes them all.

Snapshot prefixes are validated as single path segments at the RPC
boundary: a nested prefix would nest on write but be pruned as its
top-level directory, deleting the fresh snapshot.
Comment thread cmd/atelet/main.go
if err := s.uploadExternalCheckpoint(ctx, req, checkpointDir, sandboxRec); err != nil {
return nil, ateerrors.NewGRPCError(ctx, codes.DataLoss, ateerrors.ReasonFaileSaveSnapshot, ateerrors.ActorCrashedMetadata(), fmt.Errorf("%w: while uploading external snapshot: %w", ateerrors.ReasonFaileSaveSnapshot, err))
}
// The durable snapshot supersedes local pause snapshots.

@dberkov Dmitry Berkovich (dberkov) Aug 4, 2026

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.

There are 2 possible flows:

  1. run->pause [1]->resume->pause [2]
  2. run->pause[1] -> resume -> suspend

In both cases, all old snapshots can be deleted, regardless if it is just pause[1] or any other old snapshot.

Current implementation of the "func (s *AteomHerder) copyLocalCheckpoint(ctx context.Context, snapshotPrefix string, srcDir, dstDir string, files []string) error {
" method, copied previous snapshot to new location that takes time. I did in one of my PoC following code:

func (s *AteomHerder) copyLocalCheckpoint(ctx context.Context, snapshotPrefix string, srcDir, dstDir string, files []string) error {
	for _, fileName := range files {
		if ctx.Err() != nil {
			return fmt.Errorf("context cancelled: %w", ctx.Err())
		}
		src := filepath.Join(srcDir, snapshotPrefix, fileName)
		dst := filepath.Join(dstDir, fileName)
		// Hardlink first — constant-time regardless of file size, safe here
		// because runsc restore only reads these files. Fall back to a byte
		// copy on any error (cross-device, permissions, etc.).
		if err := os.Link(src, dst); err == nil {
			span.SetAttributes(attribute.String("method", "link"))
			span.End()
			continue
		}
		n, err := copyFile(src, dst)
		if err != nil {
			return fmt.Errorf("failed to copy %s to %s: %w", src, dst, err)
		}
	}

	return nil
}

it significantly improved the performance. The point if, you will do the "os.link" optimization, the pruneLocalCheckpoints(ctx, actorUID, "") logic need to be executed before new snapshot been copied either to external storage or moved to local storage location.

It means the logic can be moved above the "switch req.GetType() {" (line 396)

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.

Pause snapshots are not cleaned up

2 participants