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 "log"
5 "reflect"
6 "strings"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/hostclaim"
10 "github.com/carlosframework/platform/internal/moves"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// count is how many times s was logged (housekeeper_test.go's buffer).
16func (l *lockedBuffer) count(s string) int { return strings.Count(l.String(), s) }
17
18// lostLog sends the log to a buffer for the rest of the test; a pass may
19// log from goroutines, so the buffer is housekeeper_test.go's locked one.
20func lostLog(t *testing.T) *lockedBuffer {
21 t.Helper()
22 buf := &lockedBuffer{}
23 out, flags := log.Writer(), log.Flags()
24 log.SetOutput(buf)
25 t.Cleanup(func() { log.SetOutput(out); log.SetFlags(flags) })
26 return buf
27}
28
29// rcLose lists box in the host claim's Lost, as a declaration leaves it.
30func rcLose(t *testing.T, h *phase1Harness, box string) {
31 t.Helper()
32 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
33 c.Lost = append(c.Lost, box)
34 if c.LostRevisions == nil {
35 c.LostRevisions = map[string]uint64{}
36 }
37 c.LostRevisions[box] = 1
38 })
39}
40
41// Q9's gate (Plan 4 Task 10, witness wLostQuarantine; delta M15): a box
42// the host's claim lists in Lost runs no move step for the host, whatever
43// its role and the record's state, and logs one standing notice. The
44// quarantine's local retirement is the claim's (readmission requires its
45// receipt); moves adds only this gate.
46func TestReturningLostBoxRefusesEveryMoveStep(t *testing.T) {
47 type snapshot struct {
48 rec provision.Record
49 claim hostclaim.Claim
50 }
51 take := func(t *testing.T, h *phase1Harness) snapshot {
52 t.Helper()
53 return snapshot{rec: h.read(t, p1Host), claim: *h.claim(t)}
54 }
55 t.Run("the source before the flip", func(t *testing.T) {
56 buf := lostLog(t)
57 h := newPhase1Harness(t)
58 h.start(t, "auto")
59 rcLose(t, h, "ie-box")
60 before := take(t, h)
61 h.pass(t)
62 h.pass(t)
63 if after := take(t, h); !reflect.DeepEqual(before, after) {
64 t.Fatalf("a lost source moved the host:\n%+v\n%+v", before.rec.Move, after.rec.Move)
65 }
66 if r, err := readSourceReport(t.Context(), h.s, h.read(t, p1Host)); err != nil || r != nil {
67 t.Fatalf("a lost source reported: %+v %v", r, err)
68 }
69 if n := buf.count("is lost on this host's claim"); n != 1 {
70 t.Fatalf("notices %d:\n%s", n, buf.String())
71 }
72 // The gate is this box's: the same record moves on once the claim
73 // no longer lists it.
74 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { c.Lost, c.LostRevisions = nil, nil })
75 h.pass(t)
76 if r, err := readSourceReport(t.Context(), h.s, h.read(t, p1Host)); err != nil || r == nil {
77 t.Fatalf("the readmitted source did not report: %v", err)
78 }
79 })
80 t.Run("the target before the flip", func(t *testing.T) {
81 lostLog(t)
82 h := newPhase1Harness(t)
83 h.start(t, "auto")
84 h.pass(t)
85 rcLose(t, h, "uk-box")
86 before := take(t, h)
87 h.targetPass(t, h.targetCfg())
88 if after := take(t, h); !reflect.DeepEqual(before, after) {
89 t.Fatalf("a lost target moved the host:\n%+v\n%+v", before.rec.Move, after.rec.Move)
90 }
91 if r, err := readTargetReport(t.Context(), h.s, h.read(t, p1Host)); err != nil || r != nil {
92 t.Fatalf("a lost target reported: %+v %v", r, err)
93 }
94 })
95 t.Run("the source's recovery", func(t *testing.T) {
96 buf := lostLog(t)
97 h := newRecoverHarness(t, false)
98 h.released(t)
99 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
100 c.Lost, c.LostRevisions = append(c.Lost, "ie-box"), map[string]uint64{"ie-box": 1}
101 })
102 rec, claim := h.read(t, p1Host), *h.claim(t)
103 h.srcPass(t)
104 if !reflect.DeepEqual(rec, h.read(t, p1Host)) || !reflect.DeepEqual(claim, *h.claim(t)) || len(h.probes) != 0 || h.srcRoute.HoldOp != p1ID {
105 t.Fatalf("a lost source recovered: %+v", h.read(t, p1Host).Move.Rollback)
106 }
107 if n := buf.count("is lost on this host's claim"); n != 1 {
108 t.Fatalf("notices %d", n)
109 }
110 })
111}
112
113// rcCorruptClaim makes the host claim unreadable: its object no longer
114// decodes.
115func rcCorruptClaim(t *testing.T, s release.Store) {
116 t.Helper()
117 key, err := hostclaim.Key(p1Host)
118 if err != nil {
119 t.Fatal(err)
120 }
121 _, etag, err := s.Get(t.Context(), key)
122 if err != nil {
123 t.Fatal(err)
124 }
125 if _, err := s.Put(t.Context(), key, []byte("{not a claim"), etag); err != nil {
126 t.Fatal(err)
127 }
128}
129
130// Fix round 1, item 3: the gate covers the whole pass. A host the gate
131// blocks, confirmed lost or with a claim that does not read, gets no target
132// report and no journal sweep: nothing is reported, committed or retired.
133func TestLostBoxGateCoversTheWholePass(t *testing.T) {
134 t.Run("an unreadable claim blocks the target's report", func(t *testing.T) {
135 buf := lostLog(t)
136 h := newPhase1Harness(t)
137 h.start(t, "auto")
138 h.pass(t)
139 rcCorruptClaim(t, h.s)
140 h.targetPass(t, h.targetCfg())
141 if r, err := readTargetReport(t.Context(), h.s, h.read(t, p1Host)); err != nil || r != nil {
142 t.Fatalf("a target with an unreadable claim reported: %+v %v", r, err)
143 }
144 if buf.count("does not read") == 0 {
145 t.Fatalf("no notice:\n%s", buf.String())
146 }
147 })
148 for name, block := range map[string]func(t *testing.T, h *revertHarness){
149 "a confirmed lost host": func(t *testing.T, h *revertHarness) {
150 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
151 c.Lost, c.LostRevisions = append(c.Lost, "ie-box"), map[string]uint64{"ie-box": 1}
152 })
153 },
154 "an unreadable claim": func(t *testing.T, h *revertHarness) { rcCorruptClaim(t, h.s) },
155 } {
156 t.Run(name+" blocks the journal sweep", func(t *testing.T) {
157 lostLog(t)
158 h := newRevertHarness(t)
159 h.fromStopped(t)
160 h.arm(t)
161 h.pass(t)
162 h.reopened(t)
163 h.rvAt = "after-close"
164 o := h.laneChore(t)
165 h.tPass(t)
166 h.pastHorizon(t, o)
167 h.tPass(t)
168 h.pass(t)
169 if !h.rvDone || h.read(t, p1Host).Move != nil || !moveJournalPresent(h.cfg.DataDir, p1Host, p1ID) {
170 t.Fatalf("the setup did not leave the closed revert's journal: %v", h.rvHits)
171 }
172 journal, err := moves.ReadJournal(h.cfg.DataDir, p1Host, p1ID)
173 if err != nil {
174 t.Fatal(err)
175 }
176 block(t, h)
177 h.freshWorker()
178 h.pass(t)
179 if !moveJournalPresent(h.cfg.DataDir, p1Host, p1ID) {
180 t.Fatal("the sweep retired a blocked host's journal")
181 }
182 if after, err := moves.ReadJournal(h.cfg.DataDir, p1Host, p1ID); err != nil || !reflect.DeepEqual(after.Steps, journal.Steps) {
183 t.Fatalf("the sweep committed a blocked host's journal: %v", err)
184 }
185 })
186 }
187}
188
189// Fix round 1, item 4: the gate names this box's region through the shared
190// placement rule (park.PlacementMatches under the verified home binding),
191// never raw string equality: on the home box (placement code "") a record
192// that spells the home by its roster name names this box, and is gated;
193// without the verified binding it names another region.
194func TestLostBoxGateReadsHomeSpellingsUnderTheBinding(t *testing.T) {
195 s := release.NewDirStore(t.TempDir())
196 inc, err := hostclaim.Incarnation("bab", "app", p1Host, p1Created)
197 if err != nil {
198 t.Fatal(err)
199 }
200 if _, err := hostclaim.Update(t.Context(), s, hostclaim.Observed{Host: p1Host, Absent: true}, func(c *hostclaim.Claim) error {
201 c.Holder = &hostclaim.Holder{Family: "instance", Key: provision.RecordKey("bab", "app", p1Host), Account: "bab", App: "app", Incarnation: inc, CreatedAt: p1Created, Location: "instances/" + p1Host, Generation: "cur", Stamp: 1}
202 c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "home-box"}
203 c.Lost, c.LostRevisions = []string{"home-box"}, map[string]uint64{"home-box": 1}
204 return nil
205 }); err != nil {
206 t.Fatal(err)
207 }
208 for _, spelling := range []provision.Record{
209 {Host: p1Host, Region: "ie", Move: &provision.MoveState{ID: p1ID, From: "ie", To: "uk"}},
210 {Host: p1Host, Region: "uk", Move: &provision.MoveState{ID: p1ID, From: "uk", To: "ie"}},
211 {Host: p1Host, Region: "", Move: &provision.MoveState{ID: p1ID, From: "", To: "uk"}},
212 } {
213 bound := &movesWorker{cfg: moveCfg{Box: "home-box", Region: "", ParkHomeRegion: "ie"}}
214 if !movesNamesBox(bound.cfg, spelling) || !bound.lostHere(t.Context(), s, spelling) {
215 t.Fatalf("the home box under the binding does not gate %+v", spelling.Move)
216 }
217 }
218 unbound := moveCfg{Box: "home-box", Region: ""}
219 if movesNamesBox(unbound, provision.Record{Host: p1Host, Region: "ie", Move: &provision.MoveState{ID: p1ID, From: "ie", To: "uk"}}) {
220 t.Fatal("a roster name matched the home without the verified binding")
221 }
222 named := moveCfg{Box: "de-box", Region: "de", ParkHomeRegion: "ie"}
223 if movesNamesBox(named, provision.Record{Host: p1Host, Region: "", Move: &provision.MoveState{ID: p1ID, From: "", To: "uk"}}) {
224 t.Fatal("a named box matched the home")
225 }
226}
227