package rollout import ( "context" "testing" "time" "github.com/carlosframework/platform/internal/activator" ) // Final review wave 2, N3: an older wake W1's healthy event passes the token // check (it is still the host's newest wake), then pauses in served's // registry write. Meanwhile a newer wake W2 is planned, with config c2 // delivered, and its healthy event activates c2. W1 resumes. Its c1 must // not be activated: active stays c2, with nothing left pending. Checked // before the pause and acted on after it, W1 put c1 back, and the host was // shown with config pending for a revision whose attempt had already // cleared. func TestAStaleWakeCannotActivateItsConfigAfterANewerWake(t *testing.T) { ctx := context.Background() h := newHarness(t, "a") h.stage("v2") h.pass() // The queue stays out of it: no config attempt runs beside the wakes, // so only their events activate anything. s := h.m.schedulerOf("a") s.mu.Lock() s.q.deferHost("a", t0.Add(time.Hour)) s.mu.Unlock() // W1: an ordinary wake of the good v1, planned with c1 delivered. h.wakeHost("a") h.m.ConfigDelivered("a", "sha256:c1") w1 := h.m.Plan(ctx, h.rows.get("a")) if w1.FirstWake || w1.Boot.Version != "v1" || w1.ConfigRev != "sha256:c1" { t.Fatalf("W1 = %+v: want an ordinary wake of v1 carrying c1", w1) } h.act.serve("a", "v1") g1, _ := h.act.Current("a") paused, resume := make(chan struct{}), make(chan struct{}) h.rows.mu.Lock() h.rows.setServingHook = func(string) { h.rows.mu.Lock() h.rows.setServingHook = nil h.rows.mu.Unlock() close(paused) <-resume } h.rows.mu.Unlock() w1Done := make(chan struct{}) go func() { defer close(w1Done) h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: g1, Token: w1.Token, ConfigRev: w1.ConfigRev}) }() <-paused // W1 is past its token check, inside served // W2: v2 installed, c2 delivered, a first wake planned and healthy. h.install("v2", "a") h.m.ConfigDelivered("a", "sha256:c2") w2 := h.m.Plan(ctx, h.rows.get("a")) if !w2.FirstWake || w2.Boot.Version != "v2" || w2.ConfigRev != "sha256:c2" { t.Fatalf("W2 = %+v: want a first wake of v2 carrying c2", w2) } h.m.OnGen(activator.GenEvent{Kind: activator.GenHealthy, Route: h.rows.get("a"), Gen: activator.Gen{N: 99, Build: "v2"}, FirstWake: true, Token: w2.Token, ConfigRev: w2.ConfigRev}) if got := h.activeConfig("a"); got != "sha256:c2" { t.Fatalf("active = %q after W2's healthy event, want c2", got) } close(resume) <-w1Done if got := h.activeConfig("a"); got != "sha256:c2" { t.Fatalf("active = %q: the older wake activated its revision after the newer wake's", got) } if got := h.m.Status().Hosts["a"]; got.ConfigPending { t.Fatalf("a = %+v: config pending after the newer wake activated the delivered revision", got) } } // activeConfig is host's active config revision. func (h *harness) activeConfig(host string) string { h.m.mu.Lock() defer h.m.mu.Unlock() return h.m.track(host).active }