| 1 | package rollout |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/activator" |
| 8 | "github.com/carlosframework/platform/internal/appconfig" |
| 9 | ) |
| 10 | |
| 11 | // sleepHost makes host a sleeper: no current generation, no entry. |
| 12 | func (h *harness) sleepHost(host string) { |
| 13 | h.act.mu.Lock() |
| 14 | defer h.act.mu.Unlock() |
| 15 | delete(h.act.gens, host) |
| 16 | delete(h.act.owned, host) |
| 17 | } |
| 18 | |
| 19 | // Final review wave 2, N2, the reviewer's sequence: instance h serves its |
| 20 | // good build v1, and its hold attempt onto v2 runs v2's process. A sibling's |
| 21 | // first wake then fails v2, and the cohort-failure transition supersedes h's |
| 22 | // attempt, which the activator abandons after v2 ran (Executed). Nothing is |
| 23 | // held down on h: it was superseded, not seen to fail. h stops. Its next |
| 24 | // wake must not boot v1: v2 may have migrated h's database (spec §6.4). Nor |
| 25 | // may it boot v2 (Astra R3 N7): the sibling's record holds v2 for h, which |
| 26 | // only passed nothing, and the marker lifts no record (spec §8.4 exempts an |
| 27 | // instance that passed on the build, not one that merely ran it). So h |
| 28 | // stays down until a retry (`carlos instances retry`, `carlos promote |
| 29 | // --retry`) or a newer build, and its plan's check refuses both builds. |
| 30 | func TestASupersededHoldThatRanKeepsTheInstanceOffItsGoodBuild(t *testing.T) { |
| 31 | ctx := context.Background() |
| 32 | h := newHarness(t, "h", "s") |
| 33 | h.swap = appconfig.SwapHold |
| 34 | h.stage("v2") |
| 35 | h.act.mode = activator.ModeHold |
| 36 | h.act.executed = func(a activator.Attempt, _ activator.SwapOutcome) bool { return a.Build == "v2" } |
| 37 | h.sleepHost("s") |
| 38 | h.pass() |
| 39 | |
| 40 | gate := h.act.hold("h") |
| 41 | h.install("v2", "h", "s") |
| 42 | h.pass() |
| 43 | waitFor(t, "h's hold attempt onto v2", func() bool { |
| 44 | _, started, _ := h.act.snapshot() |
| 45 | return len(started) == 1 && started[0] == "h:v2" |
| 46 | }) |
| 47 | |
| 48 | // The sibling's first wake of v2 fails, in hold: a record for v2 (held |
| 49 | // down on s), and the halt supersedes h's attempt. |
| 50 | h.wakeHost("s") |
| 51 | sp := h.m.Plan(ctx, h.rows.get("s")) |
| 52 | if !sp.FirstWake || sp.Boot.Version != "v2" || sp.Mode != activator.ModeHold { |
| 53 | t.Fatalf("s's plan %+v: want a hold first wake of v2", sp) |
| 54 | } |
| 55 | h.m.OnGen(activator.GenEvent{Kind: activator.GenFailed, Route: h.rows.get("s"), Failed: sp.Boot, Mode: sp.Mode, |
| 56 | FirstWake: true, Reason: "health check failed", Token: sp.Token, ConfigRev: sp.ConfigRev}) |
| 57 | close(gate) |
| 58 | waitFor(t, "h's attempt to finish", func() bool { return !h.m.attemptRunning("h") }) |
| 59 | calls, _, _ := h.act.snapshot() |
| 60 | if len(calls) != 1 || calls[0].outcome != activator.SwapAbandoned { |
| 61 | t.Fatalf("calls %+v: want h's attempt abandoned (superseded by the halt)", calls) |
| 62 | } |
| 63 | if h.book.HeldDown("bab", "blog", "v2", "h") { |
| 64 | t.Fatal("h is held down: a superseded attempt is no failure of the build") |
| 65 | } |
| 66 | |
| 67 | // h stops, and wakes again. |
| 68 | h.sleepHost("h") |
| 69 | h.wakeHost("h") |
| 70 | p := h.m.Plan(ctx, h.rows.get("h")) |
| 71 | if p.Refuse != HeldDownRefusal("v2") || p.Boot.StagedPath != "" { |
| 72 | t.Fatalf("h's plan %+v: want the wake refused in the hold-down words, neither v2 nor the good v1", p) |
| 73 | } |
| 74 | // The same rule at the exec, for a plan made before the record landed: |
| 75 | // sp's check is the sibling's, so build one for h from a plan that boots |
| 76 | // v2 under the marker with no record standing. |
| 77 | check := h.m.planCheck(h.rows.get("h"), activator.WakePlan{Boot: sp.Boot, Mode: activator.ModeHold, FirstWake: true}) |
| 78 | if check("v2") == nil { |
| 79 | t.Fatal("h's check lets v2 boot while the sibling's record of it stands") |
| 80 | } |
| 81 | if check("v1") == nil { |
| 82 | t.Fatal("h's check lets the good v1 boot after the hold build ran") |
| 83 | } |
| 84 | } |
| 85 | |