package hostbox import ( "context" "errors" "testing" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" ) func relocating(phase, exec string) *hostclaim.Claim { return &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "rel-1", Phase: phase, Exec: exec, Participants: []string{"src", "dst"}, Intent: map[string]string{"source_box": "src", "target_box": "dst", "region": "ie"}}} } func TestRelocationRowCases(t *testing.T) { withChore := func(c *hostclaim.Claim, box string) *hostclaim.Claim { c.Obligations = append(c.Obligations, hostclaim.Obligation{ID: "o", Op: "rel-0", Kind: "park-local", Box: box, Owner: "box:" + box, Retirement: "r"}) return c } serving := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "src", Participants: []string{"src"}}} lostSource := relocating(hostclaim.PhaseStarting, "src") lostSource.Lost = []string{"src"} for _, tc := range []struct { name string c *hostclaim.Claim box string exists, fenceClosed bool want RowDecision }{ {"the serving executor", serving, "src", false, false, RowDecision{Case: RowReady}}, {"a serving host elsewhere, no row", serving, "dst", false, false, RowDecision{Case: RowNone}}, {"a serving host elsewhere, a row", serving, "dst", true, false, RowDecision{Case: RowUnexplained}}, {"the source in starting", relocating(hostclaim.PhaseStarting, "src"), "src", true, false, RowDecision{Case: RowReady}}, {"the source releasing, fence open", relocating(hostclaim.PhaseReleasing, "src"), "src", true, false, RowDecision{Case: RowReady}}, {"the source releasing, fence closed", relocating(hostclaim.PhaseReleasing, "src"), "src", true, true, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, {"the source in pinning", relocating(hostclaim.PhasePinning, "none"), "src", true, false, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, {"the target in starting", relocating(hostclaim.PhaseStarting, "src"), "dst", false, false, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, {"the target executing in probing", relocating(hostclaim.PhaseRelocatingProbing, "dst"), "dst", true, false, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, {"a lost source", lostSource, "src", true, false, RowDecision{Case: RowUnexplained}}, {"a former executor owing park-local", withChore(serving, "old"), "old", true, false, RowDecision{Case: RowRetiring, HoldOp: "rel-0"}}, {"a former executor with no row", withChore(serving, "old"), "old", false, false, RowDecision{Case: RowNone}}, {"no claim, a row", nil, "src", true, false, RowDecision{Case: RowUnexplained}}, } { t.Run(tc.name, func(t *testing.T) { if got := RelocationRow(tc.c, tc.box, tc.exists, tc.fenceClosed); got != tc.want { t.Fatalf("got %+v, want %+v", got, tc.want) } }) } } func TestWakePhaseRelocating(t *testing.T) { for _, tc := range []struct { phase, exec, box string probe, want bool }{ // Claim Plan 11 R1 as accepted: no ordinary relocating wake, the // source's in starting and releasing included. {hostclaim.PhaseStarting, "src", "src", false, false}, {hostclaim.PhaseReleasing, "src", "src", false, false}, {hostclaim.PhaseStarting, "src", "src", true, false}, {hostclaim.PhaseInstalling, "dst", "dst", false, false}, {hostclaim.PhaseInstalled, "dst", "dst", true, false}, {hostclaim.PhaseRelocatingProbing, "dst", "dst", true, true}, {hostclaim.PhaseRelocatingProbing, "dst", "dst", false, false}, {hostclaim.PhaseProbeFailed, "dst", "dst", true, false}, {hostclaim.PhasePinning, "none", "src", false, false}, } { if got := WakePhase(relocating(tc.phase, tc.exec), tc.box, tc.probe) == nil; got != tc.want { t.Fatalf("%+v: %v", tc, got) } } stuck := relocating(hostclaim.PhaseRelocatingProbing, "dst") stuck.Op.Journal = map[string]string{"stuck": "operator"} if WakePhase(stuck, "dst", true) == nil { t.Fatal("a stuck operation probed") } } // Claim Plan 11 (R1, R8) owns Policy for a relocating claim: its own gate // admits only op.exec's probe in probing (or that probe's session serving), // and Policy.Relocation can only refuse after it. The region HA rights this // branch first gave a relocating claim (a participant's reconcile, the // source's ordinary wake in starting, the serving executor clearing an // obsolete hold) are refused, whatever Relocation says. func TestPolicyUnderRelocatingAdmitsOnlyTheProbe(t *testing.T) { ctx := context.Background() none := func(context.Context, string) (string, error) { return "", nil } held := func(context.Context, string) (string, error) { return "rel-1", nil } allow := func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil } for name, tc := range map[string]struct { c *hostclaim.Claim held func(context.Context, string) (string, error) s Selection }{ "the source spawns in starting": {relocating(hostclaim.PhaseStarting, "src"), none, Selection{Host: "x.example", Box: "src", Op: "rel-1", Action: "spawn"}}, "the source reconciles": {relocating(hostclaim.PhaseStarting, "src"), held, Selection{Host: "x.example", Box: "src", Op: "rel-1", Action: "reconcile"}}, "a held target reconciles": {relocating(hostclaim.PhaseStarting, "src"), held, Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "reconcile"}}, "the executor installs": {relocating(hostclaim.PhaseInstalled, "dst"), none, Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "install"}}, "the probing target spawns": {relocating(hostclaim.PhaseRelocatingProbing, "dst"), none, Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "spawn"}}, "the source probes": {relocating(hostclaim.PhaseRelocatingProbing, "dst"), none, Selection{Host: "x.example", Box: "src", Op: "rel-1", Action: "probe"}}, "a serving executor under a hold": {&hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "dst", Participants: []string{"dst"}}}, held, Selection{Host: "x.example", Box: "dst", Action: "reconcile"}}, } { if err := (Policy{Held: tc.held, Moves: allow, Relocation: allow}).Check(ctx, tc.c, tc.s, hostwrite.Identity{}); !errors.Is(err, ErrChanged) { t.Fatalf("%s: %v", name, err) } } probing := relocating(hostclaim.PhaseRelocatingProbing, "dst") probe := Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "probe", Phase: hostclaim.PhaseRelocatingProbing} if err := (Policy{Held: none, Relocation: allow}).Check(ctx, probing, probe, hostwrite.Identity{}); err != nil { t.Fatalf("the target's probe: %v", err) } if err := (Policy{Held: none}).Check(ctx, probing, probe, hostwrite.Identity{}); !errors.Is(err, ErrChanged) { t.Fatalf("a probe with no relocation owner: %v", err) } }