carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package dbreads
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "net"
12 "os"
13 "sync"
14 "sync/atomic"
15 "time"
16
17 mrand "math/rand/v2"
18
19 "github.com/carlosframework/platform/internal/dbdoor"
20 "github.com/carlosframework/platform/internal/hostclaim"
21 "github.com/carlosframework/platform/internal/provision"
22 "github.com/carlosframework/platform/internal/registry"
23 "github.com/carlosframework/platform/internal/release"
24)
25
26// AgentConfig is what the box's db-reads loop needs (spec §7, §8).
27// internal/host fills it from the box's environment; tests fill it with
28// stand-ins.
29type AgentConfig struct {
30 Store release.Store // the deployment bucket
31 Region string // CARLOS_REGION
32 Box string // this box's label, as its edges file names it
33 Boot string // this agent process's id
34 Pub ed25519.PublicKey // CARLOS_RELEASE_PUBKEY
35 HibernatePrefix string // CARLOS_HIBERNATE_PREFIX
36 // OpenReplica opens the read side of this box's own region bucket at a
37 // prefix the agent derived (lease.ReadReplicaClient in production).
38 OpenReplica func(ctx context.Context, prefix string) (dbdoor.ReplicaReader, error)
39 // DialWorker connects to carlos-dbread.socket.
40 DialWorker func(ctx context.Context) (net.Conn, error)
41 // Units counts carlos-dbread@ units active, activating or deactivating.
42 Units func(ctx context.Context) (int, error)
43 // Free reads the filesystem holding /var/tmp.
44 Free func() (avail, total uint64, err error)
45 MaxRestored int64 // DefaultMaxRestored when zero
46 // MaxPull is a pull's cap on its planned LTX files: MaxPullBytes when
47 // zero or larger, so no box ships more than a CLI accepts, and never
48 // more than the worker holds (PlanBytesFactor times MaxRestored).
49 MaxPull int64
50 Now func() time.Time
51 Jitter func() time.Duration // 0 to MaxJitter
52 Logf func(format string, args ...any)
53 // Draining is this box's own published draining flag (its edges file's
54 // draining field); a draining box takes no new read. Never draining
55 // when nil.
56 Draining func() bool
57
58 // Test seams; zero is the spec's value.
59 OwnerDeadline time.Duration
60 PlanBudget time.Duration
61 CancelEvery time.Duration
62 ChunkEvery time.Duration
63 MaxOutput int64
64}
65
66// Agent is the box's half of forwarded reads (spec §8.1): it lists the
67// region's queue, verifies requests, elects an owner, runs the identity
68// bracket around planning and download, streams bytes to a worker, and
69// seals and writes output. It never opens SQLite, decodes pages, runs SQL or
70// writes a file.
71type Agent struct {
72 cfg AgentConfig
73 adm *admission
74
75 mu sync.Mutex
76 decided map[string]time.Time
77
78 probeOK atomic.Bool
79 probeAfter chan struct{}
80 reads sync.WaitGroup
81}
82
83func NewAgent(cfg AgentConfig) *Agent {
84 if cfg.MaxRestored <= 0 {
85 cfg.MaxRestored = DefaultMaxRestored
86 }
87 if cfg.MaxPull <= 0 || cfg.MaxPull > MaxPullBytes {
88 cfg.MaxPull = MaxPullBytes
89 }
90 // The worker refuses a plan whose files total more than this (its
91 // planWithinBounds), so a pull over it ends too-large here instead.
92 if held := PlanBytesFactor * cfg.MaxRestored; cfg.MaxPull > held {
93 cfg.MaxPull = held
94 }
95 if cfg.Now == nil {
96 cfg.Now = time.Now
97 }
98 if cfg.Jitter == nil {
99 cfg.Jitter = func() time.Duration { return mrand.N(MaxJitter) }
100 }
101 if cfg.Logf == nil {
102 cfg.Logf = func(string, ...any) {}
103 }
104 if cfg.Draining == nil {
105 cfg.Draining = func() bool { return false }
106 }
107 if cfg.OwnerDeadline == 0 {
108 cfg.OwnerDeadline = OwnerDeadline
109 }
110 if cfg.PlanBudget == 0 {
111 cfg.PlanBudget = PlanBudget
112 }
113 if cfg.CancelEvery == 0 {
114 cfg.CancelEvery = CancelEvery
115 }
116 if cfg.ChunkEvery == 0 {
117 cfg.ChunkEvery = ChunkEvery
118 }
119 if cfg.MaxOutput == 0 {
120 cfg.MaxOutput = MaxOutputBytes
121 }
122 return &Agent{cfg: cfg, adm: newAdmission(cfg.Units, cfg.Free), decided: map[string]time.Time{},
123 probeAfter: make(chan struct{}, 1)}
124}
125
126// Capable is whether this box may publish db-read-1: its last readiness
127// probe passed (spec §7.1).
128func (a *Agent) Capable() bool { return a.probeOK.Load() }
129
130// Wait blocks until every read this agent owns has written its result.
131func (a *Agent) Wait() { a.reads.Wait() }
132
133func (a *Agent) now() time.Time { return a.cfg.Now().UTC() }
134
135func (a *Agent) logf(format string, args ...any) { a.cfg.Logf(format, args...) }
136
137func (a *Agent) decide(id string) {
138 a.mu.Lock()
139 defer a.mu.Unlock()
140 a.decided[id] = a.now()
141}
142
143func (a *Agent) isDecided(id string) bool {
144 a.mu.Lock()
145 defer a.mu.Unlock()
146 _, ok := a.decided[id]
147 return ok
148}
149
150// prune forgets decisions older than any listing can show again.
151func (a *Agent) prune(now time.Time) {
152 a.mu.Lock()
153 defer a.mu.Unlock()
154 for id, at := range a.decided {
155 if now.Sub(at) > 2*time.Hour {
156 delete(a.decided, id)
157 }
158 }
159}
160
161// askProbe asks the loop for a probe now: a read's worker failed to start
162// (spec §7.1).
163func (a *Agent) askProbe() {
164 select {
165 case a.probeAfter <- struct{}{}:
166 default:
167 }
168}
169
170// closeThenProbe gives the failed worker's connection slot back before
171// asking for a probe, so admission sees the worker's unit as the orphan it
172// now is and holds the probe until the unit has stopped (plan review
173// finding 29). The deferred Close that follows is a no-op (countedConn).
174func closeThenProbe(a *Agent, conn net.Conn) {
175 conn.Close()
176 a.askProbe()
177}
178
179// countedConn gives a worker connection's slot back exactly once.
180type countedConn struct {
181 net.Conn
182 once sync.Once
183 adm *admission
184}
185
186func (c *countedConn) Close() error {
187 err := c.Conn.Close()
188 c.once.Do(c.adm.closed)
189 return err
190}
191
192// dial opens a worker connection inside the three the socket allows.
193func (a *Agent) dial(ctx context.Context) (net.Conn, error) {
194 if !a.adm.opened() {
195 return nil, errBusy
196 }
197 c, err := a.cfg.DialWorker(ctx)
198 if err != nil {
199 a.adm.closed()
200 return nil, err
201 }
202 return &countedConn{Conn: c, adm: a.adm}, nil
203}
204
205// check is one half of the identity bracket (spec §8.2): the record read
206// fresh, and refused unless it is the instance the envelope named, still a
207// replicated instance, still in this box's region and not moving. A class,
208// pool or fleet change after the console admitted the read is a record
209// change.
210func (a *Agent) check(ctx context.Context, req Request) (provision.Record, string, error) {
211 rec, err := provision.GetRecord(ctx, a.cfg.Store, req.Account, req.App, req.Host)
212 if errors.Is(err, os.ErrNotExist) {
213 return provision.Record{}, CodeRecordChanged, nil
214 }
215 if err != nil {
216 return provision.Record{}, "", err
217 }
218 inc, ierr := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
219 _, fleet := registry.FleetPlacement(rec.Region)
220 switch {
221 case ierr != nil, inc != req.Incarnation, rec.Account != req.Account, rec.App != req.App,
222 rec.Host != req.Host, !provision.ValidHostShape(rec.Host), ClassRefusal(*rec) != "",
223 rec.Pool != "", fleet, rec.Region != a.cfg.Region:
224 return *rec, CodeRecordChanged, nil
225 case rec.Move != nil:
226 return *rec, CodeMoving, nil
227 }
228 return *rec, "", nil
229}
230
231func nonce() string {
232 var b [16]byte
233 rand.Read(b[:])
234 return hex.EncodeToString(b[:])
235}
236
237// elect is J3: the first create of picked owns the read. A create that
238// answers an error or a conflict is decided by reading picked back: this
239// agent's own nonce owns it, anything else, or a read that fails, does not.
240func (a *Agent) elect(ctx context.Context, id string) (Picked, bool) {
241 p := Picked{Box: a.cfg.Box, Boot: a.cfg.Boot, Nonce: nonce(), Deadline: a.now().Add(a.cfg.OwnerDeadline)}
242 body, err := json.Marshal(p)
243 if err != nil {
244 return Picked{}, false
245 }
246 if _, err := a.cfg.Store.Put(ctx, PickedKey(id), body, ""); err == nil {
247 return p, true
248 }
249 raw, _, err := a.cfg.Store.Get(ctx, PickedKey(id))
250 if err != nil {
251 return Picked{}, false
252 }
253 var got Picked
254 if json.Unmarshal(raw, &got) != nil || got.Nonce != p.Nonce {
255 return Picked{}, false
256 }
257 return got, true
258}
259
260// consider decides one in-window queue entry (spec §5.5 step 4): read and
261// verify the request against its entry, check admission, wait the jitter,
262// and enter the election. A refused or forged request is decided, and so
263// logged once. A read that fails admission is not decided: a later pass may
264// admit it while the window lasts.
265func (a *Agent) consider(ctx context.Context, e QueueEntry) {
266 raw, _, err := a.cfg.Store.Get(ctx, RequestKey(e.ID))
267 if err != nil {
268 if errors.Is(err, os.ErrNotExist) {
269 a.decide(e.ID)
270 a.logf("dbreads: %s: a queue entry with no request; ignored", e.ID)
271 }
272 return
273 }
274 req, err := Verify(a.cfg.Pub, raw, e.ID, a.cfg.Region, a.now())
275 if err == nil && !QueueMatches(e, req) {
276 err = ErrEnvelope
277 }
278 if err != nil {
279 a.decide(e.ID)
280 a.logf("dbreads: %s: refused the request: %v", e.ID, err)
281 return
282 }
283 // A request whose console has already given up (its request ended
284 // before it was announced) is never picked (spec §5.5 step 4).
285 if _, _, err := a.cfg.Store.Get(ctx, CancelKey(e.ID)); err == nil {
286 a.decide(e.ID)
287 return
288 }
289 // A draining box does not enter the election (spec §7.1, D2).
290 if a.cfg.Draining() {
291 return
292 }
293 if err := a.adm.admitRead(ctx, e.ID); err != nil {
294 return
295 }
296 if d := a.cfg.Jitter(); d > 0 {
297 select {
298 case <-ctx.Done():
299 a.adm.abandon(e.ID)
300 return
301 case <-time.After(d):
302 }
303 }
304 // It may have started draining during the jitter.
305 if a.cfg.Draining() {
306 a.adm.abandon(e.ID)
307 return
308 }
309 p, owned := a.elect(ctx, e.ID)
310 a.decide(e.ID)
311 if !owned {
312 a.adm.abandon(e.ID)
313 return
314 }
315 a.reads.Add(1)
316 go a.serve(ctx, req, p)
317}
318
319var errSandbox = errors.New("dbreads: the worker can see more than its sandbox allows")
320
321// probeRetry is how soon a probe that admission skipped is tried again.
322var probeRetry = time.Second
323
324// Run is the db-reads loop: a probe at start, a pass on every nudge (a
325// serial move) and every ListEvery, a probe every ProbeEvery and whenever a
326// read's worker failed to start (spec §5.5 step 4, §7.1). A probe that
327// admission skips (the failed worker's unit is still stopping) stays owed
328// and is tried every probeRetry until it runs (plan review finding 29).
329func (a *Agent) Run(ctx context.Context, nudge <-chan struct{}) {
330 var retry <-chan time.Time
331 tryProbe := func() {
332 retry = nil
333 if !a.Probe(ctx) {
334 retry = time.After(probeRetry)
335 }
336 }
337 tryProbe()
338 list := time.NewTicker(ListEvery)
339 defer list.Stop()
340 probe := time.NewTicker(ProbeEvery)
341 defer probe.Stop()
342 for {
343 select {
344 case <-ctx.Done():
345 a.reads.Wait()
346 return
347 case <-nudge:
348 a.Pass(ctx)
349 case <-list.C:
350 a.Pass(ctx)
351 case <-probe.C:
352 tryProbe()
353 case <-a.probeAfter:
354 tryProbe()
355 case <-retry:
356 tryProbe()
357 }
358 }
359}
360
361// Pass lists this region's queue for this hour and the last, and considers
362// every in-window entry it has not decided. An out-of-window entry is
363// skipped on its key alone. A box whose capability is withdrawn takes no
364// new read; one it already holds runs on (spec §11.2).
365func (a *Agent) Pass(ctx context.Context) {
366 if !a.Capable() {
367 return
368 }
369 now := a.now()
370 a.prune(now)
371 for _, hour := range []time.Time{now.Add(-time.Hour), now} {
372 keys, err := a.cfg.Store.List(ctx, QueueHourPrefix(a.cfg.Region, hour))
373 if err != nil {
374 a.logf("dbreads: list the %s queue: %v", a.cfg.Region, err)
375 continue
376 }
377 for _, key := range keys {
378 e, ok := ParseQueueKey(key)
379 if !ok || e.Region != a.cfg.Region || !InWindow(e.Issued, now) || a.isDecided(e.ID) {
380 continue
381 }
382 a.consider(ctx, e)
383 }
384 }
385}
386
387// Probe is the readiness probe (spec §7.1): a hello on its own connection,
388// so a busy box is never mistaken for a broken one. While admission is
389// blocked (an orphan unit, or no unit listing) it is skipped, the last
390// result stands, and it answers false so Run keeps it owed.
391func (a *Agent) Probe(ctx context.Context) bool {
392 if !a.adm.admitProbe(ctx) {
393 return false
394 }
395 defer a.adm.probeDone()
396 err := a.probe(ctx)
397 if err != nil {
398 a.logf("dbreads: readiness probe: %v", err)
399 }
400 a.probeOK.Store(err == nil)
401 return true
402}
403
404func (a *Agent) probe(ctx context.Context) error {
405 pctx, cancel := context.WithTimeout(ctx, 30*time.Second)
406 defer cancel()
407 conn, err := a.dial(pctx)
408 if err != nil {
409 return err
410 }
411 defer conn.Close()
412 defer context.AfterFunc(pctx, func() { conn.Close() })()
413 if err := writeJSONFrame(conn, frameHello, hello{V: ProtocolVersion}); err != nil {
414 return err
415 }
416 typ, p, err := readFrame(conn)
417 if err != nil {
418 return err
419 }
420 var h HelloReply
421 if typ != frameHelloReply || json.Unmarshal(p, &h) != nil {
422 return errProtocol
423 }
424 if h.V != ProtocolVersion {
425 return fmt.Errorf("dbreads: the worker speaks protocol %d, this agent %d", h.V, ProtocolVersion)
426 }
427 if len(h.Errors) != 0 || len(h.Interfaces) != 1 || h.Interfaces[0] != "lo" || h.DataEntries != 0 || h.EtcReadable {
428 return fmt.Errorf("%w: %+v", errSandbox, h)
429 }
430 return nil
431}
432