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
32 changes: 19 additions & 13 deletions internal/controller/migration/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"sync"
"syscall"
Expand Down Expand Up @@ -288,11 +289,11 @@ func (c *Controller) Finalize(ctx context.Context, sessionID string, action migr
return fmt.Errorf("session id %q does not match active session %q: %w", sessionID, c.sessionID, errdefs.ErrInvalidArgument)
}

// Nothing to finalize, so return early: the session is already finalized
// (idempotent repeat), idle (no active session), or a canceled destination,
// which winds down directly through Cleanup rather than Finalize.
// Nothing to finalize: already finalized, idle, or a canceled destination not
// being resumed—a resume is allowed through to bring its sandbox back up here.
if c.state == StateFinalized || c.state == StateIdle ||
(c.origin == hcsschema.MigrationOriginDestination && c.state == StateCancelled) {
(c.origin == hcsschema.MigrationOriginDestination && c.state == StateCancelled &&
action != migration.FinalizeAction_FINALIZE_ACTION_RESUME) {
return nil
}

Expand Down Expand Up @@ -420,16 +421,17 @@ func (c *Controller) Cancel(ctx context.Context, sessionID string) error {
// instead of completing; a successful abort then marks the session cancelled.
c.state = StateCancelling

if err := c.cancelLiveMigration(ctx); err != nil {
// Abort did not take hold; mark failed so the caller can retry Cancel.
c.state = StateFailed
return fmt.Errorf("cancel live migration: %w", err)
err := c.cancelLiveMigration(ctx)
// A transfer that already completed leaves nothing to abort, so the session
// is canceled, not failed; any other error fails it.
next := StateCancelled
if err != nil && !errors.Is(err, hcs.ErrInvalidState) {
next = StateFailed
}
c.state = next

c.state = StateCancelled

log.G(ctx).Info("migration session cancelled")
return nil
log.G(ctx).WithError(err).Info("migration session cancellation")
return err
}

// cancelLiveMigration issues the HCS abort with c.mu released so the possibly
Expand All @@ -438,7 +440,11 @@ func (c *Controller) Cancel(ctx context.Context, sessionID string) error {
func (c *Controller) cancelLiveMigration(ctx context.Context) error {
c.mu.Unlock()
defer c.mu.Lock()
return c.vmController.CancelLiveMigration(ctx, &hcsschema.MigrationCancelOptions{Origin: c.origin})

if err := c.vmController.CancelLiveMigration(ctx, &hcsschema.MigrationCancelOptions{Origin: c.origin}); err != nil {
return fmt.Errorf("cancel live migration: %w", err)
}
return nil
}

// Cleanup is the terminal call of a migration session on either side. It
Expand Down
27 changes: 25 additions & 2 deletions internal/controller/migration/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ func TestFinalize_IdempotentFinalized(t *testing.T) {
}
}

// TestFinalize_DestinationCancelledNoop verifies a cancelled destination, which
// winds down through Cleanup rather than Finalize, treats Finalize as a no-op.
// TestFinalize_DestinationCancelledNoop verifies a cancelled destination treats a
// non-resume Finalize as a no-op, winding down through Cleanup rather than Finalize.
func TestFinalize_DestinationCancelledNoop(t *testing.T) {
c := New()
c.sessionID = "sess-1"
Expand All @@ -233,6 +233,29 @@ func TestFinalize_DestinationCancelledNoop(t *testing.T) {
}
}

// TestFinalize_DestinationCancelledResumeSucceeds verifies a resume Finalize on a
// cancelled destination is allowed through, resuming the VM and advancing to finalized.
func TestFinalize_DestinationCancelledResumeSucceeds(t *testing.T) {
ctrl := gomock.NewController(t)
vm := mocks.NewMockvmController(ctrl)
vm.EXPECT().State().Return(vmpkg.StateMigrationTransferCompleted)
vm.EXPECT().FinalizeLiveMigration(gomock.Any(), gomock.Any()).Return(nil)
vm.EXPECT().Resume(gomock.Any(), false).Return(nil)

c := New()
c.sessionID = "sess-1"
c.state = StateCancelled
c.origin = hcsschema.MigrationOriginDestination
c.vmController = vm

if err := c.Finalize(t.Context(), "sess-1", migration.FinalizeAction_FINALIZE_ACTION_RESUME, nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if c.State() != StateFinalized {
t.Errorf("state = %s; want Finalized", c.State())
}
}

// TestFinalize_RejectsWrongState verifies finalize is rejected before a transfer
// has completed or been cancelled.
func TestFinalize_RejectsWrongState(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions internal/controller/migration/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@
// {StateTransferCompleted | StateFailed | StateCancelled}
// ── Finalize ──▶ StateFinalized ── Cleanup ──▶ StateIdle
//
// A cancelled destination is the exception: its [Controller.Finalize] is a no-op,
// so it cleans up directly from [StateCancelled].
// A cancelled destination is the exception: unless it is resumed—which brings the
// sandbox back up on this host—its [Controller.Finalize] is a no-op, so it cleans
// up directly from [StateCancelled].
//
// # Notifications
//
Expand Down
5 changes: 5 additions & 0 deletions internal/hcs/v2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ var (
// ErrVmcomputeOperationInvalidState is an error encountered when the compute system is not in a valid state for the requested operation
ErrVmcomputeOperationInvalidState = syscall.Errno(0xc0370105)

// ErrInvalidState is HCS_E_INVALID_STATE, the CoError HRESULT the HCS v2 APIs
// return when an operation is not valid in the current state (distinct from the
// ERROR_VMCOMPUTE_INVALID_STATE Win32 form above).
ErrInvalidState = syscall.Errno(0x80370105)

// ErrProcNotFound is an error encountered when a procedure look up fails.
ErrProcNotFound = syscall.Errno(0x7f)

Expand Down
Loading