| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | // plan11Relocating is a relocating claim at phase whose target, uk-box-2, |
| 9 | // executes from installing on. The source, uk-box, executes before pinning. |
| 10 | func plan11Relocating(phase string) *Claim { |
| 11 | exec := "uk-box-2" |
| 12 | switch phase { |
| 13 | case "starting", "releasing": |
| 14 | exec = "uk-box" |
| 15 | case "pinning": |
| 16 | exec = "none" |
| 17 | } |
| 18 | return &Claim{V: schemaVersion, Host: "a.go.example.com", Gen: 20, |
| 19 | Holder: &Holder{Family: "instance", Incarnation: "inc", Location: "instances/a.go.example.com", Generation: "g1", Stamp: 19, Session: 18, WriterBox: "uk-box", |
| 20 | Admissions: []AdmissionRecord{{Session: 18, Box: "uk-box", Generation: "g1", Mode: "cold", Region: "uk", PredecessorRegion: "uk"}}}, |
| 21 | Op: Op{Kind: KindRelocating, ID: "r1", Phase: phase, Exec: exec, Participants: []string{"uk-box", "uk-box-2"}, |
| 22 | Intent: map[string]string{"region": "uk", "source_box": "uk-box", "target_box": "uk-box-2"}}} |
| 23 | } |
| 24 | |
| 25 | func plan11Probe(c *Claim) Admission { |
| 26 | return Admission{ObservedOpID: c.Op.ID, ObservedPhase: c.Op.Phase, ObservedSession: c.Holder.Session, ObservedGeneration: c.Holder.Generation, |
| 27 | Box: c.Op.Exec, Mode: "cold", NewGeneration: "p1", Region: "uk", PredecessorRegion: "uk"} |
| 28 | } |
| 29 | |
| 30 | // The relocating session row is probing then serving, adjacent and forward |
| 31 | // only (claim §4 phase table: probing admits cold probes; serving is the |
| 32 | // successful target serving on the admitted session). |
| 33 | func TestPlan11RelocatingSessionStepsAreAdjacentAndForward(t *testing.T) { |
| 34 | for _, tc := range []struct { |
| 35 | from, to string |
| 36 | want bool |
| 37 | }{ |
| 38 | {"probing", "probing", true}, |
| 39 | {"probing", "serving", true}, |
| 40 | {"serving", "serving", true}, |
| 41 | {"serving", "probing", false}, |
| 42 | {"probing", "probe-failed", false}, |
| 43 | {"installed", "probing", false}, |
| 44 | {"installing", "probing", false}, |
| 45 | {"releasing", "pinning", false}, |
| 46 | {"starting", "releasing", false}, |
| 47 | } { |
| 48 | if got := SessionContinues(KindRelocating, tc.from, tc.to); got != tc.want { |
| 49 | t.Errorf("SessionContinues(relocating, %q, %q) = %v, want %v", tc.from, tc.to, got, tc.want) |
| 50 | } |
| 51 | } |
| 52 | for phase, want := range map[string]bool{"probing": true, "serving": true, "starting": false, "releasing": false, "pinning": false, "installing": false, "installed": false, "probe-failed": false} { |
| 53 | if got := SessionPhase(KindRelocating, phase); got != want { |
| 54 | t.Errorf("SessionPhase(relocating, %q) = %v, want %v", phase, got, want) |
| 55 | } |
| 56 | } |
| 57 | if !SessionObserves(KindRelocating, "probing", "serving") || SessionObserves(KindRelocating, "serving", "probing") { |
| 58 | t.Fatal("observation follows the row forward only") |
| 59 | } |
| 60 | // The landing row is unchanged. |
| 61 | if !SessionContinues(KindLanding, "probed", "committed") || SessionContinues(KindLanding, "probing", "committed") { |
| 62 | t.Fatal("landing row changed") |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | func TestPlan11KeepWriterSessionCarriesTheProbeIntoServing(t *testing.T) { |
| 67 | toServing := func(c *Claim) error { c.Op.Phase = "serving"; return nil } |
| 68 | c := plan11Relocating("probing") |
| 69 | if err := KeepWriterSession("r1", toServing)(c); err != nil { |
| 70 | t.Fatalf("probing to serving keeps the probe's session: %v", err) |
| 71 | } |
| 72 | c = plan11Relocating("probing") |
| 73 | failed := func(c *Claim) error { c.Op.Phase = "probe-failed"; return nil } |
| 74 | if err := KeepWriterSession("r1", failed)(c); !errors.Is(err, ErrWrongState) { |
| 75 | t.Fatalf("probe-failed ends the session: %v", err) |
| 76 | } |
| 77 | c = plan11Relocating("probing") |
| 78 | newSession := func(c *Claim) error { c.Op.Phase = "serving"; c.Holder.Session++; return nil } |
| 79 | if err := KeepWriterSession("r1", newSession)(c); !errors.Is(err, ErrWrongState) { |
| 80 | t.Fatalf("a step that moves the session is refused: %v", err) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | // AdmitWriter under relocating admits only op.exec's cold probe at probing. |
| 85 | // The source's ordinary admission in starting or releasing stays refused |
| 86 | // until its admission-fence authority exists (claim Plan 11, R1 as accepted). |
| 87 | func TestPlan11AdmitWriterUnderRelocatingAdmitsOnlyTheExecutorsColdProbe(t *testing.T) { |
| 88 | c := plan11Relocating("probing") |
| 89 | in := plan11Probe(c) |
| 90 | if err := AdmitWriter(in)(c); err != nil { |
| 91 | t.Fatalf("op.exec's cold probe at probing: %v", err) |
| 92 | } |
| 93 | if c.Holder.WriterBox != "uk-box-2" || c.Holder.Generation != "p1" || c.Holder.Session != 21 { |
| 94 | t.Fatalf("admission not recorded: %+v", c.Holder) |
| 95 | } |
| 96 | refused := map[string]func() (*Claim, Admission){ |
| 97 | "not the executor": func() (*Claim, Admission) { |
| 98 | c := plan11Relocating("probing") |
| 99 | in := plan11Probe(c) |
| 100 | c.Op.Exec, in.Box = "uk-box-2", "uk-box" |
| 101 | return c, in |
| 102 | }, |
| 103 | "warm": func() (*Claim, Admission) { |
| 104 | c := plan11Relocating("probing") |
| 105 | in := plan11Probe(c) |
| 106 | in.Mode, in.NewGeneration, in.PredecessorCertified = "warm", "", true |
| 107 | return c, in |
| 108 | }, |
| 109 | "released coverage": func() (*Claim, Admission) { |
| 110 | c := plan11Relocating("probing") |
| 111 | in := plan11Probe(c) |
| 112 | in.Coverage = "released" |
| 113 | return c, in |
| 114 | }, |
| 115 | "stuck": func() (*Claim, Admission) { |
| 116 | c := plan11Relocating("probing") |
| 117 | c.Op.Journal = map[string]string{"stuck": "operator"} |
| 118 | return c, plan11Probe(c) |
| 119 | }, |
| 120 | } |
| 121 | for _, phase := range []string{"starting", "releasing", "pinning", "installing", "installed", "probe-failed", "serving"} { |
| 122 | phase := phase |
| 123 | refused["phase "+phase] = func() (*Claim, Admission) { |
| 124 | c := plan11Relocating(phase) |
| 125 | in := plan11Probe(c) |
| 126 | if c.Op.Exec == "none" { |
| 127 | c.Op.Exec = "uk-box-2" |
| 128 | in.Box = "uk-box-2" |
| 129 | } |
| 130 | return c, in |
| 131 | } |
| 132 | } |
| 133 | for name, build := range refused { |
| 134 | c, in := build() |
| 135 | before := *c.Holder |
| 136 | if err := AdmitWriter(in)(c); !errors.Is(err, ErrWrongState) { |
| 137 | t.Errorf("%s: %v", name, err) |
| 138 | } |
| 139 | if c.Holder.Session != before.Session || c.Holder.Generation != before.Generation { |
| 140 | t.Errorf("%s: refused admission changed the holder", name) |
| 141 | } |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | func TestPlan11PublishGenerationUnderRelocatingNeedsAWriterPhase(t *testing.T) { |
| 146 | for phase, want := range map[string]bool{"probing": true, "serving": true, "installing": false, "installed": false, "probe-failed": false, "releasing": false} { |
| 147 | c := plan11Relocating(phase) |
| 148 | err := PublishGeneration("r1", phase, c.Holder.Session, "g1", "uk", func() string { return "chore" })(c) |
| 149 | if (err == nil) != want { |
| 150 | t.Errorf("relocating/%s publish: %v, want allowed=%v", phase, err, want) |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |