| 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 | "github.com/carlosframework/platform/internal/status" |
| 10 | ) |
| 11 | |
| 12 | // Final review F4: every plan carries a token of its own, never zero, and |
| 13 | // the delivered config revision at the moment it was planned. |
| 14 | func TestEveryPlanCarriesItsOwnTokenAndConfigRevision(t *testing.T) { |
| 15 | ctx := context.Background() |
| 16 | h := newHarness(t, "a") |
| 17 | h.stage("v2") |
| 18 | h.registerThenInstall("v2", "a") |
| 19 | h.wakeHost("a") |
| 20 | h.m.ConfigDelivered("a", "sha256:c1") |
| 21 | p1 := h.m.Plan(ctx, h.rows.get("a")) |
| 22 | h.m.ConfigDelivered("a", "sha256:c2") |
| 23 | p2 := h.m.Plan(ctx, h.rows.get("a")) |
| 24 | if p1.Token == 0 || p2.Token == 0 || p1.Token == p2.Token { |
| 25 | t.Fatalf("tokens %d and %d: want two distinct, nonzero tokens", p1.Token, p2.Token) |
| 26 | } |
| 27 | if p1.ConfigRev != "sha256:c1" || p2.ConfigRev != "sha256:c2" { |
| 28 | t.Fatalf("config revisions %q and %q: want each plan's delivered revision", p1.ConfigRev, p2.ConfigRev) |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | // Final review F4, the reviewer's sequence: an old first wake's observer |
| 33 | // delivers its GenCancelled late, after the host's new first wake of B has |
| 34 | // registered. A sibling then fails B, and root moves the box's routes on to |
| 35 | // C. The late event belongs to the old wake: it must not clear the new |
| 36 | // wake's registration, so the Pass keeps B's record and the new wake's |
| 37 | // check refuses B. Keyed by host alone, the old event deleted the newer |
| 38 | // entry, the Pass pruned the record, and the check let the failed B boot. |
| 39 | func TestAStaleOutcomeLeavesANewerWakesRegistration(t *testing.T) { |
| 40 | ctx := context.Background() |
| 41 | sibling := FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "b", At: t0} |
| 42 | h := newHarness(t, "a") |
| 43 | h.stage("v2", "v3") |
| 44 | h.install("v2", "a") |
| 45 | h.wakeHost("a") |
| 46 | old := h.m.Plan(ctx, h.rows.get("a")) |
| 47 | g := activator.Gen{N: 7, Build: "v2", Digest: "sha-v2"} |
| 48 | h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g, FirstWake: true, |
| 49 | Token: old.Token, ConfigRev: old.ConfigRev}) |
| 50 | |
| 51 | // The instance stopped inside its observation and woke again: a new first |
| 52 | // wake of v2, planned while the old observer's outcome is still on its way. |
| 53 | h.wakeHost("a") |
| 54 | cur := h.m.Plan(ctx, h.rows.get("a")) |
| 55 | if !cur.FirstWake || cur.Boot.Version != "v2" || cur.Check == nil { |
| 56 | t.Fatalf("plan %+v: want a new first wake onto v2 with its check", cur) |
| 57 | } |
| 58 | h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), Gen: g, FirstWake: true, |
| 59 | Token: old.Token, ConfigRev: old.ConfigRev}) |
| 60 | |
| 61 | if _, err := h.book.Record(sibling); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | h.install("v3", "a") // no route on the box installs v2 now |
| 65 | h.pass() |
| 66 | if _, ok := h.book.Lookup("bab", "blog", "v2"); !ok { |
| 67 | t.Fatal("the Pass pruned v2's record: the old wake's late event cleared the new wake's registration") |
| 68 | } |
| 69 | if err := cur.Check("v2"); err == nil { |
| 70 | t.Fatal("the new wake's check let v2 boot after it failed on a sibling") |
| 71 | } |
| 72 | |
| 73 | // The new wake's own outcome does let the record go. |
| 74 | h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), FirstWake: true, |
| 75 | Token: cur.Token, ConfigRev: cur.ConfigRev}) |
| 76 | h.pass() |
| 77 | if _, ok := h.book.Lookup("bab", "blog", "v2"); ok { |
| 78 | t.Fatal("the new wake reported its outcome, and the next Pass still kept v2's record") |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // Final review F4: an event of an older wake does nothing to a newer |
| 83 | // wake's state. A late GenHealthy of the older wake neither opens an |
| 84 | // observation nor activates a config revision: with the revision read from |
| 85 | // the host (the newest plan's snapshot), it acknowledged c2 while the old |
| 86 | // wake's process ran c1. A late GenCancelled of it does not close the newer |
| 87 | // wake's observation. The newer wake's own events activate the revision |
| 88 | // they carry. |
| 89 | func TestAStaleOutcomeLeavesANewerWakesObservationAndConfig(t *testing.T) { |
| 90 | ctx := context.Background() |
| 91 | h := newHarness(t, "a") |
| 92 | h.stage("v2") |
| 93 | h.registerThenInstall("v2", "a") |
| 94 | h.wakeHost("a") |
| 95 | var acks []appconfig.ConfigAck |
| 96 | ack := h.m.DeferAck(func(_ context.Context, a appconfig.ConfigAck) { acks = append(acks, a) }) |
| 97 | |
| 98 | h.m.ConfigDelivered("a", "sha256:c1") |
| 99 | old := h.m.Plan(ctx, h.rows.get("a")) |
| 100 | // The older wake's plan was replaced (a replan's first token never gets |
| 101 | // an event) after config sync delivered c2. |
| 102 | h.m.ConfigDelivered("a", "sha256:c2") |
| 103 | ack(appconfig.ConfigAck{Host: "a"}) |
| 104 | cur := h.m.Plan(ctx, h.rows.get("a")) |
| 105 | |
| 106 | g := activator.Gen{N: 7, Build: "v2", Digest: "sha-v2"} |
| 107 | h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g, FirstWake: true, |
| 108 | Token: old.Token, ConfigRev: old.ConfigRev}) |
| 109 | h.flushAcks() |
| 110 | if len(acks) != 0 { |
| 111 | t.Fatalf("acks %+v: an older wake's late event acknowledged a revision", acks) |
| 112 | } |
| 113 | if got := h.m.Status().Hosts["a"]; got.Attempt == status.AttemptSettling || !got.ConfigPending { |
| 114 | t.Fatalf("a = %+v: an older wake's late event opened an observation or activated a config", got) |
| 115 | } |
| 116 | |
| 117 | h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g, FirstWake: true, |
| 118 | Token: cur.Token, ConfigRev: cur.ConfigRev}) |
| 119 | h.flushAcks() |
| 120 | if len(acks) != 1 { |
| 121 | t.Fatalf("acks %+v after the current wake's healthy event, want c2's", acks) |
| 122 | } |
| 123 | if got := h.m.Status().Hosts["a"]; got.Attempt != status.AttemptSettling || got.ConfigPending { |
| 124 | t.Fatalf("a = %+v: want settling with c2 active", got) |
| 125 | } |
| 126 | |
| 127 | h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), Gen: g, FirstWake: true, |
| 128 | Token: old.Token, ConfigRev: old.ConfigRev}) |
| 129 | if got := h.m.Status().Hosts["a"]; got.Attempt != status.AttemptSettling { |
| 130 | t.Fatalf("a = %+v: an older wake's late GenCancelled closed the newer wake's observation", got) |
| 131 | } |
| 132 | h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), Gen: g, FirstWake: true, |
| 133 | Token: cur.Token, ConfigRev: cur.ConfigRev}) |
| 134 | if got := h.m.Status().Hosts["a"]; got.Attempt == status.AttemptSettling { |
| 135 | t.Fatalf("a = %+v: the wake's own GenCancelled left it settling", got) |
| 136 | } |
| 137 | } |
| 138 | |