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 "time"
10
11 "github.com/benbjohnson/litestream"
12 "github.com/superfly/ltx"
13)
14
15// RestorePlan is what a restore will read and produce, known before
16// anything is downloaded (seeded-instances spec §4 step 5, codex R2-19).
17type RestorePlan struct {
18 // TXID is the last planned file's MaxTXID: where the restore ends. For
19 // a target it is the target itself; for the latest copy it is the
20 // latest transaction when planned. RestoreToTarget at TXID reproduces
21 // exactly this plan's RestoredBytes, however far the source has moved
22 // on since, as long as that history is still kept.
23 TXID uint64
24 LTXBytes int64 // the planned files' compressed sizes, summed
25 RestoredBytes int64 // the restored database's size: Commit pages times PageSize
26 Files int
27 // Target is what was planned for: the requested target, or 0 (latest)
28 // when the request is at or past the latest transaction, which
29 // litestream would otherwise refuse as not available.
30 Target uint64
31 // Planned is the plan's files in restore order (box side: what the
32 // worker is sent, db region forward spec §8.2).
33 Planned []LTXFile
34 // At is the last planned file's header time: the time of the copy's
35 // newest transaction, as RestoreAsOf's Restored.At reports it.
36 At time.Time
37}
38
39// PlanRestore plans a restore to target (0: latest) through c and reads the
40// header of the last planned file with a ranged open of its first
41// ltx.HeaderSize bytes. A target older than the retained history, or one
42// no retained chain of files ends at, is ErrHistoryGone; a replica with
43// nothing in it is ErrNothingReplicated, with or without a target.
44func PlanRestore(ctx context.Context, c *Client, target uint64) (RestorePlan, error) {
45 if target != 0 {
46 latest, err := PlanRestore(ctx, c, 0)
47 if err != nil {
48 return RestorePlan{}, err
49 }
50 if target >= latest.TXID {
51 return latest, nil // at or past the end: the latest copy
52 }
53 }
54 infos, err := litestream.CalcRestorePlan(ctx, c, ltx.TXID(target), time.Time{}, slog.Default())
55 switch {
56 case errors.Is(err, litestream.ErrTxNotAvailable) || errors.Is(err, litestream.ErrNoSnapshots):
57 if target != 0 {
58 return RestorePlan{}, ErrHistoryGone
59 }
60 return RestorePlan{}, ErrNothingReplicated
61 case err != nil:
62 return RestorePlan{}, err
63 case len(infos) == 0:
64 return RestorePlan{}, ErrNothingReplicated
65 }
66 return finishPlan(ctx, c, infos, target)
67}
68
69// finishPlan sums the plan, records its files, and reads and bounds the
70// last planned file's header: the size check's one decode (db region
71// forward spec §8.1). PlanRestore and PlanFrom both end here, so both read
72// and check the last file's header the same way.
73func finishPlan(ctx context.Context, c litestream.ReplicaClient, infos []*ltx.FileInfo, target uint64) (RestorePlan, error) {
74 p := RestorePlan{Target: target, Files: len(infos)}
75 for _, f := range infos {
76 p.LTXBytes += f.Size
77 p.Planned = append(p.Planned, LTXFile{Level: f.Level, MinTXID: uint64(f.MinTXID), MaxTXID: uint64(f.MaxTXID), Size: f.Size, CreatedAt: f.CreatedAt})
78 }
79 last := infos[len(infos)-1]
80 p.TXID = uint64(last.MaxTXID)
81 rc, err := c.OpenLTXFile(ctx, last.Level, last.MinTXID, last.MaxTXID, 0, ltx.HeaderSize)
82 if err != nil {
83 return RestorePlan{}, fmt.Errorf("dbdoor: read the last planned file's header: %w", err)
84 }
85 defer rc.Close()
86 buf := make([]byte, ltx.HeaderSize)
87 if _, err := io.ReadFull(rc, buf); err != nil {
88 return RestorePlan{}, fmt.Errorf("dbdoor: read the last planned file's header: %w", err)
89 }
90 var h ltx.Header
91 if err := h.UnmarshalBinary(buf); err != nil {
92 return RestorePlan{}, fmt.Errorf("dbdoor: the last planned file's header: %w", err)
93 }
94 // The header is the replica's word, so untrusted, and UnmarshalBinary
95 // checks only its magic. A page size SQLite allows (512 to 65536)
96 // bounds the product below 2^48, so it cannot wrap; a zero commit is
97 // no database; and TXIDs other than the listed file's mean the header
98 // describes some other file.
99 if !ltx.IsValidPageSize(h.PageSize) || h.Commit == 0 || h.MinTXID != last.MinTXID || h.MaxTXID != last.MaxTXID {
100 return RestorePlan{}, fmt.Errorf("dbdoor: the last planned file's header does not describe it (level %d %s: page size %d, commit %d, txids %s)",
101 last.Level, Span(uint64(last.MinTXID), uint64(last.MaxTXID)), h.PageSize, h.Commit, Span(uint64(h.MinTXID), uint64(h.MaxTXID)))
102 }
103 p.RestoredBytes = int64(h.Commit) * int64(h.PageSize)
104 p.At = time.UnixMilli(h.Timestamp).UTC()
105 return p, nil
106}
107