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/ecdh"
6 "encoding/json"
7 "errors"
8 "io"
9 "net"
10 "os"
11 "sync/atomic"
12 "time"
13
14 "github.com/superfly/ltx"
15
16 "github.com/carlosframework/platform/internal/activator"
17 "github.com/carlosframework/platform/internal/dbdoor"
18)
19
20// serve runs one read this agent owns, inside the owner's deadline, and
21// writes its result (spec §5.5 step 5). The read slot is given back here;
22// the reservation stays until the worker unit is gone (J5).
23func (a *Agent) serve(parent context.Context, req Request, p Picked) {
24 defer a.reads.Done()
25 defer a.adm.finish(req.ID)
26 // The deadline is the one picked published (spec §5.7), measured on
27 // this agent's clock, not a fresh OwnerDeadline from now: an election
28 // whose answer came back late has already spent part of it, and the
29 // console stops waiting a minute after the published one (Astra round
30 // 1, finding 3). In production a.now is time.Now, so this is
31 // context.WithDeadline(parent, p.Deadline).
32 started := a.now()
33 left := p.Deadline.Sub(started)
34 ctx, cancel := context.WithTimeout(parent, left)
35 defer cancel()
36 var cancelled atomic.Bool
37 var res Result
38 if left <= 0 {
39 // Past the deadline before any work: start nothing.
40 a.logf("dbreads: %s: picked, but its deadline %s had passed when the election answered", req.ID, p.Deadline.Format(time.RFC3339Nano))
41 res = Result{State: StateFailed, Code: CodeInterrupted}
42 if pub, err := ParseReplyKey(req.ReplyKey); err == nil {
43 res = a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, 0)
44 }
45 } else {
46 go a.watchCancel(ctx, req.ID, cancel, &cancelled)
47 res = a.read(ctx, req)
48 }
49 if cancelled.Load() {
50 res.State, res.Code = StateCancelled, ""
51 }
52 res.Box, res.Started, res.Ended = a.cfg.Box, started, a.now()
53 body, err := json.Marshal(res)
54 if err == nil {
55 wctx, wcancel := context.WithTimeout(context.WithoutCancel(parent), 30*time.Second)
56 err = putCreateOnly(wctx, a.cfg.Store, ResultKey(req.ID), body)
57 wcancel()
58 }
59 if err != nil {
60 a.logf("dbreads: %s: write the result: %v", req.ID, err)
61 }
62}
63
64// watchCancel reads cancel every CancelEvery and ends the read when it
65// appears: the console's best-effort word that nobody is waiting.
66func (a *Agent) watchCancel(ctx context.Context, id string, cancel context.CancelFunc, seen *atomic.Bool) {
67 t := time.NewTicker(a.cfg.CancelEvery)
68 defer t.Stop()
69 for {
70 select {
71 case <-ctx.Done():
72 return
73 case <-t.C:
74 if _, _, err := a.cfg.Store.Get(ctx, CancelKey(id)); err == nil {
75 seen.Store(true)
76 cancel()
77 return
78 }
79 }
80 }
81}
82
83// ended seals the summary into a result.
84func (a *Agent) ended(pub *ecdh.PublicKey, id, state, code string, s Summary, last int64) Result {
85 sealed, err := SealSummary(pub, id, s)
86 if err != nil {
87 a.logf("dbreads: %s: seal the summary: %v", id, err)
88 }
89 return Result{State: state, Code: code, LastSeq: last, Sealed: sealed}
90}
91
92// read is today's "changed while it was being read" retry, box side: one
93// fresh plan, with both checks again, then changed-twice.
94func (a *Agent) read(ctx context.Context, req Request) Result {
95 pub, err := ParseReplyKey(req.ReplyKey)
96 if err != nil {
97 return Result{State: StateFailed, Code: CodeInterrupted}
98 }
99 at, _ := req.AtTime()
100 for attempt := 0; attempt < 2; attempt++ {
101 if attempt > 0 {
102 // The first attempt's worker may still be stopping: keep its
103 // reservation, and wait for its unit before the next one (J5).
104 a.adm.retry(req.ID, attempt)
105 if err := a.adm.settle(ctx, settlePoll); err != nil {
106 return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, 0)
107 }
108 }
109 if res, retry := a.attempt(ctx, req, pub, at); !retry {
110 return res
111 }
112 }
113 // "Changed twice" is itself the replica's word: it waits for check B
114 // like every other replica-derived ending (Astra round 1, finding 2).
115 res, _ := a.afterCheckB(ctx, req, pub, CodeChangedTwice, Summary{})
116 return res
117}
118
119// afterCheckB is how every ending that carries anything derived from the
120// replica leaves a read: a failed restore's error text, the restored size,
121// the oldest point kept, "nothing replicated", "no room", "changed twice".
122// Check B runs first (spec §8.2), and when it fails the details are
123// discarded: the read ends refused with the identity code alone, so a
124// requester never learns anything about another instance's replica read
125// under a reused prefix (Astra round 1, finding 2).
126func (a *Agent) afterCheckB(ctx context.Context, req Request, pub *ecdh.PublicKey, code string, s Summary) (Result, bool) {
127 _, bcode, err := a.check(ctx, req)
128 switch {
129 case err != nil:
130 a.logf("dbreads: %s: check B before a %s ending: %v", req.ID, code, err)
131 return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, 0), false
132 case bcode != "":
133 return a.ended(pub, req.ID, StateRefused, bcode, Summary{}, 0), false
134 }
135 return a.ended(pub, req.ID, StateFailed, code, s, 0), false
136}
137
138// settlePoll is how often a retry rereads the unit listing while it waits.
139var settlePoll = 200 * time.Millisecond
140
141// attempt is the identity bracket (spec §8.2): check A, plan, stream every
142// planned file into a worker that restores and checks it, check B after the
143// last byte has left the bucket, and only then run. No output exists before
144// check B passes. retry is true when a planned file vanished or changed.
145func (a *Agent) attempt(ctx context.Context, req Request, pub *ecdh.PublicKey, at time.Time) (Result, bool) {
146 fail := func(code string, s Summary) (Result, bool) {
147 return a.ended(pub, req.ID, StateFailed, code, s, 0), false
148 }
149 // failB is fail for an ending derived from the replica: check B first.
150 failB := func(code string, s Summary) (Result, bool) { return a.afterCheckB(ctx, req, pub, code, s) }
151 refuse := func(code string) (Result, bool) {
152 return a.ended(pub, req.ID, StateRefused, code, Summary{}, 0), false
153 }
154 attemptStart := time.Now()
155 pctx, pcancel := context.WithTimeout(ctx, a.cfg.PlanBudget)
156 defer pcancel()
157
158 rec, code, err := a.check(pctx, req) // check A
159 if err != nil {
160 a.logf("dbreads: %s: check A: %v", req.ID, err)
161 return fail(CodeInterrupted, Summary{})
162 }
163 if code != "" {
164 return refuse(code)
165 }
166 rd, err := a.cfg.OpenReplica(pctx, activator.HibernatePrefix(a.cfg.HibernatePrefix, rec.Host))
167 if err != nil {
168 a.logf("dbreads: %s: open the replica: %v", req.ID, err)
169 return fail(CodeInterrupted, Summary{})
170 }
171 plan, err := dbdoor.PlanFrom(pctx, rd, at)
172 var before *dbdoor.BeforeHistoryError
173 switch {
174 case errors.Is(err, dbdoor.ErrNothingReplicated):
175 return failB(CodeNothingReplicated, Summary{})
176 case errors.As(err, &before):
177 return failB(CodeHistoryGone, Summary{Oldest: before.Oldest})
178 case errors.Is(err, dbdoor.ErrHistoryGone):
179 return failB(CodeHistoryGone, Summary{})
180 case errors.Is(err, os.ErrNotExist):
181 return Result{}, true
182 case err != nil:
183 a.logf("dbreads: %s: plan: %v", req.ID, err)
184 return fail(CodeInterrupted, Summary{})
185 }
186 // A pull's cap is on the files it ships, a query's on the copy it
187 // restores (spec §5.7). Both endings are the replica's word, so both wait
188 // for check B (Astra round 1, finding 2).
189 pull := req.Op == OpPull
190 switch {
191 case pull && plan.LTXBytes > a.cfg.MaxPull:
192 return failB(CodeTooLarge, Summary{Restored: plan.LTXBytes, Cap: a.cfg.MaxPull})
193 case !pull && plan.RestoredBytes > a.cfg.MaxRestored:
194 return failB(CodeTooLarge, Summary{Restored: plan.RestoredBytes, Cap: a.cfg.MaxRestored})
195 }
196 need := plan.LTXBytes + plan.RestoredBytes
197 if pull {
198 need = plan.LTXBytes // the worker holds the files and restores nothing
199 }
200 if err := a.adm.grow(req.ID, need); err != nil {
201 return failB(CodeNoRoom, Summary{})
202 }
203 conn, err := a.dial(pctx)
204 if err != nil {
205 a.logf("dbreads: %s: the worker did not start: %v", req.ID, err)
206 a.askProbe()
207 return fail(CodeInterrupted, Summary{})
208 }
209 defer conn.Close()
210 defer context.AfterFunc(ctx, func() { conn.Close() })()
211 stopPlanBound := context.AfterFunc(pctx, func() { conn.Close() })
212 defer stopPlanBound()
213
214 if err := writeJSONFrame(conn, frameHeader, planHeader{V: ProtocolVersion, Op: req.Op, Files: plan.Planned,
215 Target: plan.TXID, MaxRestored: a.cfg.MaxRestored}); err != nil {
216 closeThenProbe(a, conn)
217 return fail(CodeInterrupted, Summary{})
218 }
219 for _, f := range plan.Planned {
220 vanished, err := sendFile(pctx, conn, rd, f)
221 if vanished {
222 return Result{}, true
223 }
224 if err != nil {
225 // The worker went before ready: its unit may have failed to
226 // start after the socket accepted (spec §7.1; plan review
227 // finding 15). Probe once this connection is closed.
228 closeThenProbe(a, conn)
229 return fail(CodeInterrupted, Summary{})
230 }
231 }
232 typ, p, err := readFrame(conn)
233 switch {
234 case err != nil:
235 closeThenProbe(a, conn)
236 return fail(CodeInterrupted, Summary{})
237 case typ == frameEnd:
238 var e endMsg
239 json.Unmarshal(p, &e)
240 if e.Code != CodeTooLarge {
241 e.Code = CodeCheckFailed
242 }
243 return failB(e.Code, Summary{Error: e.Error, Restored: plan.RestoredBytes, Cap: a.cfg.MaxRestored})
244 }
245 var ready readyMsg
246 if typ != frameReady || json.Unmarshal(p, &ready) != nil || ready.TXID != plan.TXID {
247 return failB(CodeCheckFailed, Summary{})
248 }
249 if _, code, err := a.check(pctx, req); err != nil || code != "" { // check B
250 writeFrame(conn, frameStop, nil)
251 if err != nil {
252 return fail(CodeInterrupted, Summary{})
253 }
254 return refuse(code)
255 }
256 stopPlanBound() // the run is bounded by the worker and the owner's deadline
257 stream, size, max := StreamRows, ChunkBytes, a.cfg.MaxOutput
258 if pull {
259 stream, size, max = StreamLTX, PullChunkBytes, plan.LTXBytes
260 }
261 // Phase timings, for operators and for plan 3's throughput measurement
262 // (plan review finding 26): a query's first phase plans and restores, a
263 // pull's plans and has the worker verify and hold the files; then the
264 // output's sealing and confirmed creates.
265 held := "restored to"
266 if pull {
267 held = "held through"
268 }
269 a.logf("dbreads: %s: planned %d files (%d bytes) and %s %d in %s", req.ID, plan.Files, plan.LTXBytes, held, plan.TXID, time.Since(attemptStart).Round(time.Millisecond))
270 runStart := time.Now()
271 if err := writeJSONFrame(conn, frameRun, runMsg{SQL: req.SQL}); err != nil {
272 return fail(CodeInterrupted, Summary{})
273 }
274 cw := newChunkWriter(ctx, a.cfg.Store, req.ID, pub, stream, a.cfg.ChunkEvery, max, size)
275 endCode, sqlErr, rerr := relay(conn, cw)
276 last, n, sum, werr := cw.close()
277 a.logf("dbreads: %s: output %d bytes in %d confirmed chunks in %s", req.ID, n, last, time.Since(runStart).Round(time.Millisecond))
278 s := Summary{TXID: plan.TXID, At: plan.At, Bytes: n, SHA256: sum, Error: sqlErr}
279 if pull {
280 s.Files = plan.Planned
281 // A worker that sends more or less than it held is a check
282 // failure, never an output cap.
283 if werr == nil && rerr == nil && endCode == "" && (cw.capped() || n != plan.LTXBytes) {
284 return a.ended(pub, req.ID, StateFailed, CodeCheckFailed, Summary{}, last), false
285 }
286 }
287 switch {
288 case werr != nil:
289 a.logf("dbreads: %s: write output: %v", req.ID, werr)
290 return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, last), false
291 case cw.capped():
292 return a.ended(pub, req.ID, StateDone, CodeOutputCap, s, last), false
293 case rerr != nil:
294 return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, last), false
295 case endCode == "":
296 return a.ended(pub, req.ID, StateDone, "", s, last), false
297 case endCode == CodeSQLError, endCode == CodeTimeout:
298 return a.ended(pub, req.ID, StateFailed, endCode, s, last), false
299 }
300 return a.ended(pub, req.ID, StateFailed, CodeInterrupted, Summary{}, last), false
301}
302
303// sendFile streams one planned file, whole, from the bucket into the
304// worker. A file that is gone, or not the size the plan listed, vanished
305// under the plan: the caller re-plans.
306func sendFile(ctx context.Context, w io.Writer, rd dbdoor.ReplicaReader, f dbdoor.LTXFile) (bool, error) {
307 rc, err := rd.OpenLTXFile(ctx, f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID), 0, 0)
308 if errors.Is(err, os.ErrNotExist) {
309 return true, nil
310 }
311 if err != nil {
312 return false, err
313 }
314 defer rc.Close()
315 buf := make([]byte, fileFrameBytes)
316 var sent int64
317 for {
318 n, rerr := rc.Read(buf)
319 if n > 0 {
320 if sent+int64(n) > f.Size {
321 return true, nil
322 }
323 if err := writeFrame(w, frameFile, buf[:n]); err != nil {
324 return false, err
325 }
326 sent += int64(n)
327 }
328 if rerr == io.EOF {
329 break
330 }
331 if errors.Is(rerr, os.ErrNotExist) {
332 return true, nil
333 }
334 if rerr != nil {
335 return false, rerr
336 }
337 }
338 return sent != f.Size, nil
339}
340
341// relay moves the worker's output into the chunk writer until the worker's
342// end frame, or until the cap: then the agent stops reading and closes the
343// connection, which the worker sees as EOF.
344func relay(conn net.Conn, cw *chunkWriter) (string, string, error) {
345 for {
346 typ, p, err := readFrame(conn)
347 if err != nil {
348 return "", "", err
349 }
350 switch typ {
351 case frameOutput:
352 if _, err := cw.Write(p); errors.Is(err, errCapped) {
353 return "", "", nil
354 } else if err != nil {
355 return "", "", err
356 }
357 case frameEnd:
358 var e endMsg
359 if json.Unmarshal(p, &e) != nil {
360 return "", "", errProtocol
361 }
362 return e.Code, e.Error, nil
363 default:
364 return "", "", errProtocol
365 }
366 }
367}
368