| 1 | package hostbox |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/hostclaim" |
| 9 | "github.com/carlosframework/platform/internal/hostwrite" |
| 10 | ) |
| 11 | |
| 12 | func relocating(phase, exec string) *hostclaim.Claim { |
| 13 | return &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "rel-1", Phase: phase, Exec: exec, |
| 14 | Participants: []string{"src", "dst"}, Intent: map[string]string{"source_box": "src", "target_box": "dst", "region": "ie"}}} |
| 15 | } |
| 16 | |
| 17 | func TestRelocationRowCases(t *testing.T) { |
| 18 | withChore := func(c *hostclaim.Claim, box string) *hostclaim.Claim { |
| 19 | c.Obligations = append(c.Obligations, hostclaim.Obligation{ID: "o", Op: "rel-0", Kind: "park-local", Box: box, Owner: "box:" + box, Retirement: "r"}) |
| 20 | return c |
| 21 | } |
| 22 | serving := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "src", Participants: []string{"src"}}} |
| 23 | lostSource := relocating(hostclaim.PhaseStarting, "src") |
| 24 | lostSource.Lost = []string{"src"} |
| 25 | for _, tc := range []struct { |
| 26 | name string |
| 27 | c *hostclaim.Claim |
| 28 | box string |
| 29 | exists, fenceClosed bool |
| 30 | want RowDecision |
| 31 | }{ |
| 32 | {"the serving executor", serving, "src", false, false, RowDecision{Case: RowReady}}, |
| 33 | {"a serving host elsewhere, no row", serving, "dst", false, false, RowDecision{Case: RowNone}}, |
| 34 | {"a serving host elsewhere, a row", serving, "dst", true, false, RowDecision{Case: RowUnexplained}}, |
| 35 | {"the source in starting", relocating(hostclaim.PhaseStarting, "src"), "src", true, false, RowDecision{Case: RowReady}}, |
| 36 | {"the source releasing, fence open", relocating(hostclaim.PhaseReleasing, "src"), "src", true, false, RowDecision{Case: RowReady}}, |
| 37 | {"the source releasing, fence closed", relocating(hostclaim.PhaseReleasing, "src"), "src", true, true, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, |
| 38 | {"the source in pinning", relocating(hostclaim.PhasePinning, "none"), "src", true, false, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, |
| 39 | {"the target in starting", relocating(hostclaim.PhaseStarting, "src"), "dst", false, false, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, |
| 40 | {"the target executing in probing", relocating(hostclaim.PhaseRelocatingProbing, "dst"), "dst", true, false, RowDecision{Case: RowHeld, HoldOp: "rel-1"}}, |
| 41 | {"a lost source", lostSource, "src", true, false, RowDecision{Case: RowUnexplained}}, |
| 42 | {"a former executor owing park-local", withChore(serving, "old"), "old", true, false, RowDecision{Case: RowRetiring, HoldOp: "rel-0"}}, |
| 43 | {"a former executor with no row", withChore(serving, "old"), "old", false, false, RowDecision{Case: RowNone}}, |
| 44 | {"no claim, a row", nil, "src", true, false, RowDecision{Case: RowUnexplained}}, |
| 45 | } { |
| 46 | t.Run(tc.name, func(t *testing.T) { |
| 47 | if got := RelocationRow(tc.c, tc.box, tc.exists, tc.fenceClosed); got != tc.want { |
| 48 | t.Fatalf("got %+v, want %+v", got, tc.want) |
| 49 | } |
| 50 | }) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestWakePhaseRelocating(t *testing.T) { |
| 55 | for _, tc := range []struct { |
| 56 | phase, exec, box string |
| 57 | probe, want bool |
| 58 | }{ |
| 59 | // Claim Plan 11 R1 as accepted: no ordinary relocating wake, the |
| 60 | // source's in starting and releasing included. |
| 61 | {hostclaim.PhaseStarting, "src", "src", false, false}, |
| 62 | {hostclaim.PhaseReleasing, "src", "src", false, false}, |
| 63 | {hostclaim.PhaseStarting, "src", "src", true, false}, |
| 64 | {hostclaim.PhaseInstalling, "dst", "dst", false, false}, |
| 65 | {hostclaim.PhaseInstalled, "dst", "dst", true, false}, |
| 66 | {hostclaim.PhaseRelocatingProbing, "dst", "dst", true, true}, |
| 67 | {hostclaim.PhaseRelocatingProbing, "dst", "dst", false, false}, |
| 68 | {hostclaim.PhaseProbeFailed, "dst", "dst", true, false}, |
| 69 | {hostclaim.PhasePinning, "none", "src", false, false}, |
| 70 | } { |
| 71 | if got := WakePhase(relocating(tc.phase, tc.exec), tc.box, tc.probe) == nil; got != tc.want { |
| 72 | t.Fatalf("%+v: %v", tc, got) |
| 73 | } |
| 74 | } |
| 75 | stuck := relocating(hostclaim.PhaseRelocatingProbing, "dst") |
| 76 | stuck.Op.Journal = map[string]string{"stuck": "operator"} |
| 77 | if WakePhase(stuck, "dst", true) == nil { |
| 78 | t.Fatal("a stuck operation probed") |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // Claim Plan 11 (R1, R8) owns Policy for a relocating claim: its own gate |
| 83 | // admits only op.exec's probe in probing (or that probe's session serving), |
| 84 | // and Policy.Relocation can only refuse after it. The region HA rights this |
| 85 | // branch first gave a relocating claim (a participant's reconcile, the |
| 86 | // source's ordinary wake in starting, the serving executor clearing an |
| 87 | // obsolete hold) are refused, whatever Relocation says. |
| 88 | func TestPolicyUnderRelocatingAdmitsOnlyTheProbe(t *testing.T) { |
| 89 | ctx := context.Background() |
| 90 | none := func(context.Context, string) (string, error) { return "", nil } |
| 91 | held := func(context.Context, string) (string, error) { return "rel-1", nil } |
| 92 | allow := func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil } |
| 93 | for name, tc := range map[string]struct { |
| 94 | c *hostclaim.Claim |
| 95 | held func(context.Context, string) (string, error) |
| 96 | s Selection |
| 97 | }{ |
| 98 | "the source spawns in starting": {relocating(hostclaim.PhaseStarting, "src"), none, Selection{Host: "x.example", Box: "src", Op: "rel-1", Action: "spawn"}}, |
| 99 | "the source reconciles": {relocating(hostclaim.PhaseStarting, "src"), held, Selection{Host: "x.example", Box: "src", Op: "rel-1", Action: "reconcile"}}, |
| 100 | "a held target reconciles": {relocating(hostclaim.PhaseStarting, "src"), held, Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "reconcile"}}, |
| 101 | "the executor installs": {relocating(hostclaim.PhaseInstalled, "dst"), none, Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "install"}}, |
| 102 | "the probing target spawns": {relocating(hostclaim.PhaseRelocatingProbing, "dst"), none, Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "spawn"}}, |
| 103 | "the source probes": {relocating(hostclaim.PhaseRelocatingProbing, "dst"), none, Selection{Host: "x.example", Box: "src", Op: "rel-1", Action: "probe"}}, |
| 104 | "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"}}, |
| 105 | } { |
| 106 | if err := (Policy{Held: tc.held, Moves: allow, Relocation: allow}).Check(ctx, tc.c, tc.s, hostwrite.Identity{}); !errors.Is(err, ErrChanged) { |
| 107 | t.Fatalf("%s: %v", name, err) |
| 108 | } |
| 109 | } |
| 110 | probing := relocating(hostclaim.PhaseRelocatingProbing, "dst") |
| 111 | probe := Selection{Host: "x.example", Box: "dst", Op: "rel-1", Action: "probe", Phase: hostclaim.PhaseRelocatingProbing} |
| 112 | if err := (Policy{Held: none, Relocation: allow}).Check(ctx, probing, probe, hostwrite.Identity{}); err != nil { |
| 113 | t.Fatalf("the target's probe: %v", err) |
| 114 | } |
| 115 | if err := (Policy{Held: none}).Check(ctx, probing, probe, hostwrite.Identity{}); !errors.Is(err, ErrChanged) { |
| 116 | t.Fatalf("a probe with no relocation owner: %v", err) |
| 117 | } |
| 118 | } |
| 119 | |