This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

spindle/engines/microvm: treat exited vms as already destroyed instead of erroring out

Signed-off-by: dawn <dawn@tangled.org>

author
dawn
date (Jul 27, 2026, 12:51 PM +0300) commit d0291f30 parent af2be8aa change-id wvmsqxum
+94 -8
+7 -1
spindle/engines/microvm/qemu.go
··· 306 306 } 307 307 if h.QMPMon != nil { 308 308 if err := h.QMPSystemPowerdown(); err != nil { 309 - return err 309 + // dead qmp socket means qemu exited concurrently so we wait 310 + select { 311 + case <-h.done: 312 + return nil 313 + case <-ctx.Done(): 314 + return err 315 + } 310 316 } 311 317 } 312 318 if h.done == nil {
+22 -7
spindle/engines/microvm/vm.go
··· 205 205 if state.VM == nil { 206 206 return nil 207 207 } 208 + if vmExited(state.VM) { 209 + return closeIO(&state.VM) 210 + } 208 211 209 - var err error 212 + var poweroffErr error 210 213 211 214 if state.Agent != nil { 212 215 gracefulCtx, cancel := context.WithTimeout(ctx, vmShutdownTimeout) 213 - poweredOff, poweroffErr := e.poweroffViaAgent(gracefulCtx, wid, state) 216 + var poweredOff bool 217 + poweredOff, poweroffErr = e.poweroffViaAgent(gracefulCtx, wid, state) 214 218 cancel() 215 219 216 - err = errors.Join(err, poweroffErr) 217 220 if poweredOff { 218 - return errors.Join(err, closeIO(&state.VM)) 221 + return closeIO(&state.VM) 222 + } 223 + if vmExited(state.VM) { 224 + return closeIO(&state.VM) 219 225 } 220 226 } 221 227 222 228 fallbackCtx, cancel := context.WithTimeout(ctx, vmShutdownTimeout) 223 229 defer cancel() 224 230 225 - if shutdownErr := state.VM.Shutdown(fallbackCtx); shutdownErr != nil { 231 + shutdownErr := state.VM.Shutdown(fallbackCtx) 232 + if shutdownErr != nil && !vmExited(state.VM) { 226 233 e.l.Warn("microVM shutdown fallback failed", "workflow", wid, "error", shutdownErr) 227 - err = errors.Join(err, shutdownErr) 234 + return errors.Join(poweroffErr, shutdownErr, closeIO(&state.VM)) 228 235 } 229 236 230 - return errors.Join(err, closeIO(&state.VM)) 237 + return closeIO(&state.VM) 238 + } 239 + 240 + func vmExited(vm VMHandle) bool { 241 + ctx, cancel := context.WithCancel(context.Background()) 242 + cancel() 243 + // a cancelled wait means the process is still live 244 + // any other result means it exited 245 + return !errors.Is(vm.WaitContext(ctx), context.Canceled) 231 246 } 232 247 233 248 func (e *Engine) poweroffViaAgent(ctx context.Context, wid models.WorkflowId, state *workflowState) (bool, error) {
+65
spindle/engines/microvm/vm_test.go
··· 1 + package microvm 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "log/slog" 7 + "testing" 8 + 9 + "tangled.org/core/spindle/models" 10 + ) 11 + 12 + type shutdownTestVM struct { 13 + exited bool 14 + waitErr error 15 + shutdownErr error 16 + closed bool 17 + } 18 + 19 + func (v *shutdownTestVM) Shutdown(context.Context) error { 20 + v.exited = true 21 + return v.shutdownErr 22 + } 23 + 24 + func (v *shutdownTestVM) WaitContext(ctx context.Context) error { 25 + if v.exited { 26 + return v.waitErr 27 + } 28 + return ctx.Err() 29 + } 30 + 31 + func (v *shutdownTestVM) Close() error { 32 + v.closed = true 33 + return nil 34 + } 35 + 36 + func (*shutdownTestVM) Logs() VMLogs { return VMLogs{} } 37 + func (*shutdownTestVM) CID() uint32 { return 0 } 38 + func (*shutdownTestVM) WorkDir() string { return "" } 39 + func (*shutdownTestVM) OOMKilled() bool { return false } 40 + 41 + func TestShutdownVM_TreatsExitedVMAsCleanedUp(t *testing.T) { 42 + for name, vm := range map[string]*shutdownTestVM{ 43 + "already exited": { 44 + exited: true, 45 + waitErr: errors.New("qemu exited"), 46 + shutdownErr: errors.New("qmp broken pipe"), 47 + }, 48 + "exits during fallback": { 49 + waitErr: errors.New("qemu exited"), 50 + shutdownErr: errors.New("qmp broken pipe"), 51 + }, 52 + } { 53 + t.Run(name, func(t *testing.T) { 54 + e := &Engine{l: slog.Default()} 55 + state := &workflowState{VM: vm} 56 + 57 + if err := e.shutdownVM(context.Background(), models.WorkflowId{}, state); err != nil { 58 + t.Fatalf("shutdownVM: %v", err) 59 + } 60 + if !vm.closed { 61 + t.Fatal("expected vm handle to be closed") 62 + } 63 + }) 64 + } 65 + }