carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "sync"
5
6 "github.com/carlosframework/platform/internal/objstore"
7)
8
9// storeWalkLocks serialises the object-storage walks per app, which is what
10// objstore has always said this package does.
11//
12// `deleteRecordIfUnchanged` narrows the grant-vs-teardown window and then
13// accepts what is left of it, for a stated reason (`walk.go:560`):
14//
15// This narrows the window; it does not close it. A grant landing between
16// the re-read and the Delete still loses its record, and only a CAS delete
17// in the Store interface could fix that. The console door serialises these
18// verbs per app, so the remaining window is a fraction of one round trip.
19//
20// Until this type there was no such serialisation anywhere in the console —
21// no lock of any kind around Grant, Rotate, RotateFinish or Teardown. The
22// premise the walks reason from was simply untrue, and the window was a whole
23// walk wide rather than a fraction of a round trip.
24//
25// It mattered less than it might have because reaching it took two operators
26// running store verbs against one app simultaneously. Self-serve object
27// storage (#381) removes that accident: a member's own `carlos store create`
28// drives a grant walk, and the create door's own comment says people re-run it
29// "from a second terminal, from a deploy script" (apistore.go:176-179).
30//
31// IN-PROCESS ONLY, and deliberately not sold as more. One console process
32// holds one of these, so two consoles fronting one deployment would still
33// interleave. That residue is what the walks' CAS guards are for — Grant's
34// step-six retry, Rotate's step-five retry, and this delete's re-read. The
35// lock removes the ordinary race; CAS survives the exotic one.
36//
37// Entries are reference-counted and dropped at zero, so an app that is walked
38// once does not hold a mutex for the life of the process.
39type storeWalkLocks struct {
40 mu sync.Mutex
41 m map[string]*storeWalkLock
42}
43
44type storeWalkLock struct {
45 mu sync.Mutex
46 refs int
47}
48
49func newStoreWalkLocks() *storeWalkLocks {
50 return &storeWalkLocks{m: make(map[string]*storeWalkLock)}
51}
52
53// storeWalkKey is the map's key shape, in one place because tests inspect the
54// map to prove a second walk really queued. When the environment joined the
55// key on 2026-09-19 the literal was restated in two test files, and both went
56// on compiling while reading a refcount that could never move — they failed
57// as "the waiter never registered", which is a sentence about the lock rather
58// than about the key. Derive it here.
59//
60// The key is the store's IAM user name rather than the triple spelled out,
61// because two different triples can resolve to ONE estate: app "shop" with
62// environment "eu" and app "shop-eu" mint the same bucket and the same user
63// (#218). objstore's name claims refuse the second of them whichever lock is
64// held, but walks over one estate are what this lock exists to serialise, so
65// it keys on the estate's name.
66func storeWalkKey(acct, app, env string) string { return objstore.UserNameIn(acct, app, env) }
67
68// lock blocks until this store's walk lock is held, and returns the release.
69// The caller defers it:
70//
71// defer s.storeWalks.lock(acct.Sqid, app, env)()
72//
73// The key covers the account, the app AND the environment ("" for the app's
74// own store): two apps in one account share no estate, and since 2026-09-19
75// two STORES of one app share none either — a bucket, an IAM user and a
76// config bundle each, all named for the triple (objstore's MintedBucketIn and
77// UserNameIn). Serialising across any of them would cost concurrency and buy
78// nothing. The one exception is triples whose joined names collide, which
79// storeWalkKey folds into one key.
80func (l *storeWalkLocks) lock(acct, app, env string) func() {
81 key := storeWalkKey(acct, app, env)
82 l.mu.Lock()
83 e := l.m[key]
84 if e == nil {
85 e = &storeWalkLock{}
86 l.m[key] = e
87 }
88 e.refs++
89 l.mu.Unlock()
90
91 e.mu.Lock()
92 return func() {
93 e.mu.Unlock()
94 l.mu.Lock()
95 e.refs--
96 if e.refs == 0 {
97 delete(l.m, key)
98 }
99 l.mu.Unlock()
100 }
101}
102