package rollout import ( "context" "testing" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/status" ) // Final review F4: every plan carries a token of its own, never zero, and // the delivered config revision at the moment it was planned. func TestEveryPlanCarriesItsOwnTokenAndConfigRevision(t *testing.T) { ctx := context.Background() h := newHarness(t, "a") h.stage("v2") h.registerThenInstall("v2", "a") h.wakeHost("a") h.m.ConfigDelivered("a", "sha256:c1") p1 := h.m.Plan(ctx, h.rows.get("a")) h.m.ConfigDelivered("a", "sha256:c2") p2 := h.m.Plan(ctx, h.rows.get("a")) if p1.Token == 0 || p2.Token == 0 || p1.Token == p2.Token { t.Fatalf("tokens %d and %d: want two distinct, nonzero tokens", p1.Token, p2.Token) } if p1.ConfigRev != "sha256:c1" || p2.ConfigRev != "sha256:c2" { t.Fatalf("config revisions %q and %q: want each plan's delivered revision", p1.ConfigRev, p2.ConfigRev) } } // Final review F4, the reviewer's sequence: an old first wake's observer // delivers its GenCancelled late, after the host's new first wake of B has // registered. A sibling then fails B, and root moves the box's routes on to // C. The late event belongs to the old wake: it must not clear the new // wake's registration, so the Pass keeps B's record and the new wake's // check refuses B. Keyed by host alone, the old event deleted the newer // entry, the Pass pruned the record, and the check let the failed B boot. func TestAStaleOutcomeLeavesANewerWakesRegistration(t *testing.T) { ctx := context.Background() sibling := FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "b", At: t0} h := newHarness(t, "a") h.stage("v2", "v3") h.install("v2", "a") h.wakeHost("a") old := h.m.Plan(ctx, h.rows.get("a")) g := activator.Gen{N: 7, Build: "v2", Digest: "sha-v2"} h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g, FirstWake: true, Token: old.Token, ConfigRev: old.ConfigRev}) // The instance stopped inside its observation and woke again: a new first // wake of v2, planned while the old observer's outcome is still on its way. h.wakeHost("a") cur := h.m.Plan(ctx, h.rows.get("a")) if !cur.FirstWake || cur.Boot.Version != "v2" || cur.Check == nil { t.Fatalf("plan %+v: want a new first wake onto v2 with its check", cur) } h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), Gen: g, FirstWake: true, Token: old.Token, ConfigRev: old.ConfigRev}) if _, err := h.book.Record(sibling); err != nil { t.Fatal(err) } h.install("v3", "a") // no route on the box installs v2 now h.pass() if _, ok := h.book.Lookup("bab", "blog", "v2"); !ok { t.Fatal("the Pass pruned v2's record: the old wake's late event cleared the new wake's registration") } if err := cur.Check("v2"); err == nil { t.Fatal("the new wake's check let v2 boot after it failed on a sibling") } // The new wake's own outcome does let the record go. h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), FirstWake: true, Token: cur.Token, ConfigRev: cur.ConfigRev}) h.pass() if _, ok := h.book.Lookup("bab", "blog", "v2"); ok { t.Fatal("the new wake reported its outcome, and the next Pass still kept v2's record") } } // Final review F4: an event of an older wake does nothing to a newer // wake's state. A late GenHealthy of the older wake neither opens an // observation nor activates a config revision: with the revision read from // the host (the newest plan's snapshot), it acknowledged c2 while the old // wake's process ran c1. A late GenCancelled of it does not close the newer // wake's observation. The newer wake's own events activate the revision // they carry. func TestAStaleOutcomeLeavesANewerWakesObservationAndConfig(t *testing.T) { ctx := context.Background() h := newHarness(t, "a") h.stage("v2") h.registerThenInstall("v2", "a") h.wakeHost("a") var acks []appconfig.ConfigAck ack := h.m.DeferAck(func(_ context.Context, a appconfig.ConfigAck) { acks = append(acks, a) }) h.m.ConfigDelivered("a", "sha256:c1") old := h.m.Plan(ctx, h.rows.get("a")) // The older wake's plan was replaced (a replan's first token never gets // an event) after config sync delivered c2. h.m.ConfigDelivered("a", "sha256:c2") ack(appconfig.ConfigAck{Host: "a"}) cur := h.m.Plan(ctx, h.rows.get("a")) g := activator.Gen{N: 7, Build: "v2", Digest: "sha-v2"} h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g, FirstWake: true, Token: old.Token, ConfigRev: old.ConfigRev}) h.flushAcks() if len(acks) != 0 { t.Fatalf("acks %+v: an older wake's late event acknowledged a revision", acks) } if got := h.m.Status().Hosts["a"]; got.Attempt == status.AttemptSettling || !got.ConfigPending { t.Fatalf("a = %+v: an older wake's late event opened an observation or activated a config", got) } h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g, FirstWake: true, Token: cur.Token, ConfigRev: cur.ConfigRev}) h.flushAcks() if len(acks) != 1 { t.Fatalf("acks %+v after the current wake's healthy event, want c2's", acks) } if got := h.m.Status().Hosts["a"]; got.Attempt != status.AttemptSettling || got.ConfigPending { t.Fatalf("a = %+v: want settling with c2 active", got) } h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), Gen: g, FirstWake: true, Token: old.Token, ConfigRev: old.ConfigRev}) if got := h.m.Status().Hosts["a"]; got.Attempt != status.AttemptSettling { t.Fatalf("a = %+v: an older wake's late GenCancelled closed the newer wake's observation", got) } h.m.OnGen(activator.GenEvent{Kind: activator.GenCancelled, Route: h.rows.get("a"), Gen: g, FirstWake: true, Token: cur.Token, ConfigRev: cur.ConfigRev}) if got := h.m.Status().Hosts["a"]; got.Attempt == status.AttemptSettling { t.Fatalf("a = %+v: the wake's own GenCancelled left it settling", got) } }