carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5// The large-box startup, end to end (amadan #281, ask 2). On 2026-10-13 a
6// box with 142 instances failed the 20 s update gate twice: the updater
7// restarts carlos-instance-control just before the new edge, and the
8// agent's startup sweep waited on that cold control service before the
9// edge listened. The hotfix (6dfbff1) moved the sweep, layout recovery,
10// hold restoration, Start and the key pass off the listen path, behind a
11// startup latch that instance admission waits on.
12//
13// TestStartupServesWhileSweepAndRecoveryWait proves the latch with the
14// steps stubbed. This test runs them for real on a box larger than the
15// one that failed:
16//
17// - the agent is a child process (this test binary, re-executed), so
18// what is timed is a process start up to the update gate's own probe;
19// - instance control is the real instancectl.Server over a unix socket,
20// made cold by its systemctl seam: every call waits until the test
21// warms it, then each of the box's 300 leftover units takes a moment
22// to stop, as a restarted control service does;
23// - the sweep is the real client call, layout recovery and hold
24// restoration are the real pass over real journals, hold markers and
25// files in a temp data dir, Start is the real activator's, and the
26// edge is edge.RunWithActivatorReady in its production (TLS) mode;
27// - every kind of admission the latch gates arrives during startup: an
28// instance request, a sidecar-marked request, a worker wake (from
29// runAgent's boot pass and from a direct call), and a scheduled
30// delivery through the real schedule runner and deliverer;
31// - the activator's backend is a fake that serves an app on the row's
32// socket and starts workers that report ready. It and the key pass's
33// writer are the only fakes in the agent, and neither is a startup
34// step.
35//
36// The child's startup is runAgent's legacy-box sequence: the same
37// activatorStart, with runAgent's Sweep and Recover closures. runAgent
38// itself cannot run in a test (its control socket path is a constant,
39// it builds AWS clients, and it log.Fatals), so the child assembles the
40// same pieces in the same order, and TestRunAgentStartupWiring reads
41// runAgent's source to fail if the two drift apart. Sharing one
42// orchestration function between them is a follow-up (#281). Two box
43// facts differ from production and are set on the pass: its socket
44// directory is the fixture's, not /run/carlos, and it has no
45// carlos-sockets group.
46//
47// The child reports what it does as timestamped lines in an events file;
48// the test reads them to order "a request reached the activator", "the
49// latch opened" and "the backend acquired a host" across the two
50// processes.
51
52import (
53 "bufio"
54 "context"
55 "crypto/ecdsa"
56 "crypto/elliptic"
57 "crypto/rand"
58 "crypto/tls"
59 "crypto/x509"
60 "crypto/x509/pkix"
61 "encoding/json"
62 "encoding/pem"
63 "errors"
64 "fmt"
65 "go/ast"
66 "go/parser"
67 "go/token"
68 "go/types"
69 "io"
70 "log"
71 "math/big"
72 "net"
73 "net/http"
74 "os"
75 "os/exec"
76 "path/filepath"
77 "runtime"
78 "strconv"
79 "strings"
80 "sync"
81 "sync/atomic"
82 "syscall"
83 "testing"
84 "time"
85
86 "github.com/carlosframework/platform/internal/activator"
87 "github.com/carlosframework/platform/internal/edge"
88 "github.com/carlosframework/platform/internal/instancectl"
89 "github.com/carlosframework/platform/internal/instancefs"
90 "github.com/carlosframework/platform/internal/layout"
91 "github.com/carlosframework/platform/internal/provision"
92 "github.com/carlosframework/platform/internal/registry"
93 "github.com/carlosframework/platform/internal/release"
94 "github.com/carlosframework/platform/internal/schedule"
95 "github.com/carlosframework/platform/internal/version"
96)
97
98const (
99 // largeBoxAgentEnv names the child's config file. Set, the test binary
100 // is the agent and nothing else.
101 largeBoxAgentEnv = "CARLOS_TEST_LARGEBOX_AGENT"
102
103 largeBoxRoutes = 300 // instance routes; the box that failed had 142
104 largeBoxJournals = 8 // hosts 0..7: a layout move was in flight at the restart
105 largeBoxHolds = 4 // hosts 8..11: held down after a refused move
106 largeBoxWakeHost = 12 // an ordinary sleeping instance, woken by a request
107 largeBoxDueHost = 13 // a sleeping instance with a scheduled delivery due
108 largeBoxSidecarHost = 14 // a sleeping instance its sidecar calls
109 largeBoxWorker = "worker-000.example.com"
110 largeBoxDuePath = "/jobs/tick"
111
112 // largeBoxListenBound is how soon the gate's probe must be answered
113 // after the agent process starts. The gate allows 20 s; the edge's own
114 // startup on this box takes a fraction of a second.
115 largeBoxListenBound = 3 * time.Second
116 // largeBoxCold is how long instance control stays cold in the scenario
117 // where it warms: longer than the listen bound, so the gate is answered
118 // while the sweep is provably still waiting.
119 largeBoxCold = 4 * time.Second
120 // largeBoxUnitStop is one leftover unit's stop once control is warm.
121 largeBoxUnitStop = 5 * time.Millisecond
122 // largeBoxLatchBound is the activator's wakeWaitBound, which the
123 // startup latch shares: a wake held longer is answered 503.
124 largeBoxLatchBound = 30 * time.Second
125 // largeBoxJoinBound is runAgent's join on the startup work after the
126 // edge returns, and largeBoxDrain the edge's drain of in-flight
127 // requests before it returns.
128 largeBoxJoinBound = 5 * time.Second
129 largeBoxDrain = 10 * time.Second
130 // largeBoxBindAttempts is how often the agent is started on fresh ports
131 // when one it was handed is taken before it binds (see startAgent).
132 largeBoxBindAttempts = 3
133)
134
135func largeBoxHost(i int) string { return fmt.Sprintf("app-%03d.example.com", i) }
136
137// largeBoxConfig is what the child agent is told.
138type largeBoxConfig struct {
139 DataDir, SocketDir, StoreDir, AcmeDir string
140 Control, Events, HTTP, HTTPS string
141}
142
143func TestStartupLargeBoxEndToEnd(t *testing.T) {
144 if path := os.Getenv(largeBoxAgentEnv); path != "" {
145 os.Exit(runLargeBoxAgent(path))
146 }
147 // The parent stays serial on purpose. awaitListening holds each child
148 // agent to a 3 s startup budget, so running beside the package's
149 // parallel tests would put their load into that measurement. The
150 // scenarios still run beside each other.
151 t.Run("cold instance control", func(t *testing.T) {
152 t.Parallel()
153 testLargeBoxColdControl(t)
154 })
155 t.Run("startup overruns the latch", func(t *testing.T) {
156 t.Parallel()
157 testLargeBoxOverrun(t)
158 })
159 t.Run("shutdown mid-startup", func(t *testing.T) {
160 t.Parallel()
161 testLargeBoxShutdownMidStartup(t)
162 })
163}
164
165// testLargeBoxColdControl is the 2026-10-13 shape: control restarted just
166// before the agent, and slow to answer. The edge must answer the gate's
167// probe while the sweep waits, hold every admission at the latch until
168// sweep, recovery and hold restoration are done, and then serve each host
169// only where it is safe to.
170func testLargeBoxColdControl(t *testing.T) {
171 b := newLargeBox(t)
172 a := b.startAgent(t)
173 t.Logf("gate probe answered %s after the agent started, %d routes, instance control cold", a.listen, largeBoxRoutes)
174 if _, ok := a.event("ready"); ok {
175 t.Fatal("the startup latch opened while instance control was still cold")
176 }
177
178 wake, held, moved, due, sidecar := largeBoxHost(largeBoxWakeHost), largeBoxHost(largeBoxJournals), largeBoxHost(0), largeBoxHost(largeBoxDueHost), largeBoxHost(largeBoxSidecarHost)
179 flatMoved := filepath.Join(b.socketDir, moved+".sock")
180 wakeAns, heldAns, movedAns := b.fire(wake, false), b.fire(held, false), b.fire(moved, false)
181 sidecarAns := b.fire(sidecar, true)
182 enterWake := a.waitEvent(t, 10*time.Second, "enter", wake)
183 a.waitEvent(t, 10*time.Second, "enter", held)
184 enterSidecar := a.waitEvent(t, 10*time.Second, "enter", sidecar)
185 enterMoved := a.waitEvent(t, 10*time.Second, "enter", moved)
186 if enterMoved.arg(1) != flatMoved {
187 t.Fatalf("the edge routed %s to %q before recovery, want its pre-move socket %q", moved, enterMoved.arg(1), flatMoved)
188 }
189 // The agent's own admissions: a direct worker wake and the scheduled
190 // tick, both started before the edge listened.
191 workerAsked := a.waitEvent(t, 10*time.Second, "worker-direct-begin")
192 ticked := a.waitEvent(t, 10*time.Second, "tick-done")
193 // Every admission is now waiting, and startup is not done.
194 time.Sleep(200 * time.Millisecond)
195 for host, ch := range map[string]chan largeBoxAnswer{wake: wakeAns, held: heldAns, moved: movedAns, sidecar: sidecarAns} {
196 select {
197 case got := <-ch:
198 t.Fatalf("%s answered before startup finished: %d %q %v", host, got.status, got.body, got.err)
199 default:
200 }
201 }
202 for _, kind := range []string{"acquire", "start", "worker-start", "served", "worker-direct", "worker-boot", "activator-start"} {
203 if evs := a.events(kind); len(evs) != 0 {
204 t.Fatalf("%s while instance control was cold: %v", kind, evs)
205 }
206 }
207
208 // Instance control warms: the sweep stops the 300 leftovers, then
209 // recovery, holds, Start, and the latch.
210 if d := time.Until(a.began.Add(largeBoxCold)); d > 0 {
211 time.Sleep(d)
212 }
213 close(b.control.warm)
214 ready := a.waitEvent(t, 30*time.Second, "ready")
215 t.Logf("startup latch opened %s after the agent started (sweep stopped %d units)", ready.at.Sub(a.began), b.control.stops.Load())
216 if got := b.control.stops.Load(); got != largeBoxRoutes {
217 t.Errorf("the sweep stopped %d units, want %d", got, largeBoxRoutes)
218 }
219 for what, at := range map[string]time.Time{"wake": enterWake.at, "sidecar request": enterSidecar.at, "worker wake": workerAsked.at, "scheduled tick": ticked.at} {
220 if !at.Before(ready.at) {
221 t.Fatalf("the %s did not arrive before the latch opened; the test proves nothing about its latch", what)
222 }
223 }
224
225 // Start ran only once the held hosts were fenced again and every
226 // journal was complete.
227 st := a.waitEvent(t, time.Second, "activator-start")
228 if st.arg(0) != "held="+strconv.Itoa(largeBoxHolds) || st.arg(1) != "fenced="+strconv.Itoa(largeBoxHolds) || st.arg(2) != "journals=0" || st.at.After(ready.at) {
229 t.Fatalf("at Start: %v", st)
230 }
231
232 // The wake was held, then served.
233 got := b.answer(t, wakeAns)
234 if got.status != http.StatusOK || got.body != "host="+wake+" socket="+filepath.Join(b.socketDir, wake+".sock") {
235 t.Fatalf("held wake after startup: %d %q %v", got.status, got.body, got.err)
236 }
237 a.assertAfter(t, ready, "acquire", wake)
238
239 // The sidecar's call was held too, then answered as the activator
240 // answers a sidecar whose instance sleeps: it does not wake it.
241 got = b.answer(t, sidecarAns)
242 if got.status != http.StatusServiceUnavailable || got.body != "instance is asleep; sidecar requests do not wake it\n" || got.at.Before(ready.at) {
243 t.Fatalf("sidecar request held through startup: %d %q %v at %s, latch at %s", got.status, got.body, got.err, got.at, ready.at)
244 }
245
246 // The worker: both wakes waited, then coalesced on one child.
247 a.assertAfter(t, ready, "worker-direct", largeBoxWorker, "true")
248 a.assertAfter(t, ready, "worker-boot", largeBoxWorker, "true")
249 a.assertAfter(t, ready, "worker-start", largeBoxWorker)
250 if n := len(a.events("worker-start")); n != 1 {
251 t.Errorf("worker started %d times, want once", n)
252 }
253
254 // The scheduled delivery waited, then woke its instance and ran.
255 a.assertAfter(t, ready, "served", due, largeBoxDuePath)
256 a.assertAfter(t, ready, "acquire", due)
257
258 // The held host was not started: its restored hold answers maintenance.
259 got = b.answer(t, heldAns)
260 if got.status != http.StatusServiceUnavailable || got.body != "instance failed to wake\n" || got.header.Get("Cache-Control") != "no-store" {
261 t.Fatalf("held host after startup: %d %q %v %v", got.status, got.body, got.header, got.err)
262 }
263
264 // The moved host was not served at the socket recovery moved it from:
265 // the request answers as a wake in progress, and its retry is routed
266 // afresh to where recovery put it.
267 got = b.answer(t, movedAns)
268 if got.status != http.StatusServiceUnavailable || got.body != "instance is waking; retry shortly\n" || got.header.Get("Retry-After") != "5" {
269 t.Fatalf("host whose recovery moved it, held through startup: %d %q %v", got.status, got.body, got.err)
270 }
271 nested, err := layout.PlanFor(b.dataDir, b.socketDir, moved, layout.ModeNested)
272 if err != nil {
273 t.Fatal(err)
274 }
275 got = b.get(moved, false)
276 if got.status != http.StatusOK || got.body != "host="+moved+" socket="+nested.To.Socket {
277 t.Fatalf("retry of the moved host: %d %q %v", got.status, got.body, got.err)
278 }
279 got = b.get(held, false)
280 if got.status != http.StatusServiceUnavailable || got.body != "instance failed to wake\n" {
281 t.Fatalf("retry of the held host: %d %q %v", got.status, got.body, got.err)
282 }
283
284 // The background key pass ran after Start, and its slot refused the
285 // held hosts.
286 a.waitEvent(t, 30*time.Second, "startup-done")
287 keys := a.events("key")
288 if len(keys) != largeBoxRoutes-largeBoxHolds {
289 t.Errorf("key pass wrote %d rows, want %d", len(keys), largeBoxRoutes-largeBoxHolds)
290 }
291 for _, e := range keys {
292 if e.at.Before(ready.at) {
293 t.Errorf("key pass reached %s before Start", e.arg(0))
294 }
295 }
296
297 b.assertNoWrongService(t, a)
298 b.assertRecovered(t)
299
300 a.signal(t, syscall.SIGTERM)
301 a.waitExit(t, largeBoxJoinBound+3*time.Second)
302 a.assertCleanShutdown(t)
303}
304
305// testLargeBoxOverrun: instance control never warms, so startup never
306// finishes. A wake must be answered with the wake-timeout 503 at the
307// latch's bound: not an error, not a hang, and not a wake.
308func testLargeBoxOverrun(t *testing.T) {
309 b := newLargeBox(t)
310 a := b.startAgent(t)
311 t.Logf("gate probe answered %s after the agent started, instance control never warms", a.listen)
312
313 wake := largeBoxHost(largeBoxWakeHost)
314 began := time.Now()
315 got := b.get(wake, false)
316 took := time.Since(began)
317 t.Logf("wake held %s by a startup that does not finish", took)
318 if got.err != nil {
319 t.Fatalf("wake during an overrunning startup: %v", got.err)
320 }
321 if got.status != http.StatusServiceUnavailable || got.body != "instance is waking; retry shortly\n" || got.header.Get("Retry-After") != "5" {
322 t.Fatalf("wake during an overrunning startup: %d %q %v", got.status, got.body, got.header)
323 }
324 if took < largeBoxLatchBound-time.Second || took > largeBoxLatchBound+5*time.Second {
325 t.Fatalf("wake answered after %s, want the latch bound %s", took, largeBoxLatchBound)
326 }
327 a.assertNeverStarted(t)
328 b.assertNoWrongService(t, a)
329
330 // Shutting down with the sweep still waiting: nothing in flight, so the
331 // edge returns at once and runAgent's join bounds the rest.
332 at := time.Now()
333 a.signal(t, syscall.SIGTERM)
334 a.waitExit(t, largeBoxJoinBound+3*time.Second)
335 t.Logf("agent exited %s after SIGTERM with the sweep still waiting", time.Since(at))
336 a.assertNeverStarted(t)
337 a.assertCleanShutdown(t)
338}
339
340// testLargeBoxShutdownMidStartup: SIGTERM with startup still waiting on
341// the sweep and a wake held at the latch. The held request is answered
342// 503 (never proxied), the background work is joined within runAgent's
343// bound, and nothing of startup outlives it.
344func testLargeBoxShutdownMidStartup(t *testing.T) {
345 b := newLargeBox(t)
346 a := b.startAgent(t)
347
348 wake := largeBoxHost(largeBoxWakeHost)
349 ans := b.fire(wake, false)
350 a.waitEvent(t, 10*time.Second, "enter", wake)
351
352 at := time.Now()
353 a.signal(t, syscall.SIGTERM)
354 // The edge drains in-flight requests for up to largeBoxDrain before it
355 // returns, and the held wake is in flight until the activator shuts
356 // down after the join.
357 a.waitExit(t, largeBoxDrain+largeBoxJoinBound+3*time.Second)
358 t.Logf("agent exited %s after SIGTERM with a wake held at the latch", time.Since(at))
359 got := b.answer(t, ans)
360 if got.err != nil || got.status != http.StatusServiceUnavailable {
361 t.Fatalf("wake held through shutdown: %d %q %v", got.status, got.body, got.err)
362 }
363 a.assertNeverStarted(t)
364 b.assertNoWrongService(t, a)
365 a.assertCleanShutdown(t)
366}
367
368// --- runAgent's wiring ------------------------------------------------------------
369
370// TestRunAgentStartupWiring reads the startup's source and fails if it
371// drifts from what runLargeBoxAgent rebuilds, or if anything on runAgent's
372// path to the edge waits for startup to finish. Calls are matched on the
373// callee's own name (a selector's Sel), and order means execution order on
374// one goroutine: a step inside a go or defer statement, or inside a
375// function literal that is not invoked on the spot, does not count as
376// taken. Where the hotfix does mean a step to run in the background, the
377// one goroutine it runs on is found structurally and its order checked
378// inside it.
379func TestRunAgentStartupWiring(t *testing.T) {
380 t.Parallel()
381 fset := token.NewFileSet()
382 decls := map[string]*ast.FuncDecl{} // runAgent, startActivator, recoverLayoutAtStart, activatorStart.Run
383 files, err := filepath.Glob("*.go")
384 if err != nil {
385 t.Fatal(err)
386 }
387 for _, name := range files {
388 if strings.HasSuffix(name, "_test.go") {
389 continue
390 }
391 f, err := parser.ParseFile(fset, name, nil, 0)
392 if err != nil {
393 t.Fatal(err)
394 }
395 for _, d := range f.Decls {
396 fd, ok := d.(*ast.FuncDecl)
397 if !ok {
398 continue
399 }
400 key := fd.Name.Name
401 if fd.Recv != nil && len(fd.Recv.List) == 1 {
402 key = types.ExprString(fd.Recv.List[0].Type) + "." + key
403 }
404 switch key {
405 case "runAgent", "startActivator", "recoverLayoutAtStart", "activatorStart.Run", "joinAgentShutdown":
406 decls[key] = fd
407 }
408 }
409 }
410 for _, key := range []string{"runAgent", "startActivator", "recoverLayoutAtStart", "activatorStart.Run", "joinAgentShutdown"} {
411 if decls[key] == nil {
412 t.Fatalf("%s not found in package host", key)
413 }
414 }
415 str := types.ExprString
416
417 // runAgent: the latch is set, the start sequence is handed the startup
418 // context, the edge runs, then cancel, join and Shutdown, all on
419 // runAgent's own goroutine.
420 runAgent := decls["runAgent"].Body
421 sync := syncCalls(runAgent)
422 var run, edgeCall *ast.CallExpr
423 var lit *ast.CompositeLit
424 for _, c := range sync {
425 if sel, ok := c.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Run" {
426 if cl, ok := sel.X.(*ast.CompositeLit); ok && str(cl.Type) == "activatorStart" {
427 run, lit = c, cl
428 }
429 }
430 if calleeName(c) == "RunWithActivatorReady" {
431 edgeCall = c
432 }
433 }
434 if run == nil || edgeCall == nil {
435 t.Fatalf("runAgent: activatorStart{...}.Run called synchronously: %v; edge.RunWithActivatorReady called synchronously: %v", run != nil, edgeCall != nil)
436 }
437 if len(run.Args) != 1 || str(run.Args[0]) != "startupCtx" {
438 t.Errorf("activatorStart.Run is given %v, want startupCtx", run.Args)
439 }
440 var latch token.Pos
441 ast.Inspect(runAgent, func(n ast.Node) bool {
442 if a, ok := n.(*ast.AssignStmt); ok && len(a.Lhs) == 1 && len(a.Rhs) == 1 && selName(a.Lhs[0]) == "Startup" && str(a.Rhs[0]) == "started" {
443 latch = a.Pos()
444 }
445 return true
446 })
447 if latch == token.NoPos || latch > run.Pos() {
448 t.Error("runAgent: svc.Startup = started is not set before activatorStart.Run")
449 }
450 after := func(name string, arg string) token.Pos {
451 for _, c := range sync {
452 if c.Pos() > edgeCall.Pos() && calleeName(c) == name && (arg == "" || (len(c.Args) > 0 && str(c.Args[len(c.Args)-1]) == arg)) {
453 return c.Pos()
454 }
455 }
456 return token.NoPos
457 }
458 prev, prevName := edgeCall.Pos(), "edge.RunWithActivatorReady"
459 for _, step := range []struct{ name, arg string }{{"joinAgentShutdown", "dutiesDone"}, {"Shutdown", ""}} {
460 p := after(step.name, step.arg)
461 if p == token.NoPos || p < prev {
462 t.Errorf("runAgent: %s is not called synchronously after %s", step.name, prevName)
463 continue
464 }
465 prev, prevName = p, step.name
466 }
467
468 // Follow the synchronous helper without weakening cancel/join ordering or
469 // allowing the caller to pass different startup channels or cancellation.
470 for _, call := range sync {
471 if calleeName(call) != "joinAgentShutdown" {
472 continue
473 }
474 want := []string{"seedCtx", "seedCancel", "seedServer", "cancelStartup", "startupDone", "dutiesDone"}
475 if len(call.Args) != len(want) {
476 t.Fatal("shutdown helper argument count", len(call.Args))
477 }
478 for i, arg := range call.Args {
479 if str(arg) != want[i] {
480 t.Errorf("shutdown argument %d: %s, want %s", i, str(arg), want[i])
481 }
482 }
483 }
484 helper := syncCalls(decls["joinAgentShutdown"].Body)
485 previous := token.NoPos
486 for _, step := range []struct{ name, arg string }{{"cancelStartup", ""}, {"joinAgentStartup", "startupDone"}, {"joinAgentStartup", "dutiesDone"}} {
487 found := token.NoPos
488 for _, call := range helper {
489 if calleeName(call) == step.name && (step.arg == "" || len(call.Args) > 0 && str(call.Args[len(call.Args)-1]) == step.arg) {
490 found = call.Pos()
491 break
492 }
493 }
494 if found == token.NoPos || found <= previous {
495 t.Fatalf("shutdown helper lost synchronous order at %s(%s)", step.name, step.arg)
496 }
497 previous = found
498 }
499
500 // The activatorStart fields the child copies, and the steps inside
501 // them. Run calls each field on its background goroutine, so inside a
502 // field each step must be synchronous.
503 fields := map[string]ast.Expr{}
504 for _, el := range lit.Elts {
505 if kv, ok := el.(*ast.KeyValueExpr); ok {
506 fields[str(kv.Key)] = kv.Value
507 }
508 }
509 for field, want := range map[string]string{
510 "Start": "Start", "Publish": "Store", "Slot": "PrepareScheduledToken",
511 "Routes": "List", "Route": "Get", "Claims": "Enabled",
512 } {
513 if got := selName(fields[field]); got != want {
514 t.Errorf("activatorStart.%s is %s, want a %s method value", field, exprOrNil(fields[field]), want)
515 }
516 }
517 funcField := func(name string) ast.Node {
518 fl, _ := fields[name].(*ast.FuncLit)
519 if fl == nil {
520 t.Errorf("activatorStart.%s is not a func literal", name)
521 return nil
522 }
523 return fl.Body
524 }
525 if body := funcField("Recover"); body != nil {
526 requireSteps(t, "activatorStart.Recover", body, "newLayoutPass", "startupRecoveryIntent", "recoverLayoutAtStart")
527 }
528 if body := funcField("Sweep"); body != nil {
529 requireSteps(t, "activatorStart.Sweep", body, "Sweep")
530 }
531 if body := funcField("Ready"); body != nil {
532 closes := false
533 for _, c := range syncCalls(body) {
534 closes = closes || (calleeName(c) == "close" && len(c.Args) == 1 && str(c.Args[0]) == "started")
535 }
536 if !closes {
537 t.Error("activatorStart.Ready does not close started synchronously")
538 }
539 }
540
541 // activatorStart.Run: on a legacy box, one goroutine runs the start
542 // sequence and then the key pass, in that order; start itself is
543 // Start, then Ready.
544 runBody := decls["activatorStart.Run"].Body
545 var background []*ast.FuncLit
546 ast.Inspect(runBody, func(n ast.Node) bool {
547 if g, ok := n.(*ast.GoStmt); ok {
548 if fl, ok := g.Call.Fun.(*ast.FuncLit); ok && hasCall(syncCalls(fl.Body), "startActivator") {
549 background = append(background, fl)
550 }
551 }
552 return true
553 })
554 if len(background) != 1 {
555 t.Errorf("activatorStart.Run: %d goroutines run startActivator, want the one background start", len(background))
556 } else {
557 requireSteps(t, "activatorStart.Run's background goroutine", background[0].Body, "startActivator", "backfillInstanceKeys")
558 }
559 var startBody ast.Node
560 ast.Inspect(runBody, func(n ast.Node) bool {
561 if a, ok := n.(*ast.AssignStmt); ok && len(a.Lhs) == 1 && len(a.Rhs) == 1 && str(a.Lhs[0]) == "start" {
562 if fl, ok := a.Rhs[0].(*ast.FuncLit); ok {
563 startBody = fl.Body
564 }
565 }
566 return true
567 })
568 if startBody == nil {
569 t.Error("activatorStart.Run: no start closure")
570 } else {
571 requireSteps(t, "activatorStart.Run's start", startBody, "Start", "Ready")
572 }
573
574 // startActivator: its sweep, recoverLayout and start parameters, called
575 // in that order, synchronously (matched by position, not name).
576 sa := decls["startActivator"]
577 var params []string
578 for _, f := range sa.Type.Params.List {
579 for _, n := range f.Names {
580 params = append(params, n.Name)
581 }
582 }
583 if len(params) != 4 {
584 t.Errorf("startActivator takes %v, want (ctx, sweep, recoverLayout, start)", params)
585 } else {
586 requireSteps(t, "startActivator", sa.Body, params[1], params[2], params[3])
587 }
588
589 // recoverLayoutAtStart: journal recovery, then hold restoration.
590 requireSteps(t, "recoverLayoutAtStart", decls["recoverLayoutAtStart"].Body, "Recover", "RestoreHolds")
591
592 // The worker boot pass waits on the latch or the startup context, then
593 // lists and wakes.
594 var bootPass *ast.FuncLit
595 ast.Inspect(runAgent, func(n ast.Node) bool {
596 if g, ok := n.(*ast.GoStmt); ok {
597 if fl, ok := g.Call.Fun.(*ast.FuncLit); ok && hasCall(syncCalls(fl.Body), "runWorkerBootPass") {
598 bootPass = fl
599 }
600 }
601 return true
602 })
603 if bootPass == nil {
604 t.Error("runAgent: no goroutine runs runWorkerBootPass")
605 } else if !receivesFrom(bootPass.Body, "started") || !hasCall(syncCalls(bootPass.Body), "Done") {
606 t.Error("runAgent: the worker boot pass does not wait on started and startupCtx.Done")
607 }
608
609 // Nothing before the edge waits for startup. On runAgent's own
610 // goroutine (closures invoked on the spot included): no receive at all,
611 // no join, and a closure that waits on startup is only called directly
612 // on a claim box, where Run has already started the activator before
613 // it returns.
614 waiters := map[string]bool{}
615 ast.Inspect(runAgent, func(n ast.Node) bool {
616 if a, ok := n.(*ast.AssignStmt); ok && len(a.Lhs) == 1 && len(a.Rhs) == 1 {
617 if fl, ok := a.Rhs[0].(*ast.FuncLit); ok && (receivesFrom(fl.Body, "started") || receivesFrom(fl.Body, "startupDone") || hasCall(syncCalls(fl.Body), "joinAgentStartup")) {
618 waiters[str(a.Lhs[0])] = true
619 }
620 }
621 return true
622 })
623 var walk func(n ast.Node, claimBranch bool)
624 walk = func(n ast.Node, claimBranch bool) {
625 ast.Inspect(n, func(n ast.Node) bool {
626 if n == nil || n.Pos() >= edgeCall.Pos() {
627 return false
628 }
629 switch n := n.(type) {
630 case *ast.FuncLit, *ast.GoStmt, *ast.DeferStmt:
631 return false
632 case *ast.IfStmt:
633 if n.Init != nil {
634 walk(n.Init, claimBranch)
635 }
636 walk(n.Cond, claimBranch)
637 walk(n.Body, claimBranch || selName(n.Cond) == "Enabled")
638 if n.Else != nil {
639 walk(n.Else, claimBranch)
640 }
641 return false
642 case *ast.UnaryExpr:
643 if n.Op == token.ARROW {
644 t.Errorf("runAgent waits on %s at %s, before the edge listens", str(n.X), fset.Position(n.Pos()))
645 }
646 case *ast.CallExpr:
647 name := calleeName(n)
648 if name == "joinAgentStartup" {
649 t.Errorf("runAgent joins startup at %s, before the edge listens", fset.Position(n.Pos()))
650 }
651 if waiters[str(n.Fun)] && !claimBranch {
652 t.Errorf("runAgent calls %s, which waits on startup, at %s, before the edge listens", str(n.Fun), fset.Position(n.Pos()))
653 }
654 if fl, ok := n.Fun.(*ast.FuncLit); ok { // invoked on the spot: its body runs here
655 walk(fl.Body, claimBranch)
656 for _, a := range n.Args {
657 walk(a, claimBranch)
658 }
659 return false
660 }
661 }
662 return true
663 })
664 }
665 walk(runAgent, false)
666}
667
668// syncCalls lists, in source order, the calls body makes on its own
669// goroutine: none inside a go or defer statement, and none inside a
670// function literal unless the literal is invoked right there.
671func syncCalls(body ast.Node) []*ast.CallExpr {
672 var out []*ast.CallExpr
673 ast.Inspect(body, func(n ast.Node) bool {
674 switch n := n.(type) {
675 case *ast.GoStmt, *ast.DeferStmt, *ast.FuncLit:
676 return false
677 case *ast.CallExpr:
678 out = append(out, n)
679 if fl, ok := n.Fun.(*ast.FuncLit); ok {
680 for _, a := range n.Args {
681 out = append(out, syncCalls(a)...)
682 }
683 out = append(out, syncCalls(fl.Body)...)
684 return false
685 }
686 }
687 return true
688 })
689 return out
690}
691
692// requireSteps fails unless body calls each step synchronously, in order,
693// and calls none of them anywhere else (in a go or defer statement, or a
694// literal that is not invoked on the spot).
695func requireSteps(t *testing.T, where string, body ast.Node, steps ...string) {
696 t.Helper()
697 sync := syncCalls(body)
698 inSync := map[*ast.CallExpr]bool{}
699 for _, c := range sync {
700 inSync[c] = true
701 }
702 ast.Inspect(body, func(n ast.Node) bool {
703 if c, ok := n.(*ast.CallExpr); ok && !inSync[c] {
704 for _, s := range steps {
705 if calleeName(c) == s {
706 t.Errorf("%s: %s is called off its goroutine (go, defer, or a literal not invoked there)", where, s)
707 }
708 }
709 }
710 return true
711 })
712 i := 0
713 for _, c := range sync {
714 if i < len(steps) && calleeName(c) == steps[i] {
715 i++
716 }
717 }
718 if i < len(steps) {
719 t.Errorf("%s: want %v called in that order on its goroutine; %s is missing or out of order", where, steps, steps[i])
720 }
721}
722
723// calleeName is the name a call is matched on: the function's own name,
724// whatever the receiver or package it is reached through.
725func calleeName(c *ast.CallExpr) string {
726 switch f := c.Fun.(type) {
727 case *ast.Ident:
728 return f.Name
729 case *ast.SelectorExpr:
730 return f.Sel.Name
731 }
732 return ""
733}
734
735func selName(e ast.Expr) string {
736 switch e := e.(type) {
737 case *ast.SelectorExpr:
738 return e.Sel.Name
739 case *ast.Ident:
740 return e.Name
741 }
742 return ""
743}
744
745func hasCall(calls []*ast.CallExpr, name string) bool {
746 for _, c := range calls {
747 if calleeName(c) == name {
748 return true
749 }
750 }
751 return false
752}
753
754// receivesFrom reports whether body receives from the channel named ch
755// (in a select case or a bare receive).
756func receivesFrom(body ast.Node, ch string) bool {
757 found := false
758 ast.Inspect(body, func(n ast.Node) bool {
759 if u, ok := n.(*ast.UnaryExpr); ok && u.Op == token.ARROW && types.ExprString(u.X) == ch {
760 found = true
761 }
762 return !found
763 })
764 return found
765}
766
767func exprOrNil(e ast.Expr) string {
768 if e == nil {
769 return "unset"
770 }
771 return types.ExprString(e)
772}
773
774// --- the box ------------------------------------------------------------------
775
776type largeBox struct {
777 dataDir, socketDir, storeDir, acmeDir string
778 reg *registry.Registry
779 control *largeBoxControl
780 httpAddr, httpsAddr string
781 client *http.Client
782}
783
784// largeBoxSocketDir makes the box's run directory. It must be short: a
785// nested instance's socket is <dir>/instances/<instance id>/app.sock, the
786// activator binds a ".control" socket beside it, and the whole path must
787// fit a unix socket address (provision.MaxUnixPath). The default temp dir
788// is tried first, then a directory of the test's own directly under /tmp;
789// when neither is short enough the test is skipped, not failed, since
790// nothing about the code under test is wrong.
791func largeBoxSocketDir(t *testing.T) string {
792 t.Helper()
793 fits := func(dir string) bool {
794 return len(filepath.Join(dir, "instances", provision.InstanceID(largeBoxHost(0)), "app.sock.control")) <= provision.MaxUnixPath
795 }
796 var tried []string
797 for _, base := range []string{os.TempDir(), "/tmp"} {
798 dir, err := os.MkdirTemp(base, "lb")
799 if err != nil {
800 tried = append(tried, base+": "+err.Error())
801 continue
802 }
803 if fits(dir) {
804 t.Cleanup(func() { os.RemoveAll(dir) })
805 return dir
806 }
807 os.RemoveAll(dir)
808 tried = append(tried, dir+": too long")
809 }
810 t.Skipf("no directory short enough for a nested instance's control socket (%d bytes at most): %s", provision.MaxUnixPath, strings.Join(tried, "; "))
811 return ""
812}
813
814// newLargeBox builds the box the agent restarts on: largeBoxRoutes
815// sleeping instances on flat paths, largeBoxJournals of them with a layout
816// move cut short at three different points, largeBoxHolds held down after
817// a refused move (the reason, a second name for the database, persists),
818// a scheduled delivery due on one instance, an always-on worker the boot
819// pass starts, a nested layout intent in the deployment store, a cached
820// certificate for every host the test asks for, and a cold instance
821// control.
822func newLargeBox(t *testing.T) *largeBox {
823 t.Helper()
824 sd := largeBoxSocketDir(t)
825 b := &largeBox{dataDir: t.TempDir(), storeDir: t.TempDir(), acmeDir: t.TempDir(), socketDir: sd}
826
827 var err error
828 b.reg, err = registry.Open(filepath.Join(b.dataDir, "registry.db"))
829 if err != nil {
830 t.Fatal(err)
831 }
832 t.Cleanup(func() { b.reg.Close() })
833 var hosts []string
834 for i := 0; i < largeBoxRoutes; i++ {
835 h := largeBoxHost(i)
836 hosts = append(hosts, h)
837 if err := b.reg.Upsert(registry.Route{
838 Host: h, Kind: "instance", Account: "acct", App: "shop", Status: "ready", Hibernate: true,
839 Socket: filepath.Join(sd, h+".sock"), DBPath: provision.DefaultDBPath(b.dataDir, h),
840 }); err != nil {
841 t.Fatal(err)
842 }
843 }
844 if err := b.reg.Upsert(registry.Route{
845 Host: largeBoxWorker, Kind: "sidecar", Account: "acct", App: "shop", Status: "ready", Args: `["serve"]`,
846 }); err != nil {
847 t.Fatal(err)
848 }
849 due := time.Now().Unix() - 5
850 if err := b.reg.UpsertTimer(registry.Timer{
851 Host: largeBoxHost(largeBoxDueHost), Name: "tick", Kind: schedule.KindSchedule, Spec: "every 6h0m0s",
852 Path: largeBoxDuePath, NextAt: due, DueAt: due,
853 }); err != nil {
854 t.Fatal(err)
855 }
856 for i := 0; i < largeBoxJournals; i++ {
857 crash := "after-journal"
858 switch {
859 case i >= 6:
860 crash = "before-remove"
861 case i >= 4:
862 crash = "mid-files"
863 }
864 b.crashedMove(t, largeBoxHost(i), crash)
865 }
866 for i := largeBoxJournals; i < largeBoxJournals+largeBoxHolds; i++ {
867 b.heldMove(t, largeBoxHost(i), i)
868 }
869 appendIntent(t, release.NewDirStore(b.storeDir), release.LayoutModeNested, "")
870
871 served := []string{largeBoxHost(largeBoxWakeHost), largeBoxHost(largeBoxSidecarHost)}
872 for i := 0; i < largeBoxJournals+largeBoxHolds; i++ {
873 served = append(served, largeBoxHost(i))
874 }
875 pool := writeLargeBoxCerts(t, b.acmeDir, served)
876
877 b.control = startLargeBoxControl(t, filepath.Join(sd, "control.sock"), hosts)
878 b.client = &http.Client{
879 Timeout: largeBoxLatchBound + 15*time.Second,
880 Transport: &http.Transport{
881 Proxy: nil,
882 DisableKeepAlives: true,
883 TLSClientConfig: &tls.Config{RootCAs: pool},
884 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
885 return (&net.Dialer{}).DialContext(ctx, "tcp", b.httpsAddr)
886 },
887 },
888 CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
889 }
890 return b
891}
892
893func (b *largeBox) seedFlat(t *testing.T, h string) {
894 t.Helper()
895 db := provision.DefaultDBPath(b.dataDir, h)
896 for path, body := range map[string]string{db: "db " + h, db + "-wal": "wal", filepath.Join(b.dataDir, h+".env"): "CARLOS_ADMIN_TOKEN=t\n"} {
897 if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
898 t.Fatal(err)
899 }
900 }
901}
902
903// crashedMove leaves h as an agent killed mid-migration to nested leaves
904// it: the instance directories made and the journal written, then
905// nothing moved (after-journal), the database moved (mid-files), or
906// everything moved and the row flipped with only the journal's removal
907// left (before-remove).
908func (b *largeBox) crashedMove(t *testing.T, h, crash string) {
909 t.Helper()
910 b.seedFlat(t, h)
911 if err := layout.EnsureDirs(b.dataDir, b.socketDir, h, -1); err != nil {
912 t.Fatal(err)
913 }
914 plan, err := layout.PlanFor(b.dataDir, b.socketDir, h, layout.ModeNested)
915 if err != nil {
916 t.Fatal(err)
917 }
918 present := make([]bool, len(plan.Artifacts))
919 for i, a := range plan.Artifacts {
920 if a.Name == "socket" {
921 continue
922 }
923 _, err := os.Lstat(a.Src)
924 present[i] = err == nil
925 }
926 j := plan.Journal(present)
927 if err := layout.WriteJournal(b.dataDir, j); err != nil {
928 t.Fatal(err)
929 }
930 move := func(a layout.Artifact) {
931 if err := os.Rename(a.Src, a.Dst); err != nil {
932 t.Fatal(err)
933 }
934 }
935 switch crash {
936 case "mid-files":
937 move(plan.Artifacts[0])
938 case "before-remove":
939 for i, a := range plan.Artifacts {
940 if present[i] {
941 move(a)
942 }
943 }
944 if err := b.reg.SetInstancePaths(h, plan.From.DB, plan.From.Socket, plan.To.DB, plan.To.Socket); err != nil {
945 t.Fatal(err)
946 }
947 if err := layout.UpdatePhase(b.dataDir, j, layout.PhaseDone); err != nil {
948 t.Fatal(err)
949 }
950 }
951}
952
953// heldMove leaves h held down after a refused move to nested: its hold
954// marker, and the reason, which persists across the restart (a second
955// name for its database, which a flat tenant can plant).
956func (b *largeBox) heldMove(t *testing.T, h string, i int) {
957 t.Helper()
958 b.seedFlat(t, h)
959 if err := os.Link(provision.DefaultDBPath(b.dataDir, h), filepath.Join(b.dataDir, ".x"+strconv.Itoa(i))); err != nil {
960 t.Fatal(err)
961 }
962 dir := filepath.Join(b.dataDir, "layout")
963 if err := os.MkdirAll(dir, 0o700); err != nil {
964 t.Fatal(err)
965 }
966 marker, _ := json.Marshal(map[string]any{"host": h, "reason": "db has 2 names", "since": time.Now().UTC()})
967 if err := os.WriteFile(filepath.Join(dir, h+".hold"), marker, 0o600); err != nil {
968 t.Fatal(err)
969 }
970}
971
972// assertRecovered checks the files: every journal completed and removed,
973// each journalled host's row and database at the nested layout, and every
974// hold marker kept.
975func (b *largeBox) assertRecovered(t *testing.T) {
976 t.Helper()
977 if hosts, err := layout.JournalHosts(b.dataDir); err != nil || len(hosts) != 0 {
978 t.Errorf("journals after startup: %v %v", hosts, err)
979 }
980 for i := 0; i < largeBoxJournals; i++ {
981 h := largeBoxHost(i)
982 plan, err := layout.PlanFor(b.dataDir, b.socketDir, h, layout.ModeNested)
983 if err != nil {
984 t.Fatal(err)
985 }
986 rt, err := b.reg.Get(h)
987 if err != nil || rt == nil || rt.DBPath != plan.To.DB || rt.Socket != plan.To.Socket {
988 t.Errorf("%s row after recovery: %+v %v", h, rt, err)
989 }
990 if body, err := os.ReadFile(plan.Artifacts[0].Dst); err != nil || string(body) != "db "+h {
991 t.Errorf("%s database after recovery: %q %v", h, body, err)
992 }
993 }
994 held, err := layout.HoldMarkers(b.dataDir)
995 if err != nil || len(held) != largeBoxHolds {
996 t.Errorf("hold markers after startup: %v %v", held, err)
997 }
998}
999
1000// assertNoWrongService reads the backend's record: no wake admitted before
1001// the latch opened, no held host started, and no journalled host started
1002// at its pre-recovery socket.
1003func (b *largeBox) assertNoWrongService(t *testing.T, a *largeBoxAgent) {
1004 t.Helper()
1005 if v := a.events("violation"); len(v) != 0 {
1006 t.Errorf("backend violations: %v", v)
1007 }
1008 for _, e := range append(a.events("acquire"), a.events("start")...) {
1009 for i := largeBoxJournals; i < largeBoxJournals+largeBoxHolds; i++ {
1010 if e.arg(0) == largeBoxHost(i) {
1011 t.Errorf("held host %s reached the backend: %v", e.arg(0), e)
1012 }
1013 }
1014 }
1015 for _, e := range a.events("start") {
1016 for i := 0; i < largeBoxJournals; i++ {
1017 if h := largeBoxHost(i); e.arg(0) == h && e.arg(1) == filepath.Join(b.socketDir, h+".sock") {
1018 t.Errorf("%s started at its pre-recovery socket", h)
1019 }
1020 }
1021 }
1022}
1023
1024type largeBoxAnswer struct {
1025 status int
1026 body string
1027 header http.Header
1028 err error
1029 at time.Time // when the answer was read
1030}
1031
1032// get asks the edge for host's root over TLS. sidecar marks the request
1033// as the host's sidecar's (activator.SidecarHeader).
1034func (b *largeBox) get(host string, sidecar bool) largeBoxAnswer {
1035 req, err := http.NewRequest(http.MethodGet, "https://"+host+"/", nil)
1036 if err != nil {
1037 return largeBoxAnswer{err: err, at: time.Now()}
1038 }
1039 if sidecar {
1040 req.Header.Set(activator.SidecarHeader, "1")
1041 }
1042 resp, err := b.client.Do(req)
1043 if err != nil {
1044 return largeBoxAnswer{err: err, at: time.Now()}
1045 }
1046 defer resp.Body.Close()
1047 body, err := io.ReadAll(resp.Body)
1048 return largeBoxAnswer{status: resp.StatusCode, body: string(body), header: resp.Header, err: err, at: time.Now()}
1049}
1050
1051func (b *largeBox) fire(host string, sidecar bool) chan largeBoxAnswer {
1052 ch := make(chan largeBoxAnswer, 1)
1053 go func() { ch <- b.get(host, sidecar) }()
1054 return ch
1055}
1056
1057func (b *largeBox) answer(t *testing.T, ch chan largeBoxAnswer) largeBoxAnswer {
1058 t.Helper()
1059 select {
1060 case got := <-ch:
1061 return got
1062 case <-time.After(10 * time.Second):
1063 t.Fatal("request did not finish")
1064 return largeBoxAnswer{}
1065 }
1066}
1067
1068func freeLoopbackAddr(t *testing.T) string {
1069 t.Helper()
1070 ln, err := net.Listen("tcp", "127.0.0.1:0")
1071 if err != nil {
1072 t.Fatal(err)
1073 }
1074 defer ln.Close()
1075 return ln.Addr().String()
1076}
1077
1078// writeLargeBoxCerts puts one self-signed certificate for hosts into the
1079// edge's autocert directory cache, under each host's name, so the edge in
1080// production mode serves them TLS without ordering anything.
1081func writeLargeBoxCerts(t *testing.T, dir string, hosts []string) *x509.CertPool {
1082 t.Helper()
1083 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
1084 if err != nil {
1085 t.Fatal(err)
1086 }
1087 tmpl := &x509.Certificate{
1088 SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "large box"}, DNSNames: hosts,
1089 NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour),
1090 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
1091 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true,
1092 }
1093 der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
1094 if err != nil {
1095 t.Fatal(err)
1096 }
1097 keyDER, err := x509.MarshalECPrivateKey(key)
1098 if err != nil {
1099 t.Fatal(err)
1100 }
1101 pemBytes := append(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...)
1102 for _, h := range hosts {
1103 if err := os.WriteFile(filepath.Join(dir, h), pemBytes, 0o600); err != nil {
1104 t.Fatal(err)
1105 }
1106 }
1107 cert, err := x509.ParseCertificate(der)
1108 if err != nil {
1109 t.Fatal(err)
1110 }
1111 pool := x509.NewCertPool()
1112 pool.AddCert(cert)
1113 return pool
1114}
1115
1116// --- instance control ---------------------------------------------------------
1117
1118// largeBoxControl is the real instance-control server with a cold systemd
1119// behind its Systemctl seam: every call waits for warm, then the box's
1120// leftover units are listed active and each stop takes largeBoxUnitStop.
1121type largeBoxControl struct {
1122 warm chan struct{}
1123 units string
1124 stops atomic.Int32
1125}
1126
1127func startLargeBoxControl(t *testing.T, path string, hosts []string) *largeBoxControl {
1128 t.Helper()
1129 c := &largeBoxControl{warm: make(chan struct{})}
1130 var units strings.Builder
1131 for _, h := range hosts {
1132 fmt.Fprintf(&units, "%s loaded active running carlos instance\n", instancectl.UnitName(instancectl.RoleRun, h))
1133 }
1134 c.units = units.String()
1135 srv := &instancectl.Server{
1136 Systemctl: c.systemctl,
1137 PeerOK: func(net.Conn) error { return nil },
1138 CgroupRoot: t.TempDir(),
1139 Build: version.Build,
1140 Logf: func(string, ...any) {},
1141 }
1142 ln, err := net.Listen("unix", path)
1143 if err != nil {
1144 t.Fatal(err)
1145 }
1146 ctx, cancel := context.WithCancel(context.Background())
1147 done := make(chan struct{})
1148 go func() { defer close(done); _ = srv.Serve(ctx, ln) }()
1149 t.Cleanup(func() { cancel(); <-done })
1150 return c
1151}
1152
1153func (c *largeBoxControl) systemctl(ctx context.Context, args ...string) (string, error) {
1154 select {
1155 case <-c.warm:
1156 case <-ctx.Done():
1157 return "", ctx.Err()
1158 }
1159 switch args[0] {
1160 case "list-units":
1161 return c.units, nil
1162 case "list-jobs":
1163 return "", nil
1164 case "stop":
1165 select {
1166 case <-time.After(largeBoxUnitStop):
1167 case <-ctx.Done():
1168 return "", ctx.Err()
1169 }
1170 c.stops.Add(1)
1171 return "", nil
1172 case "show":
1173 return "inactive\n", nil
1174 }
1175 return "", fmt.Errorf("systemctl %v: not part of a sweep", args)
1176}
1177
1178// --- the agent process ----------------------------------------------------------
1179
1180type largeBoxAgent struct {
1181 cmd *exec.Cmd
1182 began time.Time
1183 listen time.Duration // process start to the gate probe's first answer
1184 eventsPath string
1185 log string
1186 exited chan struct{} // closed once the process has been waited for
1187 exitErr error // Wait's answer, read after exited closes
1188 b *largeBox
1189}
1190
1191// startAgent starts the agent and returns it once the gate's probe is
1192// answered and its TLS listener accepts. The edge's -http and -https take
1193// an address, and :0 would bind, but the edge never says which port it
1194// got, so the test picks free ports and hands them over. A port taken in
1195// between makes the edge's listener fail and the agent exit; only that is
1196// retried on fresh ports (largeBoxRetryable). A deadline missed is the
1197// failure this test exists to catch, and is never retried.
1198func (b *largeBox) startAgent(t *testing.T) *largeBoxAgent {
1199 t.Helper()
1200 for attempt := 1; ; attempt++ {
1201 b.httpAddr, b.httpsAddr = freeLoopbackAddr(t), freeLoopbackAddr(t)
1202 a := b.launchAgent(t)
1203 outcome, err := a.awaitListening()
1204 if outcome == largeBoxUp {
1205 outcome, err = a.awaitTLS()
1206 }
1207 if outcome == largeBoxUp {
1208 return a
1209 }
1210 agentLog, _ := os.ReadFile(a.log)
1211 if largeBoxRetryable(outcome, string(agentLog), b.httpAddr, b.httpsAddr) && attempt < largeBoxBindAttempts {
1212 t.Logf("attempt %d: %v; a port the test handed the agent was taken before it bound it, starting again on fresh ports", attempt, err)
1213 continue
1214 }
1215 t.Fatal(err)
1216 }
1217}
1218
1219// largeBoxOutcome is how waiting for the agent to come up ended.
1220type largeBoxOutcome int
1221
1222const (
1223 largeBoxUp largeBoxOutcome = iota
1224 largeBoxLate // a deadline passed: a failure, never retried
1225 largeBoxExited // the agent exited before it was up
1226)
1227
1228// largeBoxRetryable reports whether a start may be retried on fresh
1229// ports: only when the agent exited, and its log says the edge could not
1230// bind one of the addresses the test handed it because it was in use.
1231// Edge listener failures are logged as "edge: http listener <addr>: ..."
1232// (edge.RunWithActivatorReady).
1233func largeBoxRetryable(o largeBoxOutcome, agentLog string, addrs ...string) bool {
1234 if o != largeBoxExited {
1235 return false
1236 }
1237 for _, line := range strings.Split(agentLog, "\n") {
1238 if !strings.Contains(line, "address already in use") {
1239 continue
1240 }
1241 for _, addr := range addrs {
1242 if strings.Contains(line, " listener "+addr+": ") {
1243 return true
1244 }
1245 }
1246 }
1247 return false
1248}
1249
1250func TestLargeBoxRetriesOnlyItsOwnBindFailure(t *testing.T) {
1251 const plain, tls = "127.0.0.1:40001", "127.0.0.1:40002"
1252 bind := "edge: https listener " + tls + ": listen tcp " + tls + ": bind: address already in use\n"
1253 for _, c := range []struct {
1254 name string
1255 o largeBoxOutcome
1256 log string
1257 want bool
1258 }{
1259 {"own bind failure", largeBoxExited, "edge: serving 300 route(s)\n" + bind, true},
1260 {"deadline missed, bind failure logged too", largeBoxLate, bind, false},
1261 {"exited for another reason", largeBoxExited, "edge: tunnels: invalid\n", false},
1262 {"someone else's address", largeBoxExited, "edge: http listener 127.0.0.1:1: listen tcp 127.0.0.1:1: bind: address already in use\n", false},
1263 {"in use, but not a listener line", largeBoxExited, "agent: something: address already in use\n", false},
1264 } {
1265 if got := largeBoxRetryable(c.o, c.log, plain, tls); got != c.want {
1266 t.Errorf("%s: retry = %v, want %v", c.name, got, c.want)
1267 }
1268 }
1269}
1270
1271func (b *largeBox) launchAgent(t *testing.T) *largeBoxAgent {
1272 t.Helper()
1273 cfgDir := t.TempDir()
1274 cfg := largeBoxConfig{
1275 DataDir: b.dataDir, SocketDir: b.socketDir, StoreDir: b.storeDir, AcmeDir: b.acmeDir,
1276 Control: filepath.Join(b.socketDir, "control.sock"), Events: filepath.Join(cfgDir, "events"),
1277 HTTP: b.httpAddr, HTTPS: b.httpsAddr,
1278 }
1279 raw, err := json.Marshal(cfg)
1280 if err != nil {
1281 t.Fatal(err)
1282 }
1283 cfgPath := filepath.Join(cfgDir, "agent.json")
1284 if err := os.WriteFile(cfgPath, raw, 0o600); err != nil {
1285 t.Fatal(err)
1286 }
1287 exe, err := os.Executable()
1288 if err != nil {
1289 t.Fatal(err)
1290 }
1291 a := &largeBoxAgent{eventsPath: cfg.Events, log: filepath.Join(cfgDir, "agent.log"), exited: make(chan struct{}), b: b}
1292 logFile, err := os.Create(a.log)
1293 if err != nil {
1294 t.Fatal(err)
1295 }
1296 t.Cleanup(func() { logFile.Close() })
1297 a.cmd = exec.Command(exe, "-test.run=^TestStartupLargeBoxEndToEnd$", "-test.count=1")
1298 // A clean environment: nothing of the developer's or the runner's
1299 // (a deployment bucket, AWS credentials, CARLOS_*) reaches the agent.
1300 a.cmd.Env = []string{"HOME=" + b.dataDir, "TMPDIR=" + os.TempDir(), "CARLOS_DATA_DIR=" + b.dataDir, largeBoxAgentEnv + "=" + cfgPath}
1301 a.cmd.Stdout, a.cmd.Stderr = logFile, logFile
1302 a.began = time.Now()
1303 if err := a.cmd.Start(); err != nil {
1304 t.Fatal(err)
1305 }
1306 go func() {
1307 a.exitErr = a.cmd.Wait()
1308 close(a.exited)
1309 }()
1310 t.Cleanup(func() {
1311 select {
1312 case <-a.exited:
1313 default:
1314 _ = a.cmd.Process.Kill()
1315 <-a.exited
1316 }
1317 if t.Failed() {
1318 if out, err := os.ReadFile(a.log); err == nil {
1319 if len(out) > 16<<10 {
1320 out = out[len(out)-16<<10:]
1321 }
1322 t.Logf("agent log (tail):\n%s", out)
1323 }
1324 }
1325 })
1326 return a
1327}
1328
1329// awaitListening probes as the update gate does (systemapply.go
1330// productionSysOps.probeEdge): GET / on the edge's plain-HTTP listener,
1331// three-second client timeout, no redirect followed, and any HTTP answer
1332// passes. It records how long after the process started the first answer
1333// came; an answer past largeBoxListenBound, or none by then, is late.
1334func (a *largeBoxAgent) awaitListening() (largeBoxOutcome, error) {
1335 probe := &http.Client{
1336 Timeout: 3 * time.Second,
1337 Transport: &http.Transport{Proxy: nil, DisableKeepAlives: true},
1338 CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
1339 }
1340 deadline := a.began.Add(largeBoxListenBound)
1341 for {
1342 resp, err := probe.Get("http://" + a.b.httpAddr + "/")
1343 if err == nil {
1344 resp.Body.Close()
1345 a.listen = time.Since(a.began)
1346 if a.listen > largeBoxListenBound {
1347 return largeBoxLate, fmt.Errorf("gate probe answered %s after the agent started, over %s", a.listen, largeBoxListenBound)
1348 }
1349 return largeBoxUp, nil
1350 }
1351 if time.Now().After(deadline) {
1352 return largeBoxLate, fmt.Errorf("gate probe unanswered %s after the agent started: %v", time.Since(a.began), err)
1353 }
1354 select {
1355 case <-a.exited:
1356 return largeBoxExited, fmt.Errorf("agent exited before listening: %v", a.exitErr)
1357 default:
1358 }
1359 time.Sleep(20 * time.Millisecond)
1360 }
1361}
1362
1363// awaitTLS waits, boundedly, for the edge's TLS listener: the edge binds
1364// it in its own goroutine, beside the plain-HTTP one the gate probes, so
1365// it can come up a moment later. Not part of the gate's measurement.
1366func (a *largeBoxAgent) awaitTLS() (largeBoxOutcome, error) {
1367 deadline := time.Now().Add(largeBoxListenBound)
1368 for {
1369 c, err := net.DialTimeout("tcp", a.b.httpsAddr, time.Second)
1370 if err == nil {
1371 c.Close()
1372 }
1373 select {
1374 case <-a.exited:
1375 return largeBoxExited, fmt.Errorf("agent exited before its TLS listener was up: %v", a.exitErr)
1376 default:
1377 }
1378 if err == nil {
1379 return largeBoxUp, nil
1380 }
1381 if time.Now().After(deadline) {
1382 return largeBoxLate, fmt.Errorf("TLS listener not up %s after the gate probe: %v", largeBoxListenBound, err)
1383 }
1384 time.Sleep(20 * time.Millisecond)
1385 }
1386}
1387
1388func (a *largeBoxAgent) signal(t *testing.T, sig os.Signal) {
1389 t.Helper()
1390 if err := a.cmd.Process.Signal(sig); err != nil {
1391 t.Fatal(err)
1392 }
1393}
1394
1395func (a *largeBoxAgent) waitExit(t *testing.T, bound time.Duration) {
1396 t.Helper()
1397 select {
1398 case <-a.exited:
1399 if a.exitErr != nil {
1400 t.Fatalf("agent exit: %v", a.exitErr)
1401 }
1402 case <-time.After(bound):
1403 t.Fatalf("agent did not exit within %s of the signal", bound)
1404 }
1405}
1406
1407// assertNeverStarted: startup never finished, so the activator never
1408// started and nothing reached the backend or ran.
1409func (a *largeBoxAgent) assertNeverStarted(t *testing.T) {
1410 t.Helper()
1411 for _, kind := range []string{"ready", "activator-start", "acquire", "start", "worker-start", "served"} {
1412 if evs := a.events(kind); len(evs) != 0 {
1413 t.Errorf("%s with startup unfinished: %v", kind, evs)
1414 }
1415 }
1416}
1417
1418// assertAfter waits briefly for the event kind with args and fails unless
1419// it came after ref.
1420func (a *largeBoxAgent) assertAfter(t *testing.T, ref largeBoxEvent, kind string, args ...string) {
1421 t.Helper()
1422 e := a.waitEvent(t, 10*time.Second, kind, args...)
1423 if e.at.Before(ref.at) {
1424 t.Errorf("%s %v came %s before %s", kind, args, ref.at.Sub(e.at), ref.kind)
1425 }
1426}
1427
1428// assertCleanShutdown reads the agent's own account of its shutdown: the
1429// startup work joined within runAgent's bound, and no goroutine of the
1430// startup sequence, the activator or the fake instances left after it.
1431func (a *largeBoxAgent) assertCleanShutdown(t *testing.T) {
1432 t.Helper()
1433 j, ok := a.event("joined")
1434 if !ok {
1435 t.Fatal("agent did not report its startup join")
1436 }
1437 took, err := time.ParseDuration(j.arg(1))
1438 if j.arg(0) != "true" || err != nil || took > largeBoxJoinBound {
1439 t.Fatalf("startup join: %v", j)
1440 }
1441 t.Logf("startup work joined %s after the edge returned", took)
1442 if _, ok := a.event("leak-free"); !ok {
1443 t.Fatal("goroutines of the startup sequence outlived shutdown; see the agent log")
1444 }
1445}
1446
1447type largeBoxEvent struct {
1448 at time.Time
1449 kind string
1450 args []string
1451}
1452
1453func (e largeBoxEvent) arg(i int) string {
1454 if i < len(e.args) {
1455 return e.args[i]
1456 }
1457 return ""
1458}
1459
1460func (a *largeBoxAgent) all() []largeBoxEvent {
1461 f, err := os.Open(a.eventsPath)
1462 if err != nil {
1463 return nil
1464 }
1465 defer f.Close()
1466 var out []largeBoxEvent
1467 sc := bufio.NewScanner(f)
1468 for sc.Scan() {
1469 fields := strings.Fields(sc.Text())
1470 if len(fields) < 2 {
1471 continue
1472 }
1473 ns, err := strconv.ParseInt(fields[0], 10, 64)
1474 if err != nil {
1475 continue
1476 }
1477 out = append(out, largeBoxEvent{at: time.Unix(0, ns), kind: fields[1], args: fields[2:]})
1478 }
1479 return out
1480}
1481
1482func (a *largeBoxAgent) events(kind string) []largeBoxEvent {
1483 var out []largeBoxEvent
1484 for _, e := range a.all() {
1485 if e.kind == kind {
1486 out = append(out, e)
1487 }
1488 }
1489 return out
1490}
1491
1492func (a *largeBoxAgent) event(kind string, args ...string) (largeBoxEvent, bool) {
1493 for _, e := range a.events(kind) {
1494 match := true
1495 for i, want := range args {
1496 if e.arg(i) != want {
1497 match = false
1498 }
1499 }
1500 if match {
1501 return e, true
1502 }
1503 }
1504 return largeBoxEvent{}, false
1505}
1506
1507func (a *largeBoxAgent) waitEvent(t *testing.T, bound time.Duration, kind string, args ...string) largeBoxEvent {
1508 t.Helper()
1509 deadline := time.Now().Add(bound)
1510 for {
1511 if e, ok := a.event(kind, args...); ok {
1512 return e
1513 }
1514 if time.Now().After(deadline) {
1515 t.Fatalf("agent did not report %s %v within %s", kind, args, bound)
1516 }
1517 time.Sleep(10 * time.Millisecond)
1518 }
1519}
1520
1521// --- inside the agent process ---------------------------------------------------
1522
1523type largeBoxEvents struct {
1524 mu sync.Mutex
1525 f *os.File
1526}
1527
1528func (e *largeBoxEvents) log(kind string, args ...any) {
1529 var b strings.Builder
1530 fmt.Fprintf(&b, "%d %s", time.Now().UnixNano(), kind)
1531 for _, a := range args {
1532 fmt.Fprintf(&b, " %v", a)
1533 }
1534 b.WriteByte('\n')
1535 e.mu.Lock()
1536 defer e.mu.Unlock()
1537 _, _ = e.f.WriteString(b.String())
1538}
1539
1540// runLargeBoxAgent is the agent: runAgent's legacy-box startup over the
1541// fixture, then the edge, then runAgent's shutdown. It returns the exit
1542// code.
1543func runLargeBoxAgent(cfgPath string) int {
1544 raw, err := os.ReadFile(cfgPath)
1545 if err != nil {
1546 log.Printf("largebox agent: %v", err)
1547 return 2
1548 }
1549 var cfg largeBoxConfig
1550 if err := json.Unmarshal(raw, &cfg); err != nil {
1551 log.Printf("largebox agent: %v", err)
1552 return 2
1553 }
1554 f, err := os.OpenFile(cfg.Events, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
1555 if err != nil {
1556 log.Printf("largebox agent: %v", err)
1557 return 2
1558 }
1559 defer f.Close()
1560 ev := &largeBoxEvents{f: f}
1561
1562 agentCtx, cancelAgent := context.WithCancel(context.Background())
1563 defer cancelAgent()
1564 startupCtx, cancelStartup := context.WithCancel(agentCtx)
1565 defer cancelStartup()
1566
1567 dd := cfg.DataDir
1568 regPath := filepath.Join(dd, "registry.db")
1569 reg, err := registry.Open(regPath)
1570 if err != nil {
1571 log.Printf("largebox agent: %v", err)
1572 return 2
1573 }
1574 defer reg.Close()
1575
1576 control := instancectl.NewClient(cfg.Control)
1577 nestedOK := nestedGate(func() error { return instancefs.SelfCheck(dd) }, instancefs.MountNoSymfollow, control.Ready)
1578 started := make(chan struct{})
1579 backend := &largeBoxBackend{ev: ev, started: started}
1580 svc := activator.New(activator.Config{Bucket: "unused", Idle: time.Hour}, backend, reg)
1581 backend.svc = svc
1582 svc.Routes = reg
1583 svc.Startup = started
1584 openStore := func(context.Context) (release.Store, error) { return release.NewDirStore(cfg.StoreDir), nil }
1585
1586 startupDone := activatorStart{
1587 Ready: func() {
1588 ev.log("ready") // before the close, so no acquire can be stamped earlier
1589 close(started)
1590 },
1591 Sweep: func(ctx context.Context) error {
1592 sw, err := control.Sweep(ctx)
1593 if err != nil {
1594 log.Printf("agent: instance sweep: %v (nested wakes refuse until the channel answers)", err)
1595 } else if len(sw.Units) > 0 {
1596 log.Printf("agent: swept %d instance unit(s) left from a previous agent", len(sw.Units))
1597 }
1598 ev.log("swept", len(sw.Units), err == nil)
1599 return err
1600 },
1601 Recover: func(ctx context.Context, swept bool) *layout.Pass {
1602 pass := newLayoutPass(reg, svc, dd, nestedOK, swept)
1603 pass.SocketDir, pass.SocketsGID = cfg.SocketDir, -1 // the fixture's run dir; no carlos-sockets group
1604 intent := startupRecoveryIntent(ctx, dd, openStore, "largebox")
1605 recoverLayoutAtStart(ctx, pass, intent)
1606 ev.log("recovered", intent)
1607 return pass
1608 },
1609 Routes: reg.List,
1610 Route: reg.Get,
1611 Keys: largeBoxKeys{ev},
1612 Slot: svc.PrepareScheduledToken,
1613 Publish: armedLayoutPass.Store,
1614 // The checkpoint: when Start runs, every hold marker's host must
1615 // already be fenced again and every journal complete.
1616 Start: func() {
1617 held, _ := layout.HoldMarkers(dd)
1618 fenced := 0
1619 for _, h := range held {
1620 if svc.FenceGeneration(h) != 0 {
1621 fenced++
1622 }
1623 }
1624 journals, _ := layout.JournalHosts(dd)
1625 ev.log("activator-start", "held="+strconv.Itoa(len(held)), "fenced="+strconv.Itoa(fenced), "journals="+strconv.Itoa(len(journals)))
1626 svc.Start()
1627 },
1628 }.Run(startupCtx)
1629 go func() {
1630 <-startupDone
1631 ev.log("startup-done")
1632 }()
1633 // runAgent's worker boot pass, which waits on the same latch.
1634 go func() {
1635 select {
1636 case <-started:
1637 case <-startupCtx.Done():
1638 return
1639 }
1640 routes, err := reg.List()
1641 if err != nil {
1642 return
1643 }
1644 runWorkerBootPass(routes, func(rt registry.Route) error {
1645 err := svc.WakeWorker(rt)
1646 ev.log("worker-boot", rt.Host, err == nil)
1647 return err
1648 })
1649 }()
1650
1651 // Admissions from inside the agent that do not pass runAgent's own
1652 // gates, so they meet the activator's latch on their own: a worker
1653 // wake as an operator command or the sidecar-wake leg makes it, and
1654 // one scheduler tick through the real runner and deliverer with a
1655 // delivery already due.
1656 if rt, err := reg.Get(largeBoxWorker); err == nil && rt != nil {
1657 go func() {
1658 ev.log("worker-direct-begin", rt.Host)
1659 err := svc.WakeWorker(*rt)
1660 ev.log("worker-direct", rt.Host, err == nil)
1661 }()
1662 }
1663 runner := &schedule.Runner{
1664 Reg: reg, Deliver: &activatorDeliverer{svc: svc}, Token: scheduleToken(dd, svc, reg),
1665 Now: time.Now, Timeout: largeBoxLatchBound + 15*time.Second, Box: "largebox", Build: "test",
1666 }
1667 runner.Tick(agentCtx) // delivers in the background, and returns
1668 ev.log("tick-done")
1669
1670 edge.RunWithActivatorReady([]string{"-registry", regPath, "-acme-cache", cfg.AcmeDir, "-http", cfg.HTTP, "-https", cfg.HTTPS},
1671 largeBoxActivator{Service: svc, ev: ev}, edge.NewCache(edge.DefaultCacheMaxEntries, edge.DefaultCacheMaxBodyBytes),
1672 func(e *edge.Edge) {
1673 reload := func() { _ = e.Reload() }
1674 layoutReload.Store(&reload)
1675 })
1676
1677 // runAgent's shutdown, in its order.
1678 cancelStartup()
1679 joinCtx, joinCancel := context.WithTimeout(context.Background(), largeBoxJoinBound)
1680 began := time.Now()
1681 joined := joinAgentStartup(joinCtx, startupDone)
1682 joinCancel()
1683 ev.log("joined", joined, time.Since(began))
1684 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
1685 svc.Shutdown(ctx)
1686 cancel()
1687 cancelAgent()
1688
1689 deadline := time.Now().Add(2 * time.Second)
1690 for {
1691 left := largeBoxStartupGoroutines()
1692 if len(left) == 0 {
1693 ev.log("leak-free")
1694 return 0
1695 }
1696 if time.Now().After(deadline) {
1697 log.Printf("largebox agent: %d goroutine(s) outlived shutdown:\n\n%s", len(left), strings.Join(left, "\n\n"))
1698 ev.log("leak", len(left))
1699 return 0
1700 }
1701 time.Sleep(20 * time.Millisecond)
1702 }
1703}
1704
1705// largeBoxStartupGoroutines lists every goroutine but this one that runs,
1706// or was started by, the startup sequence, the layout pass, the control
1707// client, the activator, the scheduler, or this agent's own fakes, and
1708// every server connection still open (a request held at the latch is
1709// answered only once the activator shuts down, after the edge has stopped
1710// waiting for it). The edge's process-lifetime loops (its registry watch,
1711// its SIGHUP reader) are not startup's and are left alone.
1712func largeBoxStartupGoroutines() []string {
1713 buf := make([]byte, 8<<20)
1714 n := runtime.Stack(buf, true)
1715 blocks := strings.Split(string(buf[:n]), "\n\n")
1716 markers := []string{
1717 "host.activatorStart", "host.startActivator", "host.backfillInstanceKeys", "host.keyBackfillInSlot",
1718 "host.recoverLayoutAtStart", "host.startupRecoveryIntent", "host.runWorkerBootPass",
1719 "host.runLargeBoxAgent", "host.(*largeBoxBackend)", "host.largeBoxActivator", "host.(*activatorDeliverer)",
1720 "layout.(*Pass)", "instancectl.(*Client)", "activator.(*Service)", "schedule.(*Runner)",
1721 "net/http.(*conn).serve",
1722 }
1723 var left []string
1724 for _, g := range blocks[1:] { // [0] is the calling goroutine
1725 for _, m := range markers {
1726 if strings.Contains(g, m) {
1727 left = append(left, g)
1728 break
1729 }
1730 }
1731 }
1732 return left
1733}
1734
1735// largeBoxActivator is the activator as the edge sees it, reporting each
1736// instance request as it reaches it.
1737type largeBoxActivator struct {
1738 *activator.Service
1739 ev *largeBoxEvents
1740}
1741
1742func (a largeBoxActivator) ServeHTTP(w http.ResponseWriter, r *http.Request, rt registry.Route, proxy http.Handler) bool {
1743 if rt.Kind == "instance" {
1744 a.ev.log("enter", rt.Host, rt.Socket)
1745 }
1746 return a.Service.ServeHTTP(w, r, rt, proxy)
1747}
1748
1749// largeBoxBackend is activator.Backend with no S3 and no processes: a wake
1750// serves an app on the row's socket that names the host and the socket it
1751// was started at, and a worker reports ready at once, as a child does
1752// over the agent socket. It records every acquire, start and request
1753// served, and any acquire the startup latch should have prevented.
1754type largeBoxBackend struct {
1755 ev *largeBoxEvents
1756 started <-chan struct{}
1757 svc *activator.Service
1758}
1759
1760func (b *largeBoxBackend) Acquire(_ context.Context, rt registry.Route) (activator.Lease, error) {
1761 select {
1762 case <-b.started:
1763 default:
1764 b.ev.log("violation", "acquire-before-ready", rt.Host)
1765 }
1766 b.ev.log("acquire", rt.Host)
1767 return nopLease{}, nil
1768}
1769
1770func (b *largeBoxBackend) Restore(context.Context, registry.Route) (bool, error) { return false, nil }
1771func (b *largeBoxBackend) Seed(context.Context, registry.Route) error { return nil }
1772
1773func (b *largeBoxBackend) Start(_ context.Context, rt registry.Route) (activator.Proc, string, error) {
1774 b.ev.log("start", rt.Host, rt.Socket)
1775 if err := os.MkdirAll(filepath.Dir(rt.Socket), 0o750); err != nil {
1776 return nil, "", err
1777 }
1778 ln, err := net.Listen("unix", rt.Socket)
1779 if err != nil {
1780 return nil, "", err
1781 }
1782 body := "host=" + rt.Host + " socket=" + rt.Socket
1783 p := &largeBoxProc{done: make(chan struct{})}
1784 p.srv = &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1785 b.ev.log("served", rt.Host, r.URL.Path)
1786 io.WriteString(w, body)
1787 })}
1788 go func() { defer close(p.done); _ = p.srv.Serve(ln) }()
1789 return p, "", nil
1790}
1791
1792func (b *largeBoxBackend) NextDue(context.Context, registry.Route) (int64, error) { return 0, nil }
1793func (b *largeBoxBackend) HasSidecar(registry.Route) bool { return false }
1794func (b *largeBoxBackend) StartSidecar(context.Context, registry.Route) (activator.Proc, error) {
1795 return nil, errors.New("largeBoxBackend: no co-spawned sidecars on this box")
1796}
1797
1798func (b *largeBoxBackend) StartWorker(_ context.Context, rt registry.Route, _ map[string]string) (activator.Proc, string, error) {
1799 b.ev.log("worker-start", rt.Host)
1800 go b.svc.NoteWorkerReady(rt.Host, "")
1801 return &largeBoxWorkerProc{}, "", nil
1802}
1803
1804type largeBoxProc struct {
1805 srv *http.Server
1806 done chan struct{}
1807 exited atomic.Bool
1808}
1809
1810func (p *largeBoxProc) Stop(context.Context) error {
1811 _ = p.srv.Close()
1812 <-p.done
1813 p.exited.Store(true)
1814 return nil
1815}
1816
1817func (p *largeBoxProc) Exited() bool { return p.exited.Load() }
1818
1819type largeBoxWorkerProc struct{ exited atomic.Bool }
1820
1821func (p *largeBoxWorkerProc) Stop(context.Context) error { p.exited.Store(true); return nil }
1822func (p *largeBoxWorkerProc) Exited() bool { return p.exited.Load() }
1823
1824// largeBoxKeys is the exec backend's key writer as the key pass sees it:
1825// every row it is handed already has its key.
1826type largeBoxKeys struct{ ev *largeBoxEvents }
1827
1828func (k largeBoxKeys) BackfillInstanceKey(_ context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
1829 k.ev.log("key", rt.Host)
1830 return activator.InstanceKeyPresent, nil
1831}
1832