package activator import ( "context" "errors" "sync" "testing" "time" ) // Final review wave 2, N2: a planned first wake that ran its build and was // ended with no verdict tells the scheduler the build ran (GenCancelled, // Executed, Gen.Build naming it), so a hold build's run holds the instance // off its good build. Two ways it happens: an intentional stop cancels the // observation of a healthy first wake, or the wake's lifecycle ends it after // the exec (here Shutdown during the health check). The second, which // reported nothing before, is reported for a hold first wake only, under the // host's outcome fence, so no new wake plans before the scheduler has it. // Since Task 5 every planned wake with a token ends with a terminal event: // one that ran nothing is cancelled, not Executed, and an overlap wake ended // the same way is cancelled Executed. Neither records anything, so neither // is delivered under the fence. func TestAFirstWakeEndedAfterItsExecReportsThatItRan(t *testing.T) { const host = "one.example.com" endInHealth := func(t *testing.T, mode SwapMode, refuse bool) (*Service, *genLog, *planGenBackend, chan bool) { dir := t.TempDir() good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2") plan := WakePlan{Boot: next, Mode: mode, FirstWake: true, Token: 41, ConfigRev: "sha256:c1"} if mode == ModeOverlap { plan.Fallback = &good } if refuse { plan.Check = func(string) error { return errors.New("held") } } a, b, evs := planActivator(t, plan) fenced := make(chan bool, 4) a.OnGen = func(ev GenEvent) { if ev.Kind == GenCancelled { a.mu.Lock() _, held := a.outcomeFences[host] a.mu.Unlock() fenced <- held } evs.add(ev) } entered := make(chan struct{}) var once sync.Once a.genHealth = func(ctx context.Context, _ Gen) error { once.Do(func() { close(entered) }) <-ctx.Done() return ctx.Err() } go a.beginRequest(context.Background(), testRoute(host)) //nolint:errcheck if !refuse { <-entered } else { time.Sleep(100 * time.Millisecond) } a.Shutdown(context.Background()) return a, evs, b, fenced } t.Run("hold, ended in its health check", func(t *testing.T) { _, evs, _, fenced := endInHealth(t, ModeHold, false) ev := evs.wait(t, GenCancelled) if !ev.Executed || ev.Gen.Build != "v2" || !ev.FirstWake || ev.Mode != ModeHold || ev.Token != 41 { t.Fatalf("event %+v: want GenCancelled, Executed, naming v2, with the plan's token", ev) } if !<-fenced { t.Fatal("the host's outcome fence was not held while the event was delivered") } }) t.Run("overlap, ended in its health check", func(t *testing.T) { _, evs, _, fenced := endInHealth(t, ModeOverlap, false) ev := evs.wait(t, GenCancelled) if !ev.Executed || ev.Gen.Build != "v2" || ev.Mode != ModeOverlap || ev.Token != 41 { t.Fatalf("event %+v: want GenCancelled, Executed, naming v2, in overlap mode, with the plan's token", ev) } if <-fenced { t.Fatal("an overlap wake's cancellation records nothing, yet it held the outcome fence") } time.Sleep(100 * time.Millisecond) if k := evs.kinds(); len(k) != 1 { t.Fatalf("events %v: want the one cancellation", k) } }) t.Run("hold, refused before its exec", func(t *testing.T) { _, evs, b, fenced := endInHealth(t, ModeHold, true) ev := evs.wait(t, GenCancelled) if ev.Executed || ev.Token != 41 || len(b.started()) != 0 { t.Fatalf("event %+v, started %+v: a wake that ran nothing is cancelled, not Executed", ev, b.started()) } if <-fenced { t.Fatal("a cancellation with nothing run held the outcome fence") } time.Sleep(100 * time.Millisecond) if k := evs.kinds(); len(k) != 1 { t.Fatalf("events %v: want the one cancellation", k) } }) t.Run("hold, its observation cancelled", func(t *testing.T) { shrinkFirstWake(t, 55*time.Second, 25*time.Second, 2*time.Second) next := stagedID(t, t.TempDir(), "v2") a, _, evs := planActivator(t, WakePlan{Boot: next, Mode: ModeHold, FirstWake: true, Token: 42}) mustServe(t, a) evs.wait(t, GenHealthy) a.BounceFor(host, CauseConfig) ev := evs.wait(t, GenCancelled) if !ev.Executed || ev.Gen.Build != "v2" || ev.Mode != ModeHold || ev.Token != 42 { t.Fatalf("event %+v: want the cancelled observation reported as executed, naming v2", ev) } }) }