carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package rollout
2
3import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/activator"
11 "github.com/carlosframework/platform/internal/status"
12)
13
14// retry is the console's signed host retry as the box applies it: a grant
15// naming build, carrying the generation the box's status publishes for it.
16func (h *harness) retry(host, build string) bool {
17 return h.m.RetryHost(host, build, h.book.ID(), h.book.PublishedEpoch("bab", "blog", build, host))
18}
19
20// Plan review finding 13, under the crash-epoch hard limit (readiness round
21// 2, Task 1): a restriction's epoch is its app's generation, a persisted
22// book sequence that moves whenever any restriction of the app changes.
23func TestRestrictionEpochFollowsTheRecordAndTheMarker(t *testing.T) {
24 dd := t.TempDir()
25 b, err := OpenFailureBook(dd)
26 if err != nil {
27 t.Fatal(err)
28 }
29 if e := b.RestrictionEpoch("bab", "blog", "v2", "a"); e != 0 {
30 t.Fatalf("epoch with nothing recorded = %d, want 0", e)
31 }
32 b.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "a"})
33 e1 := b.RestrictionEpoch("bab", "blog", "v2", "a")
34 if e1 == 0 || b.RestrictionEpoch("bab", "blog", "v2", "b") != e1 {
35 t.Fatalf("after the first failure: a %d, sibling b %d; want one non-zero epoch for both", e1, b.RestrictionEpoch("bab", "blog", "v2", "b"))
36 }
37 if res, _ := b.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "a"}); res != Unchanged ||
38 b.RestrictionEpoch("bab", "blog", "v2", "a") != e1 {
39 t.Fatalf("an unchanged failure moved the epoch to %d", b.RestrictionEpoch("bab", "blog", "v2", "a"))
40 }
41 b.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "a", HeldDown: []string{"a"}})
42 e2 := b.RestrictionEpoch("bab", "blog", "v2", "a")
43 if e2 <= e1 || b.RestrictionEpoch("bab", "blog", "v2", "b") <= e1 {
44 t.Fatalf("a merged hold-down: a %d, b %d; both must move past %d", e2, b.RestrictionEpoch("bab", "blog", "v2", "b"), e1)
45 }
46 b.MarkHoldRan("c", HoldRan{Account: "bab", App: "blog", Build: "v3"})
47 e3 := b.RestrictionEpoch("bab", "blog", "v3", "c")
48 if e3 <= e2 {
49 t.Fatalf("a marker-only restriction's epoch = %d, want the app's generation moved past %d", e3, e2)
50 }
51 if e := b.RestrictionEpoch("bab", "blog", "v2", "b"); e != e3 {
52 t.Fatalf("v2's epoch for b = %d after c's marker: one generation per app, want %d", e, e3)
53 }
54 if e := b.RestrictionEpoch("bab", "blog", "v3", "b"); e != 0 {
55 t.Fatalf("b's epoch for v3 = %d: c's marker of v3 restricts only c, want 0", e)
56 }
57 if e := b.RestrictionEpoch("bab", "shop", "v3", "c"); e != 0 {
58 t.Fatalf("another app's epoch = %d, want 0", e)
59 }
60
61 b2, err := OpenFailureBook(dd)
62 if err != nil {
63 t.Fatal(err)
64 }
65 for _, c := range [][2]string{{"v2", "a"}, {"v2", "b"}, {"v3", "c"}} {
66 if got, want := b2.RestrictionEpoch("bab", "blog", c[0], c[1]), b.RestrictionEpoch("bab", "blog", c[0], c[1]); got != want {
67 t.Fatalf("after a reopen %s on %s has epoch %d, want %d", c[0], c[1], got, want)
68 }
69 }
70}
71
72// A book an earlier agent wrote has no generations: the open gives its app
73// one, and its marker a sequence, newer than anything in the file, so its
74// holds can still be retried (a zero epoch is never accepted). One app has
75// one generation (the crash-epoch hard limit, rule 1), so the record's and
76// the marker's restrictions share it.
77func TestOpenGivesAnOlderFilesRestrictionsAnEpoch(t *testing.T) {
78 dd := t.TempDir()
79 raw, _ := json.Marshal(map[string]any{
80 "schema": 1,
81 "records": []map[string]any{{"account": "bab", "app": "blog", "build": "v2", "host": "a"}},
82 "hold_ran": map[string]any{"c": map[string]any{"account": "bab", "app": "blog", "build": "v3"}},
83 "seq": 5,
84 })
85 if err := os.WriteFile(filepath.Join(dd, FailureFile), raw, 0o644); err != nil {
86 t.Fatal(err)
87 }
88 b, err := OpenFailureBook(dd)
89 if err != nil {
90 t.Fatal(err)
91 }
92 rec := b.RestrictionEpoch("bab", "blog", "v2", "a")
93 mark := b.RestrictionEpoch("bab", "blog", "v3", "c")
94 if rec <= 5 || rec != mark {
95 t.Fatalf("epochs record %d, marker %d: want the app's one generation, newer than the file's 5", rec, mark)
96 }
97 if m, _ := b.HoldRanOn("c"); m.Seq <= 5 {
98 t.Fatalf("the marker's sequence = %d, want one newer than the file's 5", m.Seq)
99 }
100 if b.Snapshot() <= max(rec, mark) {
101 t.Fatal("a snapshot after the open must be newer than the epochs it handed out")
102 }
103}
104
105// Plan review finding 5: a grant names the build whose restriction it lifts.
106// One naming another build than the record's clears nothing, the marker
107// included.
108func TestARetryGrantForAnotherBuildClearsNothing(t *testing.T) {
109 h := newHarness(t, "a", "b")
110 h.stage("v2")
111 h.book.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "c", PromotedAt: promotedAt, At: t0})
112 h.book.MarkHoldRan("b", markV2)
113 h.install("v2", "a", "b")
114 h.pass()
115 epoch := h.book.RestrictionEpoch("bab", "blog", "v2", "b")
116 if h.m.RetryHost("b", "v9", h.book.ID(), epoch) {
117 t.Fatal("a grant for v9 cleared something")
118 }
119 if !h.book.Blocks("bab", "blog", "v2", "b", false) {
120 t.Fatal("a grant for v9 cleared v2's record for b")
121 }
122 if _, ok := h.book.HoldRanOn("b"); !ok {
123 t.Fatal("a grant for v9 cleared b's v2 marker")
124 }
125 if h.m.RetryHost("b", "v2", h.book.ID(), 0) {
126 t.Fatal("a zero epoch cleared something")
127 }
128 if !h.retry("b", "v2") {
129 t.Fatal("the grant for v2 at its epoch cleared nothing")
130 }
131 if _, ok := h.book.HoldRanOn("b"); ok || h.book.Blocks("bab", "blog", "v2", "b", false) {
132 t.Fatal("the grant for v2 left b restricted")
133 }
134 waitFor(t, "b on v2", func() bool { return h.servingAll("v2", "b") })
135}
136
137// The marker names B while the slot wants A: a grant for B lifts the marker
138// and nothing of A's.
139func TestARetryGrantOfTheMarkersBuildClearsOnlyTheMarker(t *testing.T) {
140 h := newHarness(t, "a")
141 h.stage("v2")
142 h.install("v2", "a")
143 h.sleepHost("a")
144 h.pass()
145 h.book.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "sib", At: t0})
146 h.book.MarkHoldRan("a", HoldRan{Account: "bab", App: "blog", Build: "v3"})
147 if !h.retry("a", "v3") {
148 t.Fatal("the grant for the marker's build cleared nothing")
149 }
150 if _, ok := h.book.HoldRanOn("a"); ok {
151 t.Fatal("the marker of v3 survived its grant")
152 }
153 if !h.book.Blocks("bab", "blog", "v2", "a", false) {
154 t.Fatal("a grant for v3 cleared v2's record for a")
155 }
156}
157
158// Plan review finding 13: a grant is for the restriction the member saw.
159// One whose epoch predates a second failure of the same build on the same
160// host clears nothing, and a fresh grant does.
161func TestARetryGrantPredatingASecondFailureClearsNothing(t *testing.T) {
162 h := newHarness(t, "a", "b")
163 h.stage("v2")
164 h.failing.Store(true)
165 h.act.outcome = func(a activator.Attempt) activator.SwapOutcome {
166 if a.Build == "v2" && h.failing.Load() {
167 return activator.SwapFailed
168 }
169 return activator.SwapDone
170 }
171 h.install("v2", "a", "b")
172 h.pass()
173 waitFor(t, "a failure record", func() bool { _, ok := h.book.Lookup("bab", "blog", "v2"); return ok })
174 first := h.book.RestrictionEpoch("bab", "blog", "v2", "b")
175 if !h.m.RetryHost("b", "v2", h.book.ID(), first) {
176 t.Fatal("the first grant cleared nothing")
177 }
178 // b's canary fails: the same build fails again on the same host.
179 waitFor(t, "b's canary to fail", func() bool { return !h.m.attemptRunning("b") && h.book.Blocks("bab", "blog", "v2", "b", false) })
180 if h.m.RetryHost("b", "v2", h.book.ID(), first) {
181 t.Fatal("a grant from before b's second failure cleared it")
182 }
183 if !h.book.Blocks("bab", "blog", "v2", "b", false) {
184 t.Fatal("b is not held after the stale grant")
185 }
186 h.failing.Store(false)
187 if !h.retry("b", "v2") {
188 t.Fatal("a fresh grant cleared nothing")
189 }
190 waitFor(t, "b on v2", func() bool { return h.servingAll("v2", "b") })
191}
192
193// The same at the book: a merge that holds the host down after the grant
194// was signed moves the epoch, so the grant clears nothing.
195func TestARetryGrantPredatingAHoldDownClearsNothing(t *testing.T) {
196 b := &FailureBook{}
197 b.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "h"})
198 signed := b.RestrictionEpoch("bab", "blog", "v2", "h")
199 b.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "h", HeldDown: []string{"h"}})
200 if rec, mark, _, _ := b.RetryHostAt("bab", "blog", "v2", "h", b.ID(), signed); rec || mark {
201 t.Fatalf("a grant predating the hold-down cleared record %v, marker %v", rec, mark)
202 }
203 if !b.HeldDown("bab", "blog", "v2", "h") {
204 t.Fatal("h is no longer held down")
205 }
206}
207
208// A used grant replayed after the agent reopens its book clears nothing
209// new: the clear moved the host's epoch, and the epoch is on disk.
210func TestARetryGrantReplayedAfterAReopenClearsNothing(t *testing.T) {
211 dd := t.TempDir()
212 b, err := OpenFailureBook(dd)
213 if err != nil {
214 t.Fatal(err)
215 }
216 b.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "h", Mode: string(activator.ModeHold),
217 HeldDown: []string{"h"}, MarkHoldRan: true, HoldRanExec: "a1", HoldRanAt: t0})
218 grant := b.RestrictionEpoch("bab", "blog", "v2", "h")
219 if rec, mark, _, err := b.RetryHostAt("bab", "blog", "v2", "h", b.ID(), grant); !rec || !mark || err != nil {
220 t.Fatalf("the grant cleared record %v, marker %v (%v); want both", rec, mark, err)
221 }
222 b2, err := OpenFailureBook(dd)
223 if err != nil {
224 t.Fatal(err)
225 }
226 // The retried canary fails again, after the reopen.
227 b2.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "h", Mode: string(activator.ModeHold),
228 HeldDown: []string{"h"}, MarkHoldRan: true, HoldRanExec: "a2", HoldRanAt: t0.Add(time.Minute), Seq: b2.Snapshot()})
229 if rec, mark, _, _ := b2.RetryHostAt("bab", "blog", "v2", "h", b.ID(), grant); rec || mark {
230 t.Fatalf("the replayed grant cleared record %v, marker %v", rec, mark)
231 }
232 if !b2.HeldDown("bab", "blog", "v2", "h") {
233 t.Fatal("the replay lifted the second hold-down")
234 }
235 if _, ok := b2.HoldRanOn("h"); !ok {
236 t.Fatal("the replay lifted the second marker")
237 }
238}
239
240// Status publishes the epoch the console signs, beside HeldBy, on both the
241// record arm and the marker arm.
242func TestStatusPublishesTheHeldEpoch(t *testing.T) {
243 h := newHarness(t, "a", "b")
244 h.stage("v2", "v3")
245 h.install("v2", "a")
246 h.install("v3", "b")
247 h.sleepHost("a")
248 h.sleepHost("b")
249 h.pass()
250 h.book.Record(FailureRecord{Account: "bab", App: "blog", Build: "v2", Host: "sib", Reason: "exited early", At: t0})
251 h.book.MarkHoldRan("b", HoldRan{Account: "bab", App: "blog", Build: "v4"})
252 r := h.m.Status()
253 if got := r.Hosts["a"]; got.HeldBy != "v2" || got.HeldEpoch == 0 || got.HeldEpoch != h.book.RestrictionEpoch("bab", "blog", "v2", "a") {
254 t.Fatalf("a = %+v: want held by v2 at its epoch %d", got, h.book.RestrictionEpoch("bab", "blog", "v2", "a"))
255 }
256 if got := r.Hosts["b"]; got.HeldBy != "v4" || got.HeldEpoch == 0 || got.HeldEpoch != h.book.RestrictionEpoch("bab", "blog", "v4", "b") {
257 t.Fatalf("b = %+v: want held by the marker's v4 at its epoch", got)
258 }
259 if got := r.Hosts["b"]; got.HeldBook == "" || got.HeldBook != h.book.ID() || r.Hosts["a"].HeldBook != h.book.ID() {
260 t.Fatalf("held books %q and %q: want the book's identity %q on both arms", r.Hosts["a"].HeldBook, got.HeldBook, h.book.ID())
261 }
262 // Beside HeldBook, the retry protocol this box takes (readiness round 2,
263 // Task 3): its grants ride the host-retry log, which the console checks
264 // before it writes one.
265 if r.Hosts["a"].RetryV != status.HostRetryV || r.Hosts["b"].RetryV != status.HostRetryV || status.HostRetryV != 1 {
266 t.Fatalf("retry_v %d and %d: want %d on both arms", r.Hosts["a"].RetryV, r.Hosts["b"].RetryV, status.HostRetryV)
267 }
268 if !h.m.RetryHost("b", r.Hosts["b"].HeldBy, r.Hosts["b"].HeldBook, r.Hosts["b"].HeldEpoch) {
269 t.Fatal("a grant signed from the status report cleared nothing")
270 }
271}
272