carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "maps"
9 "os"
10 "slices"
11 "strings"
12 "sync"
13
14 "github.com/carlosframework/platform/internal/domains"
15 "github.com/carlosframework/platform/internal/release"
16)
17
18// ViewSnapshot is one immutable publication of a RecordView. Epoch increases
19// with every publication. Complete is false while any scope has a failed or
20// outstanding re-read, after a walk that could not list an account's apps,
21// or when the record walk over the snapshot would itself be partial (an
22// account manifest that will not read cleanly, an app listing that failed).
23// It is never read and changed at once: a Refresh builds a new one.
24type ViewSnapshot struct {
25 Epoch uint64
26 Complete bool
27
28 accounts map[string]*acctEntry // sqid -> accounts/<sqid>/
29 strays []string // keys under accounts/ outside every account scope, from the last walk
30 apps map[string]map[string]*appEntry // acct -> app -> apps/<acct>/<app>/
31 dirErrs map[string]error // acct -> why the last walk could not list apps/<acct>/
32 walkErr error // the last walk failed at the top: accounts/ and apps/ answer it
33}
34
35// acctEntry is one account scope: its complete key listing and account.json,
36// or the listing failure (which the manifest then carries too, so a reader
37// asking for the manifest learns the read failed rather than that it is
38// absent).
39type acctEntry struct {
40 keys []release.KeyETag
41 listErr error
42 manifest object
43}
44
45// appEntry is one app scope: both record listings (or why each failed), and
46// objs, every listed record body plus the two manifests and the trash marker.
47type appEntry struct {
48 inst, side []release.KeyETag
49 instErr, sideErr error
50 objs map[string]object
51}
52
53// object is one cached read: a body and its ETag, a not-found error wrapping
54// os.ErrNotExist, or the read error itself.
55type object struct {
56 body []byte
57 etag string
58 err error
59}
60
61func absent(key string) object {
62 return object{err: fmt.Errorf("provision: record view: %s: %w", key, os.ErrNotExist)}
63}
64
65// failed reports a read that did not answer: anything but a body or absence.
66func (o object) failed() bool { return o.err != nil && !errors.Is(o.err, os.ErrNotExist) }
67
68// result is the object as Get returns it, with a body the caller may keep.
69func (o object) result() ([]byte, string, error) {
70 if o.err != nil {
71 return nil, "", o.err
72 }
73 return slices.Clone(o.body), o.etag, nil
74}
75
76func accountManifestKey(a string) string { return "accounts/" + a + "/account.json" }
77
78func (e *acctEntry) empty() bool {
79 return e == nil || (len(e.keys) == 0 && e.listErr == nil)
80}
81
82// empty reports an app with no record-family state at all: nothing listed,
83// nothing failed, and all three per-app objects absent. A purged app is
84// dropped from the view, and from ListDirs over it.
85func (e *appEntry) empty() bool {
86 if e == nil {
87 return true
88 }
89 if len(e.inst) > 0 || len(e.side) > 0 || e.instErr != nil || e.sideErr != nil {
90 return false
91 }
92 for _, o := range e.objs {
93 if !errors.Is(o.err, os.ErrNotExist) {
94 return false
95 }
96 }
97 return true
98}
99
100// object is the cached read of key, or nil when the entry holds none.
101func (e *appEntry) object(key string) *object {
102 if e == nil {
103 return nil
104 }
105 o, ok := e.objs[key]
106 if !ok {
107 return nil
108 }
109 return &o
110}
111
112func (sn *ViewSnapshot) app(a, b string) *appEntry {
113 if sn == nil {
114 return nil
115 }
116 return sn.apps[a][b]
117}
118
119// clone copies the maps a Refresh changes; entries are shared, never mutated.
120func (sn *ViewSnapshot) clone() *ViewSnapshot {
121 out := &ViewSnapshot{
122 accounts: maps.Clone(sn.accounts),
123 strays: sn.strays,
124 apps: make(map[string]map[string]*appEntry, len(sn.apps)),
125 dirErrs: sn.dirErrs,
126 walkErr: sn.walkErr,
127 }
128 for a, inner := range sn.apps {
129 out.apps[a] = maps.Clone(inner)
130 }
131 return out
132}
133
134func (sn *ViewSnapshot) setAccount(a string, e *acctEntry) {
135 if e.empty() {
136 delete(sn.accounts, a)
137 return
138 }
139 sn.accounts[a] = e
140}
141
142func (sn *ViewSnapshot) setApp(a, b string, e *appEntry) {
143 if e.empty() {
144 delete(sn.apps[a], b)
145 if len(sn.apps[a]) == 0 {
146 delete(sn.apps, a)
147 }
148 return
149 }
150 if sn.apps[a] == nil {
151 sn.apps[a] = map[string]*appEntry{}
152 }
153 sn.apps[a][b] = e
154}
155
156// appKey splits apps/<a>/<b>/<rest> with all three parts non-empty.
157func appKey(key string) (a, b, rest string, ok bool) {
158 parts := strings.SplitN(key, "/", 4)
159 if len(parts) < 4 || parts[0] != "apps" || parts[1] == "" || parts[2] == "" || parts[3] == "" {
160 return "", "", "", false
161 }
162 return parts[1], parts[2], parts[3], true
163}
164
165// lookup answers Get of a key in a cached family; handled is false for every
166// other key. A key the view does not hold is absent: its family's listing is
167// complete. A record under a listing that failed answers that failure, and
168// so does any key of an app the view never read because the walk could not
169// list its account's apps.
170func (sn *ViewSnapshot) lookup(key string) (o object, handled bool) {
171 if sn == nil {
172 return object{}, false
173 }
174 if a, ok := domains.AccountManifestSqid(key); ok {
175 e := sn.accounts[a]
176 if e == nil {
177 return absent(key), true
178 }
179 return e.manifest, true
180 }
181 a, b, rest, ok := appKey(key)
182 if !ok {
183 return object{}, false
184 }
185 var listErr error
186 switch {
187 case rest == "instances.json" || rest == "sidecars.json" || rest == "deleted.json":
188 case strings.HasPrefix(rest, "instances/") && rest != "instances/":
189 listErr = sn.app(a, b).listErr(true)
190 case strings.HasPrefix(rest, "sidecars/") && rest != "sidecars/":
191 listErr = sn.app(a, b).listErr(false)
192 default:
193 return object{}, false
194 }
195 if listErr != nil {
196 return object{err: listErr}, true
197 }
198 if err := sn.dirErrs[a]; err != nil && sn.app(a, b) == nil {
199 // The last walk could not list this account's apps, and no refresh
200 // has read this one since: the view does not know, so it must not
201 // answer absent.
202 return object{err: err}, true
203 }
204 if p := sn.app(a, b).object(key); p != nil {
205 return *p, true
206 }
207 return absent(key), true
208}
209
210func (e *appEntry) listErr(inst bool) error {
211 switch {
212 case e == nil:
213 return nil
214 case inst:
215 return e.instErr
216 }
217 return e.sideErr
218}
219
220// list answers List of a cached family, sorted as the stores list.
221func (sn *ViewSnapshot) list(prefix string) (keys []string, handled bool, err error) {
222 if prefix == "accounts/" {
223 if sn.walkErr != nil {
224 return nil, true, sn.walkErr
225 }
226 out := slices.Clone(sn.strays)
227 for a, e := range sn.accounts {
228 if e.listErr != nil {
229 // Unknown keys; the manifest is listed so a reader asks for it
230 // and learns the read failed (knownAccounts' readFailed).
231 out = append(out, accountManifestKey(a))
232 continue
233 }
234 out = append(out, keysOf(e.keys)...)
235 }
236 slices.Sort(out)
237 return out, true, nil
238 }
239 if a, ok := strings.CutPrefix(prefix, "accounts/"); ok {
240 a, ok = strings.CutSuffix(a, "/")
241 if !ok || a == "" || strings.Contains(a, "/") {
242 return nil, false, nil
243 }
244 e := sn.accounts[a]
245 if e == nil {
246 return nil, true, nil
247 }
248 return keysOf(e.keys), true, e.listErr
249 }
250 a, b, rest, ok := appKey(prefix)
251 if !ok || (rest != "instances/" && rest != "sidecars/") {
252 return nil, false, nil
253 }
254 e := sn.app(a, b)
255 if e == nil {
256 return nil, true, sn.dirErrs[a] // unknown, not empty, when the walk could not list the account's apps
257 }
258 if rest == "instances/" {
259 return keysOf(e.inst), true, e.instErr
260 }
261 return keysOf(e.side), true, e.sideErr
262}
263
264// listDirs answers ListDirs("apps/") and ListDirs("apps/<a>/"): accounts and
265// apps holding record-family state, as full prefixes, sorted. An account whose
266// apps the last walk could not list is listed, and listing it fails, as it did.
267func (sn *ViewSnapshot) listDirs(prefix string) (dirs []string, handled bool, err error) {
268 if prefix == "apps/" {
269 if sn.walkErr != nil {
270 return nil, true, sn.walkErr
271 }
272 var out []string
273 for a := range sn.apps {
274 out = append(out, "apps/"+a+"/")
275 }
276 for a := range sn.dirErrs {
277 if sn.apps[a] == nil {
278 out = append(out, "apps/"+a+"/")
279 }
280 }
281 slices.Sort(out)
282 return out, true, nil
283 }
284 a, ok := strings.CutPrefix(prefix, "apps/")
285 if !ok {
286 return nil, false, nil
287 }
288 a, ok = strings.CutSuffix(a, "/")
289 if !ok || a == "" || strings.Contains(a, "/") {
290 return nil, false, nil
291 }
292 if err := sn.dirErrs[a]; err != nil {
293 return nil, true, err
294 }
295 var out []string
296 for b := range sn.apps[a] {
297 out = append(out, prefix+b+"/")
298 }
299 slices.Sort(out)
300 return out, true, nil
301}
302
303// Store is the snapshot as a release.Store over live: the cached families are
304// answered from the snapshot, everything else from live. Every write goes to
305// live, and from then on that key, and every listing it falls under, is read
306// from live for the rest of this store's life, so a caller reads its own
307// writes and a CAS retry re-reads the bucket. Each call returns a fresh store;
308// the snapshot itself never changes.
309func (sn *ViewSnapshot) Store(live release.Store) release.Store {
310 return &snapStore{sn: sn, live: live}
311}
312
313// freshStore is the store a reader that must see the bucket as it is now
314// reads through: live under a snapshot store, s itself otherwise. A guard
315// that re-reads a record to check that nothing moved since the listing
316// (holdExisting) would only read the listing's own snapshot back.
317func freshStore(s release.Store) release.Store {
318 if ss, ok := s.(*snapStore); ok {
319 return ss.live
320 }
321 return s
322}
323
324// onSnapshot reports whether s is a record view snapshot store. A snapshot's
325// absence is not the bucket's decision: a record written without its bump (a
326// failed bump, an old binary, a direct-bucket writer) is missing from a
327// Complete snapshot until the backstop rebuild. So Reconcile and
328// ReconcileSidecars over a snapshot decide any removal from the bucket's own
329// walk (scanRecords, scanSidecars over freshStore), and over any other store
330// from the walk they already made.
331func onSnapshot(s release.Store) bool {
332 _, ok := s.(*snapStore)
333 return ok
334}
335
336// walkErrSuffix renders the error of a bucket walk that could not be read
337// cleanly for a log line: nothing when the walk returned but read partially
338// (its own lines name what failed).
339func walkErrSuffix(err error) string {
340 if err == nil {
341 return ""
342 }
343 return ": " + err.Error()
344}
345
346type snapStore struct {
347 sn *ViewSnapshot
348 live release.Store
349
350 mu sync.Mutex
351 wrote []string
352}
353
354var (
355 _ release.Store = (*snapStore)(nil)
356 _ release.BucketListDirs = (*snapStore)(nil)
357 _ release.Opener = (*snapStore)(nil)
358 _ release.PutStreamer = (*snapStore)(nil)
359 _ release.ConditionalDeleter = (*snapStore)(nil)
360 _ release.ModLister = (*snapStore)(nil)
361 _ release.SizeLister = (*snapStore)(nil)
362 _ release.PageLister = (*snapStore)(nil)
363 _ release.ConditionalGetter = (*snapStore)(nil)
364 _ release.ETagLister = (*snapStore)(nil)
365 _ release.AppMutationRunner = (*snapStore)(nil)
366)
367
368func (s *snapStore) mark(key string) {
369 s.mu.Lock()
370 s.wrote = append(s.wrote, key)
371 s.mu.Unlock()
372}
373
374// wroteUnder reports a write through this store to a key under prefix (or,
375// with exact, to key itself).
376func (s *snapStore) wroteUnder(prefix string, exact bool) bool {
377 s.mu.Lock()
378 defer s.mu.Unlock()
379 for _, k := range s.wrote {
380 if k == prefix || (!exact && strings.HasPrefix(k, prefix)) {
381 return true
382 }
383 }
384 return false
385}
386
387// BucketShapedListDirs: the snapshot answers ListDirs("apps/") and
388// ListDirs("apps/<a>/") in the bucket's shape whatever live's shape is (after
389// a write it asks live only when live is bucket-shaped too), so the record
390// walk lists per app instead of the whole of apps/.
391func (s *snapStore) BucketShapedListDirs() bool { return true }
392
393func (s *snapStore) Get(ctx context.Context, key string) ([]byte, string, error) {
394 if !s.wroteUnder(key, true) {
395 if o, ok := s.sn.lookup(key); ok {
396 return o.result()
397 }
398 }
399 return s.live.Get(ctx, key)
400}
401
402func (s *snapStore) List(ctx context.Context, prefix string) ([]string, error) {
403 if !s.wroteUnder(prefix, false) {
404 if keys, ok, err := s.sn.list(prefix); ok {
405 return keys, err
406 }
407 }
408 return s.live.List(ctx, prefix)
409}
410
411func (s *snapStore) ListDirs(ctx context.Context, prefix string) ([]string, error) {
412 norm := prefix
413 if norm != "" && !strings.HasSuffix(norm, "/") {
414 norm += "/"
415 }
416 dirs, ok, err := s.sn.listDirs(norm)
417 if !ok {
418 return s.live.ListDirs(ctx, prefix)
419 }
420 if err != nil || !s.wroteUnder(norm, false) {
421 return dirs, err
422 }
423 // A write under prefix: live knows the directories it made, but only a
424 // bucket-shaped live answers in the shape this store promises. The fleet
425 // door's ListDirs("apps/") is its account's apps, and walking that as
426 // accounts found no records at all (listAppSubtree). Otherwise the
427 // snapshot's directories plus those the writes imply.
428 if release.HasBucketListDirs(s.live) {
429 return s.live.ListDirs(ctx, prefix)
430 }
431 s.mu.Lock()
432 for _, k := range s.wrote {
433 rest, ok := strings.CutPrefix(k, norm)
434 if !ok {
435 continue
436 }
437 if child, _, ok := strings.Cut(rest, "/"); ok && child != "" {
438 if d := norm + child + "/"; !slices.Contains(dirs, d) {
439 dirs = append(dirs, d)
440 }
441 }
442 }
443 s.mu.Unlock()
444 slices.Sort(dirs)
445 return dirs, nil
446}
447
448func (s *snapStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
449 s.mark(key)
450 return s.live.Put(ctx, key, body, ifMatch)
451}
452
453func (s *snapStore) PutFrom(ctx context.Context, key string, body io.ReadSeeker, size int64, ifMatch string) (string, error) {
454 s.mark(key)
455 return release.PutObjectFrom(ctx, s.live, key, body, size, ifMatch)
456}
457
458func (s *snapStore) Delete(ctx context.Context, key string) error {
459 s.mark(key)
460 return s.live.Delete(ctx, key)
461}
462
463func (s *snapStore) DeleteIfMatch(ctx context.Context, key, etag string) error {
464 s.mark(key)
465 return release.DeleteIfMatch(ctx, s.live, key, etag)
466}
467
468// The remaining extensions are not cached families: forwarded to live, and
469// errors.ErrUnsupported when live lacks one.
470
471func (s *snapStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
472 return release.OpenObject(ctx, s.live, key)
473}
474
475func (s *snapStore) GetIfChanged(ctx context.Context, key, etag string) ([]byte, string, bool, error) {
476 if g, ok := s.live.(release.ConditionalGetter); ok {
477 return g.GetIfChanged(ctx, key, etag)
478 }
479 return nil, "", false, errors.ErrUnsupported
480}
481
482func (s *snapStore) ListETags(ctx context.Context, prefix string) ([]release.KeyETag, error) {
483 if l, ok := s.live.(release.ETagLister); ok {
484 return l.ListETags(ctx, prefix)
485 }
486 return nil, errors.ErrUnsupported
487}
488
489func (s *snapStore) ListMod(ctx context.Context, prefix string) ([]release.KeyMod, error) {
490 if l, ok := s.live.(release.ModLister); ok {
491 return l.ListMod(ctx, prefix)
492 }
493 return nil, errors.ErrUnsupported
494}
495
496func (s *snapStore) ListSizes(ctx context.Context, prefix string) ([]release.KeySize, error) {
497 if l, ok := s.live.(release.SizeLister); ok {
498 return l.ListSizes(ctx, prefix)
499 }
500 return nil, errors.ErrUnsupported
501}
502
503func (s *snapStore) ListPage(ctx context.Context, prefix, startAfter string, limit int) ([]string, error) {
504 if l, ok := s.live.(release.PageLister); ok {
505 return l.ListPage(ctx, prefix, startAfter, limit)
506 }
507 return nil, errors.ErrUnsupported
508}
509
510// RunAppMutation keeps live's claim boundary for writes made through the
511// snapshot store; a live store without one runs f directly, as
512// release.WithAppMutation would.
513func (s *snapStore) RunAppMutation(ctx context.Context, acct, app, action string, f func(context.Context) error) error {
514 if r, ok := s.live.(release.AppMutationRunner); ok {
515 return r.RunAppMutation(ctx, acct, app, action, f)
516 }
517 return f(ctx)
518}
519
520// uncachedStore refuses everything. walkComplete runs the record walk over a
521// snapshot store backed by it, so a read the cache does not answer shows up
522// as a failure instead of a bucket request.
523type uncachedStore struct{}
524
525var errUncached = errors.New("provision: record view: not in the snapshot")
526
527func (uncachedStore) Get(context.Context, string) ([]byte, string, error) {
528 return nil, "", errUncached
529}
530func (uncachedStore) Put(context.Context, string, []byte, string) (string, error) {
531 return "", errUncached
532}
533func (uncachedStore) List(context.Context, string) ([]string, error) { return nil, errUncached }
534func (uncachedStore) ListDirs(context.Context, string) ([]string, error) { return nil, errUncached }
535func (uncachedStore) Delete(context.Context, string) error { return errUncached }
536