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 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13
14 lsfile "github.com/benbjohnson/litestream/file"
15 "github.com/superfly/ltx"
16
17 "github.com/carlosframework/platform/internal/dbdoor"
18 "github.com/carlosframework/platform/internal/dbdoor/dbdoortest"
19)
20
21// sendPullPlan sends a pull header and the planned files; it returns the
22// files' bytes concatenated, which is what the worker must send back.
23// net.Pipe to the real worker code: not acceptance (Task 5's root-runner
24// test is).
25func sendPullPlan(t *testing.T, w io.Writer, dir string, corrupt bool) (dbdoor.RestorePlan, []byte) {
26 t.Helper()
27 rd := lsfile.NewReplicaClient(dir)
28 plan, err := dbdoor.PlanFrom(context.Background(), rd, time.Time{})
29 if err != nil {
30 t.Fatal(err)
31 }
32 if err := writeJSONFrame(w, frameHeader, planHeader{V: ProtocolVersion, Op: OpPull, Files: plan.Planned, Target: plan.TXID, MaxRestored: DefaultMaxRestored}); err != nil {
33 t.Fatal(err)
34 }
35 var all bytes.Buffer
36 for i, f := range plan.Planned {
37 rc, err := rd.OpenLTXFile(context.Background(), f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID), 0, 0)
38 if err != nil {
39 t.Fatal(err)
40 }
41 b, _ := io.ReadAll(rc)
42 rc.Close()
43 if corrupt && i == 0 {
44 b[len(b)/2] ^= 0xff
45 }
46 all.Write(b)
47 if err := writeFrame(w, frameFile, b); err != nil {
48 t.Fatal(err)
49 }
50 }
51 return plan, all.Bytes()
52}
53
54func TestWorkerHoldsThenSendsThePlannedFiles(t *testing.T) {
55 t.Parallel()
56 dir := t.TempDir()
57 dbdoortest.Replicate(t, dir, 3)
58 conn, done := workerPipe(t, WorkerOptions{})
59 plan, want := sendPullPlan(t, conn, dir, false)
60 var ready readyMsg
61 readTyped(t, conn, frameReady, &ready)
62 if ready.TXID != plan.TXID {
63 t.Fatalf("ready at %d", ready.TXID)
64 }
65 if err := writeJSONFrame(conn, frameRun, runMsg{}); err != nil {
66 t.Fatal(err)
67 }
68 var got bytes.Buffer
69 for {
70 typ, p, err := readFrame(conn)
71 if err != nil {
72 t.Fatal(err)
73 }
74 if typ == frameOutput {
75 got.Write(p)
76 continue
77 }
78 var end endMsg
79 if typ != frameEnd || json.Unmarshal(p, &end) != nil || end.Code != "" {
80 t.Fatalf("end %q %s", typ, p)
81 }
82 break
83 }
84 if !bytes.Equal(got.Bytes(), want) {
85 t.Fatalf("sent back %d bytes, held %d", got.Len(), len(want))
86 }
87 if err := <-done; err != nil {
88 t.Fatal(err)
89 }
90}
91
92// Review Focus 2: a corrupt file is refused before ready, never shipped.
93// Mutation: holdAndSend skips verifyLTX (red: ready, not check-failed).
94func TestWorkerHoldRefusesACorruptFile(t *testing.T) {
95 t.Parallel()
96 dir := t.TempDir()
97 dbdoortest.Replicate(t, dir, 2)
98 conn, done := workerPipe(t, WorkerOptions{})
99 sendPullPlan(t, conn, dir, true)
100 var end endMsg
101 readTyped(t, conn, frameEnd, &end)
102 if end.Code != CodeCheckFailed {
103 t.Fatalf("end = %+v", end)
104 }
105 <-done
106}
107
108func TestWorkerHoldStopsWithoutSending(t *testing.T) {
109 t.Parallel()
110 dir := t.TempDir()
111 dbdoortest.Replicate(t, dir, 1)
112 conn, done := workerPipe(t, WorkerOptions{})
113 sendPullPlan(t, conn, dir, false)
114 readTyped(t, conn, frameReady, nil)
115 if err := writeFrame(conn, frameStop, nil); err != nil {
116 t.Fatal(err)
117 }
118 if err := <-done; err != nil {
119 t.Fatal(err)
120 }
121}
122
123func (fx *fixture) pullRequest(at string) Request {
124 req := fx.request("", at)
125 req.Op = OpPull
126 return req
127}
128
129// answerPull opens a pull's chunks as the CLI does: the ltx stream.
130func (fx *fixture) answerPull(id string) ([]byte, Summary, Result) {
131 fx.t.Helper()
132 raw, _, err := fx.store.Get(context.Background(), ResultKey(id))
133 if err != nil {
134 fx.t.Fatal(err)
135 }
136 var r Result
137 if err := json.Unmarshal(raw, &r); err != nil {
138 fx.t.Fatal(err)
139 }
140 s, err := OpenSummary(fx.reply, id, r.Sealed)
141 if err != nil {
142 fx.t.Fatal(err)
143 }
144 var out bytes.Buffer
145 for seq := int64(1); seq <= r.LastSeq; seq++ {
146 sealed, _, _ := fx.store.Get(context.Background(), ChunkKey(id, seq))
147 plain, err := OpenChunk(fx.reply, id, seq, StreamLTX, seq == r.LastSeq, sealed)
148 if err != nil {
149 fx.t.Fatalf("chunk %d: %v", seq, err)
150 }
151 out.Write(plain)
152 }
153 return out.Bytes(), s, r
154}
155
156// plannedBytes is dir's planned files, concatenated in plan order.
157func plannedBytes(t *testing.T, dir string) (dbdoor.RestorePlan, []byte) {
158 t.Helper()
159 rd := lsfile.NewReplicaClient(dir)
160 plan, err := dbdoor.PlanFrom(context.Background(), rd, time.Time{})
161 if err != nil {
162 t.Fatal(err)
163 }
164 var want bytes.Buffer
165 for _, f := range plan.Planned {
166 rc, err := rd.OpenLTXFile(context.Background(), f.Level, ltx.TXID(f.MinTXID), ltx.TXID(f.MaxTXID), 0, 0)
167 if err != nil {
168 t.Fatal(err)
169 }
170 io.Copy(&want, rc)
171 rc.Close()
172 }
173 return plan, want.Bytes()
174}
175
176// The agent's pull ships the planned files whole, in plan order, as the
177// ltx stream in chunks of at most PullChunkBytes, with the file list in the
178// summary, and logs plan 1's two timing lines (plan review finding 26).
179// Mutation: drop the timing lines (red).
180func TestPullShipsThePlannedFiles(t *testing.T) {
181 t.Parallel()
182 fx := newFixture(t, 4)
183 var logMu sync.Mutex
184 var logs []string
185 a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
186 c.Logf = func(format string, args ...any) {
187 logMu.Lock()
188 defer logMu.Unlock()
189 logs = append(logs, fmt.Sprintf(format, args...))
190 }
191 })
192 req := fx.pullRequest("")
193 fx.submit(req)
194 a.Pass(context.Background())
195 a.Wait()
196 got, s, r := fx.answerPull(req.ID)
197 plan, want := plannedBytes(t, fx.replica)
198 if r.State != StateDone || !bytes.Equal(got, want) || s.TXID != plan.TXID || len(s.Files) != len(plan.Planned) || s.Bytes != plan.LTXBytes {
199 t.Fatalf("result %+v summary %+v: %d bytes, want %d", r, s, len(got), len(want))
200 }
201 for i, f := range s.Files {
202 if f.Level != plan.Planned[i].Level || f.MinTXID != plan.Planned[i].MinTXID || f.MaxTXID != plan.Planned[i].MaxTXID || f.Size != plan.Planned[i].Size {
203 t.Fatalf("file %d is %+v, planned %+v", i, f, plan.Planned[i])
204 }
205 }
206 for seq := int64(1); seq < r.LastSeq; seq++ {
207 sealed, _, _ := fx.store.Get(context.Background(), ChunkKey(req.ID, seq))
208 if len(sealed)-SealOverhead > PullChunkBytes {
209 t.Fatalf("chunk %d is %d bytes", seq, len(sealed))
210 }
211 }
212 // Task 1's measurement reads these two lines (plan review finding 26).
213 logMu.Lock()
214 all := strings.Join(logs, "\n")
215 logMu.Unlock()
216 if !strings.Contains(all, "and held through") || !strings.Contains(all, "confirmed chunks in") {
217 t.Fatalf("the pull's timing lines are missing:\n%s", all)
218 }
219}
220
221// A pull over the agent's cap ends too-large with the planned bytes and the
222// cap, and a record change after the last byte left the bucket (T9 for a
223// pull) writes no chunk and never sends the worker run. Mutation: the
224// agent's pull sends run before check B (red: the recorded frames hold a
225// run).
226func TestPullTooLargeAndNoChunkBeforeCheckB(t *testing.T) {
227 t.Parallel()
228 fx := newFixture(t, 2)
229 a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.MaxPull = 100 })
230 req := fx.pullRequest("")
231 fx.submit(req)
232 a.Pass(context.Background())
233 a.Wait()
234 _, s, r := fx.answerPull(req.ID)
235 if r.Code != CodeTooLarge || s.Cap != 100 || s.Restored <= 100 {
236 t.Fatalf("%+v %+v", r, s)
237 }
238 fx2 := newFixture(t, 2)
239 plan, _ := plannedBytes(t, fx2.replica)
240 last := plan.Planned[len(plan.Planned)-1]
241 hook := &stageHook{ReplicaReader: lsfile.NewReplicaClient(fx2.replica), stage: "after the last byte",
242 lastSpan: dbdoor.Span(last.MinTXID, last.MaxTXID), change: func() { r := fx2.record(); r.Region = "ie"; fx2.putRecord(r) }}
243 b, w := fx2.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
244 c.OpenReplica = func(context.Context, string) (dbdoor.ReplicaReader, error) { return hook, nil }
245 })
246 req2 := fx2.pullRequest("")
247 fx2.submit(req2)
248 b.Pass(context.Background())
249 b.Wait()
250 if _, _, r := fx2.answerPull(req2.ID); r.Code != CodeRecordChanged || len(fx2.chunkKeys(req2.ID)) != 0 {
251 t.Fatalf("%+v, chunks %v", r, fx2.chunkKeys(req2.ID))
252 }
253 // The worker never heard run: check B failed, so the agent said stop
254 // (plan review finding 24). Without this, an agent that sent run first
255 // and refused the files afterwards would pass the two checks above.
256 sent := w.sentFrames()
257 if !bytes.Contains(sent, []byte{frameStop}) {
258 t.Fatalf("the agent never said stop: %q", sent)
259 }
260 if bytes.Contains(sent, []byte{frameRun}) {
261 t.Fatalf("the agent sent run before check B passed: %q", sent)
262 }
263}
264
265// Plan review finding 24: NewAgent itself clamps the pull's cap, whatever a
266// box's settings hand it. Mutation: NewAgent keeps a MaxPull above
267// MaxPullBytes (red).
268func TestNewAgentClampsMaxPull(t *testing.T) {
269 t.Parallel()
270 fx := newFixture(t, 1)
271 for give, want := range map[int64]int64{0: MaxPullBytes, -1: MaxPullBytes, 1 << 20: 1 << 20, MaxPullBytes + 1: MaxPullBytes} {
272 a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.MaxPull = give })
273 if a.cfg.MaxPull != want {
274 t.Errorf("MaxPull %d became %d, want %d", give, a.cfg.MaxPull, want)
275 }
276 }
277 // Never more than the worker holds: PlanBytesFactor times MaxRestored.
278 a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) { c.MaxRestored = 256 << 20 })
279 if a.cfg.MaxPull != PlanBytesFactor*(256<<20) {
280 t.Errorf("MaxPull %d with a 256 MiB restored cap", a.cfg.MaxPull)
281 }
282}
283
284// relabelled is the bytes of dir's first planned file, a valid snapshot at
285// transaction 1, under the coordinates of a file that ends at 2.
286func relabelled(t *testing.T, dir string) (dbdoor.LTXFile, []byte) {
287 t.Helper()
288 rd := lsfile.NewReplicaClient(dir)
289 rc, err := rd.OpenLTXFile(context.Background(), 0, 1, 1, 0, 0)
290 if err != nil {
291 t.Fatal(err)
292 }
293 b, _ := io.ReadAll(rc)
294 rc.Close()
295 return dbdoor.LTXFile{Level: 0, MinTXID: 1, MaxTXID: 2, Size: int64(len(b))}, b
296}
297
298// Astra round 1 on plan 3, finding 1: a valid snapshot under another file's
299// coordinates is refused before ready, for a pull and for a query alike: the
300// worker checks each file's LTX header against its plan entry, so a copy
301// can never be reported at a transaction it does not hold. Mutation: drop
302// the header comparison (red: the query restores and answers ready at 2;
303// the pull verifies and answers ready).
304func TestWorkerRefusesARelabelledSnapshot(t *testing.T) {
305 t.Parallel()
306 dir := t.TempDir()
307 dbdoortest.Replicate(t, dir, 2)
308 for _, op := range []string{OpQuery, OpPull} {
309 lf, b := relabelled(t, dir)
310 conn, done := workerPipe(t, WorkerOptions{})
311 if err := writeJSONFrame(conn, frameHeader, planHeader{V: ProtocolVersion, Op: op, Files: []dbdoor.LTXFile{lf}, Target: 2, MaxRestored: DefaultMaxRestored}); err != nil {
312 t.Fatal(err)
313 }
314 if err := writeFrame(conn, frameFile, b); err != nil {
315 t.Fatal(err)
316 }
317 typ, p, err := readFrame(conn)
318 var end endMsg
319 if err != nil || typ != frameEnd || json.Unmarshal(p, &end) != nil || end.Code != CodeCheckFailed {
320 t.Errorf("%s: a relabelled snapshot answered %q %s %v", op, typ, p, err)
321 writeFrame(conn, frameStop, nil)
322 }
323 <-done
324 }
325}
326