carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "log/slog"
9 "os"
10 "strconv"
11 "strings"
12
13 "github.com/carlosframework/platform/internal/lease"
14 "github.com/superfly/ltx"
15)
16
17// SealedObject is one pinned object version of a generation build's
18// source: an LTX file named by its full key, the exact version id the seal
19// (or a genselect plan, or a current listing) pinned, and its size. Task 5's
20// Seal lists the staging lane's replica/ objects in this shape (plan:688);
21// region HA's relocation feeds the same shape from its own pins (L16).
22type SealedObject struct {
23 Key string `json:"key"`
24 VersionID string `json:"version_id"`
25 Size int64 `json:"size"`
26}
27
28// ErrSealedReadOnly is every write a SealedClient refuses: the pinned set
29// is immutable after the flip, and the build never deletes (§7.6).
30var ErrSealedReadOnly = errors.New("moves: the sealed replica is read-only")
31
32// SealedClient is a litestream.ReplicaClient over exactly one pinned set of
33// object versions. Litestream's planner and restore read through it and
34// never see a version the seal did not name, whether a newer write to a
35// sealed key or an unsealed sibling under the same prefix. A key's position
36// (level, min TXID, max TXID) is its last two path elements,
37// `<level>/<min>-<max>.ltx`, so the client names no lane or generation.
38type SealedClient struct {
39 store lease.VersionedStore
40 bucket string
41 byPos map[sealedPos]SealedObject
42 infos []ltx.FileInfo
43 logger *slog.Logger
44}
45
46type sealedPos struct {
47 level int
48 min, max ltx.TXID
49}
50
51var _ interface {
52 Type() string
53 Init(ctx context.Context) error
54 LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error)
55 OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error)
56 WriteLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, r io.Reader) (*ltx.FileInfo, error)
57 DeleteLTXFiles(ctx context.Context, a []*ltx.FileInfo) error
58 DeleteAll(ctx context.Context) error
59 SetLogger(logger *slog.Logger)
60} = (*SealedClient)(nil)
61
62// NewSealedClient pins source. It refuses, before any read, an object with
63// no usable version id (G27: "" is no version and "null" is what a
64// versioning-suspended bucket answers, so neither pins anything), a key
65// that is not an LTX file under a level directory, a size below an LTX
66// header (litestream's restore refuses such a file, and a size of zero
67// would make its premature-EOF detection blind), and two objects at one
68// position, which would make the plan ambiguous.
69func NewSealedClient(store lease.VersionedStore, bucket string, source []SealedObject) (*SealedClient, error) {
70 if store == nil || bucket == "" {
71 return nil, errors.New("moves: sealed client needs a store and a bucket")
72 }
73 c := &SealedClient{store: store, bucket: bucket, byPos: map[sealedPos]SealedObject{}, logger: slog.Default()}
74 for _, o := range source {
75 if !lease.UsableVersionID(o.VersionID) {
76 return nil, fmt.Errorf("moves: sealed object %s has no usable version id %q", o.Key, o.VersionID)
77 }
78 pos, err := sealedPosition(o.Key)
79 if err != nil {
80 return nil, err
81 }
82 if o.Size < ltx.HeaderSize {
83 return nil, fmt.Errorf("moves: sealed object %s has size %d, below an LTX header", o.Key, o.Size)
84 }
85 if prev, dup := c.byPos[pos]; dup {
86 return nil, fmt.Errorf("moves: sealed objects %s and %s share one position", prev.Key, o.Key)
87 }
88 c.byPos[pos] = o
89 c.infos = append(c.infos, ltx.FileInfo{Level: pos.level, MinTXID: pos.min, MaxTXID: pos.max, Size: o.Size})
90 }
91 return c, nil
92}
93
94// sealedPosition reads `<level>/<min>-<max>.ltx` off the end of key, with
95// the level directory's four lowercase hex digits and the canonical file
96// name, the same shape ReplicaSource's keep admits.
97func sealedPosition(key string) (sealedPos, error) {
98 parts := strings.Split(key, "/")
99 if len(parts) < 2 {
100 return sealedPos{}, fmt.Errorf("moves: sealed object %s is not <level>/<min>-<max>.ltx", key)
101 }
102 dir, name := parts[len(parts)-2], parts[len(parts)-1]
103 level, err := strconv.ParseUint(dir, 16, 16)
104 if err != nil || len(dir) != 4 || fmt.Sprintf("%04x", level) != dir {
105 return sealedPos{}, fmt.Errorf("moves: sealed object %s has no level directory", key)
106 }
107 min, max, err := ltx.ParseFilename(name)
108 if err != nil || min == 0 || min > max || ltx.FormatFilename(min, max) != name {
109 return sealedPos{}, fmt.Errorf("moves: sealed object %s is not an LTX file name", key)
110 }
111 return sealedPos{level: int(level), min: min, max: max}, nil
112}
113
114// Type names the client in litestream's logs.
115func (c *SealedClient) Type() string { return "sealed" }
116
117// Init has nothing to open: the store is already bound.
118func (c *SealedClient) Init(ctx context.Context) error { return nil }
119
120// SetLogger stores the logger litestream hands over.
121func (c *SealedClient) SetLogger(logger *slog.Logger) {
122 if logger != nil {
123 c.logger = logger
124 }
125}
126
127// LTXFiles lists the pinned files at level from seek on, in TXID order.
128// useMetadata is ignored: a pinned set has no timestamps to fetch, and the
129// build restores by TXID, never by time.
130func (c *SealedClient) LTXFiles(ctx context.Context, level int, seek ltx.TXID, useMetadata bool) (ltx.FileIterator, error) {
131 var out []*ltx.FileInfo
132 for i := range c.infos {
133 info := c.infos[i]
134 if info.Level != level || info.MinTXID < seek {
135 continue
136 }
137 out = append(out, &info) // a copy: the iterator sorts in place
138 }
139 return ltx.NewFileInfoSliceIterator(out), nil
140}
141
142// OpenLTXFile streams exactly the pinned version of the file at the
143// position, or os.ErrNotExist when the seal names none. offset and size
144// follow litestream's convention (size 0 reads to the end) and go through
145// the store's VersionRangeOpener when it has one, otherwise the whole
146// version is opened and the range is cut locally.
147func (c *SealedClient) OpenLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, offset, size int64) (io.ReadCloser, error) {
148 o, ok := c.byPos[sealedPos{level: level, min: minTXID, max: maxTXID}]
149 if !ok {
150 return nil, fmt.Errorf("moves: no sealed object at level %d %s: %w", level, ltx.FormatFilename(minTXID, maxTXID), os.ErrNotExist)
151 }
152 if offset < 0 || size < 0 {
153 return nil, fmt.Errorf("moves: invalid range %d+%d for %s", offset, size, o.Key)
154 }
155 if offset == 0 && size == 0 {
156 return c.store.OpenVersion(ctx, c.bucket, o.Key, o.VersionID)
157 }
158 if ranged, ok := c.store.(lease.VersionRangeOpener); ok {
159 length := int64(-1)
160 if size > 0 {
161 length = size
162 }
163 return ranged.OpenVersionRange(ctx, c.bucket, o.Key, o.VersionID, offset, length)
164 }
165 rc, err := c.store.OpenVersion(ctx, c.bucket, o.Key, o.VersionID)
166 if err != nil {
167 return nil, err
168 }
169 if offset > 0 {
170 if _, err := io.CopyN(io.Discard, rc, offset); err != nil {
171 rc.Close()
172 return nil, fmt.Errorf("moves: skip %d bytes of %s: %w", offset, o.Key, err)
173 }
174 }
175 if size > 0 {
176 return struct {
177 io.Reader
178 io.Closer
179 }{io.LimitReader(rc, size), rc}, nil
180 }
181 return rc, nil
182}
183
184// WriteLTXFile is refused: the pinned set is immutable.
185func (c *SealedClient) WriteLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID, r io.Reader) (*ltx.FileInfo, error) {
186 return nil, ErrSealedReadOnly
187}
188
189// DeleteLTXFiles is refused: the build never deletes.
190func (c *SealedClient) DeleteLTXFiles(ctx context.Context, a []*ltx.FileInfo) error {
191 return ErrSealedReadOnly
192}
193
194// DeleteAll is refused: the build never deletes.
195func (c *SealedClient) DeleteAll(ctx context.Context) error { return ErrSealedReadOnly }
196