diff --git a/hypervisor/restore.go b/hypervisor/restore.go index e0566f56..0c2c1d4a 100644 --- a/hypervisor/restore.go +++ b/hypervisor/restore.go @@ -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. diff --git a/hypervisor/restore_test.go b/hypervisor/restore_test.go index 4e949681..17f45324 100644 --- a/hypervisor/restore_test.go +++ b/hypervisor/restore_test.go @@ -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) + } + }) + } +}