carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package rollout
2
3import (
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.
12func (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.
30func 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