Skip to content
Merged
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
6 changes: 4 additions & 2 deletions hypervisor/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ import (
func (b *Backend) KillForRestore(ctx context.Context, vmID string, rec *VMRecord, terminate func(pid int) error, runtimeFiles []string) error {
killErr := b.WithRunningVM(ctx, rec, terminate)
if killErr != nil && !errors.Is(killErr, ErrNotRunning) {
b.MarkError(ctx, vmID)
// A stopped origin has no live VMM, so a transient liveness-scan error
// here must not brick the wake — nothing was mutated, retry converges.
b.FailRestore(ctx, vmID, rec.State)
return fmt.Errorf("stop running VM: %w", killErr)
}
CleanupRuntimeFiles(ctx, rec.RunDir, runtimeFiles)
return nil
}

// FailRestore marks the VM error after a post-kill restore failure; a stopped
// FailRestore marks the VM error after a restore-path failure; a stopped
// origin is spared so hibernate wake stays retryable. Run-dir-mutating steps
// (staged merge, direct populate) quarantine unconditionally at their own
// site; origin is the pre-kill state — the DB may read stopped after the kill.
Expand Down
39 changes: 39 additions & 0 deletions hypervisor/restore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,42 @@ func TestResolveForRestoreStates(t *testing.T) {
})
}
}

func TestKillForRestoreFailureKeepsOriginContract(t *testing.T) {
tests := []struct {
name string
origin types.VMState
want types.VMState
}{
{"stopped origin stays restorable (hibernate wake)", types.VMStateStopped, types.VMStateStopped},
{"running origin quarantines", types.VMStateRunning, types.VMStateError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, id := newHibernateTestVM(t)
if err := b.DB.Update(t.Context(), func(idx *VMIndex) error {
idx.VMs[id].State = tt.origin
return nil
}); err != nil {
t.Fatalf("seed state: %v", err)
}
rec, err := b.LoadRecord(t.Context(), id)
if err != nil {
t.Fatalf("load record: %v", err)
}

killErr := b.KillForRestore(t.Context(), id, &rec, func(int) error { return errors.New("kill refused") }, nil)
if killErr == nil || !strings.Contains(killErr.Error(), "stop running VM") {
t.Fatalf("KillForRestore: %v, want stop running VM error", killErr)
}

got, err := b.LoadRecord(t.Context(), id)
if err != nil {
t.Fatalf("reload record: %v", err)
}
if got.State != tt.want {
t.Errorf("state %s, want %s", got.State, tt.want)
}
})
}
}
Loading