Skip to content

Feature request: Fallback to local snapshot when failing to upload external snapshot. #362

Description

@zoez7

Current behaviour: when an Actor has configured SnapshotType_SNAPSHOT_TYPE_EXTERNAL but substrate fails to upload the external snapshot after checkpointg, we have to set the Actor to CRASHED state, because the most recent snapshot is lost.

Feature request: if we can fallback to storing the snapshot locally and upload later, we won't have to set the Actor to CRASHED state right away.

Activity

  1. eren23 commented on Oct 7, 2026

    @eren23
    Contributor

    This has a real trigger. AX cancels SuspendActor after 15 s, the ax suspend CLI deadline. With a workspace of about 1.5 GiB or more, the cancel lands during the durable dir upload, after ateom has already removed the sandbox. The retried suspend then fails on runsc pause and crashes the actor, and AX reverts it on the next resume, so the workspace is lost. I reproduced it on main 0b91488d. Details in google/ax#453.

  2. eren23 commented on Oct 7, 2026

    @eren23
    Contributor

    Would you take a PR for the retry part of this? From what I read on main, most of it is already in place. After a failed upload, Checkpoint returns before resetActorDirs, so the checkpoint dir with the durable dir tars stays on the node until the next Run, Restore or terminate. A retried Checkpoint then calls CheckpointWorkload again and fails because the sandbox is already gone.

    My idea is to write the snapshot file list to the sandbox record before the upload starts. A retried Checkpoint for the same actor would find it, skip CheckpointWorkload and only upload again. The actor stays SUSPENDING until that works, and terminate still cleans up as it does now. Resume does not change, so this is smaller than the local fallback in the title.

    This overlaps with #372, and I saw troychiu offered to help there. If that work already covers Checkpoint, I will leave it.

  3. eren23 commented on Oct 9, 2026

    @eren23
    Contributor

    I retested this on main 288694e. When a running actor suspends, a storage outage during the upload still crashes it and loses everything since the last snapshot, 2 of 2. Killing atelet or the new snapshot plugin sidecar during that upload does the same.

    Pausing first and then suspending goes through UploadPausedCheckpoint, which keeps the local snapshot until the upload lands. When I killed the plugin 1 s into that upload, the call failed with Unavailable, the actor stayed SUSPENDING, and a second suspend finished with nothing lost, 2 of 2. A 60 s storage outage at the same point failed with Internal instead (while putting object ... connect: connection refused), and handleAteletError crashed the actor. The local snapshot was still on the node, but from CRASHED the only way back is RevertActor, which drops it. So the new files were gone in 2 of 2.

    I first tried returning Unavailable for network errors in the plugin's toStatus, but that also catches TLS, DNS and credential failures and leaves actors stuck in SUSPENDING, which RevertActor does not accept, so I dropped it.

    What looks cleaner to me is to put the actor back to PAUSED when UploadPausedCheckpoint fails and the local snapshot is still there, and to keep crashing when it is gone. As far as I can see the paused path only sets the state and InProgressSnapshotUri when it marks the actor SUSPENDING, so going back is small. Nothing is lost, nothing gets stuck, and the caller can suspend again later or resume. Would you take a PR along those lines, or do you have a different shape in mind?

  4. eren23 commented on Oct 10, 2026

    @eren23
    Contributor

    I wrote the PAUSED idea up as a branch so it is easier to judge. It is eren23/substrate@main...eren23:substrate:fix/paused-upload-back-to-paused (one commit on main f200630).

    When UploadPausedCheckpoint fails with anything other than NotFound, the suspend now puts the actor back to PAUSED with the same local snapshot and deletes the partial upload best effort. The actor can then resume or suspend again. NotFound still crashes it, and atelet now returns NotFound when the local snapshot is gone and no uploaded copy exists. Its interceptor used to send that as Internal. If the local snapshot is gone and the probe for an uploaded copy fails, atelet returns Unavailable, so the actor stays SUSPENDING and a retry can still find an upload that an earlier attempt committed.

    In my kind lab I paused an actor, blocked TCP to the object store, suspended, unblocked and suspended again. On main f200630 the data written after the last durable snapshot was gone in 2 of 2 trials. With the branch the first suspend failed with "the actor is PAUSED again", the retry succeeded, and the data was intact in 2 of 2.

    This flips TestSuspendActor_FromPaused_UploadFailureCrashes, which says the crash is intended, so I wanted to ask before I open a PR. I can open one if this direction works for you.

  5. aditya-shantanu commented on Oct 20, 2026

    @aditya-shantanu
    Collaborator

    Data point from a pre-GA chaos run on main 4b4fbb2 showing how cheap it is to hit this. Stopping the objectstore-plugin sidecar of one atelet pod once (crictl stop, kubelet restarted it immediately, restartCount 0 -> 1) while 12 Locust users were doing sync suspends of 256 MiB gVisor actors crashed the two actors whose uploads were in flight on that node:

    21:35:05.198  atelet          Handle RPC err: while uploading external snapshot: while uploading snapshot files: rpc error: code = Unavailable desc = object-store plugin upload of gs://snapshot-.../actors/c9cf6433-.../snapshots/61edef79-... failed (Unavailable)
    21:35:05.198  ate-api-server  Setting Actor to crashed due to Atelet error: rpc error: code = Internal desc = while uploading external snapshot: ... failed (Unavailable)
    21:35:05.203  ate-api-server  workflow failed at step CallAteletSuspend: actor benchmark/sb-88b9ec0a-... crashed: ...
    21:35:05.205  ate-api-server  workflow failed at step CallAteletSuspend: actor benchmark/sb-f057c9ec-... crashed: ...
    

    Two things compound here. The checkpoint itself had completed (the sandbox was already gone), so the only thing lost was a retriable upload, which is this issue. And the plugin returned Unavailable, but atelet wraps it with fmt.Errorf so the RPC reaches ate-api-server as Internal, which ateletErrorCrashesActor treats as fatal. Even without a local fallback, preserving the plugin's status code would have made this a retriable suspend failure instead of a crash. A sidecar restart is a routine event (OOM, image rollout, node pressure), so this is worth treating as a reliability bug rather than only a feature request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/apiUser-facing API changesarea/storagekind/featureAn enhancement / feature request or implementation

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions