carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "slices"
8 "time"
9
10 "github.com/carlosframework/platform/internal/release"
11 "github.com/carlosframework/platform/internal/seeds"
12)
13
14// SeedSource is a record's seed (seeded-instances spec §3.2). The run series
15// declared its Kind and State; those keep their json tags.
16type SeedSource struct {
17 ID string `json:"id"` // random, minted with the seed; never reused
18 Kind string `json:"kind"` // seeds.KindReplica | seeds.KindImport
19 From string `json:"from,omitempty"` // replica: the source host
20 TXID string `json:"txid,omitempty"` // replica: the TXID actually restored
21 SHA256 string `json:"sha256"` // of snapshot.db
22 Bytes int64 `json:"bytes"`
23 State string `json:"state"` // seeds.State*
24 Reason string `json:"reason,omitempty"` // set with refused, and with aborted when the abort names why
25 By string `json:"by"` // the caller
26 At time.Time `json:"at"`
27 // import only:
28 Replaces string `json:"replaces,omitempty"` // the addr the swap replaces
29 VerifyHash string `json:"verify_hash,omitempty"` // sha256 of the verify token (§5.4)
30}
31
32// ErrSeedProtected is an ordinary record writer trying to change Seed.
33var ErrSeedProtected = errors.New("provision: the seed changes only through a seed transition")
34
35// The two reasons a seed refuses a move (spec §5.8). Plan 8 gives each its
36// sentence.
37const (
38 SeedMoveUnplaced = "unplaced"
39 SeedMoveUncommitted = "uncommitted"
40)
41
42// Import reports whether the seed is an import.
43func (s *SeedSource) Import() bool { return s != nil && s.Kind == seeds.KindImport }
44
45// Abandoned reports an aborted seed, before or after the box rolled it back.
46func (s *SeedSource) Abandoned() bool {
47 return s != nil && (s.State == seeds.StateAborted || s.State == seeds.StateRolledBack)
48}
49
50// Unfinished reports a seed that still needs a door, a box or the console
51// to act before it is done: the states claim activation refuses over
52// (hostops, merge review S2), because none of those acts on a claim
53// console or a claim-armed box.
54//
55// - reserving, awaiting-upload, uploaded: a door is still writing it;
56// - ready: a --from seed waits for its first wake to place it, and an
57// import for its swap;
58// - an import at swapped or placed: not committed;
59// - refused: an import the box would not swap, which only the delete
60// door's import rule can remove;
61// - aborted, rolled-back: its rollback or its finish is still owed.
62//
63// Finished, and the only two: a --from seed at placed and an import at
64// committed. Any other kind and state is unfinished, including a state or a
65// kind this build does not know and a pairing no door writes (a committed
66// --from seed).
67func (s *SeedSource) Unfinished() bool {
68 if s == nil {
69 return false
70 }
71 finished := (s.Kind == seeds.KindReplica && s.State == seeds.StatePlaced) ||
72 (s.Kind == seeds.KindImport && s.State == seeds.StateCommitted)
73 return !finished
74}
75
76// BlocksRun is run admission's predicate (run spec §5.1 step 9, codex
77// R2-2): an import that is not committed refuses carlos run.
78func (s *SeedSource) BlocksRun() bool { return s.Import() && s.State != seeds.StateCommitted }
79
80// MoveBlock is the move doors' and workers' predicate (spec §5.8): a
81// --from seed moves only once placed, an import only once committed.
82func (s *SeedSource) MoveBlock() string {
83 switch {
84 case s == nil:
85 return ""
86 case s.Import():
87 if s.State != seeds.StateCommitted {
88 return SeedMoveUncommitted
89 }
90 case s.State != seeds.StatePlaced:
91 return SeedMoveUnplaced
92 }
93 return ""
94}
95
96// cloneSeed is a deep copy: SeedSource holds no pointers, so a struct copy
97// is one.
98func cloneSeed(s *SeedSource) *SeedSource {
99 if s == nil {
100 return nil
101 }
102 c := *s
103 return &c
104}
105
106func seedEqual(a, b *SeedSource) bool {
107 if a == nil || b == nil {
108 return a == nil && b == nil
109 }
110 x, y := *a, *b
111 if !x.At.Equal(y.At) {
112 return false
113 }
114 x.At, y.At = time.Time{}, time.Time{}
115 return x == y
116}
117
118// ErrSeedConflict is a seed transition the record's seed does not allow:
119// another seed's id, the wrong kind, or the wrong state. It is the answer,
120// not a race (a race is retried once by the scaffold, which re-reads and
121// re-applies the transition).
122var ErrSeedConflict = errors.New("provision: seed transition refused")
123
124// SetRecordSeed applies one seed transition to the record by CAS on its
125// etag, retrying once on a lost race (updateRecord's scaffold). It is the
126// only writer of Record.Seed after creation.
127func SetRecordSeed(ctx context.Context, s release.Store, acct, app, host string, mut func(*Record) error) (*Record, error) {
128 return updateRecordSeeding(ctx, s, acct, app, host, mut, true)
129}
130
131// seedIn checks one row of the transition table: the record's seed must be
132// id, of kind (any kind when ""), in one of from.
133func seedIn(rec *Record, id, kind string, from ...string) error {
134 switch {
135 case rec.Seed == nil || rec.Seed.ID != id:
136 return fmt.Errorf("%w: the record's seed is not %s", ErrSeedConflict, id)
137 case kind != "" && rec.Seed.Kind != kind:
138 return fmt.Errorf("%w: seed %s is a %s seed", ErrSeedConflict, id, rec.Seed.Kind)
139 case !slices.Contains(from, rec.Seed.State):
140 return fmt.Errorf("%w: seed %s is %s", ErrSeedConflict, id, rec.Seed.State)
141 }
142 return nil
143}
144
145// seedStep is a transition that is one row of the table: checked by
146// seedIn, then the seed moves to to and also (when set) records the rest.
147func seedStep(id, kind string, from []string, to string, also func(*SeedSource)) func(*Record) error {
148 return func(rec *Record) error {
149 if err := seedIn(rec, id, kind, from...); err != nil {
150 return err
151 }
152 rec.Seed.State = to
153 if also != nil {
154 also(rec.Seed)
155 }
156 return nil
157 }
158}
159
160// SeedPublishReplica is --from's publish (spec §4 step 7.3): reserving to
161// ready, with what the restore actually produced.
162func SeedPublishReplica(id, txid, sha string, bytes int64) func(*Record) error {
163 return seedStep(id, seeds.KindReplica, []string{seeds.StateReserving}, seeds.StateReady, func(s *SeedSource) {
164 s.TXID, s.SHA256, s.Bytes = txid, sha, bytes
165 })
166}
167
168// SeedUploaded is the upload's last step (spec §5.2 step 5).
169func SeedUploaded(id string) func(*Record) error {
170 return seedStep(id, seeds.KindImport, []string{seeds.StateAwaitingUpload}, seeds.StateUploaded, nil)
171}
172
173// SeedStopped is the app saying its own process is down (spec §5.3).
174func SeedStopped(id string) func(*Record) error {
175 return seedStep(id, seeds.KindImport, []string{seeds.StateUploaded}, seeds.StateReady, nil)
176}
177
178// SeedSwapped is the box's CAS after its swap transaction (spec §5.4).
179func SeedSwapped(id string) func(*Record) error {
180 return seedStep(id, seeds.KindImport, []string{seeds.StateReady}, seeds.StateSwapped, nil)
181}
182
183// SeedPlace is the box's CAS after the first upload of the host's own
184// replica (spec §3.4 step 5): a --from seed from ready, an import from
185// swapped. An import is never placed from ready: that would skip its swap.
186func SeedPlace(id string) func(*Record) error {
187 return func(rec *Record) error {
188 from := seeds.StateReady
189 if rec.Seed.Import() {
190 from = seeds.StateSwapped
191 }
192 return seedStep(id, "", []string{from}, seeds.StatePlaced, nil)(rec)
193 }
194}
195
196// SeedCommit is the app's verdict that the instance serves (spec §5.4):
197// refused before placed, because nothing has been verified yet.
198func SeedCommit(id string) func(*Record) error {
199 return seedStep(id, seeds.KindImport, []string{seeds.StatePlaced}, seeds.StateCommitted, nil)
200}
201
202// SeedRefuse is the box declining the swap, naming why (spec §5.4).
203func SeedRefuse(id, reason string) func(*Record) error {
204 return seedStep(id, seeds.KindImport, []string{seeds.StateReady}, seeds.StateRefused, func(s *SeedSource) {
205 s.Reason = reason
206 })
207}
208
209// SeedAbort abandons a seed (spec §5.6, §4 step 7): an import from any
210// state before committed, a --from seed while reserving or ready.
211//
212// An import's abort is also refused while a run is open on the instance
213// (spec §5.6, run spec §5.1), although run admission already refuses an
214// uncommitted import: a RunOpenError, so errors.Is(err, ErrRunOpen). Both
215// specs scope that rule to the import; a --from seed's abort is its door's
216// own failure path, and a run admitted on the reserved record must not
217// leave it stuck reserving.
218func SeedAbort(id string) func(*Record) error { return SeedAbortBecause(id, "") }
219
220// SeedAbortBecause is SeedAbort recording why in Reason: the sentence the
221// --from door answers its caller with when its copy fails after the record
222// is reserved (spec §4, Plan 5 ruling R2). An empty reason keeps whatever
223// Reason the seed already had (an import refused before its abort).
224func SeedAbortBecause(id, reason string) func(*Record) error {
225 return func(rec *Record) error {
226 from := []string{seeds.StateReserving, seeds.StateReady}
227 if rec.Seed.Import() {
228 from = []string{seeds.StateAwaitingUpload, seeds.StateUploaded, seeds.StateReady,
229 seeds.StateSwapped, seeds.StatePlaced, seeds.StateRefused}
230 }
231 if err := seedIn(rec, id, "", from...); err != nil {
232 return err
233 }
234 if rec.Seed.Import() && rec.ActiveRun.Open() {
235 return &RunOpenError{Run: *rec.ActiveRun}
236 }
237 rec.Seed.State = seeds.StateAborted
238 if reason != "" {
239 rec.Seed.Reason = reason
240 }
241 return nil
242 }
243}
244
245// SeedAbortUploadBecause is the import upload's own failure (spec §5.2,
246// Plan 6 Task 2 fix round 1): SeedAbortBecause, but only while the seed
247// still waits for its upload. An upload whose import has moved on (another
248// upload landed it, or it was aborted or rolled back) gets ErrSeedConflict
249// and aborts nothing.
250func SeedAbortUploadBecause(id, reason string) func(*Record) error {
251 abort := SeedAbortBecause(id, reason)
252 return func(rec *Record) error {
253 if err := seedIn(rec, id, seeds.KindImport, seeds.StateAwaitingUpload); err != nil {
254 return err
255 }
256 return abort(rec)
257 }
258}
259
260// SeedAbortStale is the console's seeds sweep abandoning a seed it judged
261// stale (spec §3.1, §5.6): SeedAbort, but only while the record's seed is
262// exactly what the sweep read, the same id in the same state since the same
263// At. The sweep judged staleness from that state and At, so a seed that
264// moved on in any way (published, uploaded, stopped, aborted by its own
265// door) is refused with ErrSeedConflict, and SetRecordSeed's retry after a
266// lost race never aborts a seed the sweep did not judge. Every state
267// SeedAbort refuses stays refused.
268func SeedAbortStale(id, state string, at time.Time) func(*Record) error {
269 abort := SeedAbort(id)
270 return func(rec *Record) error {
271 if err := seedIn(rec, id, "", state); err != nil {
272 return err
273 }
274 if !rec.Seed.At.Equal(at) {
275 return fmt.Errorf("%w: seed %s was stamped again (at %s, read at %s)", ErrSeedConflict, id,
276 rec.Seed.At.UTC().Format(time.RFC3339Nano), at.UTC().Format(time.RFC3339Nano))
277 }
278 return abort(rec)
279 }
280}
281
282// SeedRolledBack is the box's last word on an aborted seed (spec §5.6, and
283// §3.2's no-journal path). The console finishes from here.
284func SeedRolledBack(id string) func(*Record) error {
285 return seedStep(id, "", []string{seeds.StateAborted}, seeds.StateRolledBack, nil)
286}
287
288// SeedRolledBackBeforeSwap is the delete door's shortcut for an import no
289// box has swapped (spec §5.6, codex R2-7).
290func SeedRolledBackBeforeSwap(id string) func(*Record) error {
291 return seedStep(id, seeds.KindImport, []string{seeds.StateAwaitingUpload, seeds.StateUploaded, seeds.StateRefused},
292 seeds.StateRolledBack, nil)
293}
294
295// SeedColumn is the registry column a record's row carries (seed_source):
296// "" for a seedless record.
297func SeedColumn(rec Record) string {
298 if rec.Seed == nil {
299 return ""
300 }
301 return seeds.RowSeed{ID: rec.Seed.ID, Kind: rec.Seed.Kind, SHA256: rec.Seed.SHA256,
302 Bytes: rec.Seed.Bytes, State: rec.Seed.State}.Encode()
303}
304
305// SeedMoveSentence is a move refusal's sentence (spec §5.8), "" for any
306// block but the two.
307func SeedMoveSentence(block string) string {
308 switch block {
309 case SeedMoveUnplaced:
310 return "wake the instance once before moving it"
311 case SeedMoveUncommitted:
312 return "commit or abort the import first"
313 }
314 return ""
315}
316
317// SeedMoveError is a move the record's seed refuses: an ErrMoveConflict
318// whose sentence is SeedMoveSentence(Block). It is typed so a door that
319// meets it under a CAS (carlos apps place) can answer the sentence alone.
320type SeedMoveError struct{ Block string }
321
322func (e *SeedMoveError) Error() string {
323 return ErrMoveConflict.Error() + ": " + SeedMoveSentence(e.Block)
324}
325
326func (e *SeedMoveError) Unwrap() error { return ErrMoveConflict }
327
328// seedMoveRefusal is the seed rule of every write that moves a record (the
329// two move-start transitions and SetRecordRegion), under the record CAS.
330func seedMoveRefusal(rec *Record) error {
331 if b := rec.Seed.MoveBlock(); b != "" {
332 return &SeedMoveError{Block: b}
333 }
334 return nil
335}
336