Skip to content

Commit b40af92

Browse files
authored
[Snapshot-Agent Memory-Regions 2/N] Adding slots field to state-machine (#165)
* snapshot-agent: slot-aware snapshot/restore transitions in the state machine StartSnapshotSlot/StartRestoreSlot extend StartSnapshot/StartRestore with a named snapshot slot, recorded as the job's loaded slot on success, for backends where several snapshots of one process coexist: - restore while RUNNING only short-circuits to "already-running" when the requested slot is the loaded one; a different slot proceeds (live slot swap); - a FAULTED job may be snapshotted or restored again, since faults are typically transient (dead workload PID, timed-out checkpoint client) and should not require an agent redeploy. With slot == "" both entry points behave exactly like the existing ones, and StartSnapshot/StartRestore now delegate to them, so current backends (CUDA/app) are unchanged — pinned by slot_test.go alongside the new slot-swap and fault-recovery coverage (gotest.tools joins go.mod for it). Nothing calls the slot variants yet. Signed-off-by: Edwinhr716 <edandres249@gmail.com> * snapshot-agent: fold slot tests into the state-machine test tables Review feedback on the slot slice: slot_test.go invented its own conventions — one function per scenario, sequence-style drivers, and gotest.tools asserts — next to a state-manager_test.go that already does table-driven transitions with injected initial state. Since every slot scenario reduces to a single transition once State and Slot are seeded through the test exports, the coverage now lives in the TestStartSnapshotSlot/TestStartRestoreSlot tables beside TestStartSnapshot/TestStartRestore (same runner shape, plus slot columns and an InternalJobSlot read-side export). Net new pin: a failed worker leaves the previously loaded slot recorded. gotest.tools leaves go.mod again. Signed-off-by: Edwinhr716 <edandres249@gmail.com> --------- Signed-off-by: Edwinhr716 <edandres249@gmail.com>
1 parent a8b5611 commit b40af92

3 files changed

Lines changed: 317 additions & 7 deletions

File tree

pkg/snapshot-agent/state-machine/export_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,17 @@ func (sm *StateManager) InternalMu() *sync.RWMutex {
2525
func (sm *StateManager) InternalGetOrCreateJob(jobID, group string) *Job {
2626
return sm.getOrCreateJob(jobID, group)
2727
}
28+
29+
// InternalJobSlot reads the job's loaded slot under the locks the async
30+
// operation goroutines write it under.
31+
func (sm *StateManager) InternalJobSlot(jobID string) string {
32+
sm.mu.RLock()
33+
job := sm.jobs[jobID]
34+
sm.mu.RUnlock()
35+
if job == nil {
36+
return ""
37+
}
38+
job.mu.Lock()
39+
defer job.mu.Unlock()
40+
return job.Slot
41+
}

pkg/snapshot-agent/state-machine/state-manager.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package statemachine
22

33
import (
4+
"log/slog"
45
"sync"
56
"time"
67

@@ -24,7 +25,11 @@ type Job struct {
2425
Group string
2526
State pb.JobState
2627
PIDs []int
27-
mu sync.Mutex
28+
// Slot is the snapshot slot currently loaded on the device, for backends
29+
// with named snapshot slots (memory-regions). Empty for backends without
30+
// slot semantics.
31+
Slot string
32+
mu sync.Mutex
2833
}
2934

3035
// Operation represents a long-running snapshot or restore task.
@@ -82,6 +87,17 @@ func (sm *StateManager) RegisterJob(jobID, group string) {
8287

8388
// StartSnapshot initiates a snapshot operation if the job state allows it.
8489
func (sm *StateManager) StartSnapshot(jobID, group string, worker func() error) (string, error) {
90+
return sm.StartSnapshotSlot(jobID, group, "", worker)
91+
}
92+
93+
// StartSnapshotSlot is StartSnapshot for backends with named snapshot slots
94+
// (memory-regions): slot names the snapshot being taken and is recorded as
95+
// the job's loaded slot on success. With slot == "" the behavior is exactly
96+
// StartSnapshot's. With a non-empty slot, a FAULTED job may also be
97+
// snapshotted: faults are typically transient (dead workload PID, timed-out
98+
// cr_client) and a fresh attempt should reset the job rather than requiring
99+
// an agent redeploy.
100+
func (sm *StateManager) StartSnapshotSlot(jobID, group, slot string, worker func() error) (string, error) {
85101
sm.mu.Lock()
86102
defer sm.mu.Unlock()
87103
job := sm.getOrCreateJob(jobID, group)
@@ -94,8 +110,11 @@ func (sm *StateManager) StartSnapshot(jobID, group string, worker func() error)
94110
return "", status.Errorf(codes.Aborted, "job %s is already transitioning", jobID)
95111
}
96112

97-
// 2. State Validation: Only allow snapshotting of RUNNING jobs
98-
if job.State != pb.JobState_JOB_STATE_RUNNING {
113+
// 2. Fault Recovery (slot-aware backends only)
114+
if slot != "" && job.State == pb.JobState_JOB_STATE_FAULTED {
115+
slog.Warn("Job is FAULTED; allowing new snapshot to reset it", "jobID", jobID, "slot", slot)
116+
} else if job.State != pb.JobState_JOB_STATE_RUNNING {
117+
// 3. State Validation: Only allow snapshotting of RUNNING jobs
99118
return "", status.Errorf(codes.FailedPrecondition, "cannot snapshot job %s in state %s (must be RUNNING)", jobID, job.State)
100119
}
101120

@@ -132,6 +151,7 @@ func (sm *StateManager) StartSnapshot(jobID, group string, worker func() error)
132151
op.Status = pb.OperationStatus_OPERATION_STATUS_COMPLETE
133152
op.StorageBytes = 1024
134153
job.State = pb.JobState_JOB_STATE_SAVED
154+
job.Slot = slot
135155
}
136156
}()
137157

@@ -140,15 +160,26 @@ func (sm *StateManager) StartSnapshot(jobID, group string, worker func() error)
140160

141161
// StartRestore initiates a restore operation if the job state allows it.
142162
func (sm *StateManager) StartRestore(jobID, group string, worker func() error) (string, error) {
163+
return sm.StartRestoreSlot(jobID, group, "", worker)
164+
}
165+
166+
// StartRestoreSlot is StartRestore for backends with named snapshot slots
167+
// (memory-regions). With slot == "" the behavior is exactly StartRestore's.
168+
// With a non-empty slot:
169+
// - a RUNNING job only short-circuits to "already-running" when the
170+
// requested slot is already the loaded one; restoring a different slot
171+
// proceeds (live slot swap, the core memory-regions use case);
172+
// - a FAULTED job may be restored (fault recovery; see StartSnapshotSlot).
173+
func (sm *StateManager) StartRestoreSlot(jobID, group, slot string, worker func() error) (string, error) {
143174
sm.mu.Lock()
144175
defer sm.mu.Unlock()
145176
job := sm.getOrCreateJob(jobID, group)
146177

147178
job.mu.Lock()
148179
defer job.mu.Unlock()
149180

150-
// 1. Redundancy Optimization
151-
if job.State == pb.JobState_JOB_STATE_RUNNING {
181+
// 1. Redundancy Optimization: the requested state is already live.
182+
if job.State == pb.JobState_JOB_STATE_RUNNING && job.Slot == slot {
152183
return "already-running", nil
153184
}
154185

@@ -157,8 +188,12 @@ func (sm *StateManager) StartRestore(jobID, group string, worker func() error) (
157188
return "", status.Errorf(codes.Aborted, "job %s is already transitioning", jobID)
158189
}
159190

160-
// 3. State Validation: Only allow restoring of SAVED jobs
161-
if job.State != pb.JobState_JOB_STATE_SAVED {
191+
// 3. Fault Recovery (slot-aware backends only)
192+
if slot != "" && job.State == pb.JobState_JOB_STATE_FAULTED {
193+
slog.Warn("Job is FAULTED; allowing new restore to reset it", "jobID", jobID, "slot", slot)
194+
} else if job.State != pb.JobState_JOB_STATE_SAVED && (slot == "" || job.State != pb.JobState_JOB_STATE_RUNNING) {
195+
// 4. State Validation: restores need a SAVED job — or, for
196+
// slot-aware backends, a RUNNING job swapping to a different slot.
162197
return "", status.Errorf(codes.FailedPrecondition, "cannot restore job %s in state %s (must be SAVED)", jobID, job.State)
163198
}
164199

@@ -194,6 +229,7 @@ func (sm *StateManager) StartRestore(jobID, group string, worker func() error) (
194229
} else {
195230
op.Status = pb.OperationStatus_OPERATION_STATUS_COMPLETE
196231
job.State = pb.JobState_JOB_STATE_RUNNING
232+
job.Slot = slot
197233
op.SnapshotDeviceBytes = 1024
198234
}
199235
}()

0 commit comments

Comments
 (0)