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 "context"
5 "errors"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/activator"
11 "github.com/carlosframework/platform/internal/appconfig"
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15func TestPolicyFromManifest(t *testing.T) {
16 cases := []struct {
17 m *appconfig.Manifest
18 want AppPolicy
19 }{
20 {nil, AppPolicy{Swap: activator.ModeOverlap, Concurrency: 1}},
21 {&appconfig.Manifest{}, AppPolicy{Swap: activator.ModeOverlap, Concurrency: 1}},
22 {&appconfig.Manifest{Swap: "overlap", SwapConcurrency: 4}, AppPolicy{Swap: activator.ModeOverlap, Concurrency: 4}},
23 {&appconfig.Manifest{Swap: "hold"}, AppPolicy{Swap: activator.ModeHold, Concurrency: 1}},
24 // A value this binary does not know never permits an overlap.
25 {&appconfig.Manifest{Swap: "blue-green"}, AppPolicy{Swap: activator.ModeHold, Concurrency: 1}},
26 {&appconfig.Manifest{SwapConcurrency: -3}, AppPolicy{Swap: activator.ModeOverlap, Concurrency: 1}},
27 }
28 for _, c := range cases {
29 if got := PolicyFromManifest(c.m); got != c.want {
30 t.Errorf("PolicyFromManifest(%+v) = %+v, want %+v", c.m, got, c.want)
31 }
32 }
33}
34
35func TestModesHoldIfEitherSaysHold(t *testing.T) {
36 overlap, hold := AppPolicy{Swap: activator.ModeOverlap, Concurrency: 1}, AppPolicy{Swap: activator.ModeHold, Concurrency: 1}
37 cases := []struct {
38 p AppPolicy
39 demand string
40 want activator.SwapMode
41 }{
42 {overlap, "", activator.ModeOverlap},
43 {overlap, "hold", activator.ModeHold},
44 {hold, "", activator.ModeHold},
45 {hold, "hold", activator.ModeHold},
46 {overlap, "something-new", activator.ModeHold},
47 }
48 for _, c := range cases {
49 if got := AttemptMode(c.p, c.demand); got != c.want {
50 t.Errorf("AttemptMode(%v, %q) = %v, want %v", c.p.Swap, c.demand, got, c.want)
51 }
52 // Recovery reads the RETAINED demand with the CURRENT policy (spec
53 // §4.2, round 12): an app switched to hold later still gets hold.
54 if got := RecoveryMode(c.p, c.demand); got != c.want {
55 t.Errorf("RecoveryMode(%v, %q) = %v, want %v", c.p.Swap, c.demand, got, c.want)
56 }
57 }
58}
59
60// Review focus 2: a bucket blip while the policy is read.
61func TestPolicyCacheNeverGuessesOverlap(t *testing.T) {
62 now := time.Unix(1_700_000_000, 0)
63 var calls int
64 var answer *appconfig.Manifest
65 var fail error
66 c := &policyCache{
67 load: func(context.Context, string, string) (*appconfig.Manifest, error) {
68 calls++
69 return answer, fail
70 },
71 ttl: 30 * time.Second,
72 now: func() time.Time { return now },
73 m: map[appKey]*policyEntry{},
74 }
75 ctx := context.Background()
76 fail = errors.New("SlowDown")
77 if got := c.get(ctx, "bab", "blog"); got.Swap != activator.ModeHold {
78 t.Fatalf("never-read policy = %v, want hold", got.Swap)
79 }
80 now = now.Add(policyRetryAfter) // the failed read's backoff (Task 11)
81 fail, answer = nil, &appconfig.Manifest{SwapConcurrency: 2}
82 if got := c.get(ctx, "bab", "blog"); got != (AppPolicy{Swap: activator.ModeOverlap, Concurrency: 2}) {
83 t.Fatalf("first read = %+v", got)
84 }
85 n := calls
86 if c.get(ctx, "bab", "blog"); calls != n {
87 t.Fatal("a fresh entry was read again inside the TTL")
88 }
89 now = now.Add(31 * time.Second)
90 fail = errors.New("SlowDown")
91 if got := c.get(ctx, "bab", "blog"); got != (AppPolicy{Swap: activator.ModeOverlap, Concurrency: 2}) {
92 t.Fatalf("after a failed refresh = %+v, want the last known policy", got)
93 }
94}
95
96// Task 11 rule 1 (Astra R1 Flag triage T3): two fills of one app's policy
97// can overlap, and the older one can finish last. Its result is older than
98// what the newer fill stored, so it must not replace it.
99func TestPolicyCacheAnOlderFillNeverOverwritesANewerOne(t *testing.T) {
100 now := time.Unix(1_700_000_000, 0)
101 entered, release := make(chan struct{}), make(chan struct{})
102 var mu sync.Mutex
103 calls := 0
104 c := &policyCache{
105 load: func(ctx context.Context, _, _ string) (*appconfig.Manifest, error) {
106 if _, ok := ctx.Deadline(); !ok {
107 t.Error("a policy load ran with no deadline (readBound)")
108 }
109 mu.Lock()
110 calls++
111 n := calls
112 mu.Unlock()
113 if n == 1 {
114 close(entered)
115 <-release
116 return &appconfig.Manifest{Swap: "hold"}, nil
117 }
118 return &appconfig.Manifest{SwapConcurrency: 3}, nil
119 },
120 ttl: 30 * time.Second,
121 now: func() time.Time { return now },
122 m: map[appKey]*policyEntry{},
123 }
124 ctx := context.Background()
125 older := make(chan AppPolicy)
126 go func() { older <- c.get(ctx, "bab", "blog") }()
127 <-entered
128 newer := AppPolicy{Swap: activator.ModeOverlap, Concurrency: 3}
129 if got := c.get(ctx, "bab", "blog"); got != newer {
130 t.Fatalf("the newer fill = %+v, want %+v", got, newer)
131 }
132 close(release)
133 if got := <-older; got != newer {
134 t.Fatalf("the older fill, finishing last, answered %+v; want the newer stored %+v", got, newer)
135 }
136 if got := c.get(ctx, "bab", "blog"); got != newer {
137 t.Fatalf("after the older fill finished = %+v, want the newer fill's %+v", got, newer)
138 }
139 if calls != 2 {
140 t.Fatalf("%d loads, want 2: the stored value is fresh", calls)
141 }
142}
143
144// Task 11 rule 1 (Astra R1 Flag triage T3): a failed read is not retried for
145// policyRetryAfter, whether or not a value was ever read. Meanwhile the
146// cache answers its last value, or hold for an app never read. A missing
147// bundle (appconfig.Get's empty manifest) is the defaults; an error never is.
148func TestPolicyCacheBacksOffAFailedRead(t *testing.T) {
149 now := time.Unix(1_700_000_000, 0)
150 calls := 0
151 var answer *appconfig.Manifest
152 fail := errors.New("SlowDown")
153 c := &policyCache{
154 load: func(context.Context, string, string) (*appconfig.Manifest, error) {
155 calls++
156 return answer, fail
157 },
158 ttl: 30 * time.Second,
159 now: func() time.Time { return now },
160 m: map[appKey]*policyEntry{},
161 }
162 ctx := context.Background()
163 hold := AppPolicy{Swap: activator.ModeHold, Concurrency: 1}
164 for i := range 3 {
165 if got := c.get(ctx, "bab", "blog"); got != hold {
166 t.Fatalf("never-read policy, read %d = %+v, want hold", i, got)
167 }
168 now = now.Add(time.Second)
169 }
170 if calls != 1 {
171 t.Fatalf("%d loads inside policyRetryAfter of a failure, want 1", calls)
172 }
173 now = now.Add(policyRetryAfter)
174 fail, answer = nil, &appconfig.Manifest{Schema: 1, App: "blog"} // missing: appconfig.Get's empty manifest
175 if got := c.get(ctx, "bab", "blog"); got != DefaultPolicy() || calls != 2 {
176 t.Fatalf("after the backoff = %+v (%d loads), want the defaults from a second load", got, calls)
177 }
178 now = now.Add(31 * time.Second)
179 fail = errors.New("SlowDown")
180 for i := range 3 {
181 if got := c.get(ctx, "bab", "blog"); got != DefaultPolicy() {
182 t.Fatalf("stale policy with a failing read, read %d = %+v, want the last known", i, got)
183 }
184 now = now.Add(time.Second)
185 }
186 if calls != 3 {
187 t.Fatalf("%d loads, want 3: one failed refresh, then none inside policyRetryAfter", calls)
188 }
189}
190
191func TestManagedIsTheActivatorsExecRoutes(t *testing.T) {
192 cases := []struct {
193 rt registry.Route
194 want bool
195 }{
196 {registry.Route{Kind: "instance", App: "blog", Status: "ready", Hibernate: true}, true},
197 {registry.Route{Kind: "instance", App: "blog", Status: "ready"}, false}, // a systemd unit, not an activator child
198 {registry.Route{Kind: "instance", App: "blog", Status: "ready", Hibernate: true, Unit: "console.service"}, false}, // named-unit route (spec §13)
199 {registry.Route{Kind: "instance", App: "blog", Status: "ready", Hibernate: true, Backing: registry.BackingUnit}, false}, // carlos-app@
200 {registry.Route{Kind: "instance", App: "blog", Status: "ready", Hibernate: true, Platform: true}, false},
201 {registry.Route{Kind: "sidecar", App: "eleven", Status: "ready"}, true},
202 {registry.Route{Kind: "static", App: "site", Status: "ready", Hibernate: true}, false},
203 }
204 for _, c := range cases {
205 if got := Managed(c.rt); got != c.want {
206 t.Errorf("Managed(%+v) = %v, want %v", c.rt, got, c.want)
207 }
208 }
209}
210
211// Task 11 fix round 1, review finding 3: a read the caller abandoned (its
212// own context ended, a wake or an attempt that stopped) says nothing about
213// the store, so it is not a failed read and does not hold off the next
214// caller for policyRetryAfter. The read bound's own deadline, with the
215// caller still waiting, is a failed read.
216func TestPolicyCacheACallersCancellationIsNotAFailedRead(t *testing.T) {
217 now := time.Unix(1_700_000_000, 0)
218 calls := 0
219 var fail error
220 c := &policyCache{
221 load: func(ctx context.Context, _, _ string) (*appconfig.Manifest, error) {
222 calls++
223 if err := ctx.Err(); err != nil {
224 return nil, err
225 }
226 if fail != nil {
227 return nil, fail
228 }
229 return &appconfig.Manifest{Schema: 1, App: "blog", Swap: appconfig.SwapOverlap, SwapConcurrency: 2}, nil
230 },
231 ttl: 30 * time.Second,
232 now: func() time.Time { return now },
233 m: map[appKey]*policyEntry{},
234 }
235 hold := AppPolicy{Swap: activator.ModeHold, Concurrency: 1}
236 gone, cancel := context.WithCancel(context.Background())
237 cancel()
238 if got := c.get(gone, "bab", "blog"); got != hold {
239 t.Fatalf("a cancelled caller's read = %+v, want hold", got)
240 }
241 want := AppPolicy{Swap: activator.ModeOverlap, Concurrency: 2}
242 if got := c.get(context.Background(), "bab", "blog"); got != want || calls != 2 {
243 t.Fatalf("the next caller's read = %+v after %d loads, want %+v from a second load: a cancelled caller held it off", got, calls, want)
244 }
245 now = now.Add(31 * time.Second)
246 fail = context.DeadlineExceeded // the read bound ran out, the caller still waiting
247 c.get(context.Background(), "bab", "blog")
248 now = now.Add(time.Second)
249 if got := c.get(context.Background(), "bab", "blog"); got != want || calls != 3 {
250 t.Fatalf("read inside policyRetryAfter of a timed-out read = %+v after %d loads, want the last value and 3 loads", got, calls)
251 }
252}
253