carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/registry"
11)
12
13// Task 5, plan review #1: the scheduler ends a planned first wake's
14// write-ahead intent only on that wake's own terminal event, never on the
15// activator letting the host go (a failed wake's entry goes before its
16// outcome is delivered). So every planned wake that carries a token ends
17// with exactly one token-carrying terminal event. The exits that sent none
18// before now send GenCancelled: not Executed when the plan's build never
19// ran (refused at the lease, at its check, replaced by a replan), Executed
20// when it ran and its lifecycle ended it with no verdict. None of them is an
21// outcome the fence must cover: they record nothing, so a new wake may plan
22// before they arrive.
23
24// terminal reports the token's events that settle its build's fate (the
25// scheduler's End): every kind but a first wake's healthy.
26func terminal(evs []GenEvent, tok uint64) []GenEvent {
27 var out []GenEvent
28 for _, ev := range evs {
29 if ev.Token == tok && (ev.Kind != GenHealthy || !ev.FirstWake) {
30 out = append(out, ev)
31 }
32 }
33 return out
34}
35
36// settle waits until the activator has gone quiet: events arrive on the
37// wake's goroutine after the request returns.
38func settle() { time.Sleep(100 * time.Millisecond) }
39
40func TestAPlannedWakeRefusedAtItsLeaseIsCancelledUnrun(t *testing.T) {
41 dir := t.TempDir()
42 good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2")
43 a, b, evs := planActivator(t, WakePlan{Boot: next, Mode: ModeOverlap, FirstWake: true, Fallback: &good, Token: 51, ConfigRev: "c1"})
44 var fenced atomic.Bool
45 a.OnGen = func(ev GenEvent) {
46 a.mu.Lock()
47 _, held := a.outcomeFences["one.example.com"]
48 a.mu.Unlock()
49 fenced.Store(fenced.Load() || held)
50 evs.add(ev)
51 }
52 b.mu.Lock()
53 b.acquireErr = errors.New("lease held by a previous incarnation")
54 b.mu.Unlock()
55 if _, err := a.beginRequest(context.Background(), testRoute("one.example.com")); err == nil {
56 t.Fatal("the wake was served without its lease")
57 }
58 settle()
59 got := terminal(evs.all(), 51)
60 if len(got) != 1 || got[0].Kind != GenCancelled || got[0].Executed || got[0].ConfigRev != "c1" {
61 t.Fatalf("events %+v: want one GenCancelled, not executed, with the plan's token", evs.all())
62 }
63 if len(evs.all()) != 1 {
64 t.Fatalf("events %+v: want the one", evs.all())
65 }
66 if fenced.Load() {
67 t.Fatal("an event that records nothing was delivered under the outcome fence")
68 }
69}
70
71// A plan refused by its check before the exec, whose replan's check refuses
72// too: both plans are cancelled unrun, each once, by its own token.
73func TestARefusedReplanCancelsBothPlans(t *testing.T) {
74 t.Parallel()
75 dir := t.TempDir()
76 good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2")
77 a, b, evs := planActivator(t, WakePlan{})
78 refuse := func(string) error { return errors.New("held") }
79 var plans atomic.Uint64
80 a.Planner = func(context.Context, registry.Route) WakePlan {
81 return WakePlan{Boot: next, Mode: ModeHold, FirstWake: true, Fallback: &good, Check: refuse, Token: 60 + plans.Add(1)}
82 }
83 if _, err := a.beginRequest(context.Background(), testRoute("one.example.com")); !errors.Is(err, errPlanStale) {
84 t.Fatalf("err = %v, want the stale plan", err)
85 }
86 settle()
87 if gens := b.started(); len(gens) != 0 {
88 t.Fatalf("spawned %+v", gens)
89 }
90 all := evs.all()
91 if len(all) != 2 {
92 t.Fatalf("events %+v: want one per plan", all)
93 }
94 for _, tok := range []uint64{61, 62} {
95 got := terminal(all, tok)
96 if len(got) != 1 || got[0].Kind != GenCancelled || got[0].Executed {
97 t.Fatalf("token %d: events %+v, want one GenCancelled, not executed", tok, all)
98 }
99 }
100}
101
102// A plan replaced by a replan that boots: the replaced plan is cancelled
103// unrun, and the replan's own events follow.
104func TestAReplacedPlanIsCancelledUnrun(t *testing.T) {
105 t.Parallel()
106 dir := t.TempDir()
107 good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2")
108 a, b, evs := planActivator(t, WakePlan{})
109 held := func(build string) error {
110 if build == "v2" {
111 return errors.New("v2 failed on a sibling")
112 }
113 return nil
114 }
115 var plans atomic.Int32
116 a.Planner = func(context.Context, registry.Route) WakePlan {
117 if plans.Add(1) == 1 {
118 return WakePlan{Boot: next, Mode: ModeOverlap, FirstWake: true, Fallback: &good, Check: held, Token: 71}
119 }
120 return WakePlan{Boot: good, Mode: ModeOverlap, Check: held, Token: 72}
121 }
122 mustServe(t, a)
123 settle()
124 if gens := b.started(); len(gens) != 1 || gens[0].Build != "v1" {
125 t.Fatalf("spawned %+v, want the good v1 alone", gens)
126 }
127 old := terminal(evs.all(), 71)
128 if len(old) != 1 || old[0].Kind != GenCancelled || old[0].Executed {
129 t.Fatalf("replaced plan's events %+v: want one GenCancelled, not executed", evs.all())
130 }
131 cur := terminal(evs.all(), 72)
132 if len(cur) != 1 || cur[0].Kind != GenHealthy {
133 t.Fatalf("replan's events %+v: want its healthy wake", evs.all())
134 }
135}
136
137// A planned wake whose worker token could not be minted reports no
138// wokenInstance at all; it still ends with its cancellation. And the flag
139// off (no OnGen) hears nothing, whatever the plan carries.
140func TestAWakeWithNoResultIsStillCancelled(t *testing.T) {
141 t.Parallel()
142 plan := WakePlan{Boot: stagedID(t, t.TempDir(), "v2"), Mode: ModeHold, FirstWake: true, Token: 81}
143 a, _, evs := planActivator(t, plan)
144 e := &actEntry{host: "one.example.com", route: testRoute("one.example.com"), plan: plan}
145 a.afterWake(e, nil, errors.New("start: no token"))
146 if got := terminal(evs.all(), 81); len(got) != 1 || got[0].Kind != GenCancelled || got[0].Executed {
147 t.Fatalf("events %+v: want one GenCancelled, not executed", evs.all())
148 }
149 a.OnGen = nil
150 a.afterWake(e, nil, errors.New("start: no token")) // must not panic or emit
151 a.afterWake(e, &wokenInstance{}, errors.New("refused"))
152 if len(evs.all()) != 1 {
153 t.Fatalf("events %+v after OnGen was unset", evs.all())
154 }
155}
156