| 1 | package rollout |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/activator" |
| 9 | ) |
| 10 | |
| 11 | // Final review wave 2, N3: an older wake W1's healthy event passes the token |
| 12 | // check (it is still the host's newest wake), then pauses in served's |
| 13 | // registry write. Meanwhile a newer wake W2 is planned, with config c2 |
| 14 | // delivered, and its healthy event activates c2. W1 resumes. Its c1 must |
| 15 | // not be activated: active stays c2, with nothing left pending. Checked |
| 16 | // before the pause and acted on after it, W1 put c1 back, and the host was |
| 17 | // shown with config pending for a revision whose attempt had already |
| 18 | // cleared. |
| 19 | func TestAStaleWakeCannotActivateItsConfigAfterANewerWake(t *testing.T) { |
| 20 | ctx := context.Background() |
| 21 | h := newHarness(t, "a") |
| 22 | h.stage("v2") |
| 23 | h.pass() |
| 24 | // The queue stays out of it: no config attempt runs beside the wakes, |
| 25 | // so only their events activate anything. |
| 26 | s := h.m.schedulerOf("a") |
| 27 | s.mu.Lock() |
| 28 | s.q.deferHost("a", t0.Add(time.Hour)) |
| 29 | s.mu.Unlock() |
| 30 | |
| 31 | // W1: an ordinary wake of the good v1, planned with c1 delivered. |
| 32 | h.wakeHost("a") |
| 33 | h.m.ConfigDelivered("a", "sha256:c1") |
| 34 | w1 := h.m.Plan(ctx, h.rows.get("a")) |
| 35 | if w1.FirstWake || w1.Boot.Version != "v1" || w1.ConfigRev != "sha256:c1" { |
| 36 | t.Fatalf("W1 = %+v: want an ordinary wake of v1 carrying c1", w1) |
| 37 | } |
| 38 | h.act.serve("a", "v1") |
| 39 | g1, _ := h.act.Current("a") |
| 40 | |
| 41 | paused, resume := make(chan struct{}), make(chan struct{}) |
| 42 | h.rows.mu.Lock() |
| 43 | h.rows.setServingHook = func(string) { |
| 44 | h.rows.mu.Lock() |
| 45 | h.rows.setServingHook = nil |
| 46 | h.rows.mu.Unlock() |
| 47 | close(paused) |
| 48 | <-resume |
| 49 | } |
| 50 | h.rows.mu.Unlock() |
| 51 | w1Done := make(chan struct{}) |
| 52 | go func() { |
| 53 | defer close(w1Done) |
| 54 | h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g1, |
| 55 | Token: w1.Token, ConfigRev: w1.ConfigRev}) |
| 56 | }() |
| 57 | <-paused // W1 is past its token check, inside served |
| 58 | |
| 59 | // W2: v2 installed, c2 delivered, a first wake planned and healthy. |
| 60 | h.install("v2", "a") |
| 61 | h.m.ConfigDelivered("a", "sha256:c2") |
| 62 | w2 := h.m.Plan(ctx, h.rows.get("a")) |
| 63 | if !w2.FirstWake || w2.Boot.Version != "v2" || w2.ConfigRev != "sha256:c2" { |
| 64 | t.Fatalf("W2 = %+v: want a first wake of v2 carrying c2", w2) |
| 65 | } |
| 66 | h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: activator.Gen{N: 99, Build: "v2"}, |
| 67 | FirstWake: true, Token: w2.Token, ConfigRev: w2.ConfigRev}) |
| 68 | if got := h.activeConfig("a"); got != "sha256:c2" { |
| 69 | t.Fatalf("active = %q after W2's healthy event, want c2", got) |
| 70 | } |
| 71 | |
| 72 | close(resume) |
| 73 | <-w1Done |
| 74 | if got := h.activeConfig("a"); got != "sha256:c2" { |
| 75 | t.Fatalf("active = %q: the older wake activated its revision after the newer wake's", got) |
| 76 | } |
| 77 | if got := h.m.Status().Hosts["a"]; got.ConfigPending { |
| 78 | t.Fatalf("a = %+v: config pending after the newer wake activated the delivered revision", got) |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // activeConfig is host's active config revision. |
| 83 | func (h *harness) activeConfig(host string) string { |
| 84 | h.m.mu.Lock() |
| 85 | defer h.m.mu.Unlock() |
| 86 | return h.m.track(host).active |
| 87 | } |
| 88 | |