carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "reflect"
10 "strconv"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/activator"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/moves"
18 "github.com/carlosframework/platform/internal/moves/archive"
19 "github.com/carlosframework/platform/internal/provision"
20)
21
22// probeLog is a fake probe wake. Each call admits a writer on the claim as
23// the claim's runtime would (a cold admission into the move id, or a fresh
24// generation once the move id is admitted), journals the session's binding
25// to the record's attempt as Task 5's attempts do (and, when openWindow is
26// set, the attempt's opened time), then answers from its script. A passing
27// call leaves a live entry for the admitted session.
28type probeLog struct {
29 t *testing.T
30 h *landingHarness
31 configs []activator.ProbeConfig
32 answers []error
33 before func(n int)
34 admissions []probeAdmission
35 openWindow bool
36 // noBind leaves the admission without Task 5's binding: a crash
37 // between the admission CAS and Reserve(0).
38 noBind bool
39 last activator.ProbeResult
40 live bool
41}
42
43// probeAdmission is one admission the fake made: the record attempt it was
44// made under and the session it admitted.
45type probeAdmission struct {
46 attempt int
47 session uint64
48}
49
50func (p *probeLog) wake(ctx context.Context, cfg activator.ProbeConfig) (activator.ProbeResult, error) {
51 p.configs = append(p.configs, cfg)
52 n := len(p.configs)
53 if p.before != nil {
54 p.before(n)
55 }
56 m := p.h.read(p.t, p1Host).Move
57 k := m.Landing.Attempts
58 obs, err := hostclaim.Read(ctx, p.h.s, p1Host)
59 if err != nil {
60 return activator.ProbeResult{}, err
61 }
62 h := obs.Claim.Holder
63 gen := p1ID
64 if h.WriterBox == p.h.dstBox {
65 gen = "fresh" + strconv.Itoa(n)
66 }
67 // The predecessor's region is the probe owners' own rule: the last
68 // admission's region, or the move's source's engine region.
69 from, err := predecessorRegion(h, m.From)
70 if err != nil {
71 return activator.ProbeResult{}, err
72 }
73 admitted, err := hostclaim.Update(ctx, p.h.s, obs, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: p1ID, ObservedPhase: provision.LandProbing,
74 ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: p.h.dstBox, Mode: "cold", NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: p.h.dstEngine, PredecessorRegion: from}))
75 if err != nil {
76 return activator.ProbeResult{}, err
77 }
78 ah := admitted.Claim.Holder
79 p.admissions = append(p.admissions, probeAdmission{attempt: k, session: ah.Session})
80 j, err := moves.ReadJournal(p.h.tdir, p1Host, p1ID)
81 if err != nil {
82 return activator.ProbeResult{}, err
83 }
84 if !p.noBind {
85 j.Mark(probeStepSession+strconv.FormatUint(ah.Session, 10), strconv.Itoa(k))
86 }
87 if p.openWindow {
88 j.Mark(probeStepOpened+strconv.Itoa(k), p.h.clock.Now().UTC().Format(time.RFC3339Nano))
89 }
90 if err := j.Commit(p.h.tdir); err != nil {
91 return activator.ProbeResult{}, err
92 }
93 p.last = activator.ProbeResult{Session: ah.Session, Stamp: ah.Stamp, Generation: ah.Generation}
94 if n <= len(p.answers) && p.answers[n-1] != nil {
95 p.live = false
96 return activator.ProbeResult{}, p.answers[n-1]
97 }
98 p.live = true
99 return p.last, nil
100}
101
102// liveProbe is the activator's LiveProbe over the fake's last passing call.
103func (p *probeLog) liveProbe(host string) (string, activator.ProbeResult, bool) {
104 if host != p1Host || !p.live {
105 return "", activator.ProbeResult{}, false
106 }
107 return p1ID, p.last, true
108}
109
110func failedAttempt(why string) error {
111 return errors.Join(activator.ErrProbeFailed, errors.New(why))
112}
113
114// installedLanding is a landing at restored, with the cut's admitted probe
115// attempts set to attempts (0 clears them) through the record's writer.
116func installedLanding(t *testing.T, attempts int) (*landingHarness, *probeLog) {
117 t.Helper()
118 h := newLandingHarness(t)
119 h.flip(t)
120 h.land(t)
121 h.installed(t)
122 if _, err := provision.SetRecordMove(context.Background(), h.s, "bab", "app", p1Host, func(rec *provision.Record) error {
123 rec.Move.CutAttempts = attempts
124 return nil
125 }); err != nil {
126 t.Fatal(err)
127 }
128 p := &probeLog{t: t, h: h}
129 h.probe, h.probeLive = p.wake, p.liveProbe
130 h.holdProbed = true
131 return h, p
132}
133
134func (h *landingHarness) atPhase(t *testing.T, phase string) *provision.MoveState {
135 t.Helper()
136 m := h.read(t, p1Host).Move
137 if m.State != provision.MoveLanding || m.LandingPhase != phase || m.Reason != "" {
138 t.Fatalf("the record is not landing/%s: %+v", phase, m)
139 }
140 if c := h.claim(t); c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != phase {
141 t.Fatalf("the claim is not landing/%s: %+v", phase, c.Op)
142 }
143 return m
144}
145
146// TestLandingProbesWithinTheBudget (brief): two admitted attempts, the
147// first fails and the second passes, so the landing is probed, record and
148// claim; a third is never attempted. Each attempt is counted on the record
149// and journalled before its call, and the probe is given the landing's
150// deadline (H8), the build's manifest and the pins.
151func TestLandingProbesWithinTheBudget(t *testing.T) {
152 t.Parallel()
153 h, p := installedLanding(t, 2)
154 p.answers = []error{failedAttempt("not ready")}
155 h.land(t)
156 m := h.atPhase(t, provision.LandProbed)
157 if len(p.configs) != 2 || m.Landing == nil || m.Landing.Attempts != 2 || h.landJournal(t).Attempt != 2 {
158 t.Fatalf("attempts: calls %d record %+v journal %d", len(p.configs), m.Landing, h.landJournal(t).Attempt)
159 }
160 deadline, _ := landingDeadlineOf(m)
161 want := activator.ProbeConfig{Host: p1Host, Op: p1ID, HoldOp: p1ID, Deadline: *deadline, Prepared: h.preparedManifest(t), Inputs: m.Inputs}
162 for i, got := range p.configs {
163 if !reflect.DeepEqual(got, want) {
164 t.Fatalf("probe %d was given %+v, want %+v", i+1, got, want)
165 }
166 }
167 // The passing attempt's admitted writer (fix round 1, Minor): its
168 // session, stamp and generation survive the probed mirroring.
169 if c := h.claim(t); len(p.admissions) != 2 || c.Holder.Session != p.last.Session || c.Holder.Stamp != p.last.Stamp || c.Holder.Generation != p.last.Generation || c.Holder.WriterBox != "uk-box" {
170 t.Fatalf("the probed step did not keep the admitted writer %+v: holder %+v", p.last, c.Holder)
171 }
172 // probed is the probe's end: another pass probes nothing.
173 h.land(t)
174 if len(p.configs) != 2 {
175 t.Fatalf("a pass at probed probed again: %d", len(p.configs))
176 }
177 h.atPhase(t, provision.LandProbed)
178}
179
180// preparedManifest is the build's install manifest from the op journal.
181func (h *landingHarness) preparedManifest(t *testing.T) *moves.InstallManifest {
182 t.Helper()
183 m := h.installedManifest(t)
184 return &m
185}
186
187func (h *landingHarness) installedManifest(t *testing.T) moves.InstallManifest {
188 t.Helper()
189 j := h.landJournal(t)
190 var m moves.InstallManifest
191 if err := json.Unmarshal([]byte(j.Steps[landStepInstall]), &m); err != nil {
192 t.Fatal(err)
193 }
194 return m
195}
196
197// TestLandingExhaustedAttemptsMarkRollback (brief): every admitted attempt
198// fails: the landing records the attempts-exhausted rollback sentence,
199// which the next pass's rollback decision turns into the rollback.
200func TestLandingExhaustedAttemptsMarkRollback(t *testing.T) {
201 t.Parallel()
202 h, p := installedLanding(t, 2)
203 p.answers = []error{failedAttempt("one"), errors.Join(activator.ErrProbeFailed, activator.ErrAttemptOver)}
204 h.land(t)
205 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackProbeAttempts || len(p.configs) != 2 {
206 t.Fatalf("exhausted: %d calls, %+v", len(p.configs), m)
207 }
208 h.land(t)
209 rolledBack(t, h, provision.RollbackProbeAttempts, provision.LandProbing)
210 if len(p.configs) != 2 {
211 t.Fatalf("a rolled-back landing probed again: %d", len(p.configs))
212 }
213}
214
215// A changed configuration found by the probe (Task 5's ProbeInputsChanged,
216// before or after the admission) is the configuration rollback, at once.
217func TestLandingConfigChangedDuringTheProbeRollsBack(t *testing.T) {
218 t.Parallel()
219 for name, answer := range map[string]error{
220 "before the admission": &ProbeInputsChanged{Reason: provision.RollbackConfigChanged},
221 "after the admission": errors.Join(activator.ErrProbeFailed, &ProbeInputsChanged{Reason: provision.RollbackConfigChanged}),
222 } {
223 t.Run(name, func(t *testing.T) {
224 h, p := installedLanding(t, 3)
225 p.answers = []error{answer}
226 h.land(t)
227 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackConfigChanged || len(p.configs) != 1 {
228 t.Fatalf("%d calls, %+v", len(p.configs), m)
229 }
230 h.land(t)
231 rolledBack(t, h, provision.RollbackConfigChanged, provision.LandProbing)
232 })
233 }
234}
235
236// H10: a cut that recorded no probe attempts refuses the probe with its
237// recorded sentence; nothing is probed.
238func TestLandingProbeNeedsTheCutsAttempts(t *testing.T) {
239 t.Parallel()
240 h, p := installedLanding(t, 0)
241 h.land(t)
242 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackProbeUnpriced || len(p.configs) != 0 {
243 t.Fatalf("%d calls, %+v", len(p.configs), m)
244 }
245 h.land(t)
246 rolledBack(t, h, provision.RollbackProbeUnpriced, provision.LandProbing)
247}
248
249// H8: probing and probed check the landing's deadline at entry. A deadline
250// that passes during an attempt ends the probing with the deadline's
251// sentence, and no further attempt is made; a probed landing past its
252// deadline rolls back.
253func TestLandingProbingChecksTheDeadline(t *testing.T) {
254 t.Parallel()
255 h, p := installedLanding(t, 3)
256 p.before = func(int) { h.clock.Advance(cutLandingWindow + time.Minute) }
257 p.answers = []error{errors.Join(activator.ErrProbeFailed, activator.ErrAttemptOver)}
258 h.land(t)
259 if len(p.configs) != 1 {
260 t.Fatalf("probed past the deadline: %d calls", len(p.configs))
261 }
262 h.land(t)
263 rolledBack(t, h, provision.RollbackLandingDeadline, provision.LandProbing)
264
265 h2, _ := installedLanding(t, 3)
266 h2.land(t)
267 h2.atPhase(t, provision.LandProbed)
268 h2.clock.Advance(cutLandingWindow + time.Minute)
269 h2.land(t)
270 rolledBack(t, h2, provision.RollbackLandingDeadline, provision.LandProbed)
271}
272
273// TestLandingProbeCrashRestartsOnlyTheProbe (brief, §7.13): a crash after
274// an attempt is counted and before its probe returns restarts only the
275// probe: a fresh worker probes the same attempt again (its window never
276// opened), and local and restored are not rerun, their journal steps and
277// .files-gen untouched. An attempt whose window had opened and passed
278// counts as failed, and the next one is made.
279func TestLandingProbeCrashRestartsOnlyTheProbe(t *testing.T) {
280 t.Parallel()
281 h, p := installedLanding(t, 2)
282 steps := map[string]string{}
283 for k, v := range h.landJournal(t).Steps {
284 steps[k] = v
285 }
286 marker := filepath.Join(h.tdir, p1Host, archive.MarkerName)
287 before, err := os.Stat(marker)
288 if err != nil {
289 t.Fatal(err)
290 }
291 renders, timers := h.renders, len(h.replaced)
292 h.landAt = "before-probe-1"
293 h.crashLand(t)
294 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandProbing || m.Landing.Attempts != 1 || h.landJournal(t).Attempt != 1 || len(p.configs) != 0 {
295 t.Fatalf("the crash point: %+v journal %d calls %d", m, h.landJournal(t).Attempt, len(p.configs))
296 }
297 h.freshTarget()
298 h.land(t)
299 m := h.atPhase(t, provision.LandProbed)
300 if len(p.configs) != 1 || m.Landing.Attempts != 1 {
301 t.Fatalf("after the restart: %d calls, record %+v", len(p.configs), m.Landing)
302 }
303 after, err := os.Stat(marker)
304 if err != nil || !after.ModTime().Equal(before.ModTime()) {
305 t.Fatalf(".files-gen was written again: %v", err)
306 }
307 j := h.landJournal(t)
308 for k, v := range steps {
309 if j.Steps[k] != v {
310 t.Fatalf("the journal step %s changed", k)
311 }
312 }
313 for k := range j.Steps {
314 if _, ok := steps[k]; !ok && !strings.HasPrefix(k, "probe_") {
315 t.Fatalf("a non-probe step %s was added", k)
316 }
317 }
318 if h.renders != renders || len(h.replaced) != timers {
319 t.Fatalf("configured or local ran again: renders %d->%d timers %d->%d", renders, h.renders, timers, len(h.replaced))
320 }
321
322 // An attempt whose window opened and passed before the crash counts as
323 // failed: the restarted worker makes the next one.
324 h2, p2 := installedLanding(t, 2)
325 h2.landAt = "before-probe-1"
326 h2.crashLand(t)
327 j2 := h2.landJournal(t)
328 j2.Mark("probe_opened_1", h2.clock.Now().Add(-31*time.Second).UTC().Format(time.RFC3339Nano))
329 if err := j2.Commit(h2.tdir); err != nil {
330 t.Fatal(err)
331 }
332 h2.freshTarget()
333 h2.land(t)
334 if m := h2.atPhase(t, provision.LandProbed); len(p2.configs) != 1 || m.Landing.Attempts != 2 {
335 t.Fatalf("the spent attempt was retried: %d calls, record %+v", len(p2.configs), m.Landing)
336 }
337}
338
339// Unarmed (production until claim Plan 11's launcher and Plan 5 Task 9):
340// with no probe wake the landing waits at restored and writes nothing.
341func TestLandingUnarmedProbeWaitsAtRestored(t *testing.T) {
342 t.Parallel()
343 h, _ := installedLanding(t, 3)
344 h.probe = nil
345 before := h.read(t, p1Host).Move.UpdatedAt
346 h.land(t)
347 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandRestored || !m.UpdatedAt.Equal(before) {
348 t.Fatalf("an unarmed probe moved the landing: %+v", m)
349 }
350}
351
352// H10: Task 5's Admitted hook reads the admission's recorded count, and
353// refuses a record that has none.
354func TestMovesRecordAdmittedAttempts(t *testing.T) {
355 ctx := context.Background()
356 if n, err := MovesRecordAdmittedAttempts(ctx, &provision.Record{Move: &provision.MoveState{CutAttempts: 2}}); err != nil || n != 2 {
357 t.Fatalf("recorded: %d %v", n, err)
358 }
359 for _, rec := range []*provision.Record{nil, {}, {Move: &provision.MoveState{}}} {
360 if _, err := MovesRecordAdmittedAttempts(ctx, rec); err == nil {
361 t.Fatalf("no recorded count was admitted: %+v", rec)
362 }
363 }
364}
365
366// Fix round 1, ruling 1: a passing probe whose probing to probed record
367// write fails is not probed again. probe_passed_<k> names the admitted
368// writer; the next pass validates it against the claim and the activator's
369// live entry and retries only the phase writes, record then claim.
370func TestLandingProbePassReplaysTheRecordWrite(t *testing.T) {
371 t.Parallel()
372 h, p := installedLanding(t, 2)
373 h.landAt = "before-record-probed"
374 h.crashLand(t)
375 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandProbing || len(p.configs) != 1 {
376 t.Fatalf("the crash point: %d calls, %+v", len(p.configs), m)
377 }
378 if !h.landJournal(t).Done("probe_passed_1") {
379 t.Fatalf("the pass was not journalled before the record write: %v", h.landJournal(t).Steps)
380 }
381 h.land(t)
382 h.atPhase(t, provision.LandProbed)
383 if len(p.configs) != 1 || len(p.admissions) != 1 {
384 t.Fatalf("the replay admitted again: %d calls, %d admissions", len(p.configs), len(p.admissions))
385 }
386 if c := h.claim(t); c.Holder.Session != p.last.Session || c.Holder.Generation != p1ID {
387 t.Fatalf("the probed writer: %+v", c.Holder)
388 }
389
390 // The session is gone (the entry did not survive): the attempt is
391 // consumed and failed, and the next one is made.
392 h2, p2 := installedLanding(t, 2)
393 h2.landAt = "before-record-probed"
394 h2.crashLand(t)
395 p2.live = false
396 h2.land(t)
397 if m := h2.atPhase(t, provision.LandProbed); len(p2.configs) != 2 || m.Landing.Attempts != 2 || p2.admissions[1].attempt != 2 {
398 t.Fatalf("a lost passing session: %d calls, record %+v, admissions %+v", len(p2.configs), m.Landing, p2.admissions)
399 }
400}
401
402// Fix round 1, ruling 2 (§7.7): one cold admission per budgeted attempt. A
403// crash after the attempt's admission, with its window still open and no
404// failure journalled, consumes the attempt: the restart never admits again
405// under the same index.
406func TestLandingProbeAdmittedAttemptIsConsumedOnRestart(t *testing.T) {
407 t.Parallel()
408 h, p := installedLanding(t, 2)
409 p.openWindow = true
410 p.answers = []error{errors.New("the controller died during the attempt")}
411 h.land(t)
412 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandProbing || len(p.admissions) != 1 || p.admissions[0].attempt != 1 || h.landJournal(t).Done("probe_failed_1") {
413 t.Fatalf("the crash: %+v admissions %+v", m, p.admissions)
414 }
415 h.freshTarget()
416 h.land(t)
417 h.atPhase(t, provision.LandProbed)
418 if len(p.admissions) != 2 || p.admissions[1].attempt != 2 {
419 t.Fatalf("an index was admitted twice: %+v", p.admissions)
420 }
421
422 // With one attempt admitted, the restart has none left.
423 h1, p1 := installedLanding(t, 1)
424 p1.openWindow = true
425 p1.answers = []error{errors.New("the controller died during the attempt")}
426 h1.land(t)
427 h1.freshTarget()
428 h1.land(t)
429 if m := h1.read(t, p1Host).Move; m.Reason != provision.RollbackProbeAttempts || len(p1.admissions) != 1 {
430 t.Fatalf("the only attempt was admitted again: %+v admissions %+v", m, p1.admissions)
431 }
432}
433
434// Fix round 2, ruling 1: the claim, not the binding, says whether attempt k
435// was admitted. A crash after the admission CAS and before Reserve's
436// binding leaves probe_admitting_<k> and a claim whose writer session moved
437// on: the restart consumes k and never admits under it again.
438func TestLandingProbeAdmissionWithoutBindingIsConsumed(t *testing.T) {
439 t.Parallel()
440 h, p := installedLanding(t, 2)
441 p.noBind = true
442 p.answers = []error{errors.New("the controller died after the admission")}
443 h.land(t)
444 if len(p.admissions) != 1 || !h.landJournal(t).Done("probe_admitting_1") || attemptAdmitted(h.landJournal(t), 1) {
445 t.Fatalf("the crash: admissions %+v journal %v", p.admissions, h.landJournal(t).Steps)
446 }
447 h.freshTarget()
448 p.noBind = false
449 h.land(t)
450 h.atPhase(t, provision.LandProbed)
451 if len(p.admissions) != 2 || p.admissions[1].attempt != 2 {
452 t.Fatalf("index 1 was admitted twice: %+v", p.admissions)
453 }
454
455 // probe_admitting_<k> with the claim unchanged (no admission happened):
456 // the same index is used.
457 h2, p2 := installedLanding(t, 2)
458 h2.landAt = "before-probe-1"
459 h2.crashLand(t)
460 if !h2.landJournal(t).Done("probe_admitting_1") {
461 t.Fatalf("no admitting step before the call: %v", h2.landJournal(t).Steps)
462 }
463 h2.freshTarget()
464 h2.land(t)
465 if m := h2.atPhase(t, provision.LandProbed); m.Landing.Attempts != 1 || len(p2.admissions) != 1 || p2.admissions[0].attempt != 1 {
466 t.Fatalf("an unadmitted attempt was not reused: %+v %+v", m.Landing, p2.admissions)
467 }
468}
469
470// Fix round 2, ruling 2: a probe that passed but whose probe_passed_<k>
471// write failed is found through the activator's live entry: its writer is
472// the claim's, so the attempt passed, and the next pass reaches probed
473// without a second ProbeWake.
474func TestLandingProbePassFoundThroughTheLiveEntry(t *testing.T) {
475 t.Parallel()
476 h, p := installedLanding(t, 2)
477 h.landAt = "before-journal-passed"
478 h.crashLand(t)
479 if j := h.landJournal(t); j.Done("probe_passed_1") || len(p.configs) != 1 || !p.live {
480 t.Fatalf("the crash point: %v calls %d live %v", j.Steps, len(p.configs), p.live)
481 }
482 h.land(t)
483 h.atPhase(t, provision.LandProbed)
484 if len(p.configs) != 1 || len(p.admissions) != 1 || !h.landJournal(t).Done("probe_passed_1") {
485 t.Fatalf("probed again: %d calls %d admissions", len(p.configs), len(p.admissions))
486 }
487 if c := h.claim(t); c.Holder.Session != p.last.Session {
488 t.Fatalf("the probed writer: %+v", c.Holder)
489 }
490}
491