carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "path/filepath"
7 "sync"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15func TestWorkerGenerations(t *testing.T) {
16 for _, on := range []bool{true, false} {
17 a, b, _, _ := testActivator(t, time.Hour)
18 a.gens = on
19 old := sidecarSettle
20 sidecarSettle = 20 * time.Millisecond
21 t.Cleanup(func() { sidecarSettle = old })
22 rt := testWorkerRoute("bot.example.com")
23 if err := a.WakeWorker(rt); err != nil {
24 t.Fatal(err)
25 }
26 first, members := a.genStateForTest(rt.Host)
27 if on != (first.N != 0) || first.Socket != "" || len(members) != 0 {
28 t.Fatalf("gens %v: worker generation %+v, members %v", on, first, members)
29 }
30 b.mu.Lock()
31 b.workerStartDelay = 200 * time.Millisecond // hold the respawn open
32 b.mu.Unlock()
33 b.lastWorker().crash()
34 a.sweepOnce() // a crashed hibernating worker respawns in place
35 waitFor(t, "the respawn to begin", func() bool { return b.workerEnters() == 2 })
36 _, members = a.genStateForTest(rt.Host)
37 if on != (len(members) == 1) {
38 t.Fatalf("gens %v: members during the respawn = %v", on, members)
39 }
40 waitFor(t, "the respawn to finish", func() bool { return b.workerStarts() == 2 })
41 waitFor(t, "the member to leave", func() bool { _, m := a.genStateForTest(rt.Host); return len(m) == 0 })
42 if on {
43 if second, _ := a.genStateForTest(rt.Host); second.N <= first.N || !second.Worker {
44 t.Fatalf("respawned worker generation %+v, want a Worker above %d", second, first.N)
45 }
46 }
47 }
48}
49
50// StartWorker spawns every worker into the flat host cgroup, whatever the
51// row's layout, so KillHost proves that cgroup for a nested-looking row too.
52// Here the cgroup is a fixture, not cgroup v2, so the proof is refused.
53// Answering nil for a nested layout skipped the proof for a worker that
54// runs in carlos-exec@<host> all the same.
55func TestKillHostProvesANestedLayoutWorkersHostCgroup(t *testing.T) {
56 host := "nw.example.com"
57 b, _ := delegatedBackend(t, host)
58 rd := t.TempDir()
59 b.RunDirOverride = rd
60 rt := registry.Route{Host: host, Kind: "sidecar", DBPath: provision.NestedDBPath(b.DataDir, host),
61 Socket: filepath.Join(rd, "instances", provision.InstanceID(host), "app.sock")}
62 if err := b.KillHost(context.Background(), rt); err == nil {
63 t.Fatal("KillHost answered a nested-layout worker's host cgroup as proven without proving it")
64 }
65}
66
67// hostKillBackend is fakeBackend plus KillHost, failing while bad.
68type hostKillBackend struct {
69 *fakeBackend
70 hmu sync.Mutex
71 bad bool
72 kills int
73}
74
75func (h *hostKillBackend) KillHost(context.Context, registry.Route) error {
76 h.hmu.Lock()
77 defer h.hmu.Unlock()
78 h.kills++
79 if h.bad {
80 return errors.New("the host cgroup still holds a process")
81 }
82 return nil
83}
84
85func (h *hostKillBackend) killCount() int {
86 h.hmu.Lock()
87 defer h.hmu.Unlock()
88 return h.kills
89}
90
91func (h *hostKillBackend) heal() {
92 h.hmu.Lock()
93 h.bad = false
94 h.hmu.Unlock()
95}
96
97// Spec §7.4, §7.9: a worker runs in its host cgroup (plan 2 D4), so that is
98// what proves it terminated, not a g<n> it never ran in (whose absence
99// KillGen counts as empty). While the host cgroup still holds a process
100// (a descendant the worker forked), a terminal stop keeps the lease and a
101// respawn execs nothing beside it.
102func TestWorkerTerminationProvesItsHostCgroup(t *testing.T) {
103 t.Run("a terminal stop keeps the lease", func(t *testing.T) {
104 a, b, _, _ := testActivator(t, time.Hour)
105 a.gens = true
106 hk := &hostKillBackend{fakeBackend: b, bad: true}
107 a.backend = hk
108 rt := testWorkerRoute("bot.example.com")
109 if err := a.WakeWorker(rt); err != nil {
110 t.Fatal(err)
111 }
112 rep := b.lastRep()
113 a.Bounce(rt.Host)
114 // The stop pends only once it has decided the lease stays: its proof
115 // (KillHost) ran and failed, and the release was not made.
116 waitFor(t, "the stop to pend", func() bool {
117 a.mu.Lock()
118 defer a.mu.Unlock()
119 e := a.entries[rt.Host]
120 return e != nil && e.pendingStop
121 })
122 if n := hk.killCount(); n == 0 {
123 t.Fatal("the stop pended without asking the host cgroup's proof")
124 }
125 if rep.isClosed() {
126 t.Fatal("the lease was released while the worker's host cgroup still held a process")
127 }
128 hk.heal()
129 a.sweepOnce()
130 waitFor(t, "the lease release", rep.isClosed)
131 })
132 t.Run("a respawn with no old handle still proves the host cgroup", func(t *testing.T) {
133 a, b, _, _ := testActivator(t, time.Hour)
134 a.gens = true
135 hk := &hostKillBackend{fakeBackend: b}
136 a.backend = hk
137 rt := testWorkerRoute("bot3.example.com")
138 if err := a.WakeWorker(rt); err != nil {
139 t.Fatal(err)
140 }
141 hk.hmu.Lock()
142 hk.bad = true
143 hk.hmu.Unlock()
144 a.mu.Lock()
145 // main's signature; once plan 4 Task 9 has merged, pass nil as its
146 // fourth (onto) argument.
147 a.respawnWorkerLocked(a.entries[rt.Host], nil, "test: a restart with no old handle", nil)
148 a.mu.Unlock()
149 waitFor(t, "the respawn to finish", func() bool { return !a.entryRespawning(rt.Host) })
150 if got := b.workerStarts(); got != 1 {
151 t.Fatalf("worker starts = %d: a nil-stopOld respawn skipped the host cgroup proof", got)
152 }
153 })
154 t.Run("a failed worker wake keeps its lease", func(t *testing.T) {
155 old := sidecarSettle
156 sidecarSettle = 20 * time.Millisecond
157 t.Cleanup(func() { sidecarSettle = old })
158 a, b, _, _ := testActivator(t, time.Hour)
159 a.gens = true
160 hk := &hostKillBackend{fakeBackend: b, bad: true}
161 a.backend = hk
162 b.mu.Lock()
163 b.workerDeadOnStart = true // the wake fails its settle
164 b.mu.Unlock()
165 if err := a.WakeWorker(testWorkerRoute("bot4.example.com")); err == nil {
166 t.Fatal("a worker dead on start woke")
167 }
168 // The lease goes back by Release, or by closing the replica when the
169 // wake had one (cleanupFailedWake).
170 given := func() bool {
171 b.mu.Lock()
172 released, reps := b.releases, append([]*fakeRep(nil), b.reps...)
173 b.mu.Unlock()
174 if released > 0 {
175 return true
176 }
177 for _, r := range reps {
178 if r.isClosed() {
179 return true
180 }
181 }
182 return false
183 }
184 if given() {
185 t.Fatal("a failed worker wake gave its lease back over an unproven host cgroup")
186 }
187 hk.heal()
188 a.sweepOnce()
189 waitFor(t, "the lease given back", given)
190 })
191 t.Run("a respawn execs nothing beside the old child", func(t *testing.T) {
192 old := sidecarSettle
193 sidecarSettle = 20 * time.Millisecond
194 t.Cleanup(func() { sidecarSettle = old })
195 a, b, clk, _ := testActivator(t, time.Hour)
196 a.gens = true
197 hk := &hostKillBackend{fakeBackend: b}
198 a.backend = hk
199 rt := testWorkerRoute("bot2.example.com")
200 if err := a.WakeWorker(rt); err != nil {
201 t.Fatal(err)
202 }
203 hk.hmu.Lock()
204 hk.bad = true
205 hk.hmu.Unlock()
206 b.lastWorker().crash()
207 kills := hk.killCount()
208 a.sweepOnce() // the respawn retires the crashed child, and its proof fails
209 waitFor(t, "the respawn to finish", func() bool { return !a.entryRespawning(rt.Host) })
210 if got := hk.killCount(); got <= kills {
211 t.Fatalf("host cgroup kills = %d (was %d): the respawn never asked the old child's proof", got, kills)
212 }
213 if got := b.workerStarts(); got != 1 {
214 t.Fatalf("worker starts = %d: a respawn exec'd beside an unproven old child", got)
215 }
216 // Positive control: once the proof succeeds, the next sweep's
217 // crash restart respawns the worker.
218 hk.heal()
219 clk.advance(sidecarRestartMax + time.Second) // past the crash restart's backoff
220 a.sweepOnce()
221 waitFor(t, "the respawn after the proof healed", func() bool { return b.workerStarts() == 2 && !a.entryRespawning(rt.Host) })
222 })
223}
224
225// Index P5/P6: every exec asks the gate's ownership check, whatever the
226// switch says. A respawn whose lease has been lost execs nothing, with
227// generations on or off.
228func TestWorkerRespawnAsksOwnershipAtItsExec(t *testing.T) {
229 t.Parallel()
230 for _, on := range []bool{true, false} {
231 a, b, _, _ := testActivator(t, time.Hour)
232 a.gens = on
233 rt := testWorkerRoute("bot5.example.com")
234 if err := a.WakeWorker(rt); err != nil {
235 t.Fatal(err)
236 }
237 b.lastRep().setErr(errors.New("renewal failed"))
238 a.mu.Lock()
239 a.respawnWorkerLocked(a.entries[rt.Host], nil, "test: the lease is lost", nil)
240 a.mu.Unlock()
241 waitFor(t, "the respawn to finish", func() bool { return !a.entryRespawning(rt.Host) })
242 if got := b.workerStarts(); got != 1 {
243 t.Fatalf("gens %v: worker starts = %d: a respawn exec'd without the lease", on, got)
244 }
245 if _, members := a.genStateForTest(rt.Host); len(members) != 0 {
246 t.Fatalf("gens %v: members after the refused respawn = %v", on, members)
247 }
248 }
249}
250