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 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/carlosframework/platform/internal/lease"
14 "github.com/superfly/ltx"
15)
16
17// The seal (§7.3 "seal by version", §7.4 staged): the exact versions of the
18// staging lane's replica/, files/, timers/ and state/ objects the cut wrote, the
19// required final TXID N, and the two facts a recovery reads, in the claim's
20// op journal under hostclaim.JournalSeal. Landing copies these versions and
21// nothing else, so a stale round's later write (a newer version, or a
22// delete marker) is never installed.
23//
24// It is written twice under one op: at the end of `stopped` with the replica
25// and the timers (the archive does not exist yet), and at the end of
26// `staged` with the archive's two objects added. Files is empty until then,
27// and the cut never flips on a seal without them (HasFiles).
28type Seal struct {
29 V int `json:"v"`
30 N uint64 `json:"n"`
31 Replica []SealedObject `json:"replica"`
32 Files []SealedObject `json:"files,omitempty"`
33 Timers []SealedObject `json:"timers,omitempty"`
34 // State is the lane's copy of the instance's build state (settings spec
35 // rev 11 §4.3a), at most one object at the lane's state/ key: the
36 // version the round that wrote this seal copied. Absent when the source
37 // had none; a seal written before this field existed decodes with none.
38 State []SealedObject `json:"state,omitempty"`
39 // SrcCovered is whether the source's own replica of holder.generation
40 // reached the stopped database's position with no pending write (§7.4,
41 // rev 7.11): it decides a rollback's clean return (§7.8).
42 SrcCovered bool `json:"src_covered"`
43 // FromDB is whether the seal was built from the stopped database itself
44 // (§5.1 step 9): false for a source that had no local database.
45 FromDB bool `json:"from_db"`
46 SealedAt time.Time `json:"sealed_at"`
47 // LeaseUntil is the latest moment a request signed by a staging writer
48 // of this lane can still land: the last guarded signature's time plus
49 // SignatureLife plus the skew (ruling G7). Bound returns it.
50 LeaseUntil time.Time `json:"lease_until"`
51}
52
53// SealVersion is the only seal format this code reads or writes.
54const SealVersion = 1
55
56// ErrBadSeal is a seal that pins nothing usable: refused on write and on
57// read, never repaired.
58var ErrBadSeal = errors.New("moves: the seal is not usable")
59
60// Bound is the time after which no staging writer that held the lease can
61// still write: the staging park's evidence_at (Plan 5).
62func (s Seal) Bound() time.Time { return s.LeaseUntil }
63
64// HasFiles reports whether the seal names the archive: `archived` and the
65// flip require it.
66func (s Seal) HasFiles() bool { return len(s.Files) > 0 }
67
68// ReachesN reports whether the sealed replica ends exactly at N: its newest
69// level file's max TXID is N. A set that stops short cannot restore to N,
70// and one that runs past it is not this stopped database's.
71func (s Seal) ReachesN() bool {
72 var newest ltx.TXID
73 for _, o := range s.Replica {
74 pos, err := sealedPosition(o.Key)
75 if err != nil {
76 return false
77 }
78 newest = max(newest, pos.max)
79 }
80 return s.N > 0 && uint64(newest) == s.N
81}
82
83// Validate refuses a seal that pins nothing: another version, no position,
84// no replica, any object without a key or a usable version id (G27: "" and
85// "null" pin nothing), a key named twice, a negative size, a replica object
86// that is not a level file or is below an LTX header (Task 8's
87// NewSealedClient refuses those), no seal time, and no bound or a bound
88// before the seal.
89func (s Seal) Validate() error {
90 if s.V != SealVersion {
91 return fmt.Errorf("%w: version %d", ErrBadSeal, s.V)
92 }
93 if s.N == 0 || len(s.Replica) == 0 {
94 return fmt.Errorf("%w: no position or no replica", ErrBadSeal)
95 }
96 if s.SealedAt.IsZero() || s.LeaseUntil.IsZero() || s.LeaseUntil.Before(s.SealedAt) {
97 return fmt.Errorf("%w: no seal time or no writers' bound", ErrBadSeal)
98 }
99 seen := map[string]bool{}
100 if len(s.State) > 1 || (len(s.State) == 1 && !strings.HasSuffix(s.State[0].Key, BuildStateSuffix)) {
101 return fmt.Errorf("%w: the state class names %d objects, want one at %s", ErrBadSeal, len(s.State), BuildStateSuffix)
102 }
103 for _, class := range [][]SealedObject{s.Replica, s.Files, s.Timers, s.State} {
104 for _, o := range class {
105 if o.Key == "" || !lease.UsableVersionID(o.VersionID) || o.Size < 0 || seen[o.Key] {
106 return fmt.Errorf("%w: object %q version %q size %d", ErrBadSeal, o.Key, o.VersionID, o.Size)
107 }
108 seen[o.Key] = true
109 }
110 }
111 for _, o := range s.Replica {
112 if _, err := sealedPosition(o.Key); err != nil {
113 return fmt.Errorf("%w: %v", ErrBadSeal, err)
114 }
115 if o.Size < ltx.HeaderSize {
116 return fmt.Errorf("%w: replica object %s is below an LTX header", ErrBadSeal, o.Key)
117 }
118 }
119 return nil
120}
121
122func sortObjects(objs []SealedObject) []SealedObject {
123 out := append([]SealedObject(nil), objs...)
124 sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
125 return out
126}
127
128// Encode is the seal as the claim journal carries it: JSON with every
129// object list sorted by key, so one seal always encodes to one string.
130func (s Seal) Encode() string {
131 c := s
132 c.Replica, c.Files, c.Timers, c.State = sortObjects(s.Replica), sortObjects(s.Files), sortObjects(s.Timers), sortObjects(s.State)
133 c.SealedAt, c.LeaseUntil = s.SealedAt.UTC(), s.LeaseUntil.UTC()
134 b, err := json.Marshal(c)
135 if err != nil {
136 // Every field is a plain value: Marshal cannot fail.
137 panic(err)
138 }
139 return string(b)
140}
141
142// DecodeSeal reads a journalled seal strictly (no unknown field, no
143// trailing data) and validates it. Anything else is ErrBadSeal.
144func DecodeSeal(raw string) (Seal, error) {
145 dec := json.NewDecoder(bytes.NewReader([]byte(raw)))
146 dec.DisallowUnknownFields()
147 var s Seal
148 if err := dec.Decode(&s); err != nil {
149 return Seal{}, fmt.Errorf("%w: %v", ErrBadSeal, err)
150 }
151 if _, err := dec.Token(); err != io.EOF {
152 return Seal{}, fmt.Errorf("%w: trailing data", ErrBadSeal)
153 }
154 if err := s.Validate(); err != nil {
155 return Seal{}, err
156 }
157 s.Replica, s.Files, s.Timers, s.State = sortObjects(s.Replica), sortObjects(s.Files), sortObjects(s.Timers), sortObjects(s.State)
158 return s, nil
159}
160