carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3// The narrowed restart's WIRE SHAPE (discussion #16): what lands in the
4// bucket, what a reader makes of it, and — the properties the whole design
5// rests on — that an old reader is untouched by it and a new reader never
6// widens a request it cannot understand.
7
8import (
9 "context"
10 "encoding/json"
11 "os"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/registry"
16)
17
18func loadScopes(t *testing.T, s Store, acct, app string) *ScopedRestartGeneration {
19 t.Helper()
20 g, _, err := LoadRestartScopes(context.Background(), s, acct, app)
21 if err != nil {
22 t.Fatalf("LoadRestartScopes: %v", err)
23 }
24 return g
25}
26
27// TestAppendRestartScopeRecordsEachRequest: the object is a LOG, not a
28// generation. Two requests inside one poll window must both survive — the etag
29// would have collapsed them into whichever landed last, and the earlier
30// selector would vanish having been answered "requested".
31func TestAppendRestartScopeRecordsEachRequest(t *testing.T) {
32 s := NewDirStore(t.TempDir())
33 ctx := context.Background()
34 first, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeHost, Value: "bill.example.com"})
35 if err != nil {
36 t.Fatalf("first append: %v", err)
37 }
38 second, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeEnvironment, Value: "relay"})
39 if err != nil {
40 t.Fatalf("second append: %v", err)
41 }
42 if first.ID == second.ID {
43 t.Fatalf("both requests share id %q — a box could not tell them apart", first.ID)
44 }
45 if _, err := time.Parse(time.RFC3339, first.RequestedAt); err != nil {
46 t.Fatalf("RequestedAt %q is not RFC3339: %v", first.RequestedAt, err)
47 }
48 g := loadScopes(t, s, "bab", "titogo")
49 if g.Schema != 1 {
50 t.Fatalf("Schema = %d, want 1", g.Schema)
51 }
52 if len(g.Requests) != 2 || g.Requests[0].ID != first.ID || g.Requests[1].ID != second.ID {
53 t.Fatalf("log = %+v, want both requests, oldest first", g.Requests)
54 }
55 if g.Newest() != second.ID {
56 t.Fatalf("Newest = %q, want the last request %q", g.Newest(), second.ID)
57 }
58}
59
60// TestAppendRestartScopeLeavesTheAppWideObjectAlone is the ADDITIVE property
61// stated as a test: an old box watches restart-gen.json's etag and nothing
62// else, so a narrowed request must not touch it. If it did, every box in the
63// fleet that predates narrowing would read the movement the only way it knows
64// how — restart the whole app — which is the accident this feature prevents.
65func TestAppendRestartScopeLeavesTheAppWideObjectAlone(t *testing.T) {
66 s := NewDirStore(t.TempDir())
67 ctx := context.Background()
68 if err := TouchRestartGen(ctx, s, "bab", "titogo"); err != nil {
69 t.Fatalf("TouchRestartGen: %v", err)
70 }
71 before := restartEtag(t, s, "bab", "titogo")
72 if _, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeHost, Value: "bill.example.com"}); err != nil {
73 t.Fatalf("append: %v", err)
74 }
75 if after := restartEtag(t, s, "bab", "titogo"); after != before {
76 t.Fatalf("restart-gen etag moved from %q to %q on a narrowed request — every old box would cycle the whole app", before, after)
77 }
78}
79
80// TestRestartScopeAbsentIsLegal: an app nobody has narrowed a restart for has
81// no object, forever, and reading that is not an error.
82func TestRestartScopeAbsentIsLegal(t *testing.T) {
83 s := NewDirStore(t.TempDir())
84 g, found, err := LoadRestartScopes(context.Background(), s, "bab", "keymail")
85 if err != nil {
86 t.Fatalf("LoadRestartScopes on an absent object: %v", err)
87 }
88 if found {
89 t.Fatal("found = true for an app with no object")
90 }
91 if g.Newest() != RestartScopeNone {
92 t.Fatalf("Newest = %q, want the recorded-nothing sentinel", g.Newest())
93 }
94 pending, ok := g.PendingSince(RestartScopeNone)
95 if !ok || len(pending) != 0 {
96 t.Fatalf("PendingSince(none) = %v, %v — an absent object owes nobody a restart", pending, ok)
97 }
98}
99
100// TestPendingSinceReturnsOnlyWhatIsNew: a box that acted up to a mark owes
101// exactly the suffix after it, and nothing else.
102func TestPendingSinceReturnsOnlyWhatIsNew(t *testing.T) {
103 s := NewDirStore(t.TempDir())
104 ctx := context.Background()
105 var ids []string
106 for _, env := range []string{"relay", "router", "demo"} {
107 req, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeEnvironment, Value: env})
108 if err != nil {
109 t.Fatalf("append %s: %v", env, err)
110 }
111 ids = append(ids, req.ID)
112 }
113 g := loadScopes(t, s, "bab", "titogo")
114
115 // A box that has never looked records "none" and then owes everything.
116 pending, ok := g.PendingSince(RestartScopeNone)
117 if !ok || len(pending) != 3 {
118 t.Fatalf("PendingSince(none) = %d requests, %v; want all three", len(pending), ok)
119 }
120 pending, ok = g.PendingSince(ids[0])
121 if !ok || len(pending) != 2 || pending[0].Scope.Value != "router" {
122 t.Fatalf("PendingSince(first) = %+v, %v; want router and demo", pending, ok)
123 }
124 if pending, ok := g.PendingSince(ids[2]); !ok || len(pending) != 0 {
125 t.Fatalf("PendingSince(newest) = %+v, %v; want nothing owed", pending, ok)
126 }
127}
128
129// TestPendingSinceReportsALostPosition: a mark that has rolled off the end of
130// the log has no honest answer, and the caller must be told so rather than
131// handed a guess. Replaying the whole log would cycle production for requests
132// served days ago; the box re-baselines instead.
133func TestPendingSinceReportsALostPosition(t *testing.T) {
134 g := &ScopedRestartGeneration{Schema: 1, Requests: []RestartRequest{
135 {ID: "b", Scope: RestartScope{Kind: RestartScopeHost, Value: "b.example.com"}},
136 }}
137 if pending, ok := g.PendingSince("a-that-rolled-off"); ok || len(pending) != 0 {
138 t.Fatalf("PendingSince(lost) = %+v, %v; want nothing and a report that the position is unknown", pending, ok)
139 }
140}
141
142// TestRestartScopeLogIsBounded: the object stays small, and the oldest entries
143// are what go. A box far enough behind to lose its position re-baselines,
144// which is the safe direction.
145func TestRestartScopeLogIsBounded(t *testing.T) {
146 s := NewDirStore(t.TempDir())
147 ctx := context.Background()
148 var first RestartRequest
149 for i := 0; i < restartScopeKeep+5; i++ {
150 req, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeHost, Value: "a.example.com"})
151 if err != nil {
152 t.Fatalf("append %d: %v", i, err)
153 }
154 if i == 0 {
155 first = req
156 }
157 }
158 g := loadScopes(t, s, "bab", "titogo")
159 if len(g.Requests) != restartScopeKeep {
160 t.Fatalf("log holds %d requests, want it trimmed to %d", len(g.Requests), restartScopeKeep)
161 }
162 if _, ok := g.PendingSince(first.ID); ok {
163 t.Fatal("the oldest request survived the trim — the log is unbounded")
164 }
165}
166
167// TestRestartScopeSelectsNarrowsAndNeverWidens is the forward-compatibility
168// rule: an unknown selector kind addresses NOTHING and says it did not
169// understand, so a `--region` invented later cannot make this binary fall back
170// to cycling the app.
171func TestRestartScopeSelectsNarrowsAndNeverWidens(t *testing.T) {
172 relay := registry.Route{Host: "connect.example.com", App: "titogo", Environment: "relay"}
173 plain := registry.Route{Host: "bill.example.com", App: "titogo"}
174
175 if sel, ok := (RestartScope{Kind: RestartScopeHost, Value: "bill.example.com"}).Selects(plain); !sel || !ok {
176 t.Fatalf("host selector missed its own route (sel %v, understood %v)", sel, ok)
177 }
178 if sel, _ := (RestartScope{Kind: RestartScopeHost, Value: "bill.example.com"}).Selects(relay); sel {
179 t.Fatal("host selector matched another route")
180 }
181 if sel, ok := (RestartScope{Kind: RestartScopeEnvironment, Value: "relay"}).Selects(relay); !sel || !ok {
182 t.Fatalf("environment selector missed a bound route (sel %v, understood %v)", sel, ok)
183 }
184 // An unbound route reads the app's DEFAULT bundle. "" is the absence of an
185 // environment, not the name of one, so an environment selector never
186 // reaches it — otherwise a bundle-shaped request would silently become an
187 // app-wide one for every route nobody had bound.
188 if sel, _ := (RestartScope{Kind: RestartScopeEnvironment, Value: ""}).Selects(plain); sel {
189 t.Fatal("an empty environment selector matched an unbound route — that is an app-wide restart in disguise")
190 }
191 for _, rt := range []registry.Route{relay, plain} {
192 sel, understood := (RestartScope{Kind: "region", Value: "eu-west-1"}).Selects(rt)
193 if sel || understood {
194 t.Fatalf("a selector kind this build does not know addressed %s (sel %v, understood %v)", rt.Host, sel, understood)
195 }
196 }
197}
198
199// TestValidRestartScopeRefusesWhatNoBoxCouldAct: the last gate before the
200// bucket. A request nothing could ever match must not be written, or a box
201// would step over it forever.
202func TestValidRestartScopeRefusesWhatNoBoxCouldAct(t *testing.T) {
203 for _, sc := range []RestartScope{
204 {Kind: "", Value: "x"},
205 {Kind: "region", Value: "eu-west-1"},
206 {Kind: RestartScopeApp, Value: ""}, // app-wide has its own object
207 {Kind: RestartScopeHost, Value: ""},
208 {Kind: RestartScopeEnvironment, Value: "bad\nvalue"},
209 } {
210 if err := ValidRestartScope(sc); err == nil {
211 t.Fatalf("ValidRestartScope(%+v) accepted it", sc)
212 }
213 }
214 if err := ValidRestartScope(RestartScope{Kind: RestartScopeHost, Value: "a.example.com"}); err != nil {
215 t.Fatalf("a good host scope was refused: %v", err)
216 }
217}
218
219// TestRestartScopeRefusesANewerSchema: a log written by a console this binary
220// is older than must not be guessed at, and must not be OVERWRITTEN either —
221// appending to it would silently drop entries this build cannot see. Both
222// paths refuse, which leaves the request to be picked up after an upgrade.
223func TestRestartScopeRefusesANewerSchema(t *testing.T) {
224 s := NewDirStore(t.TempDir())
225 ctx := context.Background()
226 raw, err := json.Marshal(map[string]any{"schema": 2, "requests": []any{}})
227 if err != nil {
228 t.Fatalf("marshal: %v", err)
229 }
230 if _, err := s.Put(ctx, RestartScopeKey("bab", "titogo"), raw, ""); err != nil {
231 t.Fatalf("seed a newer object: %v", err)
232 }
233 if _, _, err := LoadRestartScopes(ctx, s, "bab", "titogo"); err == nil {
234 t.Fatal("a schema-2 log parsed as though this build understood it")
235 }
236 if _, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeHost, Value: "a.example.com"}); err == nil {
237 t.Fatal("appended to a schema-2 log — entries this build cannot read would have been dropped")
238 }
239}
240
241// TestAppendRestartScopeRetriesACASConflict is the difference from
242// TouchRestartGen, which treats a conflict as success. There, any touch fires
243// the one signal that exists; here the other writer's request names a
244// DIFFERENT set of routes, so losing ours would answer "requested" for a
245// restart that never happens.
246func TestAppendRestartScopeRetriesACASConflict(t *testing.T) {
247 s := &conflictOnceStore{Store: NewDirStore(t.TempDir())}
248 ctx := context.Background()
249 if _, err := AppendRestartScope(ctx, s, "bab", "titogo", RestartScope{Kind: RestartScopeHost, Value: "a.example.com"}); err != nil {
250 t.Fatalf("append through one conflict: %v", err)
251 }
252 if s.conflicts != 1 {
253 t.Fatalf("the store served %d conflicts, want the one this test staged", s.conflicts)
254 }
255 if g := loadScopes(t, s.Store, "bab", "titogo"); len(g.Requests) != 1 {
256 t.Fatalf("log = %+v, want the retried request to have landed", g.Requests)
257 }
258}
259
260// conflictOnceStore fails the FIRST Put with a CAS conflict, as a racing
261// writer would, and behaves normally after that.
262type conflictOnceStore struct {
263 Store
264 conflicts int
265}
266
267func (c *conflictOnceStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
268 if c.conflicts == 0 {
269 c.conflicts++
270 return "", ErrCASConflict
271 }
272 return c.Store.Put(ctx, key, body, ifMatch)
273}
274
275// TestRestartScopeKeyIsUnderTheAppPrefix: everything an app owns lives under
276// one account-scoped prefix, permanently — trash, retention and per-app
277// deletion all depend on it.
278func TestRestartScopeKeyIsUnderTheAppPrefix(t *testing.T) {
279 if got, want := RestartScopeKey("bab", "titogo"), AppPrefix("bab", "titogo")+"restart-scope.json"; got != want {
280 t.Fatalf("RestartScopeKey = %q, want %q", got, want)
281 }
282 if RestartScopeKey("bab", "titogo") == RestartGenKey("bab", "titogo") {
283 t.Fatal("the narrowed log shares a key with the app-wide generation — an old box would read it as an app-wide request")
284 }
285}
286
287// TestAppendRestartScopeRefusesABadName keeps a hostile account or app out of
288// the key, the same gate TouchRestartGen applies.
289func TestAppendRestartScopeRefusesABadName(t *testing.T) {
290 s := NewDirStore(t.TempDir())
291 ctx := context.Background()
292 if _, err := AppendRestartScope(ctx, s, "../etc", "titogo", RestartScope{Kind: RestartScopeHost, Value: "a.example.com"}); err == nil {
293 t.Fatal("a traversal-shaped account was accepted")
294 }
295 if _, err := AppendRestartScope(ctx, s, "bab", "../x", RestartScope{Kind: RestartScopeHost, Value: "a.example.com"}); err == nil {
296 t.Fatal("a traversal-shaped app was accepted")
297 }
298 if _, err := os.Stat(t.TempDir()); err != nil {
299 t.Fatalf("temp dir: %v", err)
300 }
301}
302
303// A retry is not a restart: Selects must not read one (an older console's,
304// left in this log) as a restart of anything. No new write puts one here
305// (TestValidRestartScopeRefusesARetryAndASignature).
306func TestRetryScopeSelectsNothing(t *testing.T) {
307 sc := RestartScope{Kind: RestartScopeRetry, Value: "a.example.com"}
308 if sel, understood := sc.Selects(registry.Route{Host: "a.example.com"}); sel || understood {
309 t.Fatalf("Selects = %v, %v; a retry must never select a route for a restart", sel, understood)
310 }
311}
312