Skip to content

Commit 18323ee

Browse files
committed
Implement Direct Memory (process-level) backend in snapshot-agent
- Add BackendDirectMemory constant to BackendType - Implement DirectMemory backend struct using cr_client binary - Support direct_memory backend in server logic and PID discovery for k8s and standalone modes - Add BuildDirectMemoryConfig and ExtractDirectMemoryPIDStrings helper functions - Register DirectMemory backend in cmd/snapshot-agent - Add unit tests for DirectMemory backend and server integration
1 parent 2c4d4b6 commit 18323ee

7 files changed

Lines changed: 551 additions & 20 deletions

File tree

cmd/snapshot-agent/main.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ func main() {
6363
// server's WorkloadChannel RPC handler.
6464
channelRegistry := backends.NewChannelRegistry()
6565
registeredBackends := map[backends.BackendType]backends.Backend{
66-
backends.BackendCuda: backends.NewCudaCheckpoint(),
67-
backends.BackendNoop: backends.NewNoopBackend(),
68-
backends.BackendAppEndpoint: backends.NewAppEndpointBackend(),
69-
backends.BackendAppChannel: backends.NewAppChannelBackend(channelRegistry),
66+
backends.BackendCuda: backends.NewCudaCheckpoint(),
67+
backends.BackendNoop: backends.NewNoopBackend(),
68+
backends.BackendAppEndpoint: backends.NewAppEndpointBackend(),
69+
backends.BackendAppChannel: backends.NewAppChannelBackend(channelRegistry),
70+
backends.BackendDirectMemory: backends.NewDirectMemory(),
7071
}
7172

7273
slog.InfoContext(ctx, "Starting Snapshot Agent", "port", listenPort, "deploymentMode", depMode)

pkg/snapshot-agent/backends/checkpoint.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const (
2020
// BackendAppChannel suspends/resumes application-aware workloads through
2121
// their registered workload channels (see the WorkloadChannel RPC).
2222
BackendAppChannel BackendType = "app-channel"
23+
// BackendDirectMemory is the Direct Memory (process-level) checkpointing backend.
24+
BackendDirectMemory BackendType = "direct-memory"
2325
)
2426

2527
// Request carries one backend invocation: the job it targets and the
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package backends
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"os"
8+
"os/exec"
9+
"strconv"
10+
"sync"
11+
"time"
12+
13+
pb "github.com/llm-d-incubation/llm-d-rl-time-slicing/pkg/snapshot-agent/api/v1alpha1"
14+
)
15+
16+
// DirectMemory implements the Backend interface using cr_client.
17+
type DirectMemory struct {
18+
mu sync.Mutex
19+
execCommand func(ctx context.Context, name string, args ...string) ([]byte, error)
20+
lookPath func(string) (string, error)
21+
statFunc func(string) (os.FileInfo, error)
22+
}
23+
24+
// NewDirectMemory creates a new DirectMemory backend.
25+
func NewDirectMemory() *DirectMemory {
26+
return &DirectMemory{
27+
execCommand: func(ctx context.Context, name string, args ...string) ([]byte, error) {
28+
return exec.CommandContext(ctx, name, args...).CombinedOutput()
29+
},
30+
lookPath: exec.LookPath,
31+
statFunc: os.Stat,
32+
}
33+
}
34+
35+
// Snapshot triggers a snapshot of the target processes for a job using cr_client.
36+
func (d *DirectMemory) Snapshot(ctx context.Context, req Request) error {
37+
pids := ExtractDirectMemoryPIDStrings(req.Config)
38+
if len(pids) == 0 {
39+
return fmt.Errorf("at least one PID is required for Direct Memory snapshot")
40+
}
41+
42+
d.mu.Lock()
43+
defer d.mu.Unlock()
44+
45+
slog.InfoContext(ctx, "Snapshotting PIDs using Direct Memory", "pids", pids)
46+
47+
t0 := time.Now()
48+
for _, pid := range pids {
49+
if err := d.checkpointPID(ctx, pid); err != nil {
50+
return fmt.Errorf("cr_client checkpoint failed for PID %s: %w", pid, err)
51+
}
52+
}
53+
slog.InfoContext(ctx, "cr_client checkpoint took", "duration", time.Since(t0))
54+
return nil
55+
}
56+
57+
// Restore triggers a restoration of the target processes for a job using cr_client.
58+
func (d *DirectMemory) Restore(ctx context.Context, req Request) error {
59+
pids := ExtractDirectMemoryPIDStrings(req.Config)
60+
if len(pids) == 0 {
61+
return fmt.Errorf("at least one PID is required for Direct Memory restore")
62+
}
63+
64+
d.mu.Lock()
65+
defer d.mu.Unlock()
66+
67+
slog.InfoContext(ctx, "Restoring PIDs using Direct Memory", "pids", pids)
68+
t0 := time.Now()
69+
for _, pid := range pids {
70+
if err := d.restorePID(ctx, pid); err != nil {
71+
return fmt.Errorf("cr_client restore failed for PID %s: %w", pid, err)
72+
}
73+
}
74+
slog.InfoContext(ctx, "cr_client restore took", "duration", time.Since(t0), "pids", pids)
75+
return nil
76+
}
77+
78+
func (d *DirectMemory) getCrClientPath() string {
79+
for _, p := range []string{
80+
"cr_client",
81+
"/usr/bin/cr_client",
82+
"/bin/cr_client",
83+
"/opt/bin/cr_client",
84+
"/usr/local/bin/cr_client",
85+
} {
86+
if path, err := d.lookPath(p); err == nil {
87+
return path
88+
}
89+
if _, err := d.statFunc(p); err == nil {
90+
return p
91+
}
92+
}
93+
return "/opt/bin/cr_client"
94+
}
95+
96+
func (d *DirectMemory) runCommand(ctx context.Context, name string, args ...string) error {
97+
if out, err := d.execCommand(ctx, name, args...); err != nil {
98+
return fmt.Errorf("command failed: %w, output: %s", err, string(out))
99+
}
100+
return nil
101+
}
102+
103+
func (d *DirectMemory) checkpointPID(ctx context.Context, pid string) error {
104+
binaryPath := d.getCrClientPath()
105+
if err := d.runCommand(ctx, binaryPath, "-c", "-p", pid); err != nil {
106+
return err
107+
}
108+
return nil
109+
}
110+
111+
func (d *DirectMemory) restorePID(ctx context.Context, pid string) error {
112+
binaryPath := d.getCrClientPath()
113+
if err := d.runCommand(ctx, binaryPath, "-r", "-p", pid); err != nil {
114+
return err
115+
}
116+
return nil
117+
}
118+
119+
// HealthCheck checks if the Direct Memory backend is healthy.
120+
func (d *DirectMemory) HealthCheck(ctx context.Context) error {
121+
binaryPath := d.getCrClientPath()
122+
if _, err := d.lookPath(binaryPath); err != nil {
123+
if _, errStat := d.statFunc(binaryPath); errStat != nil {
124+
return fmt.Errorf("cr_client executable not found: %w", err)
125+
}
126+
}
127+
return nil
128+
}
129+
130+
// ExtractDirectMemoryPIDStrings extracts PID strings from a DirectMemory BackendConfig.
131+
func ExtractDirectMemoryPIDStrings(config *pb.BackendConfig) []string {
132+
if config == nil {
133+
return nil
134+
}
135+
dm := config.GetDirectMemory()
136+
if dm == nil {
137+
return nil
138+
}
139+
target := dm.GetExplicitTarget()
140+
if target == nil {
141+
return nil
142+
}
143+
pids := make([]string, 0, len(target.GetPids()))
144+
for _, pid := range target.GetPids() {
145+
pids = append(pids, strconv.Itoa(int(pid)))
146+
}
147+
return pids
148+
}
149+
150+
// BuildDirectMemoryConfig wraps PID strings into a DirectMemory BackendConfig.
151+
func BuildDirectMemoryConfig(pidStrings []string) *pb.BackendConfig {
152+
pids := make([]int32, 0, len(pidStrings))
153+
for _, s := range pidStrings {
154+
if pid, err := strconv.ParseInt(s, 10, 32); err == nil {
155+
pids = append(pids, int32(pid))
156+
}
157+
}
158+
return &pb.BackendConfig{
159+
Backend: &pb.BackendConfig_DirectMemory{
160+
DirectMemory: &pb.DirectMemoryBackendConfig{
161+
ExplicitTarget: &pb.ProcessTarget{Pids: pids},
162+
},
163+
},
164+
}
165+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package backends_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"reflect"
8+
"testing"
9+
10+
pb "github.com/llm-d-incubation/llm-d-rl-time-slicing/pkg/snapshot-agent/api/v1alpha1"
11+
"github.com/llm-d-incubation/llm-d-rl-time-slicing/pkg/snapshot-agent/backends"
12+
)
13+
14+
func directMemoryConfig(pids ...int32) *pb.BackendConfig {
15+
return &pb.BackendConfig{
16+
Backend: &pb.BackendConfig_DirectMemory{
17+
DirectMemory: &pb.DirectMemoryBackendConfig{
18+
ExplicitTarget: &pb.ProcessTarget{Pids: pids},
19+
},
20+
},
21+
}
22+
}
23+
24+
func TestNewDirectMemory(t *testing.T) {
25+
dm := backends.NewDirectMemory()
26+
if dm == nil {
27+
t.Fatal("NewDirectMemory returned nil")
28+
}
29+
}
30+
31+
func TestDirectMemorySnapshot(t *testing.T) {
32+
tests := []struct {
33+
name string
34+
config *pb.BackendConfig
35+
execErr error
36+
expectedErr bool
37+
expectArgs [][]string
38+
}{
39+
{
40+
name: "SuccessMultiplePIDs",
41+
config: directMemoryConfig(123, 456),
42+
expectArgs: [][]string{
43+
{"-c", "-p", "123"},
44+
{"-c", "-p", "456"},
45+
},
46+
},
47+
{
48+
name: "ExecFailure",
49+
config: directMemoryConfig(123),
50+
execErr: fmt.Errorf("exec error"),
51+
expectedErr: true,
52+
expectArgs: [][]string{
53+
{"-c", "-p", "123"},
54+
},
55+
},
56+
{
57+
name: "NoPIDs",
58+
config: directMemoryConfig(),
59+
expectedErr: true,
60+
},
61+
{
62+
name: "NilConfig",
63+
config: nil,
64+
expectedErr: true,
65+
},
66+
}
67+
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
dm := backends.NewDirectMemory()
71+
var calledArgs [][]string
72+
dm.SetExecCommand(func(_ context.Context, _ string, args ...string) ([]byte, error) {
73+
calledArgs = append(calledArgs, args)
74+
return nil, tt.execErr
75+
})
76+
77+
err := dm.Snapshot(context.Background(), backends.Request{JobID: "test-job", Config: tt.config})
78+
if (err != nil) != tt.expectedErr {
79+
t.Errorf("Snapshot() error = %v, expectedErr %v", err, tt.expectedErr)
80+
}
81+
if !tt.expectedErr && !reflect.DeepEqual(calledArgs, tt.expectArgs) {
82+
t.Errorf("Snapshot() calledArgs = %v, expected %v", calledArgs, tt.expectArgs)
83+
}
84+
})
85+
}
86+
}
87+
88+
func TestDirectMemoryRestore(t *testing.T) {
89+
tests := []struct {
90+
name string
91+
config *pb.BackendConfig
92+
execErr error
93+
expectedErr bool
94+
expectArgs [][]string
95+
}{
96+
{
97+
name: "SuccessMultiplePIDs",
98+
config: directMemoryConfig(123, 456),
99+
expectArgs: [][]string{
100+
{"-r", "-p", "123"},
101+
{"-r", "-p", "456"},
102+
},
103+
},
104+
{
105+
name: "NoPIDs",
106+
config: directMemoryConfig(),
107+
expectedErr: true,
108+
},
109+
{
110+
name: "NilConfig",
111+
config: nil,
112+
expectedErr: true,
113+
},
114+
{
115+
name: "ExecFailure",
116+
config: directMemoryConfig(123),
117+
execErr: fmt.Errorf("exec error"),
118+
expectedErr: true,
119+
expectArgs: [][]string{
120+
{"-r", "-p", "123"},
121+
},
122+
},
123+
}
124+
125+
for _, tt := range tests {
126+
t.Run(tt.name, func(t *testing.T) {
127+
dm := backends.NewDirectMemory()
128+
var calledArgs [][]string
129+
dm.SetExecCommand(func(_ context.Context, _ string, args ...string) ([]byte, error) {
130+
calledArgs = append(calledArgs, args)
131+
return nil, tt.execErr
132+
})
133+
134+
err := dm.Restore(context.Background(), backends.Request{JobID: "test-job", Config: tt.config})
135+
if (err != nil) != tt.expectedErr {
136+
t.Errorf("Restore() error = %v, expectedErr %v", err, tt.expectedErr)
137+
}
138+
if !tt.expectedErr && !reflect.DeepEqual(calledArgs, tt.expectArgs) {
139+
t.Errorf("Restore() calledArgs = %v, expected %v", calledArgs, tt.expectArgs)
140+
}
141+
})
142+
}
143+
}
144+
145+
func TestDirectMemoryHealthCheck(t *testing.T) {
146+
tests := []struct {
147+
name string
148+
lookErr error
149+
statErr error
150+
expectedErr bool
151+
}{
152+
{
153+
name: "SuccessInPath",
154+
lookErr: nil,
155+
statErr: nil,
156+
expectedErr: false,
157+
},
158+
{
159+
name: "SuccessViaStat",
160+
lookErr: fmt.Errorf("not in path"),
161+
statErr: nil,
162+
expectedErr: false,
163+
},
164+
{
165+
name: "NotFoundAnywhere",
166+
lookErr: fmt.Errorf("not in path"),
167+
statErr: fmt.Errorf("no stat"),
168+
expectedErr: true,
169+
},
170+
}
171+
172+
for _, tt := range tests {
173+
t.Run(tt.name, func(t *testing.T) {
174+
dm := backends.NewDirectMemory()
175+
dm.SetLookPath(func(path string) (string, error) {
176+
if tt.lookErr != nil {
177+
return "", tt.lookErr
178+
}
179+
return path, nil
180+
})
181+
dm.SetStatFunc(func(path string) (os.FileInfo, error) {
182+
if tt.statErr != nil {
183+
return nil, tt.statErr
184+
}
185+
return os.Stat(".")
186+
})
187+
188+
err := dm.HealthCheck(context.Background())
189+
if (err != nil) != tt.expectedErr {
190+
t.Errorf("HealthCheck() error = %v, expectedErr %v", err, tt.expectedErr)
191+
}
192+
})
193+
}
194+
}
195+
196+
func TestDirectMemoryConfigHelpers(t *testing.T) {
197+
pids := []string{"100", "200"}
198+
cfg := backends.BuildDirectMemoryConfig(pids)
199+
extracted := backends.ExtractDirectMemoryPIDStrings(cfg)
200+
if !reflect.DeepEqual(extracted, pids) {
201+
t.Errorf("ExtractDirectMemoryPIDStrings() = %v, want %v", extracted, pids)
202+
}
203+
204+
if len(backends.ExtractDirectMemoryPIDStrings(nil)) != 0 {
205+
t.Errorf("Expected nil when extracting from nil config")
206+
}
207+
}

0 commit comments

Comments
 (0)