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
5import (
6 "context"
7 "errors"
8 "os"
9 "path/filepath"
10 "reflect"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/rootstate"
18 "golang.org/x/sys/unix"
19)
20
21func servingCollectionFixture(t *testing.T, legacy bool) (*servingParkDiskFixture, servingParkAuthorization, servingParkAuthorization) {
22 t.Helper()
23 f := newServingParkDiskFixture(t)
24 f.put("", "old served bytes")
25 if !legacy {
26 f.put(".gen", "G10")
27 }
28 old, receipts := f.evidence(10, legacy, "started")
29 if _, err := f.park(old, receipts); err != nil {
30 t.Fatal(err)
31 }
32 f.complete(old)
33 f.put("", "new served bytes")
34 f.put(".gen", "G11")
35 next, receipts := f.evidence(11, false, "started")
36 if _, err := f.park(next, receipts); err != nil {
37 t.Fatal(err)
38 }
39 f.complete(next)
40 return f, old, next
41}
42
43func TestServingParkCollectionWindowAndReplay(t *testing.T) {
44 f, old, next := servingCollectionFixture(t, false)
45 if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
46 t.Fatal(err)
47 }
48 r, err := f.ledger.read(old.ID)
49 if err != nil || !r.CollectAfter.Equal(f.now.Add(hostbox.JournalReceiptRetention)) {
50 t.Fatalf("missing retention deadline: %+v %v", r, err)
51 }
52 before, _ := f.disk.Read("retirement-" + old.ID + ".json")
53 for _, now := range []time.Time{f.now.Add(-time.Hour), r.CollectAfter.Add(-time.Nanosecond)} {
54 f.now = now
55 if err = f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
56 t.Fatal(err)
57 }
58 after, err := f.disk.Read("retirement-" + old.ID + ".json")
59 if err != nil || string(before) != string(after) {
60 t.Fatal("deadline refreshed or record collected early", err)
61 }
62 }
63 // A restarted ledger uses only the persisted deadline.
64 restarted := *f.ledger
65 f.now = r.CollectAfter
66 if err = f.disk.Write("retirement-"+old.ID+".lock", nil); err != nil {
67 t.Fatal(err)
68 }
69 if err = restarted.recoverCompleted(t.Context(), old.ID); err != nil {
70 t.Fatal(err)
71 }
72 for _, suffix := range []string{".json", ".lock"} {
73 if _, err = f.disk.Read("retirement-" + old.ID + suffix); !errors.Is(err, os.ErrNotExist) {
74 t.Fatal("collected file survived", suffix, err)
75 }
76 }
77 f.retained(next, "new served bytes", r.ReapAfter)
78 // Even a fresh transport nonce must not rebuild an old collected intent
79 // from the current source. Root additionally checks the live claim.
80 f.put("", "current database")
81 f.put(".gen", "G10")
82 if _, err = f.park(old, r.Receipts); !errors.Is(err, hostbox.ErrChanged) {
83 t.Fatal("collected replay regained authority", err)
84 }
85 if _, err = f.ledger.read(old.ID); !errors.Is(err, os.ErrNotExist) {
86 t.Fatal("replay recreated retirement", err)
87 }
88 b, err := os.ReadFile(provision.NestedDBPath(f.data, f.base.Host))
89 if err != nil || string(b) != "current database" {
90 t.Fatal("refused replay changed live source", err)
91 }
92}
93
94func TestServingParkCollectionPreservesSlotReferences(t *testing.T) {
95 for _, reference := range []string{"ID", "Pending", "Last"} {
96 t.Run(reference, func(t *testing.T) {
97 f, old, _ := servingCollectionFixture(t, false)
98 if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
99 t.Fatal(err)
100 }
101 r, _ := f.ledger.read(old.ID)
102 f.now = r.CollectAfter.Add(time.Hour)
103 slot, err := f.ledger.slot(old)
104 if err != nil {
105 t.Fatal(err)
106 }
107 switch reference {
108 case "ID":
109 slot.ID = old.ID
110 case "Pending":
111 slot.Pending = old.ID
112 case "Last":
113 slot.Last = old.ID
114 }
115 if err = f.ledger.withNamespace(t.Context(), func() error { return f.ledger.collectClosedLocked(t.Context(), &r, slot) }); err != nil {
116 t.Fatal(err)
117 }
118 if _, err = f.ledger.read(old.ID); err != nil {
119 t.Fatal("collected a referenced retirement", err)
120 }
121 })
122 }
123}
124
125func TestServingParkCollectionKeepsLegacyConsumption(t *testing.T) {
126 f, old, next := servingCollectionFixture(t, true)
127 consumed, err := f.ledger.legacyConsumption(old.Successor)
128 if err != nil || consumed == nil {
129 t.Fatal(err)
130 }
131 if err = f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
132 t.Fatal(err)
133 }
134 r, _ := f.ledger.read(old.ID)
135 f.now = r.CollectAfter
136 if err = f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
137 t.Fatal(err)
138 }
139 after, err := f.ledger.legacyConsumption(old.Successor)
140 if err != nil || after == nil || !after.Collected {
141 t.Fatal("collection lost the permanent refusal", err)
142 }
143 after.Collected = false
144 if !reflect.DeepEqual(after, consumed) {
145 t.Fatal("collection changed legacy evidence")
146 }
147 f.retained(next, "new served bytes", r.ReapAfter)
148}
149
150func TestServingParkCollectionLegacyRootRefusesRecapture(t *testing.T) {
151 f, _ := newServingLegacyRootFixture(t, false)
152 a, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{})
153 if err != nil {
154 t.Fatal(err)
155 }
156 c, err := f.p.ledger.legacyConsumption(f.current)
157 if err != nil || c == nil {
158 t.Fatal(err)
159 }
160 c.Collected = true
161 if err = f.p.ledger.write(servingLegacyKey("legacy", f.current), c); err != nil {
162 t.Fatal(err)
163 }
164 if err = f.disk.Remove("retirement-" + a.ID + ".json"); err != nil {
165 t.Fatal(err)
166 }
167 if _, err = f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); !errors.Is(err, hostbox.ErrChanged) {
168 t.Fatal("root accepted collected legacy source", err)
169 }
170 if _, err = f.p.ledger.read(a.ID); !errors.Is(err, os.ErrNotExist) {
171 t.Fatal("root recreated collected record", err)
172 }
173}
174
175func TestServingParkCollectionLegacyConsumptionBeforeIntentStillResumes(t *testing.T) {
176 f, _ := newServingLegacyRootFixture(t, false)
177 store := &servingCollectionFaultStore{Store: f.disk}
178 f.p.ledger.state = store
179 store.write = func(name string, body []byte) error {
180 if strings.HasPrefix(name, "retirement-") {
181 return unix.EIO
182 }
183 return f.disk.Write(name, body)
184 }
185 if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); !errors.Is(err, unix.EIO) {
186 t.Fatal("intent fault was not reached", err)
187 }
188 c, err := f.p.ledger.legacyConsumption(f.current)
189 if err != nil || c == nil || c.Collected {
190 t.Fatal("pending consumption unavailable", err)
191 }
192 if _, err := f.p.ledger.read(c.Authorization.ID); !errors.Is(err, os.ErrNotExist) {
193 t.Fatal("intent was saved", err)
194 }
195 f.p.ledger.state = f.disk
196 if _, err := f.p.parkLegacyServingDatabase(t.Context(), f.conn, f.current, servingLegacyPredecessor{}); err != nil {
197 t.Fatal("interrupted first capture did not resume", err)
198 }
199}
200
201func TestServingParkCollectionRejectsInvalidDeadline(t *testing.T) {
202 f, old, _ := servingCollectionFixture(t, false)
203 r, err := f.ledger.read(old.ID)
204 if err != nil {
205 t.Fatal(err)
206 }
207 r.CollectAfter = f.now.Add(hostbox.JournalReceiptRetention)
208 for _, change := range []string{"incomplete", "open", "event-present", "namespace-unknown", "early"} {
209 t.Run(change, func(t *testing.T) {
210 bad := r
211 switch change {
212 case "incomplete":
213 bad.Complete = false
214 case "open":
215 bad.Closed = false
216 case "event-present":
217 bad.EventRemoved = false
218 case "namespace-unknown":
219 bad.Namespace = servingArtifactDirectory{}
220 case "early":
221 bad.CollectAfter = bad.ParkedAt.Add(hostbox.JournalReceiptRetention - time.Nanosecond)
222 }
223 if err := f.ledger.save(bad); !errors.Is(err, hostbox.ErrChanged) {
224 t.Fatal("invalid collection authority accepted", err)
225 }
226 })
227 }
228}
229
230type servingCollectionFaultStore struct {
231 rootstate.Store
232 remove func(string) error
233 write func(string, []byte) error
234}
235
236func (s *servingCollectionFaultStore) Remove(name string) error {
237 if s.remove != nil {
238 return s.remove(name)
239 }
240 return s.Store.Remove(name)
241}
242
243func (s *servingCollectionFaultStore) Write(name string, body []byte) error {
244 if s.write != nil {
245 return s.write(name, body)
246 }
247 return s.Store.Write(name, body)
248}
249
250func TestServingParkCollectionInterruptedWrites(t *testing.T) {
251 for _, boundary := range []string{"deadline-before", "deadline-after", "consumption-before", "consumption-after", "lock-before", "lock-after", "record-before", "record-after"} {
252 t.Run(boundary, func(t *testing.T) {
253 f, old, _ := servingCollectionFixture(t, true)
254 deadline := strings.HasPrefix(boundary, "deadline")
255 if !deadline {
256 if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
257 t.Fatal(err)
258 }
259 r, _ := f.ledger.read(old.ID)
260 f.now = r.CollectAfter
261 }
262 store := &servingCollectionFaultStore{Store: f.disk}
263 f.ledger.state = store
264 fired := false
265 fault := func(name string, apply func() error) error {
266 matches := deadline && strings.HasPrefix(name, "retirement-") && strings.HasSuffix(name, ".json") || strings.HasPrefix(boundary, "consumption") && strings.HasPrefix(name, "legacy-") || strings.HasPrefix(boundary, "lock") && strings.HasSuffix(name, ".lock") || strings.HasPrefix(boundary, "record") && strings.HasPrefix(name, "retirement-") && strings.HasSuffix(name, ".json")
267 if !matches || fired {
268 return apply()
269 }
270 fired = true
271 if strings.HasSuffix(boundary, "after") {
272 if err := apply(); err != nil && !errors.Is(err, os.ErrNotExist) {
273 return err
274 }
275 }
276 return unix.EIO
277 }
278 store.write = func(name string, body []byte) error {
279 return fault(name, func() error { return f.disk.Write(name, body) })
280 }
281 store.remove = func(name string) error { return fault(name, func() error { return f.disk.Remove(name) }) }
282 if err := f.ledger.recoverCompleted(t.Context(), old.ID); !errors.Is(err, unix.EIO) || !fired {
283 t.Fatalf("fault not reached: fired=%t err=%v", fired, err)
284 }
285 f.ledger.state = f.disk
286 if boundary != "record-after" {
287 if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
288 t.Fatal("restart did not resume", err)
289 }
290 }
291 if deadline {
292 r, err := f.ledger.read(old.ID)
293 if err != nil || !r.CollectAfter.Equal(f.now.Add(hostbox.JournalReceiptRetention)) {
294 t.Fatal("deadline lost after restart", err)
295 }
296 } else {
297 if _, err := f.ledger.read(old.ID); !errors.Is(err, os.ErrNotExist) {
298 t.Fatal("collection incomplete", err)
299 }
300 consumed, err := f.ledger.legacyConsumption(old.Successor)
301 if err != nil || consumed == nil || !consumed.Collected {
302 t.Fatal("legacy evidence lost", err)
303 }
304 }
305 })
306 }
307}
308
309func TestServingParkCollectionRefusesChangedFilesystem(t *testing.T) {
310 for _, change := range []string{"event", "namespace", "cancel"} {
311 t.Run(change, func(t *testing.T) {
312 f, old, _ := servingCollectionFixture(t, false)
313 if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
314 t.Fatal(err)
315 }
316 r, _ := f.ledger.read(old.ID)
317 f.now = r.CollectAfter
318 ctx := t.Context()
319 var err error
320 switch change {
321 case "event":
322 err = os.Mkdir(filepath.Dir(f.path(old, "")), 0700)
323 case "namespace":
324 path := filepath.Join(f.data, "instances", ".serving-parked")
325 err = os.Rename(path, path+"-moved")
326 if err == nil {
327 err = os.Mkdir(path, 0700)
328 }
329 case "cancel":
330 var cancel context.CancelFunc
331 ctx, cancel = context.WithCancel(ctx)
332 cancel()
333 }
334 if err != nil {
335 t.Fatal(err)
336 }
337 before, _ := f.disk.Read("retirement-" + old.ID + ".json")
338 if err = f.ledger.recoverCompleted(ctx, old.ID); err == nil {
339 t.Fatal("collected through changed evidence")
340 }
341 after, err := f.disk.Read("retirement-" + old.ID + ".json")
342 if err != nil || string(before) != string(after) {
343 t.Fatal("refusal changed retirement", err)
344 }
345 })
346 }
347}
348
349func TestServingParkCollectionRebootAdjuncts(t *testing.T) {
350 for _, failure := range []string{"none", "preparation-before", "preparation-after", "binding-before", "binding-after"} {
351 t.Run(failure, func(t *testing.T) {
352 f, old, next := servingCollectionFixture(t, false)
353 if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
354 t.Fatal(err)
355 }
356 r, err := f.ledger.read(old.ID)
357 if err != nil {
358 t.Fatal(err)
359 }
360 f.now = r.CollectAfter
361 // Collection treats preparation contents as inert private metadata.
362 prep := "reboot-" + old.ID + "-" + servingOtherBoot + ".json"
363 other := "reboot-" + next.ID + "-" + servingOtherBoot + ".json"
364 binding := "current-retirement-" + old.ID + ".json"
365 for _, name := range []string{prep, other} {
366 if err := f.disk.Write(name, []byte("{}")); err != nil {
367 t.Fatal(err)
368 }
369 }
370 fired := false
371 fault := &servingCollectionFaultStore{Store: f.disk}
372 fault.remove = func(name string) error {
373 match := strings.HasPrefix(failure, "preparation") && name == prep || strings.HasPrefix(failure, "binding") && name == binding
374 if match && !fired {
375 fired = true
376 if strings.HasSuffix(failure, "after") {
377 _ = f.disk.Remove(name)
378 }
379 return unix.EIO
380 }
381 return f.disk.Remove(name)
382 }
383 f.ledger.state = fault
384 err = f.ledger.recoverCompleted(t.Context(), old.ID)
385 if failure == "none" {
386 if err != nil {
387 t.Fatal(err)
388 }
389 } else if !fired || !errors.Is(err, unix.EIO) {
390 t.Fatalf("fault not reached: %v", err)
391 }
392 f.ledger.state = f.disk
393 if _, err := f.disk.Read("retirement-" + old.ID + ".json"); err == nil {
394 if err := f.ledger.recoverCompleted(t.Context(), old.ID); err != nil {
395 t.Fatal(err)
396 }
397 }
398 // A lost record-removal reply leaves only a binding; maintenance resumes it.
399 if err := f.disk.Write(binding, []byte("{}")); err != nil {
400 t.Fatal(err)
401 }
402 if err := f.ledger.maintainParks(t.Context(), func() error { return nil }); err != nil {
403 t.Fatal(err)
404 }
405 for _, name := range []string{prep, binding, "retirement-" + old.ID + ".json"} {
406 if _, err := f.disk.Read(name); !errors.Is(err, os.ErrNotExist) {
407 t.Fatalf("adjunct survived: %s %v", name, err)
408 }
409 }
410 if _, err := f.disk.Read(other); err != nil {
411 t.Fatal("collected another retirement's preparation", err)
412 }
413 })
414 }
415}
416