carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "log"
9 "os"
10 "path/filepath"
11
12 "github.com/carlosframework/platform/internal/lease"
13 "github.com/carlosframework/platform/internal/moves"
14 "github.com/carlosframework/platform/internal/moves/archive"
15 "github.com/carlosframework/platform/internal/provision"
16)
17
18// The cut's archive (moves spec §7.4 staged, §8.3, codex 19, ruling G14):
19// how the stopped files directory's one sealed archive is identified,
20// published create-only into this box's move-snapshots/ and staged into the
21// lane's files/, and how a replay finishes exactly the identity an earlier
22// pass journalled. The cut's phase loop is in movescut.go; this file is the
23// archive's publication and its recovery, on their own so the recovery
24// rules can be read in one place (Task 5 fix round 1, ruling 7).
25//
26// Source-disk headroom. The preparing round keeps the newest built archive
27// on the source's data disk under <JournalDir>/archive/ for the life of
28// the move, so the cut can publish the very object the target verified
29// (G14). That costs up to archive.Cap (256 MiB) of the source's data disk
30// while a move is in flight. A rebuild (the stopped files differ from the
31// verified archive) is worse for the length of one build: the previous
32// sealed archive is still there, buildArchive writes the compressed
33// plaintext (archive.tar.zst) to a temp directory beside it, and then
34// seals that into the new sealed archive before pruneArchives removes the
35// previous one. With incompressible files all three approach the payload
36// size, so the worst case is about three times archive.Cap plus the
37// sealing overhead. The target's T6 does not price this source-side
38// headroom; that is a Plan 6 follow-up.
39// cutArchiveStep is the archive's identity in the op journal, written
40// before any PUT so a replay publishes the same object (codex 19): the
41// archive's name and digests, the two move-snapshots/ keys it is published
42// under, and the manifest's bytes, so a replay that finds the sealed
43// object published and the manifest not can write the manifest without
44// the file (ruling 4). Reuse marks the preparing archive the target
45// verified (G14): the seal names its staged versions and nothing is staged
46// again.
47type cutArchiveStep struct {
48 builtArchive
49 Reuse bool `json:"reuse,omitempty"`
50 SealedKey string `json:"sealed_key"`
51 ManifestKey string `json:"manifest_key"`
52 Manifest json.RawMessage `json:"manifest"`
53}
54
55// errArchiveLost: the journalled identity can be finished by nobody. The
56// sealed file is gone from this disk and the sealed object was never
57// published, so there is nothing to publish it from, and a new identity
58// is not allowed once one is journalled (ruling 4). The cut reverts.
59var errArchiveLost = errors.New("the cut's archive is lost before it was published")
60
61// cutArchive publishes the cut's one sealed archive (§8.3) and returns the
62// seal's files: the sealed object and its manifest by version.
63//
64// Its identity is fixed before any PUT (sealing is randomized), in the op
65// journal, and once journalled it is final: a replay finishes that
66// identity or the cut reverts, and never reseals under a new name
67// (ruling 4). The first run reuses the preparing archive when its
68// plaintext digest is the stopped files' and its staged versions are pinned
69// and its sealed file is still on this disk (G14); otherwise it builds and
70// seals a new one. Then, from the journalled identity (publishCutArchive):
71// - the sealed file on disk: create-only into move-snapshots/ (an existing
72// object is this cut's earlier PUT), then staged into files/ unless it
73// is the reused archive, whose staged versions the ledger holds;
74// - no file: what move-snapshots/ holds decides, and only a listing that
75// answered decides it (an error is an error, never absence). The
76// sealed object there and the manifest not: the manifest is written
77// from the journalled bytes. Both there: staged into files/ by a
78// guarded server-side copy (or, reused, nothing to stage). The sealed
79// object absent: errArchiveLost, which is a revert.
80//
81// why is a recorded reason the stopped files cannot be archived.
82func (c *cutRun) cutArchive(ctx context.Context, plain string) (builtArchive, []moves.SealedObject, string, error) {
83 cfg := c.cfg()
84 host, id := c.rec.Host, c.g.ID
85 own, vs, err := c.ownStores(ctx)
86 if err != nil {
87 return builtArchive{}, nil, "", err
88 }
89 ledger, err := readMoveLedger(ctx, own, cfg.OwnBucket, moves.LedgerKey(host, id))
90 if err != nil {
91 return builtArchive{}, nil, "", err
92 }
93 j, err := c.journal()
94 if err != nil {
95 return builtArchive{}, nil, "", err
96 }
97 adir := archiveDir(cfg.DataDir, host, id)
98 snap := moves.SnapshotsPrefix(host, id)
99 var step cutArchiveStep
100 if raw, ok := j.Steps[cutStepArchive]; ok {
101 // Journalled: final. An entry that cannot be read, or names another
102 // plaintext than the record's, is a corrupt journal, and starting
103 // over on it could publish a second identity: an error.
104 if err := json.Unmarshal([]byte(raw), &step); err != nil || step.Name != archiveName(step.Sealed) || step.Plain != plain ||
105 step.SealedKey != snap+step.Name+".sealed" || step.ManifestKey != snap+step.Name+".json" || len(step.Manifest) == 0 {
106 return builtArchive{}, nil, "", fmt.Errorf("the journalled archive identity of %s is unusable (%v)", id, err)
107 }
108 step.dir = adir
109 } else {
110 reuse := ledger.ArchivePlain == plain && ledger.ArchiveVersions != nil && ledger.ArchiveVersions.Pinned() && ledger.ArchiveSealed != ""
111 if reuse {
112 kept := builtArchive{Name: archiveName(ledger.ArchiveSealed), Sealed: ledger.ArchiveSealed, Plain: plain, dir: adir}
113 keptThere, err := sealedOnDisk(kept)
114 if err != nil {
115 return builtArchive{}, nil, "", err
116 }
117 // The kept manifest is needed here, before anything is
118 // journalled: without it the verified archive is simply not
119 // reused, and a new one is built.
120 if m, ok := keptManifest(kept); ok && keptThere {
121 kept.Bytes, kept.Entries = m.Bytes, len(m.Entries)
122 if st, err := os.Stat(kept.sealedPath()); err == nil {
123 kept.SealedSize = st.Size()
124 }
125 step = cutArchiveStep{builtArchive: kept, Reuse: true}
126 } else {
127 reuse = false
128 }
129 }
130 if !reuse {
131 a, why, retry, err := c.w.buildArchive(c.rec)
132 if err != nil || why != "" {
133 return builtArchive{}, nil, why, err
134 }
135 if retry {
136 return builtArchive{}, nil, "", errors.New("a stopped file changed while it was archived")
137 }
138 if a.Plain != plain {
139 return builtArchive{}, nil, "", fmt.Errorf("the stopped files' digest is %s, recorded %s", a.Plain, plain)
140 }
141 step = cutArchiveStep{builtArchive: a}
142 }
143 manifest, err := os.ReadFile(step.manifestPath())
144 if err != nil {
145 return builtArchive{}, nil, "", err
146 }
147 step.SealedKey, step.ManifestKey, step.Manifest = snap+step.Name+".sealed", snap+step.Name+".json", manifest
148 body, err := json.Marshal(step)
149 if err != nil {
150 return builtArchive{}, nil, "", err
151 }
152 j.Mark(cutStepArchive, string(body))
153 if err := j.Commit(cfg.DataDir); err != nil {
154 return builtArchive{}, nil, "", err
155 }
156 if err := c.fault("archive-journaled"); err != nil {
157 return builtArchive{}, nil, "", err
158 }
159 }
160 a := step.builtArchive
161 files, err := c.publishCutArchive(ctx, own, vs, step, ledger)
162 if errors.Is(err, errArchiveLost) {
163 log.Printf("moves: %s: %v; the cut reverts", host, err)
164 return builtArchive{}, nil, provision.RevertArchiveRefused, nil
165 }
166 return a, files, "", err
167}
168
169// publishCutArchive is cutArchive's PUTs from a journalled identity. The
170// manifest it stages is the journalled one: the file on disk, when there
171// is one, was written by the same build, and the journal is what a replay
172// without the file has.
173func (c *cutRun) publishCutArchive(ctx context.Context, own lease.MirrorStore, vs lease.VersionedStore, step cutArchiveStep, ledger moveLedger) ([]moves.SealedObject, error) {
174 cfg := c.cfg()
175 host, id := c.rec.Host, c.g.ID
176 a := step.builtArchive
177 snap := moves.SnapshotsPrefix(host, id)
178 sealedSnap, manifestSnap := step.SealedKey, step.ManifestKey
179 manifest := []byte(step.Manifest)
180 onDisk, err := sealedOnDisk(a)
181 if err != nil {
182 return nil, err
183 }
184 if onDisk {
185 fp, ok := vs.(lease.FilePutIfAbsent)
186 if !ok {
187 return nil, errors.New("this box cannot write the archive create-only")
188 }
189 if _, err := fp.PutFileIfAbsent(ctx, cfg.OwnBucket, sealedSnap, a.sealedPath()); err != nil && !errors.Is(err, lease.ErrExists) {
190 return nil, fmt.Errorf("snapshot the sealed archive: %w", err)
191 }
192 if err := c.fault("archive-sealed-snapshot"); err != nil {
193 return nil, err
194 }
195 if _, err := vs.PutIfAbsent(ctx, cfg.OwnBucket, manifestSnap, manifest); err != nil && !errors.Is(err, lease.ErrExists) {
196 return nil, fmt.Errorf("snapshot the archive manifest: %w", err)
197 }
198 if err := c.fault("archive-own-put"); err != nil {
199 return nil, err
200 }
201 } else {
202 // No file (the read succeeded and found none, or a file that is not
203 // this archive): only what was published under the identity can
204 // finish it. A listing that failed decides nothing (ruling 4). The
205 // loss is established only here, by reads that answered.
206 sealedThere, _, err := c.snapshotState(ctx, vs, sealedSnap)
207 if err != nil {
208 return nil, err
209 }
210 if !sealedThere {
211 return nil, fmt.Errorf("%w: %s is not on this disk and not in %s", errArchiveLost, a.Name, snap)
212 }
213 manifestThere, _, err := c.snapshotState(ctx, vs, manifestSnap)
214 if err != nil {
215 return nil, err
216 }
217 if !manifestThere {
218 // A crash between the two PUTs: the manifest from the journal.
219 if _, err := vs.PutIfAbsent(ctx, cfg.OwnBucket, manifestSnap, manifest); err != nil && !errors.Is(err, lease.ErrExists) {
220 return nil, fmt.Errorf("snapshot the archive manifest from the journal: %w", err)
221 }
222 log.Printf("moves: %s: the manifest of %s was written from the journalled identity", host, a.Name)
223 }
224 }
225 if step.Reuse {
226 ref := ledger.ArchiveVersions
227 if ref == nil || !ref.Pinned() || ledger.ArchiveSealed != a.Sealed {
228 return nil, fmt.Errorf("the ledger no longer names the archive %s", a.Name)
229 }
230 return []moves.SealedObject{
231 {Key: ref.SealedObjectKey, VersionID: ref.SealedVersion, Size: a.SealedSize},
232 {Key: ref.ManifestKey, VersionID: ref.ManifestVersion, Size: int64(len(manifest))},
233 }, nil
234 }
235 prefix := moves.FilesPrefix(host, id)
236 sealedKey, manifestKey := prefix+a.Name+".sealed", prefix+a.Name+".json"
237 var sv, mv string
238 var msize int64
239 if onDisk {
240 if sv, err = c.gm.PutFileVersioned(ctx, c.gm.dst, sealedKey, a.sealedPath(), a.SealedSize, digestHex(a.Sealed)); err != nil {
241 return nil, fmt.Errorf("stage the sealed archive: %w", err)
242 }
243 if err := c.fault("archive-first-copy"); err != nil {
244 return nil, err
245 }
246 if mv, err = c.gm.PutVersioned(ctx, c.gm.dst, manifestKey, manifest); err != nil {
247 return nil, fmt.Errorf("stage the archive manifest: %w", err)
248 }
249 msize = int64(len(manifest))
250 } else {
251 // The file is gone, the snapshot is there: a server-side copy from
252 // it, guarded, as the replica lane copies.
253 objs, err := own.List(ctx, cfg.OwnBucket, snap)
254 if err != nil {
255 return nil, fmt.Errorf("list the snapshots: %w", err)
256 }
257 so, sok := objs[sealedSnap]
258 mo, mok := objs[manifestSnap]
259 if !sok || !mok {
260 return nil, fmt.Errorf("the snapshot of %s is incomplete", a.Name)
261 }
262 if _, sv, err = c.gm.CopyVersioned(ctx, cfg.OwnBucket, sealedSnap, so.ETag, c.gm.dst, sealedKey); err != nil {
263 return nil, fmt.Errorf("stage the sealed archive: %w", err)
264 }
265 if err := c.fault("archive-first-copy"); err != nil {
266 return nil, err
267 }
268 if _, mv, err = c.gm.CopyVersioned(ctx, cfg.OwnBucket, manifestSnap, mo.ETag, c.gm.dst, manifestKey); err != nil {
269 return nil, fmt.Errorf("stage the archive manifest: %w", err)
270 }
271 a.SealedSize, msize = so.Size, mo.Size
272 }
273 if err := c.fault("archive-second-copy"); err != nil {
274 return nil, err
275 }
276 if !lease.UsableVersionID(sv) || !lease.UsableVersionID(mv) {
277 return nil, fmt.Errorf("the staged archive has no usable version ids (%q, %q)", sv, mv)
278 }
279 log.Printf("moves: %s: staged the cut's archive %s", host, a.Name)
280 return []moves.SealedObject{{Key: sealedKey, VersionID: sv, Size: a.SealedSize}, {Key: manifestKey, VersionID: mv, Size: msize}}, nil
281}
282
283// sealedOnDisk reports whether the archive's sealed file is on this disk
284// and hashes to its name. The sealed file alone is what a publication
285// needs: the manifest's bytes are journalled with the identity, so losing
286// only the local manifest loses nothing (fix round 2). Absence, and a file
287// whose digest is not the name, are false with no error; a read that
288// fails is an error, never absence, since a loss is declared on this
289// answer and a read error establishes none.
290func sealedOnDisk(a builtArchive) (bool, error) {
291 sum, err := fileSHA256(a.sealedPath())
292 if errors.Is(err, os.ErrNotExist) {
293 return false, nil
294 }
295 if err != nil {
296 return false, fmt.Errorf("read %s: %w", a.sealedPath(), err)
297 }
298 return sum == a.Sealed, nil
299}
300
301// keptManifest reads the manifest of an archive kept on disk; it must
302// describe that archive.
303func keptManifest(a builtArchive) (archive.Manifest, bool) {
304 var m archive.Manifest
305 body, err := os.ReadFile(a.manifestPath())
306 if err != nil || json.Unmarshal(body, &m) != nil || m.Sealed != a.Sealed || m.Plain != a.Plain {
307 return m, false
308 }
309 return m, true
310}
311
312// snapshotState reports whether key holds a current object (not a delete
313// marker) in this box's bucket, and its size. A listing that fails is an
314// error, never absence (ruling 4): absence lets a replay decide the
315// identity was never published.
316func (c *cutRun) snapshotState(ctx context.Context, vs lease.VersionedStore, key string) (present bool, size int64, err error) {
317 vers, err := vs.ListVersions(ctx, c.cfg().OwnBucket, key)
318 if err != nil {
319 return false, 0, fmt.Errorf("list %s: %w", key, err)
320 }
321 for _, v := range vers {
322 if v.Key == key && v.IsLatest && !v.DeleteMarker {
323 return true, v.Size, nil
324 }
325 }
326 return false, 0, nil
327}
328
329// sealNamesArchive reports whether the seal's files are the archive with
330// the sealed digest given.
331func sealNamesArchive(seal moves.Seal, sealed string) bool {
332 if sealed == "" {
333 return false
334 }
335 name := archiveName(sealed)
336 n := 0
337 for _, o := range seal.Files {
338 base := filepath.Base(o.Key)
339 if base == name+".sealed" || base == name+".json" {
340 n++
341 }
342 }
343 return n == 2 && len(seal.Files) == 2
344}
345