package activator import ( "context" "errors" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/registry" ) // Task 5, plan review #1: the scheduler ends a planned first wake's // write-ahead intent only on that wake's own terminal event, never on the // activator letting the host go (a failed wake's entry goes before its // outcome is delivered). So every planned wake that carries a token ends // with exactly one token-carrying terminal event. The exits that sent none // before now send GenCancelled: not Executed when the plan's build never // ran (refused at the lease, at its check, replaced by a replan), Executed // when it ran and its lifecycle ended it with no verdict. None of them is an // outcome the fence must cover: they record nothing, so a new wake may plan // before they arrive. // terminal reports the token's events that settle its build's fate (the // scheduler's End): every kind but a first wake's healthy. func terminal(evs []GenEvent, tok uint64) []GenEvent { var out []GenEvent for _, ev := range evs { if ev.Token == tok && (ev.Kind != GenHealthy || !ev.FirstWake) { out = append(out, ev) } } return out } // settle waits until the activator has gone quiet: events arrive on the // wake's goroutine after the request returns. func settle() { time.Sleep(100 * time.Millisecond) } func TestAPlannedWakeRefusedAtItsLeaseIsCancelledUnrun(t *testing.T) { dir := t.TempDir() good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2") a, b, evs := planActivator(t, WakePlan{Boot: next, Mode: ModeOverlap, FirstWake: true, Fallback: &good, Token: 51, ConfigRev: "c1"}) var fenced atomic.Bool a.OnGen = func(ev GenEvent) { a.mu.Lock() _, held := a.outcomeFences["one.example.com"] a.mu.Unlock() fenced.Store(fenced.Load() || held) evs.add(ev) } b.mu.Lock() b.acquireErr = errors.New("lease held by a previous incarnation") b.mu.Unlock() if _, err := a.beginRequest(context.Background(), testRoute("one.example.com")); err == nil { t.Fatal("the wake was served without its lease") } settle() got := terminal(evs.all(), 51) if len(got) != 1 || got[0].Kind != GenCancelled || got[0].Executed || got[0].ConfigRev != "c1" { t.Fatalf("events %+v: want one GenCancelled, not executed, with the plan's token", evs.all()) } if len(evs.all()) != 1 { t.Fatalf("events %+v: want the one", evs.all()) } if fenced.Load() { t.Fatal("an event that records nothing was delivered under the outcome fence") } } // A plan refused by its check before the exec, whose replan's check refuses // too: both plans are cancelled unrun, each once, by its own token. func TestARefusedReplanCancelsBothPlans(t *testing.T) { t.Parallel() dir := t.TempDir() good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2") a, b, evs := planActivator(t, WakePlan{}) refuse := func(string) error { return errors.New("held") } var plans atomic.Uint64 a.Planner = func(context.Context, registry.Route) WakePlan { return WakePlan{Boot: next, Mode: ModeHold, FirstWake: true, Fallback: &good, Check: refuse, Token: 60 + plans.Add(1)} } if _, err := a.beginRequest(context.Background(), testRoute("one.example.com")); !errors.Is(err, errPlanStale) { t.Fatalf("err = %v, want the stale plan", err) } settle() if gens := b.started(); len(gens) != 0 { t.Fatalf("spawned %+v", gens) } all := evs.all() if len(all) != 2 { t.Fatalf("events %+v: want one per plan", all) } for _, tok := range []uint64{61, 62} { got := terminal(all, tok) if len(got) != 1 || got[0].Kind != GenCancelled || got[0].Executed { t.Fatalf("token %d: events %+v, want one GenCancelled, not executed", tok, all) } } } // A plan replaced by a replan that boots: the replaced plan is cancelled // unrun, and the replan's own events follow. func TestAReplacedPlanIsCancelledUnrun(t *testing.T) { t.Parallel() dir := t.TempDir() good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2") a, b, evs := planActivator(t, WakePlan{}) held := func(build string) error { if build == "v2" { return errors.New("v2 failed on a sibling") } return nil } var plans atomic.Int32 a.Planner = func(context.Context, registry.Route) WakePlan { if plans.Add(1) == 1 { return WakePlan{Boot: next, Mode: ModeOverlap, FirstWake: true, Fallback: &good, Check: held, Token: 71} } return WakePlan{Boot: good, Mode: ModeOverlap, Check: held, Token: 72} } mustServe(t, a) settle() if gens := b.started(); len(gens) != 1 || gens[0].Build != "v1" { t.Fatalf("spawned %+v, want the good v1 alone", gens) } old := terminal(evs.all(), 71) if len(old) != 1 || old[0].Kind != GenCancelled || old[0].Executed { t.Fatalf("replaced plan's events %+v: want one GenCancelled, not executed", evs.all()) } cur := terminal(evs.all(), 72) if len(cur) != 1 || cur[0].Kind != GenHealthy { t.Fatalf("replan's events %+v: want its healthy wake", evs.all()) } } // A planned wake whose worker token could not be minted reports no // wokenInstance at all; it still ends with its cancellation. And the flag // off (no OnGen) hears nothing, whatever the plan carries. func TestAWakeWithNoResultIsStillCancelled(t *testing.T) { t.Parallel() plan := WakePlan{Boot: stagedID(t, t.TempDir(), "v2"), Mode: ModeHold, FirstWake: true, Token: 81} a, _, evs := planActivator(t, plan) e := &actEntry{host: "one.example.com", route: testRoute("one.example.com"), plan: plan} a.afterWake(e, nil, errors.New("start: no token")) if got := terminal(evs.all(), 81); len(got) != 1 || got[0].Kind != GenCancelled || got[0].Executed { t.Fatalf("events %+v: want one GenCancelled, not executed", evs.all()) } a.OnGen = nil a.afterWake(e, nil, errors.New("start: no token")) // must not panic or emit a.afterWake(e, &wokenInstance{}, errors.New("refused")) if len(evs.all()) != 1 { t.Fatalf("events %+v after OnGen was unset", evs.all()) } }