carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package dbdoor
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "log/slog"
9 "sort"
10 "sync"
11 "time"
12
13 "github.com/benbjohnson/litestream"
14 "github.com/superfly/ltx"
15)
16
17// ReplicaReader is the read half of a litestream replica client: the two
18// methods a door or a planner may hold. A *lss3.ReplicaClient from
19// lease.ReadReplicaClient is one; so is a file replica client.
20type ReplicaReader interface {
21 LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error)
22 OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error)
23}
24
25// ListLevel lists one level of rd in the door's wire shape, ordered by
26// MinTXID, with an empty level as an empty list, and with S3's LastModified
27// as CreatedAt (useMetadata false). The console's door and the box's planner
28// both list through it, so a forwarded --at stops at the transaction an
29// in-region one does (spec §6, T6).
30func ListLevel(ctx context.Context, rd ReplicaReader, level int) (ListResponse, error) {
31 itr, err := rd.LTXFiles(ctx, level, 0, false)
32 if err != nil {
33 return ListResponse{}, err
34 }
35 out := ListResponse{Files: []LTXFile{}}
36 for itr.Next() {
37 f := itr.Item()
38 out.Files = append(out.Files, LTXFile{
39 Level: level, MinTXID: uint64(f.MinTXID), MaxTXID: uint64(f.MaxTXID),
40 Size: f.Size, CreatedAt: f.CreatedAt,
41 })
42 }
43 err = itr.Err()
44 if cerr := itr.Close(); err == nil {
45 err = cerr
46 }
47 if err != nil {
48 return ListResponse{}, err
49 }
50 sort.Slice(out.Files, func(i, j int) bool { return out.Files[i].MinTXID < out.Files[j].MinTXID })
51 return out, nil
52}
53
54// listing is a read-only litestream.ReplicaClient over rd that lists through
55// ListLevel. For an as-of plan it also does what the CLI's recorder does
56// over the door's listings: refuse a listing with no times (ErrNoTimes) and
57// remember the earliest file a restore could start from.
58type listing struct {
59 rd ReplicaReader
60 asOf bool
61 mu sync.Mutex
62 oldest time.Time
63}
64
65func (l *listing) Type() string { return "carlos-db-listing" }
66func (l *listing) Init(context.Context) error { return nil }
67func (l *listing) SetLogger(*slog.Logger) {}
68
69func (l *listing) LTXFiles(ctx context.Context, level int, seek ltx.TXID, _ bool) (ltx.FileIterator, error) {
70 out, err := ListLevel(ctx, l.rd, level)
71 if err != nil {
72 return nil, err
73 }
74 if l.asOf {
75 l.mu.Lock()
76 defer l.mu.Unlock()
77 for _, f := range out.Files {
78 if f.CreatedAt.IsZero() {
79 return nil, ErrNoTimes
80 }
81 if level == litestream.SnapshotLevel || f.MinTXID == 1 {
82 if l.oldest.IsZero() || f.CreatedAt.Before(l.oldest) {
83 l.oldest = f.CreatedAt
84 }
85 }
86 }
87 }
88 return fileIterator(out.Files, level, seek), nil
89}
90
91func (l *listing) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
92 return l.rd.OpenLTXFile(ctx, level, minTXID, maxTXID, offset, size)
93}
94
95func (l *listing) WriteLTXFile(context.Context, int, ltx.TXID, ltx.TXID, io.Reader) (*ltx.FileInfo, error) {
96 return nil, errReadOnly
97}
98
99func (l *listing) DeleteLTXFiles(context.Context, []*ltx.FileInfo) error { return errReadOnly }
100
101func (l *listing) DeleteAll(context.Context) error { return errReadOnly }
102
103var _ litestream.ReplicaClient = (*listing)(nil)
104
105// PlanFrom plans the restore RestoreAsOf would make through the door, from
106// rd directly: the newest copy when at is zero, else the copy holding every
107// file listed before at rounded down to its second. Its errors are
108// RestoreAsOf's: ErrNothingReplicated, a *BeforeHistoryError naming the
109// oldest point kept, ErrNoTimes.
110func PlanFrom(ctx context.Context, rd ReplicaReader, at time.Time) (RestorePlan, error) {
111 l := &listing{rd: rd, asOf: !at.IsZero()}
112 var ts time.Time
113 if !at.IsZero() {
114 if at.Truncate(time.Second).Before(EarliestAsOf) {
115 at = EarliestAsOf
116 }
117 ts = at.Truncate(time.Second)
118 }
119 infos, err := litestream.CalcRestorePlan(ctx, l, 0, ts, slog.Default())
120 switch {
121 case errors.Is(err, ErrNoTimes):
122 return RestorePlan{}, ErrNoTimes
123 case errors.Is(err, litestream.ErrTxNotAvailable) || errors.Is(err, litestream.ErrNoSnapshots):
124 l.mu.Lock()
125 oldest := l.oldest
126 l.mu.Unlock()
127 if !ts.IsZero() && !oldest.IsZero() {
128 if !oldest.Before(ts) {
129 return RestorePlan{}, &BeforeHistoryError{Oldest: oldest.Truncate(time.Second).Add(time.Second)}
130 }
131 return RestorePlan{}, err
132 }
133 return RestorePlan{}, ErrNothingReplicated
134 case err != nil:
135 return RestorePlan{}, err
136 case len(infos) == 0:
137 return RestorePlan{}, ErrNothingReplicated
138 }
139 return finishPlan(ctx, l, infos, 0)
140}
141
142// RestoreLocal restores the replica c holds, a litestream file replica in a
143// private directory (the db-read worker's, or the CLI's for a forwarded
144// pull), exactly to target, at out, and proves it with lease.CheckDBContext.
145// On failure nothing is left at out.
146func RestoreLocal(ctx context.Context, c litestream.ReplicaClient, out string, target uint64) error {
147 // litestream's compaction checks no context, so every read of the local
148 // files checks it here, as the door path's downloaded.Read does (Astra
149 // round 1 on plan 3, finding 2).
150 if err := restoreOnce(ctx, ctxReplica{ReplicaClient: c, ctx: ctx}, out, target, time.Time{}); err != nil {
151 removeCopy(out)
152 if cerr := ctx.Err(); cerr != nil {
153 return cerr
154 }
155 return err
156 }
157 if err := checkDB(ctx, out); err != nil {
158 removeCopy(out)
159 if cerr := ctx.Err(); cerr != nil {
160 return cerr
161 }
162 return fmt.Errorf("%w: %v", ErrCheckFailed, err)
163 }
164 if err := ctx.Err(); err != nil {
165 removeCopy(out)
166 return err
167 }
168 return nil
169}
170
171// ctxReplica is a local replica client whose file reads stop once ctx is
172// done.
173type ctxReplica struct {
174 litestream.ReplicaClient
175 ctx context.Context
176}
177
178func (c ctxReplica) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
179 if err := c.ctx.Err(); err != nil {
180 return nil, err
181 }
182 rc, err := c.ReplicaClient.OpenLTXFile(ctx, level, minTXID, maxTXID, offset, size)
183 if err != nil {
184 return nil, err
185 }
186 return ctxReadCloser{ReadCloser: rc, ctx: c.ctx}, nil
187}
188
189type ctxReadCloser struct {
190 io.ReadCloser
191 ctx context.Context
192}
193
194func (r ctxReadCloser) Read(p []byte) (int, error) {
195 if err := r.ctx.Err(); err != nil {
196 return 0, err
197 }
198 return r.ReadCloser.Read(p)
199}
200