Skip to content

Commit 1184d85

Browse files
committed
ateom: reuse the checkpoint image left by a cancelled attempt
A retried CheckpointWorkload ran `runsc checkpoint` against a sandbox the first attempt had already checkpointed and stopped, got exit 128 and crashed the actor. The first attempt had been cancelled by its caller during the teardown, which ran under the RPC context. Reuse the image when it exists and the pause container no longer runs, and run the teardown after a checkpoint on a detached, bounded context. cmdState now returns the container's status.
1 parent ed40e86 commit 1184d85

4 files changed

Lines changed: 99 additions & 17 deletions

File tree

‎cmd/ateom-gvisor/cleanup_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ func newFakeRunsc(containers ...string) *fakeRunsc {
4141
return f
4242
}
4343

44-
func (f *fakeRunsc) cmdState(_ context.Context, name string) error {
44+
func (f *fakeRunsc) cmdState(_ context.Context, name string) (string, error) {
4545
f.calls = append(f.calls, "state "+name)
4646
if !f.containers[name] {
47-
return errors.New("exit status 128")
47+
return "", errors.New("exit status 128")
4848
}
49-
return nil
49+
return "running", nil
5050
}
5151

5252
func (f *fakeRunsc) cmdDelete(_ context.Context, name string) error {

‎cmd/ateom-gvisor/main.go‎

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ const minUsageSampleInterval = 50 * time.Second
9292
// kubelet SIGKILL of ateom itself.
9393
const 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+
9598
func main() {
9699
pflag.Parse()
97100
if *showVersion {
@@ -444,7 +447,7 @@ var containerKillTimeout = 5 * time.Second
444447
type 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.
808845
func 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 {

‎cmd/ateom-gvisor/main_test.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package main
1818

1919
import (
2020
"context"
21+
"errors"
22+
"os"
23+
"path/filepath"
2124
"testing"
2225

2326
"google.golang.org/grpc/codes"
@@ -89,3 +92,35 @@ func TestRPCsRejectUntrustedRunscPath(t *testing.T) {
8992
}
9093
}
9194
}
95+
96+
func TestPreviousCheckpointReusable(t *testing.T) {
97+
running := func(context.Context) (string, error) { return "running", nil }
98+
stopped := func(context.Context) (string, error) { return "stopped", nil }
99+
gone := func(context.Context) (string, error) { return "", errors.New("container does not exist") }
100+
withImage := func(size int) string {
101+
dir := t.TempDir()
102+
if err := os.WriteFile(filepath.Join(dir, "checkpoint.img"), make([]byte, size), 0o600); err != nil {
103+
t.Fatal(err)
104+
}
105+
return dir
106+
}
107+
tests := []struct {
108+
name string
109+
dir string
110+
status func(context.Context) (string, error)
111+
want bool
112+
}{
113+
{"no image", t.TempDir(), gone, false},
114+
{"empty image", withImage(0), gone, false},
115+
{"image, container gone", withImage(1), gone, true},
116+
{"image, container stopped", withImage(1), stopped, true},
117+
{"image, container running", withImage(1), running, false},
118+
}
119+
for _, tt := range tests {
120+
t.Run(tt.name, func(t *testing.T) {
121+
if got := previousCheckpointReusable(context.Background(), tt.status, tt.dir); got != tt.want {
122+
t.Errorf("previousCheckpointReusable() = %v, want %v", got, tt.want)
123+
}
124+
})
125+
}
126+
}

‎cmd/ateom-gvisor/runsc.go‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"bytes"
2121
"context"
22+
"encoding/json"
2223
"errors"
2324
"fmt"
2425
"io"
@@ -302,7 +303,9 @@ func (r *runsc) cmdDelete(ctx context.Context, containerName string) error {
302303
return nil
303304
}
304305

305-
func (r *runsc) cmdState(ctx context.Context, containerName string) error {
306+
// cmdState returns the container's OCI status ("running", "stopped", ...),
307+
// or an error when runsc has no record of it.
308+
func (r *runsc) cmdState(ctx context.Context, containerName string) (string, error) {
306309
cmd := exec.CommandContext(
307310
ctx,
308311
r.path,
@@ -312,12 +315,19 @@ func (r *runsc) cmdState(ctx context.Context, containerName string) error {
312315
"state",
313316
containerName,
314317
)
315-
cmd.Stdout = os.Stdout
318+
var out bytes.Buffer
319+
cmd.Stdout = &out
316320
cmd.Stderr = os.Stderr
317321
if err := reaper.RunCommand(cmd); err != nil {
318-
return fmt.Errorf("while running `runsc state`: %w", err)
322+
return "", fmt.Errorf("while running `runsc state`: %w", err)
319323
}
320-
return nil
324+
var state struct {
325+
Status string `json:"status"`
326+
}
327+
if err := json.Unmarshal(out.Bytes(), &state); err != nil {
328+
return "", fmt.Errorf("while parsing `runsc state` output: %w", err)
329+
}
330+
return state.Status, nil
321331
}
322332

323333
// cmdList returns the container IDs runsc has a record of.

0 commit comments

Comments
 (0)