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 "maps"
5 "slices"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/activator"
10)
11
12// retryHarness is a harness whose s fails v3 and whose t fails v4: each
13// failure writes a record that holds h too, and halts that build's cohort.
14// h serves and wants v1 throughout.
15func retryHarness(t *testing.T) (*harness, func(build string) bool) {
16 t.Helper()
17 k := appKey{"bab", "blog"}
18 h := newHarness(t, "h", "s", "t")
19 h.stage("v2", "v3", "v4")
20 h.act.mu.Lock()
21 h.act.outcome = func(a activator.Attempt) activator.SwapOutcome {
22 if (a.Host == "s" && a.Build == "v3") || (a.Host == "t" && a.Build == "v4") {
23 return activator.SwapFailed
24 }
25 return activator.SwapDone
26 }
27 h.act.mu.Unlock()
28 h.pass()
29 s := h.m.scheduler(k)
30 halted := func(build string) bool {
31 s.mu.Lock()
32 defer s.mu.Unlock()
33 return s.q.cohortFor(cohortKey(build, classAdopt)).halted
34 }
35 h.install("v3", "s")
36 h.pass()
37 waitFor(t, "v3's cohort to halt", func() bool { return halted("v3") })
38 h.install("v4", "t")
39 h.pass()
40 waitFor(t, "v4's cohort to halt", func() bool { return halted("v4") })
41 return h, halted
42}
43
44// running is h's running attempt, nil when none.
45func (h *harness) running(host string) *dispatch {
46 s := h.m.scheduler(appKey{"bab", "blog"})
47 s.mu.Lock()
48 defer s.mu.Unlock()
49 if d := s.q.hosts[host].running; d != nil {
50 c := *d
51 return &c
52 }
53 return nil
54}
55
56// Final review round 2, finding 5 (Astra): retry entitlements are kept per
57// host and build. B's (v3's) cohort is halted; h is retried for v3 while it
58// wants v1, then for another restricted build C (v4) before it adopts v3.
59// The second retry must not overwrite the first: when h comes to want v3,
60// the book already lists h as retried for v3 (so a second retry of v3
61// clears nothing), and only the entitlement the first retry bound can take
62// h through v3's halt.
63func TestARetryForASecondBuildKeepsTheFirstBuildsCanary(t *testing.T) {
64 h, halted := retryHarness(t)
65 if !h.retry("h", "v3") {
66 t.Fatal("the retry for v3 cleared nothing")
67 }
68 if !h.retry("h", "v4") {
69 t.Fatal("the retry for v4 cleared nothing")
70 }
71 if h.retry("h", "v3") {
72 t.Fatal("a second retry of v3 cleared something: the case needs the book to refuse it")
73 }
74 if !halted("v3") || !halted("v4") {
75 t.Fatal("a cohort is no longer halted: the case needs both halts")
76 }
77
78 gate := h.act.hold("h")
79 h.install("v3", "h")
80 h.pass()
81 waitFor(t, "h's adoption onto v3 through the halt", func() bool {
82 _, started, _ := h.act.snapshot()
83 return slices.Contains(started, "h:v3")
84 })
85 if d := h.running("h"); d == nil || d.ID.Build != "v3" || !d.Canary || !d.Forced {
86 t.Fatalf("h's running attempt %+v: want the forced canary onto v3", d)
87 }
88 close(gate)
89 waitFor(t, "h's adoption onto v3", func() bool { return h.servingAll("v3", "h") && !h.m.attemptRunning("h") })
90
91 // v4's entitlement was neither overwritten nor spent by v3's adoption.
92 gate = h.act.hold("h")
93 defer close(gate)
94 h.install("v4", "h")
95 h.pass()
96 waitFor(t, "h's adoption onto v4 through the halt", func() bool {
97 _, started, _ := h.act.snapshot()
98 return slices.Contains(started, "h:v4")
99 })
100 if d := h.running("h"); d == nil || d.ID.Build != "v4" || !d.Canary || !d.Forced {
101 t.Fatalf("h's running attempt %+v: want the forced canary onto v4", d)
102 }
103}
104
105// An entitlement lasts only while the book lists the host as retried for its
106// build. Once the record is gone (cleared by a signed pointer, or pruned when
107// no route installs the build), the next pass drops it: the set cannot grow
108// without bound, and a record the build may get later starts with no
109// entitlement from a retry of the old one.
110func TestARetryEntitlementIsDroppedWhenTheBookNoLongerListsTheHost(t *testing.T) {
111 for _, tc := range []struct {
112 name string
113 clear func(h *harness)
114 }{
115 {"cleared by a pointer", func(h *harness) {
116 // s still wants v3 and may adopt it before h: it succeeds now,
117 // so no fresh record of v3 races h's adoption.
118 h.act.mu.Lock()
119 h.act.outcome = func(a activator.Attempt) activator.SwapOutcome {
120 if a.Host == "t" && a.Build == "v4" {
121 return activator.SwapFailed
122 }
123 return activator.SwapDone
124 }
125 h.act.mu.Unlock()
126 if ok, _ := h.book.ClearOnPointer("bab", "blog", "v3", t0.Add(time.Hour)); !ok {
127 h.t.Fatal("the pointer cleared nothing")
128 }
129 }},
130 {"pruned", func(h *harness) {
131 h.install("v1", "s") // no route installs v3 any more
132 }},
133 } {
134 t.Run(tc.name, func(t *testing.T) {
135 h, halted := retryHarness(t)
136 if !h.retry("h", "v3") {
137 t.Fatal("the retry for v3 cleared nothing")
138 }
139 if !h.retry("h", "v4") {
140 t.Fatal("the retry for v4 cleared nothing")
141 }
142 tc.clear(h)
143 h.pass()
144 if halted("v3") {
145 t.Fatal("v3's halt outlived its record")
146 }
147 s := h.m.scheduler(appKey{"bab", "blog"})
148 s.mu.Lock()
149 got := slices.Sorted(maps.Keys(s.q.hosts["h"].forceBuilds))
150 s.mu.Unlock()
151 if !slices.Equal(got, []string{"v4"}) {
152 t.Fatalf("h's entitlements %v, want only v4's: v3's record is gone", got)
153 }
154
155 gate := h.act.hold("h")
156 defer close(gate)
157 h.install("v3", "h")
158 h.pass()
159 waitFor(t, "h's adoption onto v3", func() bool {
160 _, started, _ := h.act.snapshot()
161 return slices.Contains(started, "h:v3")
162 })
163 // Whether it is the cohort's canary depends on whether s went
164 // first; either way it is not a retried host's forced canary.
165 if d := h.running("h"); d == nil || d.ID.Build != "v3" || d.Forced {
166 t.Fatalf("h's running attempt %+v: want an ordinary adoption onto v3, not a retried one", d)
167 }
168 })
169 }
170}
171