Skip to content

Commit bb72152

Browse files
committed
Restore delete's log-and-continue contract on failed vGPU release
Delete goes back to the pre-refactor behavior: a failed vGPU release is logged and teardown continues, instead of failing the delete and retaining the instance. The release-side guards still never destroy a device they cannot prove is unowned, so continuing only tolerates a leaked slot until startup reconciliation recovers it - the same tradeoff the mdev path always made. The restart-policy block before teardown stays: a delete can still fail earlier when the hypervisor cannot be confirmed dead.
1 parent 8f8a5b3 commit bb72152

2 files changed

Lines changed: 26 additions & 47 deletions

File tree

lib/instances/delete.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ func (m *manager) deleteInstanceWithOptions(
8686
}
8787

8888
// 3b. Block the restart policy before any teardown. If the delete fails
89-
// partway (e.g. a failed vGPU release) the metadata is retained with the
90-
// VMM already stopped, and without this marker the restart policy
91-
// controller would start the instance again.
89+
// partway (e.g. the hypervisor cannot be confirmed dead) the metadata is
90+
// retained with the VMM already stopped, and without this marker the
91+
// restart policy controller would start the instance again.
9292
if err := m.markRestartManualStopLocked(ctx, id); err != nil {
9393
return fmt.Errorf("block restart policy before delete: %w", err)
9494
}
@@ -141,21 +141,21 @@ func (m *manager) deleteInstanceWithOptions(
141141
m.closeFirecrackerUFFDSession(ctx, stored)
142142

143143
// 5b. Release the vGPU assignment if present, before any network, device,
144-
// or volume teardown. A failed release retains the instance metadata; the
145-
// VMM has already been stopped, but its attachments are intact and the
146-
// restart policy is blocked, so a retried delete is safe.
144+
// or volume teardown. Release failure is logged and the delete continues,
145+
// matching the pre-refactor contract: the VMM is already confirmed dead,
146+
// the guards inside the release never destroy a device they cannot prove
147+
// is unowned, and a skipped release is recovered by startup
148+
// reconciliation.
147149
hadVGPUAssignment := storedVGPUDevicePath(stored) != ""
148150
if hadVGPUAssignment {
149151
log.InfoContext(ctx, "destroying vGPU", "instance_id", id, "uuid", stored.GPUMdevUUID)
150152
}
151153
if err := releaseStoredVGPU(ctx, stored); err != nil {
152-
log.ErrorContext(ctx, "failed to destroy vGPU; retaining instance metadata", "instance_id", id, "uuid", stored.GPUMdevUUID, "error", err)
153-
return fmt.Errorf("destroy vGPU: %w", err)
154-
}
155-
if hadVGPUAssignment {
154+
// Log error but continue with cleanup.
155+
log.WarnContext(ctx, "failed to destroy vGPU, continuing with cleanup", "instance_id", id, "uuid", stored.GPUMdevUUID, "error", err)
156+
} else if hadVGPUAssignment {
156157
if err := m.saveMetadata(meta); err != nil {
157-
log.ErrorContext(ctx, "failed to save metadata after vGPU release", "instance_id", id, "error", err)
158-
return fmt.Errorf("save metadata after vGPU release: %w", err)
158+
log.WarnContext(ctx, "failed to save metadata after vGPU release", "instance_id", id, "error", err)
159159
}
160160
}
161161

lib/instances/lifecycle_noop_test.go

Lines changed: 14 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func TestLifecycleNoopStandbyWithOptionsStillRejectsStandbyInstance(t *testing.T
149149
assertNoLifecycleEvent(t, events)
150150
}
151151

152-
func TestDeleteRetainsMetadataWhenVGPUReleaseFails(t *testing.T) {
152+
func TestDeleteContinuesWhenVGPUReleaseFails(t *testing.T) {
153153
m, id := newLifecycleNoopManagerWithInstance(t, StateStopped, time.Now().UTC())
154154
meta, err := m.loadMetadata(id)
155155
require.NoError(t, err)
@@ -158,32 +158,13 @@ func TestDeleteRetainsMetadataWhenVGPUReleaseFails(t *testing.T) {
158158
meta.GPUDevicePath = "/sys/bus/pci/devices/0000:82:00.4"
159159
require.NoError(t, m.saveMetadata(meta))
160160

161-
err = m.DeleteInstance(context.Background(), id)
162-
require.Error(t, err)
163-
164-
stored, err := m.loadMetadata(id)
165-
require.NoError(t, err)
166-
assert.Equal(t, devices.VGPUFramework("future-framework"), stored.GPUFramework)
167-
assert.Equal(t, "/sys/bus/pci/devices/0000:82:00.4", stored.GPUDevicePath)
168-
}
169-
170-
func TestDeleteBlocksRestartPolicyWhenVGPUReleaseFails(t *testing.T) {
171-
m, id := newLifecycleNoopManagerWithInstance(t, StateStopped, time.Now().UTC())
172-
meta, err := m.loadMetadata(id)
173-
require.NoError(t, err)
174-
meta.RestartPolicy = &restartpolicy.Policy{Policy: restartpolicy.PolicyAlways}
175-
meta.GPUProfile = "NVIDIA L40S-2Q"
176-
meta.GPUFramework = devices.VGPUFramework("future-framework")
177-
meta.GPUDevicePath = "/sys/bus/pci/devices/0000:82:00.4"
178-
require.NoError(t, m.saveMetadata(meta))
179-
180-
err = m.DeleteInstance(context.Background(), id)
181-
require.Error(t, err)
161+
// A failed release is logged and the delete continues, matching the
162+
// pre-refactor contract; the leaked assignment is recovered by startup
163+
// reconciliation.
164+
require.NoError(t, m.DeleteInstance(context.Background(), id))
182165

183-
stored, err := m.loadMetadata(id)
184-
require.NoError(t, err)
185-
assert.Equal(t, restartpolicy.BlockedReasonManualStop, stored.RestartStatus.BlockedReason,
186-
"a failed delete must not leave the instance restartable")
166+
_, err = m.loadMetadata(id)
167+
require.Error(t, err, "instance data must be deleted despite the failed release")
187168
}
188169

189170
func TestDeletePersistsVGPUReleaseBeforeTeardown(t *testing.T) {
@@ -214,7 +195,7 @@ func TestDeletePersistsVGPUReleaseBeforeTeardown(t *testing.T) {
214195
assert.Equal(t, restartpolicy.BlockedReasonManualStop, persisted.RestartStatus.BlockedReason)
215196
}
216197

217-
func TestDeleteReleasesVGPUBeforeTeardown(t *testing.T) {
198+
func TestDeleteContinuesTeardownAfterFailedVGPURelease(t *testing.T) {
218199
m, id := newLifecycleNoopManagerWithInstance(t, StateStopped, time.Now().UTC())
219200
deviceManager := &recordingDeviceManager{}
220201
m.deviceManager = deviceManager
@@ -226,15 +207,13 @@ func TestDeleteReleasesVGPUBeforeTeardown(t *testing.T) {
226207
meta.Devices = []string{"dev-1"}
227208
require.NoError(t, m.saveMetadata(meta))
228209

229-
err = m.DeleteInstance(context.Background(), id)
230-
require.Error(t, err)
231-
assert.ErrorContains(t, err, "destroy vGPU")
232-
assert.Empty(t, deviceManager.detached)
233-
assert.Empty(t, deviceManager.unbound)
210+
// The failed release must not block the rest of the teardown: devices
211+
// are detached and the instance is fully deleted.
212+
require.NoError(t, m.DeleteInstance(context.Background(), id))
213+
assert.Equal(t, []string{"dev-1"}, deviceManager.detached)
234214

235-
stored, err := m.loadMetadata(id)
236-
require.NoError(t, err)
237-
assert.Equal(t, devices.VGPUFramework("future-framework"), stored.GPUFramework)
215+
_, err = m.loadMetadata(id)
216+
require.Error(t, err, "instance data must be deleted despite the failed release")
238217
}
239218

240219
// A stale release during start must be persisted immediately: if start fails

0 commit comments

Comments
 (0)