Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions deploy/snapshot/internal/controller/podsnapshotcontent.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,16 +441,17 @@ func (w *NodeController) executorCheckpoint(ctx context.Context, params Checkpoi
log := logr.FromContextOrDiscard(ctx)

req := executor.CheckpointRequest{
ContainerID: params.ContainerID,
ContainerName: params.ContainerName,
CheckpointID: params.CheckpointID,
CheckpointLocation: params.HostPath,
StartedAt: params.StartedAt,
NodeName: w.config.NodeName,
PodName: params.Pod.Name,
PodNamespace: params.Pod.Namespace,
PodIP: params.Pod.Status.PodIP,
Clientset: w.clientset,
ContainerID: params.ContainerID,
ContainerName: params.ContainerName,
CheckpointID: params.CheckpointID,
CheckpointLocation: params.HostPath,
StartedAt: params.StartedAt,
NodeName: w.config.NodeName,
PodName: params.Pod.Name,
PodNamespace: params.Pod.Namespace,
PodIP: params.Pod.Status.PodIP,
Clientset: w.clientset,
PageBrokerRequested: params.Pod.Annotations[snapshotprotocol.PageBrokerAnnotation] == snapshotprotocol.PageBrokerAnnotationEnabled,
}
if err := executor.Checkpoint(ctx, w.runtime, log, req, w.config); err != nil {
w.killCheckpointProcess(log, params.ContainerPID, "checkpoint failed")
Expand Down
87 changes: 59 additions & 28 deletions deploy/snapshot/internal/executor/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,26 @@ import (

"github.com/ai-dynamo/dynamo/deploy/snapshot/internal/criu"
"github.com/ai-dynamo/dynamo/deploy/snapshot/internal/cuda"
"github.com/ai-dynamo/dynamo/deploy/snapshot/internal/pagebroker"
snapshotruntime "github.com/ai-dynamo/dynamo/deploy/snapshot/internal/runtime"
"github.com/ai-dynamo/dynamo/deploy/snapshot/internal/types"
)

const pageBrokerAbortTimeout = 5 * time.Second

// CheckpointRequest holds per-checkpoint identifiers for a checkpoint operation.
type CheckpointRequest struct {
ContainerID string
ContainerName string
CheckpointID string
CheckpointLocation string
StartedAt time.Time
NodeName string
PodName string
PodNamespace string
PodIP string
Clientset kubernetes.Interface
ContainerID string
ContainerName string
CheckpointID string
CheckpointLocation string
StartedAt time.Time
NodeName string
PodName string
PodNamespace string
PodIP string
Clientset kubernetes.Interface
PageBrokerRequested bool
}

type checkpointPhaseTimings struct {
Expand All @@ -46,9 +50,8 @@ type checkpointPhaseTimings struct {
// Checkpoint performs a CRIU dump of a container.
// The operation has three phases: inspect, configure, capture.
//
// The checkpoint directory is staged under tmp/<uuid> during the operation.
// On success, the previous checkpoint is removed and the staged directory is
// renamed into place at the base path root.
// PageBroker uses its tmpfs staging directory when both the Pod and deployment enable it;
// otherwise the existing tmp/<uuid> staging and rename path is unchanged.
func Checkpoint(ctx context.Context, rt snapshotruntime.Runtime, log logr.Logger, req CheckpointRequest, cfg *types.AgentConfig) error {
checkpointStart := time.Now()
phaseTimings := checkpointPhaseTimings{}
Expand All @@ -63,15 +66,36 @@ func Checkpoint(ctx context.Context, rt snapshotruntime.Runtime, log logr.Logger
}

finalDir := req.CheckpointLocation
tmpRoot := filepath.Join(filepath.Dir(finalDir), "tmp")
if err := os.MkdirAll(tmpRoot, 0700); err != nil {
return fmt.Errorf("failed to create checkpoint staging root: %w", err)
}
tmpDir := filepath.Join(tmpRoot, uuid.NewString())
if err := os.Mkdir(tmpDir, 0700); err != nil {
return fmt.Errorf("failed to create checkpoint staging directory: %w", err)
tmpDir := ""
brokered := req.PageBrokerRequested && cfg.PageBroker.Enabled
transactionID := uuid.NewString()
var broker pagebroker.Client
committed := false
if brokered {
broker = pagebroker.Client{ControlSocketPath: cfg.PageBroker.ControlSocketPath}
defer func() {
if !committed {
abortCtx, cancel := context.WithTimeout(context.Background(), pageBrokerAbortTimeout)
defer cancel()
_ = broker.Abort(abortCtx, transactionID)
}
}()
var err error
tmpDir, err = broker.PrepareCheckpoint(ctx, transactionID, finalDir)
if err != nil {
return fmt.Errorf("prepare PageBroker checkpoint: %w", err)
}
} else {
tmpRoot := filepath.Join(filepath.Dir(finalDir), "tmp")
if err := os.MkdirAll(tmpRoot, 0700); err != nil {
return fmt.Errorf("failed to create checkpoint staging root: %w", err)
}
tmpDir = filepath.Join(tmpRoot, transactionID)
if err := os.Mkdir(tmpDir, 0700); err != nil {
return fmt.Errorf("failed to create checkpoint staging directory: %w", err)
}
defer os.RemoveAll(tmpDir)
}
defer os.RemoveAll(tmpDir)

// Phase 1: Inspect container state
state, err := inspectContainer(ctx, rt, log, req)
Expand All @@ -95,14 +119,21 @@ func Checkpoint(ctx context.Context, rt snapshotruntime.Runtime, log logr.Logger
phaseTimings.CRIUDumpDuration = captureTimings.CRIUDumpDuration
phaseTimings.OverlayCaptureDuration = captureTimings.OverlayCaptureDuration

// Remove any previous checkpoint with the same identity hash, then
// promote the staged checkpoint directory into place.
finalizeStart := time.Now()
if err := os.RemoveAll(finalDir); err != nil {
return fmt.Errorf("failed to remove previous checkpoint directory: %w", err)
}
if err := os.Rename(tmpDir, finalDir); err != nil {
return fmt.Errorf("failed to finalize checkpoint directory: %w", err)
if brokered {
Comment thread
dfeigin-nv marked this conversation as resolved.
if err := broker.Commit(ctx, transactionID); err != nil {
return fmt.Errorf("commit PageBroker checkpoint: %w", err)
}
committed = true
Comment thread
dfeigin-nv marked this conversation as resolved.
} else {
// Remove any previous checkpoint with the same identity hash, then
// promote the staged checkpoint directory into place.
if err := os.RemoveAll(finalDir); err != nil {
return fmt.Errorf("failed to remove previous checkpoint directory: %w", err)
}
if err := os.Rename(tmpDir, finalDir); err != nil {
return fmt.Errorf("failed to finalize checkpoint directory: %w", err)
}
}
phaseTimings.FinalizeDuration = time.Since(finalizeStart)

Expand Down
181 changes: 181 additions & 0 deletions deploy/snapshot/internal/pagebroker/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package pagebroker

import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"

"github.com/google/uuid"
"google.golang.org/protobuf/proto"
)

const (
// PageBroker control requests and responses are limited to 64 KiB.
maxMessageSize = 64 << 10
commitRetryDelay = 100 * time.Millisecond
)

var errMessageTooLarge = fmt.Errorf("message exceeds %d bytes", maxMessageSize)

// Client uses the deployment-wide filesystem/POSIX PageBroker plan.
type Client struct {
ControlSocketPath string
}

func (c Client) PrepareCheckpoint(ctx context.Context, transactionID, destination string) (string, error) {
response, err := c.request(ctx, transactionID, &Request_PrepareStagedCheckpoint{
PrepareStagedCheckpoint: &PrepareStagedCheckpointRequest{Destination: filesystem(destination), IoEngine: posixCopy()},
})
if err != nil {
return "", err
}
if response.GetStagedCheckpointDirectory() == nil {
return "", fmt.Errorf("unexpected PageBroker checkpoint response")
}
return response.GetStagedCheckpointDirectory().GetImageDirectory(), nil
}

Comment thread
dfeigin-nv marked this conversation as resolved.
func (c Client) Commit(ctx context.Context, transactionID string) error {
for {
response, err := c.request(ctx, transactionID, &Request_Commit{Commit: &CommitRequest{}})
if err == nil {
if response.GetCommitComplete() != nil {
return nil
}
return fmt.Errorf("unexpected PageBroker commit response")
}
Comment on lines +45 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your MR description says that commit retries are bounded but there's no bounding here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also there's retry work + tests in #13172 , might be better suited in this MR

var transport transportError
if !errors.As(err, &transport) {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(commitRetryDelay):
}
}
}

func (c Client) Abort(ctx context.Context, transactionID string) error {
response, err := c.request(ctx, transactionID, &Request_Abort{Abort: &AbortRequest{}})
if err != nil {
return err
}
if response.GetAbortComplete() == nil {
return fmt.Errorf("unexpected PageBroker abort response")
}
return nil
}

func (c Client) request(ctx context.Context, transactionID string, command isRequest_Command) (*Response, error) {
connection, err := (&net.Dialer{}).DialContext(ctx, "unix", c.ControlSocketPath)
if err != nil {
return nil, transportError{cause: fmt.Errorf("dial PageBroker: %w", err)}
}
defer connection.Close()
stopCancel := context.AfterFunc(ctx, func() { _ = connection.Close() })
defer stopCancel()

requestID := uuid.NewString()
request := &Request{RequestId: &requestID, TransactionId: &transactionID, Command: command}
message, err := proto.Marshal(request)
if err != nil {
return nil, fmt.Errorf("marshal PageBroker request: %w", err)
}
if err := writeMessage(connection, message); err != nil {
return nil, transportError{cause: fmt.Errorf("write PageBroker request: %w", err)}
}
message, err = readMessage(connection)
if err != nil {
if errors.Is(err, errMessageTooLarge) {
return nil, err
}
return nil, transportError{cause: fmt.Errorf("read PageBroker response: %w", err)}
}
response := new(Response)
if err := proto.Unmarshal(message, response); err != nil {
return nil, fmt.Errorf("unmarshal PageBroker response: %w", err)
}
if response.GetRequestId() != requestID || response.GetTransactionId() != transactionID {
return nil, fmt.Errorf("PageBroker response identifiers do not match request")
}
if failure := response.GetFailure(); failure != nil {
return nil, failureError{code: failureCode(failure.GetCode()), message: failure.GetMessage()}
}
return response, nil
}

type failureError struct {
code Failure_Code
message string
}

func failureCode(code Failure_Code) Failure_Code {
switch code {
case Failure_UNSPECIFIED, Failure_INVALID_REQUEST, Failure_TRANSACTION_NOT_FOUND, Failure_TRANSACTION_CONFLICT,
Failure_INSUFFICIENT_STORAGE, Failure_STORAGE_ERROR, Failure_INTERNAL_ERROR:
return code
default:
return Failure_UNSPECIFIED
}
}

type transportError struct {
cause error
}

func (e transportError) Error() string { return e.cause.Error() }

func (e transportError) Unwrap() error { return e.cause }

func (e failureError) Error() string {
return fmt.Sprintf("PageBroker %s: %s", e.code, e.message)
}

func filesystem(directory string) *StorageBackend {
return &StorageBackend{Kind: &StorageBackend_Filesystem{Filesystem: &FilesystemStorage{Directory: &directory}}}
}

func posixCopy() *IOEngine {
return &IOEngine{Kind: &IOEngine_PosixCopy{PosixCopy: &PosixCopyIOEngine{}}}
}

func writeMessage(writer io.Writer, message []byte) error {
if len(message) > maxMessageSize {
return fmt.Errorf("message exceeds %d bytes", maxMessageSize)
}
if err := binary.Write(writer, binary.BigEndian, uint32(len(message))); err != nil {
return err
}
for len(message) > 0 {
written, err := writer.Write(message)
if err != nil {
return err
}
if written == 0 {
return io.ErrShortWrite
}
message = message[written:]
}
return nil
}

func readMessage(reader io.Reader) ([]byte, error) {
var size uint32
if err := binary.Read(reader, binary.BigEndian, &size); err != nil {
return nil, err
}
if size > maxMessageSize {
return nil, errMessageTooLarge
}
message := make([]byte, size)
_, err := io.ReadFull(reader, message)
return message, err
}
Loading
Loading