@@ -92,6 +92,9 @@ const minUsageSampleInterval = 50 * time.Second
9292// kubelet SIGKILL of ateom itself.
9393const workloadGracePeriod = 30 * time .Minute
9494
95+ // checkpointTeardownTimeout bounds the teardown after a checkpoint, which runs detached from the caller.
96+ const checkpointTeardownTimeout = 60 * time .Second
97+
9598func main () {
9699 pflag .Parse ()
97100 if * showVersion {
@@ -444,7 +447,7 @@ var containerKillTimeout = 5 * time.Second
444447type containerRuntime interface {
445448 cmdKill (ctx context.Context , containerName , signal string ) error
446449 cmdWait (ctx context.Context , containerName string ) error
447- cmdState (ctx context.Context , containerName string ) error
450+ cmdState (ctx context.Context , containerName string ) ( string , error )
448451 cmdDelete (ctx context.Context , containerName string ) error
449452 cmdList (ctx context.Context ) ([]string , error )
450453}
@@ -766,12 +769,28 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec
766769 return nil , fmt .Errorf ("while checkpointing durable-dir volumes: %w" , err )
767770 }
768771 case ateompb .SnapshotFidelity_SNAPSHOT_FIDELITY_MEMORY :
769- // Checkpoint pause container (root of the sandbox), splitting durable-dir
770- // volumes into <checkpointPath>/fs when present.
771- err := rcmd .cmdCheckpoint (ctx , ocispec .PauseContainer , checkpointPath , durablePaths )
772- timing .checkpoint = lap (& tLast )
773- if err != nil {
774- return nil , fmt .Errorf ("while checkpointing pause: %w" , err )
772+ // A retry after a cancelled teardown finds the image already written and
773+ // the sandbox gone; checkpointing it again fails and crashes the actor.
774+ pauseStatus := func (ctx context.Context ) (string , error ) { return rcmd .cmdState (ctx , ocispec .PauseContainer ) }
775+ if previousCheckpointReusable (ctx , pauseStatus , checkpointPath ) {
776+ slog .InfoContext (ctx , "Reusing the checkpoint image of a previous attempt" ,
777+ slog .String ("actor" , attribution .Ref .String ()),
778+ slog .String ("actorUID" , attribution .UID ))
779+ timing .checkpoint = lap (& tLast )
780+ } else {
781+ // runsc opens its image files with O_EXCL; drop a partial earlier attempt.
782+ if stale , err := listSnapshotFiles (checkpointPath ); err == nil {
783+ for _ , f := range stale {
784+ _ = os .Remove (filepath .Join (checkpointPath , f ))
785+ }
786+ }
787+ // Checkpoint pause container (root of the sandbox), splitting durable-dir
788+ // volumes into <checkpointPath>/fs when present.
789+ err := rcmd .cmdCheckpoint (ctx , ocispec .PauseContainer , checkpointPath , durablePaths )
790+ timing .checkpoint = lap (& tLast )
791+ if err != nil {
792+ return nil , fmt .Errorf ("while checkpointing pause: %w" , err )
793+ }
775794 }
776795 default :
777796 return nil , fmt .Errorf ("unsupported snapshot fidelity: %v" , fidelity )
@@ -780,14 +799,17 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec
780799 // Cleanup the containers after checkpointing. This also unhosts the actor,
781800 // before the snapshot listing below can fail.
782801 // This is best-effort cleanup for actor containers that may have been left behind after checkpointing.
783- if err := s .terminateWorkload (ctx , attribution .Ref , attribution .UID , req .GetRunscPath (), req .GetActorDirs (), req .GetSpec ().GetContainers ()); err != nil {
802+ // Detached from the caller: a cancellation here leaves a half torn down sandbox.
803+ teardownCtx , cancelTeardown := context .WithTimeout (context .WithoutCancel (ctx ), checkpointTeardownTimeout )
804+ defer cancelTeardown ()
805+ if err := s .terminateWorkload (teardownCtx , attribution .Ref , attribution .UID , req .GetRunscPath (), req .GetActorDirs (), req .GetSpec ().GetContainers ()); err != nil {
784806 slog .WarnContext (ctx , "failed to terminate workload after checkpoint" ,
785807 slog .String ("actor" , attribution .Ref .String ()),
786808 slog .String ("actorUID" , attribution .UID ),
787809 slog .Any ("err" , err ))
788810 }
789811 timing .teardown = lap (& tLast )
790- s .recordFinalIfEnded (ctx , hosted )
812+ s .recordFinalIfEnded (teardownCtx , hosted )
791813
792814 // Report exactly the files runsc wrote so atelet ships precisely this set
793815 // (checkpoint.img plus any pages images and fs/ durable-dir files), rather
@@ -803,6 +825,21 @@ func (s *AteomService) CheckpointWorkload(ctx context.Context, req *ateompb.Chec
803825 return & ateompb.CheckpointWorkloadResponse {SnapshotFiles : snapshotFiles , DataSnapshotFiles : durableFiles }, nil
804826}
805827
828+ // previousCheckpointReusable reports whether checkpointPath holds the image of
829+ // an earlier attempt whose sandbox no longer runs. status is the pause
830+ // container's OCI status, or an error when runsc has no record of it.
831+ func previousCheckpointReusable (ctx context.Context , status func (context.Context ) (string , error ), checkpointPath string ) bool {
832+ info , err := os .Stat (filepath .Join (checkpointPath , "checkpoint.img" ))
833+ if err != nil || info .Size () == 0 {
834+ return false
835+ }
836+ st , err := status (ctx )
837+ if err != nil {
838+ return true
839+ }
840+ return st != "running"
841+ }
842+
806843// listSnapshotFiles returns the (relative) names of regular files directly under
807844// dir and under dir/fs, which atelet ships to object storage as the snapshot.
808845func listSnapshotFiles (dir string ) ([]string , error ) {
@@ -852,7 +889,7 @@ func cleanupContainers(ctx context.Context, rcmd containerRuntime, containers []
852889 // Without this, `runsc delete` occasionally throws an error.
853890 present := make ([]string , 0 , len (names ))
854891 for _ , name := range names {
855- if err := rcmd .cmdState (ctx , name ); err != nil {
892+ if _ , err := rcmd .cmdState (ctx , name ); err != nil {
856893 err = fmt .Errorf ("while checking state of %q container: %w" , name , err )
857894 gone , listErr := isContainerAlreadyGone (ctx , rcmd , name )
858895 if listErr != nil {
0 commit comments