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 "slices"
7 "strings"
8 "sync"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/registry"
14)
15
16// blockedOnGen is an OnGen that blocks on events of kind until release
17// closes, as a scheduler whose failure transition is slow would, and marks
18// recorded once that event's handling has returned. Only the first such
19// event is the one a test waits on.
20type blockedOnGen struct {
21 kind GenEventKind
22 once sync.Once
23 entered chan struct{}
24 release chan struct{}
25 recorded atomic.Bool
26 evs genLog
27}
28
29func newBlockedOnGen(kind GenEventKind) *blockedOnGen {
30 return &blockedOnGen{kind: kind, entered: make(chan struct{}), release: make(chan struct{})}
31}
32
33func (o *blockedOnGen) on(ev GenEvent) {
34 o.evs.add(ev)
35 if ev.Kind != o.kind {
36 return
37 }
38 o.once.Do(func() { close(o.entered) })
39 <-o.release
40 o.recorded.Store(true)
41}
42
43func (o *blockedOnGen) waitEntered(t *testing.T) {
44 t.Helper()
45 select {
46 case <-o.entered:
47 case <-time.After(5 * time.Second):
48 t.Fatalf("OnGen never received %s; saw %v", o.kind, o.evs.kinds())
49 }
50}
51
52// secondPlanner answers the first plan with first, and records, for every
53// later plan, whether the blocked outcome had been recorded when it was
54// asked. A later plan refuses once it has, as the scheduler's planner does
55// for a held-down build.
56func secondPlanner(o *blockedOnGen, first WakePlan, saw chan<- bool) func(context.Context, registry.Route) WakePlan {
57 var n atomic.Int32
58 return func(context.Context, registry.Route) WakePlan {
59 if n.Add(1) == 1 {
60 return first
61 }
62 rec := o.recorded.Load()
63 saw <- rec
64 if rec {
65 return WakePlan{Refuse: "held down"}
66 }
67 return first
68 }
69}
70
71// Final review F3 (spec §8.1, O7): a failed first wake's outcome is
72// delivered before admission reopens. A request that arrives after the
73// failed wake's entry is gone, while OnGen is still recording the failure,
74// starts a wake that waits for that delivery and only then plans, with the
75// record in place. Before the fence the second wake planned B again in the
76// gap, before the hold-down existed.
77func TestANewWakeWaitsForAFailedFirstWakesOutcome(t *testing.T) {
78 t.Parallel()
79 dir := t.TempDir()
80 next := stagedID(t, dir, "v2")
81 a, b, _ := planActivator(t, WakePlan{})
82 const host = "one.example.com"
83 a.genHealth = func(context.Context, Gen) error { return errors.New("GET / answered 500") }
84 o := newBlockedOnGen(GenFailed)
85 a.OnGen = o.on
86 saw := make(chan bool, 4)
87 a.Planner = secondPlanner(o, WakePlan{Boot: next, Mode: ModeHold, FirstWake: true}, saw)
88 if _, err := a.beginRequest(context.Background(), testRoute(host)); err == nil {
89 t.Fatal("the failed hold first wake served")
90 }
91 o.waitEntered(t)
92 if a.Owns(host) {
93 t.Fatal("fixture: the failed wake's entry is still there")
94 }
95 done := make(chan error, 1)
96 go func() {
97 _, err := a.beginRequest(context.Background(), testRoute(host))
98 done <- err
99 }()
100 select {
101 case rec := <-saw:
102 close(o.release)
103 t.Fatalf("the second wake planned while the first wake's failure was still being delivered (record in place: %v)", rec)
104 case <-time.After(300 * time.Millisecond):
105 }
106 close(o.release)
107 select {
108 case rec := <-saw:
109 if !rec {
110 t.Fatal("the second wake planned before the failure was recorded")
111 }
112 case <-time.After(5 * time.Second):
113 t.Fatal("the second wake never planned")
114 }
115 if err := <-done; err == nil || !strings.Contains(err.Error(), "held down") {
116 t.Fatalf("second wake = %v, want refused by the recorded hold-down", err)
117 }
118 if gens := b.started(); len(gens) != 1 {
119 t.Fatalf("spawned %+v: B ran again before its failure was recorded", gens)
120 }
121}
122
123// Final review wave 2 (N1): the fence fails closed. The wait is bounded
124// (outcomeFenceWait), the restriction is not. A wake still behind an
125// undelivered outcome when the bound passes is refused before it plans:
126// no Planner call, nothing launched, and the member reads the maintenance
127// fence's existing line. The fence stays, so the next request is refused
128// the same way. Once the outcome is delivered a wake proceeds to plan
129// (here the planner, with the record in place, refuses the held build).
130// Before the fix the wake planned after the bound and could boot B again
131// without its record.
132func TestTheOutcomeFenceFailsClosed(t *testing.T) {
133 old := outcomeFenceWait
134 outcomeFenceWait = 200 * time.Millisecond
135 t.Cleanup(func() { outcomeFenceWait = old })
136 dir := t.TempDir()
137 next := stagedID(t, dir, "v2")
138 a, b, _ := planActivator(t, WakePlan{})
139 const host = "one.example.com"
140 a.genHealth = func(context.Context, Gen) error { return errors.New("GET / answered 500") }
141 o := newBlockedOnGen(GenFailed)
142 var releaseOnce sync.Once
143 release := func() { releaseOnce.Do(func() { close(o.release) }) }
144 t.Cleanup(release)
145 a.OnGen = o.on
146 sink := &lockedSink{}
147 a.Logs = sink
148 saw := make(chan bool, 4)
149 a.Planner = secondPlanner(o, WakePlan{Boot: next, Mode: ModeHold, FirstWake: true}, saw)
150 if _, err := a.beginRequest(context.Background(), testRoute(host)); err == nil {
151 t.Fatal("the failed hold first wake served")
152 }
153 o.waitEntered(t)
154 for i := 1; i <= 2; i++ {
155 start := time.Now()
156 _, err := a.beginRequest(context.Background(), testRoute(host))
157 if !errors.Is(err, errOutcomeUndelivered) {
158 t.Fatalf("wake %d behind the undelivered outcome = %v, want refused (errOutcomeUndelivered)", i, err)
159 }
160 if took := time.Since(start); took < 150*time.Millisecond {
161 t.Fatalf("wake %d refused after %v: it did not wait for the fence", i, took)
162 }
163 select {
164 case rec := <-saw:
165 t.Fatalf("wake %d planned behind the undelivered outcome (record in place: %v)", i, rec)
166 default:
167 }
168 if gens := b.started(); len(gens) != 1 {
169 t.Fatalf("spawned %+v: a wake behind the undelivered outcome launched something", gens)
170 }
171 }
172 want := memberSafeWakeReason(ErrFenced)
173 got := sink.errors()
174 if len(got) != 3 || got[1] != want || got[2] != want {
175 t.Fatalf("member error events %q: want the failed wake's, then the maintenance fence's line (%q) for each refusal", got, want)
176 }
177 release()
178 _, err := a.beginRequest(context.Background(), testRoute(host))
179 select {
180 case rec := <-saw:
181 if !rec {
182 t.Fatal("the wake after the delivery planned before the failure was recorded")
183 }
184 case <-time.After(5 * time.Second):
185 t.Fatal("the wake after the delivery never planned: the fence outlived the outcome")
186 }
187 if err == nil || !strings.Contains(err.Error(), "held down") {
188 t.Fatalf("wake after the delivery = %v, want refused by the recorded hold-down", err)
189 }
190 if gens := b.started(); len(gens) != 1 {
191 t.Fatalf("spawned %+v: B ran again", gens)
192 }
193}
194
195// Final review F3, the observer's half: a first wake's 10 s observation
196// delivers its terminal event (here GenFailed, an exit inside the window)
197// BEFORE it clears firstWatch, and holds the host's outcome fence until
198// the delivery returns. While OnGen records the failure, Current still
199// hides the generation, and a request that stops the dead entry and wakes
200// it again waits for the record before planning. Before the fix the
201// observer cleared firstWatch first and nothing held the next wake back.
202func TestAFirstWakeObserverDeliversItsOutcomeBeforeAdmissionReopens(t *testing.T) {
203 shrinkFirstWake(t, 55*time.Second, 25*time.Second, 2*time.Second)
204 dir := t.TempDir()
205 good, next := stagedID(t, dir, "v1"), stagedID(t, dir, "v2")
206 a, b, _ := planActivator(t, WakePlan{})
207 const host = "one.example.com"
208 o := newBlockedOnGen(GenFailed)
209 a.OnGen = o.on
210 saw := make(chan bool, 4)
211 a.Planner = secondPlanner(o, WakePlan{Boot: next, Mode: ModeOverlap, FirstWake: true, Fallback: &good}, saw)
212 done, err := a.beginRequest(context.Background(), testRoute(host))
213 if err != nil {
214 t.Fatal(err)
215 }
216 done()
217 o.evs.wait(t, GenHealthy)
218 b.procOf(b.started()[0].N).crash()
219 o.waitEntered(t)
220 if _, ok := a.Current(host); ok {
221 close(o.release)
222 t.Fatal("Current showed the first wake while its failure was still being delivered: firstWatch was cleared first")
223 }
224 second := make(chan error, 1)
225 go func() {
226 _, err := a.beginRequest(context.Background(), testRoute(host))
227 second <- err
228 }()
229 select {
230 case rec := <-saw:
231 close(o.release)
232 t.Fatalf("the next wake planned while the observer's failure was still being delivered (record in place: %v)", rec)
233 case <-time.After(300 * time.Millisecond):
234 }
235 close(o.release)
236 select {
237 case rec := <-saw:
238 if !rec {
239 t.Fatal("the next wake planned before the failure was recorded")
240 }
241 case <-time.After(5 * time.Second):
242 t.Fatal("the next wake never planned")
243 }
244 if err := <-second; err == nil || !strings.Contains(err.Error(), "held down") {
245 t.Fatalf("next wake = %v, want refused by the recorded failure", err)
246 }
247 if k := o.evs.kinds(); !slices.Equal(k, []GenEventKind{GenHealthy, GenFailed}) {
248 t.Fatalf("events %v, want healthy then failed", k)
249 }
250}
251