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 "fmt"
6 "path/filepath"
7 "slices"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15// genFakeBackend is fakeBackend with the generation capability. A spawn
16// with perGen "binds" /gen/<n>.sock (nothing is bound: the fake proc stands
17// in for the process) and NextDueVia records the socket it was asked to
18// dial, so a test can see which generation a probe reached.
19type genFakeBackend struct {
20 *fakeBackend
21 gmu sync.Mutex
22 perGens []bool
23 dueSocks []string
24}
25
26func (b *genFakeBackend) StartGen(ctx context.Context, rt registry.Route, n uint64, perGen bool) (Proc, Gen, error) {
27 b.gmu.Lock()
28 b.perGens = append(b.perGens, perGen)
29 b.gmu.Unlock()
30 proc, ver, err := b.fakeBackend.Start(ctx, rt)
31 if err != nil {
32 return proc, Gen{}, err
33 }
34 sock := rt.Socket
35 if perGen {
36 sock = fmt.Sprintf("/gen/%d.sock", n)
37 }
38 return proc, Gen{N: n, Socket: sock, Control: sock + ".control", Build: ver}, nil
39}
40
41func (b *genFakeBackend) NextDueVia(ctx context.Context, rt registry.Route, socket string) (int64, error) {
42 b.gmu.Lock()
43 b.dueSocks = append(b.dueSocks, socket)
44 b.gmu.Unlock()
45 return b.fakeBackend.NextDue(ctx, rt)
46}
47
48func (b *genFakeBackend) seen() (perGens []bool, dueSocks []string) {
49 b.gmu.Lock()
50 defer b.gmu.Unlock()
51 return slices.Clone(b.perGens), slices.Clone(b.dueSocks)
52}
53
54// genActivator is testActivator over genFakeBackend, with the switch set
55// for the test's duration BEFORE New reads it.
56func genActivator(t *testing.T, on bool) (*Service, *genFakeBackend) {
57 t.Helper()
58 withGenerations(t, on)
59 gb := &genFakeBackend{fakeBackend: &fakeBackend{execVerFn: func() string { return "v7" }}}
60 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
61 if err != nil {
62 t.Fatal(err)
63 }
64 return New(Config{Bucket: "test-bucket", Idle: time.Hour}, gb, reg), gb
65}
66
67// genTestRoute is testRoute with its canonical socket in the test's own
68// temp dir. With generations on the activator links a route's canonical
69// path to its current generation (Task 4), and testRoute's shared
70// /tmp/nope.sock must never become a real link.
71func genTestRoute(t *testing.T, host string) registry.Route {
72 t.Helper()
73 rt := testRoute(host)
74 rt.Socket = filepath.Join(t.TempDir(), "c.sock")
75 return rt
76}
77
78// stopAndWait bounces host's awake instance and waits for the stop to finish.
79func stopAndWait(t *testing.T, a *Service, host string) {
80 t.Helper()
81 if !a.Bounce(host) {
82 t.Fatalf("Bounce(%s) started no stop", host)
83 }
84 waitFor(t, host+" to stop", func() bool { return !a.Owns(host) })
85}
86
87func TestAppSocketNamesTheCurrentGeneration(t *testing.T) {
88 a, gb := genActivator(t, true)
89 rt := genTestRoute(t, "cur.example.com")
90 if _, ok := a.AppSocket(rt.Host); ok {
91 t.Fatal("AppSocket answered for an instance that never woke")
92 }
93 arrangeAwake(t, a, rt)
94 g, ok := a.ServingGen(rt.Host)
95 if !ok || g.N == 0 {
96 t.Fatalf("ServingGen = %+v, %v; want the awake generation", g, ok)
97 }
98 sock, ok := a.AppSocket(rt.Host)
99 if !ok || sock != fmt.Sprintf("/gen/%d.sock", g.N) || g.Socket != sock || g.Build != "v7" {
100 t.Fatalf("AppSocket = %q (%v), ServingGen = %+v; want /gen/%d.sock and build v7", sock, ok, g, g.N)
101 }
102 if n, ok := a.CacheGen(rt.Host); !ok || n != g.N {
103 t.Fatalf("CacheGen = %d, %v; want %d", n, ok, g.N)
104 }
105 if perGens, _ := gb.seen(); !slices.Equal(perGens, []bool{true}) {
106 t.Fatalf("StartGen perGen = %v, want [true]", perGens)
107 }
108}
109
110// Flag off: one generation per entry, on the canonical socket.
111func TestAppSocketIsTheCanonicalSocketWithGenerationsOff(t *testing.T) {
112 a, gb := genActivator(t, false)
113 rt := genTestRoute(t, "off.example.com")
114 arrangeAwake(t, a, rt)
115 if sock, ok := a.AppSocket(rt.Host); !ok || sock != rt.Socket {
116 t.Fatalf("AppSocket = %q, %v; want the canonical %q", sock, ok, rt.Socket)
117 }
118 if perGens, _ := gb.seen(); !slices.Equal(perGens, []bool{false}) {
119 t.Fatalf("StartGen perGen = %v, want [false]", perGens)
120 }
121}
122
123// A backend without the capability (UnitBackend, older doubles) still gets a
124// numbered, canonical handle.
125func TestPlainBackendGetsTheCanonicalHandle(t *testing.T) {
126 withGenerations(t, true)
127 a, _, _, _ := testActivator(t, time.Hour)
128 rt := genTestRoute(t, "plain.example.com")
129 arrangeAwake(t, a, rt)
130 g, ok := a.ServingGen(rt.Host)
131 if !ok || g.Socket != rt.Socket || g.N == 0 {
132 t.Fatalf("ServingGen = %+v, %v; want the canonical socket and a number", g, ok)
133 }
134}
135
136// Spec §12 "Generation identity": a stop, then a crash, then a wake never
137// reuses a generation number. CacheGen keeps the last number while the
138// instance sleeps.
139func TestGenerationNumbersNeverRepeatAcrossStopCrashAndWake(t *testing.T) {
140 a, gb := genActivator(t, true)
141 rt := genTestRoute(t, "ident.example.com")
142 arrangeAwake(t, a, rt)
143 g1, _ := a.ServingGen(rt.Host)
144
145 stopAndWait(t, a, rt.Host)
146 if _, ok := a.AppSocket(rt.Host); ok {
147 t.Fatal("AppSocket answered for a sleeping instance")
148 }
149 if n, ok := a.CacheGen(rt.Host); !ok || n != g1.N {
150 t.Fatalf("CacheGen while asleep = %d, %v; want the last current generation %d", n, ok, g1.N)
151 }
152
153 arrangeAwake(t, a, rt)
154 g2, _ := a.ServingGen(rt.Host)
155 if g2.N <= g1.N {
156 t.Fatalf("wake after a stop took generation %d after %d", g2.N, g1.N)
157 }
158
159 gb.fakeBackend.mu.Lock()
160 crashed := gb.fakeBackend.procs[len(gb.fakeBackend.procs)-1]
161 gb.fakeBackend.mu.Unlock()
162 crashed.exit()
163 arrangeAwake(t, a, rt) // finds the dead child, stops it, wakes a fresh one
164 g3, _ := a.ServingGen(rt.Host)
165 if g3.N <= g2.N {
166 t.Fatalf("wake after a crash took generation %d after %d", g3.N, g2.N)
167 }
168 if n, _ := a.CacheGen(rt.Host); n != g3.N {
169 t.Fatalf("CacheGen = %d, want %d", n, g3.N)
170 }
171}
172
173// The sleep-time next-due probe dials AppSocket: the generation socket with
174// the switch on, the canonical one with it off.
175func TestNextDueAtStopDialsTheCurrentGeneration(t *testing.T) {
176 for _, on := range []bool{true, false} {
177 t.Run(fmt.Sprintf("generations=%v", on), func(t *testing.T) {
178 a, gb := genActivator(t, on)
179 rt := genTestRoute(t, "due.example.com")
180 arrangeAwake(t, a, rt)
181 g, _ := a.ServingGen(rt.Host)
182 stopAndWait(t, a, rt.Host)
183 want := rt.Socket
184 if on {
185 want = fmt.Sprintf("/gen/%d.sock", g.N)
186 }
187 if _, dueSocks := gb.seen(); !slices.Equal(dueSocks, []string{want}) {
188 t.Fatalf("next-due probe dialled %v, want [%s]", dueSocks, want)
189 }
190 })
191 }
192}
193
194// A retire hook hears about a generation that stopped being current, with
195// the switch on, and never with it off (the canonical path is shared by every
196// generation then).
197func TestGenRetiredFiresAtStopOnlyWithGenerationsOn(t *testing.T) {
198 for _, on := range []bool{true, false} {
199 t.Run(fmt.Sprintf("generations=%v", on), func(t *testing.T) {
200 a, _ := genActivator(t, on)
201 var mu sync.Mutex
202 var got []string
203 a.OnGenRetired(func(host, socket string) {
204 mu.Lock()
205 got = append(got, host+" "+socket)
206 mu.Unlock()
207 })
208 rt := genTestRoute(t, "ret.example.com")
209 arrangeAwake(t, a, rt)
210 g, _ := a.ServingGen(rt.Host)
211 stopAndWait(t, a, rt.Host)
212 mu.Lock()
213 defer mu.Unlock()
214 var want []string
215 if on {
216 want = []string{rt.Host + " " + g.Socket}
217 }
218 if !slices.Equal(got, want) {
219 t.Fatalf("retire hooks = %v, want %v", got, want)
220 }
221 })
222 }
223}
224
225// A worker binds nothing, so it has no app socket for traffic.
226func TestAppSocketIsFalseForAWorker(t *testing.T) {
227 a, _ := genActivator(t, true)
228 rt := testWorkerRoute("bot.x.internal")
229 if err := a.WakeWorker(rt); err != nil {
230 t.Fatalf("WakeWorker: %v", err)
231 }
232 waitFor(t, "the worker to wake", func() bool { return a.Owns(rt.Host) })
233 if sock, ok := a.AppSocket(rt.Host); ok {
234 t.Fatalf("AppSocket(worker) = %q, want false", sock)
235 }
236}
237