carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "reflect"
8 "sync"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/activator"
14 "github.com/carlosframework/platform/internal/layout"
15 "github.com/carlosframework/platform/internal/registry"
16)
17
18type fakeKeyBackfiller struct {
19 got map[string]activator.InstanceKeyOutcome
20 err map[string]error
21 asked []string
22}
23
24func (f *fakeKeyBackfiller) BackfillInstanceKey(_ context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
25 f.asked = append(f.asked, rt.Host)
26 return f.got[rt.Host], f.err[rt.Host]
27}
28
29func TestBackfillInstanceKeysWritesOnlyReadyActivatorInstances(t *testing.T) {
30 ready := func(host string) registry.Route {
31 return registry.Route{Host: host, Kind: "instance", App: "shop", Status: "ready", Socket: "/run/carlos/" + host + ".sock"}
32 }
33 held := ready("moving.example.com")
34 held.Status = "moving"
35 landing := ready("landing.example.com")
36 landing.Status = "landing"
37 provisioning := ready("new.example.com")
38 provisioning.Status = "provisioning"
39 unit := ready("unit.example.com")
40 unit.Backing = registry.BackingUnit
41 alias := ready("alias.example.com")
42 alias.Platform = true
43 sidecar := ready("worker.example.com")
44 sidecar.Kind = "sidecar"
45 addr := ready("addr.example.com")
46 addr.Socket, addr.Addr = "", "127.0.0.1:8444"
47 replica := ready("shop-au.example.com")
48 replica.Replica = true
49 f := &fakeKeyBackfiller{
50 got: map[string]activator.InstanceKeyOutcome{"shop.example.com": activator.InstanceKeyMinted, "old.example.com": activator.InstanceKeyPresent, "cfg.example.com": activator.InstanceKeyFromConfig},
51 err: map[string]error{"bad.example.com": errors.New("read: permission denied")},
52 }
53 st := backfillInstanceKeys(t.Context(), []registry.Route{ready("shop.example.com"), ready("old.example.com"), ready("cfg.example.com"), ready("bad.example.com"), held, landing, provisioning, unit, alias, sidecar, addr, replica}, f)
54 if len(f.asked) != 4 {
55 t.Fatalf("asked %v, want only the four ready activator instances", f.asked)
56 }
57 if st != (instanceKeyBackfill{Minted: 1, Present: 1, FromConfig: 1, Failed: 1, Skipped: 8}) {
58 t.Fatalf("stats = %+v", st)
59 }
60}
61
62// A refusal (a fenced or conflicting row) answers InstanceKeyPresent
63// together with its error (activator.BackfillInstanceKey). The error
64// decides: the row is a failure, never "present", or the rollout check
65// would read a host whose key was never written as ready.
66func TestBackfillInstanceKeysCountsARefusalAsFailedNotPresent(t *testing.T) {
67 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Status: "ready", Socket: "/run/carlos/shop.example.com.sock"}
68 f := &fakeKeyBackfiller{
69 got: map[string]activator.InstanceKeyOutcome{rt.Host: activator.InstanceKeyPresent},
70 err: map[string]error{rt.Host: errors.New("not backfilled: layout migration in flight")},
71 }
72 st := backfillInstanceKeys(t.Context(), []registry.Route{rt}, f)
73 if st != (instanceKeyBackfill{Failed: 1}) {
74 t.Fatalf("stats = %+v, want the refusal counted as failed only", st)
75 }
76}
77
78// A host with no own env file has never been started on this box: nothing
79// is created, and the pass counts it on its own line.
80func TestBackfillInstanceKeysCountsAHostNeverStartedHere(t *testing.T) {
81 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Status: "ready", Socket: "/run/carlos/shop.example.com.sock"}
82 f := &fakeKeyBackfiller{got: map[string]activator.InstanceKeyOutcome{rt.Host: activator.InstanceKeyNoEnvFile}}
83 st := backfillInstanceKeys(t.Context(), []registry.Route{rt}, f)
84 if st != (instanceKeyBackfill{NoEnvFile: 1}) {
85 t.Fatalf("stats = %+v", st)
86 }
87}
88
89// A row whose configuration is on its way (activator.ErrInstanceKeyDeferred)
90// is counted apart from a failure: not minted by design, and found at its
91// next start. The whole pass runs under one budget, so a bucket that is
92// down costs the pass at most instanceKeyBackfillBudget, not one read's
93// timeout per row.
94func TestBackfillInstanceKeysCountsADeferredRowApartWithinOneBudget(t *testing.T) {
95 ready := func(host string) registry.Route {
96 return registry.Route{Host: host, Kind: "instance", Status: "ready", Socket: "/run/carlos/" + host + ".sock"}
97 }
98 var deadlines []time.Time
99 f := keyBackfillFunc(func(ctx context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
100 d, ok := ctx.Deadline()
101 if !ok {
102 t.Fatalf("%s: the backfill's context has no deadline", rt.Host)
103 }
104 deadlines = append(deadlines, d)
105 if rt.Host == "waiting.example.com" {
106 return activator.InstanceKeyFailed, fmt.Errorf("%w: its configuration sets CARLOS_INSTANCE_KEY", activator.ErrInstanceKeyDeferred)
107 }
108 return activator.InstanceKeyMinted, nil
109 })
110 st := backfillInstanceKeys(context.Background(), []registry.Route{ready("waiting.example.com"), ready("shop.example.com")}, f)
111 if st != (instanceKeyBackfill{Minted: 1, Deferred: 1}) {
112 t.Fatalf("stats = %+v, want one minted and one waiting", st)
113 }
114 if len(deadlines) != 2 || !deadlines[0].Equal(deadlines[1]) || time.Until(deadlines[0]) > instanceKeyBackfillBudget {
115 t.Fatalf("deadlines %v, want one budget shared by both rows", deadlines)
116 }
117}
118
119// keyBackfillFunc is a backfiller made of one function.
120type keyBackfillFunc func(context.Context, registry.Route) (activator.InstanceKeyOutcome, error)
121
122func (f keyBackfillFunc) BackfillInstanceKey(ctx context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
123 return f(ctx, rt)
124}
125
126// openSlot is a preparation slot with no fence held: it runs prepare.
127func openSlot(_ string, prepare func() (string, error)) (string, error) { return prepare() }
128
129// routeOf answers the registry's Get from a fixed list.
130func routeOf(routes ...registry.Route) func(string) (*registry.Route, error) {
131 return func(host string) (*registry.Route, error) {
132 for _, rt := range routes {
133 if rt.Host == host {
134 return &rt, nil
135 }
136 }
137 return nil, nil
138 }
139}
140
141// Task 6 ruling (K1), as the v0.26.1 hotfix changed it: the start sequence
142// is activatorStart's, so the two guards runAgent cannot be tested for are
143// pinned here. A claim box starts and nothing else: no sweep, no recovery,
144// no backfill (ruling 7). A legacy box sweeps, recovers, publishes the
145// pass and starts, in that order, and the backfill runs after Start, once
146// every journal is settled. A route list that fails skips the backfill
147// only.
148func TestActivatorStartBackfillsOnlyALegacyBoxAfterRecoveryAndStart(t *testing.T) {
149 ready := registry.Route{Host: "shop.example.com", Kind: "instance", Status: "ready", Socket: "/run/carlos/shop.example.com.sock"}
150 for _, tc := range []struct {
151 name string
152 claims bool
153 listErr error
154 want []string
155 }{
156 {"legacy box", false, nil, []string{"configure", "sweep", "recover", "publish", "start", "ready", "backfill"}},
157 {"legacy box, routes unreadable", false, errors.New("db locked"), []string{"configure", "sweep", "recover", "publish", "start", "ready"}},
158 {"claim box", true, nil, []string{"configure", "start", "ready"}},
159 } {
160 t.Run(tc.name, func(t *testing.T) {
161 var mu sync.Mutex
162 var events []string
163 note := func(e string) { mu.Lock(); events = append(events, e); mu.Unlock() }
164 done := activatorStart{
165 Configure: func(context.Context) { note("configure") },
166 Ready: func() { note("ready") },
167 Claims: tc.claims,
168 Sweep: func(context.Context) error { note("sweep"); return nil },
169 Recover: func(context.Context, bool) *layout.Pass { note("recover"); return nil },
170 Routes: func() ([]registry.Route, error) { return []registry.Route{ready}, tc.listErr },
171 Route: routeOf(ready),
172 Keys: keyBackfillFunc(func(context.Context, registry.Route) (activator.InstanceKeyOutcome, error) {
173 note("backfill")
174 return activator.InstanceKeyMinted, nil
175 }),
176 Slot: openSlot,
177 Publish: func(*layout.Pass) { note("publish") },
178 Start: func() { note("start") },
179 }.Run(context.Background())
180 if tc.claims {
181 select {
182 case <-done:
183 default:
184 t.Fatal("claim-box startup became asynchronous")
185 }
186 }
187 select {
188 case <-done:
189 case <-time.After(10 * time.Second):
190 t.Fatal("the backfill never finished")
191 }
192 mu.Lock()
193 defer mu.Unlock()
194 if !reflect.DeepEqual(events, tc.want) {
195 t.Fatalf("events = %v, want %v", events, tc.want)
196 }
197 })
198 }
199}
200
201// fenceCtx is a context with the deadline FenceAndStop requires.
202func fenceCtx(t *testing.T, d time.Duration) context.Context {
203 ctx, cancel := context.WithTimeout(t.Context(), d)
204 t.Cleanup(cancel)
205 return ctx
206}
207
208// Off the startup path, the backfill shares the box with the layout pass
209// and moves, which fence a host before they move its files. A host that
210// is fenced when its turn comes is skipped and nothing is asked of it, and
211// a row whose status changed after the list (it began a move) is skipped
212// the same way; its next start mints the key. The rest are written from
213// the row as it stands, not as it was listed.
214func TestBackfillInSlotSkipsAFencedHostAndReadsTheRowAgain(t *testing.T) {
215 ready := func(host string) registry.Route {
216 return registry.Route{Host: host, Kind: "instance", Status: "ready", Socket: "/run/carlos/" + host + ".sock"}
217 }
218 fenced, moved, shop := ready("fenced.example.com"), ready("moved.example.com"), ready("shop.example.com")
219 svc := &activator.Service{}
220 if _, err := svc.FenceAndStop(fenceCtx(t, time.Second), fenced.Host, 1); err != nil && !errors.Is(err, activator.ErrStopUnproven) {
221 t.Fatalf("fence: %v", err)
222 }
223 nowMoving := moved
224 nowMoving.Status = "moving"
225 nowShop := shop
226 nowShop.Socket = "/run/carlos/nested/shop.example.com.sock"
227 f := &fakeKeyBackfiller{got: map[string]activator.InstanceKeyOutcome{shop.Host: activator.InstanceKeyMinted}}
228 var sockets []string
229 g := keyBackfillInSlot{
230 keys: keyBackfillFunc(func(ctx context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
231 sockets = append(sockets, rt.Socket)
232 return f.BackfillInstanceKey(ctx, rt)
233 }),
234 slot: svc.PrepareScheduledToken,
235 route: routeOf(fenced, nowMoving, nowShop),
236 }
237 st := backfillInstanceKeys(t.Context(), []registry.Route{fenced, moved, shop}, g)
238 if !reflect.DeepEqual(f.asked, []string{shop.Host}) {
239 t.Fatalf("asked %v, want only %s", f.asked, shop.Host)
240 }
241 if !reflect.DeepEqual(sockets, []string{nowShop.Socket}) {
242 t.Fatalf("written from %v, want the row as it stands now", sockets)
243 }
244 if st != (instanceKeyBackfill{Minted: 1, Skipped: 2}) {
245 t.Fatalf("stats = %+v, want one minted and two skipped", st)
246 }
247}
248
249// The other half of "nothing moves a file under it": a fence asked for
250// while a row is being written (the layout pass or a move about to move
251// the host's files) is installed only once the write has returned.
252func TestBackfillInSlotHoldsOffAFenceUntilTheWriteReturns(t *testing.T) {
253 t.Parallel()
254 rt := registry.Route{Host: "shop.example.com", Kind: "instance", Status: "ready", Socket: "/run/carlos/shop.example.com.sock"}
255 svc := &activator.Service{}
256 writing, release := make(chan struct{}), make(chan struct{})
257 var wrote atomic.Bool
258 g := keyBackfillInSlot{
259 keys: keyBackfillFunc(func(context.Context, registry.Route) (activator.InstanceKeyOutcome, error) {
260 close(writing)
261 <-release
262 wrote.Store(true)
263 return activator.InstanceKeyMinted, nil
264 }),
265 slot: svc.PrepareScheduledToken,
266 route: routeOf(rt),
267 }
268 done := make(chan instanceKeyBackfill)
269 go func() { done <- backfillInstanceKeys(t.Context(), []registry.Route{rt}, g) }()
270 <-writing
271 fenced := make(chan bool)
272 go func() {
273 _, err := svc.FenceAndStopIfUnfenced(fenceCtx(t, 10*time.Second), rt.Host, 1)
274 if err != nil && !errors.Is(err, activator.ErrStopUnproven) {
275 t.Errorf("fence: %v", err)
276 }
277 fenced <- wrote.Load()
278 }()
279 select {
280 case <-fenced:
281 t.Fatal("the fence was installed while the backfill was writing the host")
282 case <-time.After(200 * time.Millisecond):
283 }
284 close(release)
285 if !<-fenced {
286 t.Fatal("the fence was installed before the write returned")
287 }
288 if st := <-done; st != (instanceKeyBackfill{Minted: 1}) {
289 t.Fatalf("stats = %+v", st)
290 }
291}
292
293// v0.26.1 hotfix: the v0.26.0 roll to a 127-instance box failed its update
294// gate because the backfill ran serially before Start, one bucket read per
295// keyless row, and the edge never listened inside the gate's 20 seconds.
296// On a box with hundreds of keyless rows and a slow bucket, Start is
297// called, and Run returns (runAgent goes on to the agent socket and the
298// edge), without waiting for the backfill.
299func TestActivatorStartDoesNotWaitForTheBackfillOnALargeBox(t *testing.T) {
300 const rows, perRow, bound = 300, 30 * time.Millisecond, 2 * time.Second
301 var routes []registry.Route
302 for i := range rows {
303 host := fmt.Sprintf("shop%03d.example.com", i)
304 routes = append(routes, registry.Route{Host: host, Kind: "instance", Status: "ready", Socket: "/run/carlos/" + host + ".sock"})
305 }
306 ctx, cancel := context.WithCancel(context.Background())
307 defer cancel()
308 slowBucket := keyBackfillFunc(func(ctx context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
309 select {
310 case <-time.After(perRow):
311 return activator.InstanceKeyMinted, nil
312 case <-ctx.Done():
313 return activator.InstanceKeyFailed, fmt.Errorf("%w: %w", activator.ErrInstanceKeyDeferred, ctx.Err())
314 }
315 })
316 began := time.Now()
317 started := make(chan time.Duration, 1)
318 done := activatorStart{
319 Sweep: func(context.Context) error { return nil },
320 Recover: func(context.Context, bool) *layout.Pass { return nil },
321 Routes: func() ([]registry.Route, error) { return routes, nil },
322 Route: routeOf(routes...),
323 Keys: slowBucket,
324 Slot: (&activator.Service{}).PrepareScheduledToken,
325 Publish: func(*layout.Pass) {},
326 Start: func() { started <- time.Since(began) },
327 }.Run(ctx)
328 returned := time.Since(began)
329 select {
330 case startedAfter := <-started:
331 if startedAfter > bound {
332 t.Errorf("Start took %v, want within %v", startedAfter, bound)
333 }
334 case <-time.After(bound):
335 t.Fatal("Start waited for the backfill")
336 }
337 if returned > bound {
338 t.Errorf("Run returned after %v, want within %v: the edge waits for Run", returned, bound)
339 }
340 select {
341 case <-done:
342 t.Fatal("the backfill had finished when Run returned; it should still be running")
343 default:
344 }
345 // Agent shutdown is not this test's: cancel the pass and let it end.
346 cancel()
347 select {
348 case <-done:
349 case <-time.After(30 * time.Second):
350 t.Fatal("the backfill did not end once its context was cancelled")
351 }
352}
353