| 1 | package activator |
| 2 | |
| 3 | import ( |
| 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. |
| 19 | type genFakeBackend struct { |
| 20 | *fakeBackend |
| 21 | gmu sync.Mutex |
| 22 | perGens []bool |
| 23 | dueSocks []string |
| 24 | } |
| 25 | |
| 26 | func (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 | |
| 41 | func (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 | |
| 48 | func (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. |
| 56 | func 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. |
| 71 | func 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. |
| 79 | func 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 | |
| 87 | func 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. |
| 111 | func 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. |
| 125 | func 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. |
| 139 | func 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. |
| 175 | func 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). |
| 197 | func 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. |
| 226 | func 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 | |