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 "crypto/ecdh"
7 "crypto/ed25519"
8 "crypto/rand"
9 "crypto/sha256"
10 "encoding/hex"
11 "encoding/json"
12 "errors"
13 "net"
14 "os"
15 "sort"
16 "sync"
17 "sync/atomic"
18 "testing"
19 "time"
20
21 lsfile "github.com/benbjohnson/litestream/file"
22
23 "github.com/carlosframework/platform/internal/dbdoor"
24 "github.com/carlosframework/platform/internal/dbdoor/dbdoortest"
25 "github.com/carlosframework/platform/internal/hostclaim"
26 "github.com/carlosframework/platform/internal/provision"
27 "github.com/carlosframework/platform/internal/release"
28)
29
30const fxAccount, fxApp, fxHost = "bab", "shop", "shop.example.com"
31
32// inProcWorkers runs the real ServeWorker in process over net.Pipe: not
33// acceptance (Task 15 drives the installed units). units counts the live
34// workers, the way the box counts carlos-dbread@ units.
35type inProcWorkers struct {
36 t *testing.T
37 opt WorkerOptions
38 live atomic.Int64
39 dials atomic.Int64
40 // linger keeps a finished worker counted as a live unit for a while,
41 // as systemd's deactivation does; liveAtDial records the live count
42 // each dial saw.
43 linger time.Duration
44 mu sync.Mutex
45 liveAtDial []int64
46 // failStart makes the worker end at once without a word, as a unit
47 // whose service fails to start after the socket accepted does.
48 failStart atomic.Bool
49 // sent is the type of every frame the agent wrote, in order.
50 sent []byte
51}
52
53// frameTap records the first byte of each Write the agent makes: one frame
54// type per frame (writeFrame writes a frame in one Write).
55type frameTap struct {
56 net.Conn
57 w *inProcWorkers
58}
59
60func (f frameTap) Write(p []byte) (int, error) {
61 if len(p) > 0 {
62 f.w.mu.Lock()
63 f.w.sent = append(f.w.sent, p[0])
64 f.w.mu.Unlock()
65 }
66 return f.Conn.Write(p)
67}
68
69func (w *inProcWorkers) sentFrames() []byte {
70 w.mu.Lock()
71 defer w.mu.Unlock()
72 return append([]byte(nil), w.sent...)
73}
74
75func (w *inProcWorkers) dial(context.Context) (net.Conn, error) {
76 agentEnd, workerEnd := net.Pipe()
77 w.mu.Lock()
78 w.liveAtDial = append(w.liveAtDial, w.live.Load())
79 w.mu.Unlock()
80 w.dials.Add(1)
81 w.live.Add(1)
82 if w.failStart.Load() {
83 // The socket accepted and the service died: the plan's header is
84 // read by nobody useful, and the connection drops.
85 go func() {
86 readFrame(workerEnd)
87 workerEnd.Close()
88 time.Sleep(w.linger)
89 w.live.Add(-1)
90 }()
91 return frameTap{Conn: agentEnd, w: w}, nil
92 }
93 opt := w.opt
94 if opt.Scratch == "" {
95 opt.Scratch = w.t.TempDir()
96 }
97 if opt.Exit == nil {
98 opt.Exit = func(int) {}
99 }
100 if opt.Self == nil {
101 opt.Self = func() HelloReply { return HelloReply{V: ProtocolVersion, Interfaces: []string{"lo"}} }
102 }
103 go func() {
104 ServeWorker(workerEnd, opt)
105 workerEnd.Close()
106 time.Sleep(w.linger)
107 w.live.Add(-1)
108 }()
109 return frameTap{Conn: agentEnd, w: w}, nil
110}
111
112func (w *inProcWorkers) units(context.Context) (int, error) { return int(w.live.Load()), nil }
113
114func plentyFree() (uint64, uint64, error) { return 100 << 30, 200 << 30, nil }
115
116type fixture struct {
117 t *testing.T
118 store release.Store
119 rootPub ed25519.PublicKey
120 rootPriv ed25519.PrivateKey
121 reply *ecdh.PrivateKey
122 replica string
123 now time.Time
124}
125
126// newFixture is a deployment store holding shop.example.com's record in uk,
127// and a file replica of its database with batches*10 rows.
128func newFixture(t *testing.T, batches int) *fixture {
129 t.Helper()
130 pub, priv, err := ed25519.GenerateKey(rand.Reader)
131 if err != nil {
132 t.Fatal(err)
133 }
134 reply, err := NewReplyKey()
135 if err != nil {
136 t.Fatal(err)
137 }
138 fx := &fixture{t: t, store: release.NewDirStore(t.TempDir()), rootPub: pub, rootPriv: priv, reply: reply,
139 replica: t.TempDir(), now: time.Now().UTC().Truncate(time.Second)}
140 if batches > 0 {
141 dbdoortest.Replicate(t, fx.replica, batches)
142 }
143 fx.putRecord(provision.Record{Host: fxHost, Region: "uk", CreatedAt: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)})
144 return fx
145}
146
147func (fx *fixture) putRecordUnder(acct string, rec provision.Record) {
148 fx.t.Helper()
149 rec.Account, rec.App = acct, fxApp
150 if rec.Host == "" {
151 rec.Host = fxHost
152 }
153 if rec.Hibernate == nil {
154 yes := true
155 rec.Hibernate = &yes
156 }
157 body, err := json.Marshal(rec)
158 if err != nil {
159 fx.t.Fatal(err)
160 }
161 key := provision.RecordKey(acct, fxApp, rec.Host)
162 if err := fx.store.Delete(context.Background(), key); err != nil {
163 fx.t.Fatal(err)
164 }
165 if _, err := fx.store.Put(context.Background(), key, body, ""); err != nil {
166 fx.t.Fatal(err)
167 }
168}
169
170func (fx *fixture) putRecord(rec provision.Record) { fx.putRecordUnder(fxAccount, rec) }
171
172func (fx *fixture) record() provision.Record {
173 fx.t.Helper()
174 rec, err := provision.GetRecord(context.Background(), fx.store, fxAccount, fxApp, fxHost)
175 if err != nil {
176 fx.t.Fatal(err)
177 }
178 return *rec
179}
180
181// request is the envelope the console would sign for the current record.
182func (fx *fixture) request(sqlText, at string) Request {
183 fx.t.Helper()
184 rec := fx.record()
185 inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
186 if err != nil {
187 fx.t.Fatal(err)
188 }
189 id, err := MintID()
190 if err != nil {
191 fx.t.Fatal(err)
192 }
193 return Request{ID: id, Account: fxAccount, App: fxApp, Host: fxHost, Incarnation: inc, Region: "uk",
194 Op: OpQuery, SQL: sqlText, At: at, ReplyKey: EncodeReplyKey(fx.reply.PublicKey()),
195 By: "member@example.com", IssuedAt: fx.now.Format(time.RFC3339)}
196}
197
198// submit is the console's two creates: the signed request, then the queue
199// entry.
200func (fx *fixture) submit(req Request) QueueEntry {
201 fx.t.Helper()
202 wire, err := Sign(release.RootSigner(fx.rootPriv), req)
203 if err != nil {
204 fx.t.Fatal(err)
205 }
206 if _, err := fx.store.Put(context.Background(), RequestKey(req.ID), wire, ""); err != nil {
207 fx.t.Fatal(err)
208 }
209 issued, _ := req.Issued()
210 key := QueueKey(req.Region, issued, req.ID)
211 if _, err := fx.store.Put(context.Background(), key, nil, ""); err != nil {
212 fx.t.Fatal(err)
213 }
214 e, _ := ParseQueueKey(key)
215 return e
216}
217
218// agent is a capable agent in uk over the fixture's store and replica.
219// Its probe is Task 12's: it starts capable.
220func (fx *fixture) agent(box string, mut ...func(*AgentConfig, *inProcWorkers)) (*Agent, *inProcWorkers) {
221 w := &inProcWorkers{t: fx.t}
222 cfg := AgentConfig{
223 Store: fx.store, Region: "uk", Box: box, Boot: "boot-" + box, Pub: fx.rootPub, HibernatePrefix: "instances",
224 OpenReplica: func(_ context.Context, prefix string) (dbdoor.ReplicaReader, error) {
225 if prefix != "instances/"+fxHost {
226 fx.t.Errorf("the agent asked for prefix %q", prefix)
227 }
228 return lsfile.NewReplicaClient(fx.replica), nil
229 },
230 Units: w.units, Free: plentyFree, Logf: fx.t.Logf,
231 Jitter: func() time.Duration { return 0 },
232 }
233 for _, m := range mut {
234 m(&cfg, w)
235 }
236 cfg.DialWorker = w.dial
237 a := NewAgent(cfg)
238 a.probeOK.Store(true)
239 return a, w
240}
241
242// chunkKeys lists a read's chunk objects.
243func (fx *fixture) chunkKeys(id string) []string {
244 keys, err := fx.store.List(context.Background(), OutPrefix(id))
245 if err != nil && !errors.Is(err, os.ErrNotExist) {
246 fx.t.Fatal(err)
247 }
248 sort.Strings(keys)
249 return keys
250}
251
252// answer reads a read's result and opens its chunks as the CLI would.
253func (fx *fixture) answer(id string) ([]byte, Summary, Result) {
254 fx.t.Helper()
255 raw, _, err := fx.store.Get(context.Background(), ResultKey(id))
256 if err != nil {
257 fx.t.Fatalf("no result for %s: %v", id, err)
258 }
259 var r Result
260 if err := json.Unmarshal(raw, &r); err != nil || r.Valid() != nil {
261 fx.t.Fatalf("result %s: %v", raw, err)
262 }
263 s, err := OpenSummary(fx.reply, id, r.Sealed)
264 if err != nil {
265 fx.t.Fatal(err)
266 }
267 var out bytes.Buffer
268 for seq := int64(1); seq <= r.LastSeq; seq++ {
269 sealed, _, err := fx.store.Get(context.Background(), ChunkKey(id, seq))
270 if err != nil {
271 fx.t.Fatalf("chunk %d: %v", seq, err)
272 }
273 plain, err := OpenChunk(fx.reply, id, seq, StreamRows, seq == r.LastSeq, sealed)
274 if err != nil {
275 fx.t.Fatalf("chunk %d: %v", seq, err)
276 }
277 out.Write(plain)
278 }
279 sum := sha256.Sum256(out.Bytes())
280 if r.State == StateDone && (int64(out.Len()) != s.Bytes || hex.EncodeToString(sum[:]) != s.SHA256) {
281 fx.t.Fatalf("the answer does not add up: %d bytes, summary %+v", out.Len(), s)
282 }
283 return out.Bytes(), s, r
284}
285