carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
4 "fmt"
5 "github.com/carlosframework/platform/internal/regionname"
6 "reflect"
7 "strconv"
8)
9
10const RootGeneration = "@root"
11
12type Admission struct {
13 ObservedOpID, ObservedPhase string // Empty values are compared literally.
14 ObservedSession uint64
15 ObservedGeneration string
16 Box string
17 Mode string
18 NewGeneration string
19 PredecessorCertified bool
20 RemoteMaxTXID uint64
21 KeepStamp bool
22 Coverage string
23 Region string
24 PredecessorRegion string
25 AdditionalWritesLost []WritesLost
26 LossID string
27 lossVerified bool
28 fenceVerified bool
29}
30
31// AdmitWriter is §6.4a's writer admission CAS within the current operation.
32// Warm admission keeps the installation; cold admission installs a fresh
33// generation and stamp. The caller checks the evidence required for warm mode
34// and supplies PredecessorCertified.
35func AdmitWriter(in Admission) func(*Claim) error {
36 return func(c *Claim) error {
37 if in.Region == "" {
38 return fmt.Errorf("%w: an admission names its region", ErrWrongState)
39 }
40 if c.Holder == nil || c.Op.Exec == "" || c.Op.Exec != in.Box {
41 return fmt.Errorf("%w: only the executor may admit a writer", ErrWrongState)
42 }
43 if c.Op.ID != in.ObservedOpID || c.Op.Phase != in.ObservedPhase {
44 return fmt.Errorf("%w: operation or phase moved since it was observed", ErrWrongState)
45 }
46 switch c.Op.Kind {
47 case KindCreating, KindDeleting, KindParking, KindPurging, KindPlacing:
48 return fmt.Errorf("%w: op kind %q cannot admit a writer", ErrWrongState, c.Op.Kind)
49 case KindRelocating:
50 if err := relocatingAdmission(c, in); err != nil {
51 return err
52 }
53 if err := relocationProbeOpening(c, in); err != nil {
54 return err
55 }
56 }
57 id, err := OperationLossID(c)
58 if err != nil {
59 return err
60 }
61 if id != "" && (in.Coverage != "" || (!OperationLossConsumed(c, id) && (!in.lossVerified || in.LossID != id))) {
62 return ErrLossRecord
63 }
64
65 if c.Op.Kind == KindRestoring && c.Op.Journal["fence."+c.Op.Intent["target"]] != "" && in.Coverage != "" {
66 return ErrFence
67 }
68 if _, pending := PendingMoveFenceLoss(c); pending && !in.fenceVerified {
69 return ErrFence
70 }
71 h := c.Holder
72 if in.LossID != "" && !in.lossVerified {
73 return fmt.Errorf("%w: declaration-bound admission requires verified durable declaration", ErrWrongState)
74 }
75 if in.LossID != "" {
76 for _, prior := range h.Admissions {
77 if prior.LossID != in.LossID {
78 continue
79 }
80 if prior.Box == in.Box && prior.Mode == in.Mode && prior.Generation == in.NewGeneration &&
81 reflect.DeepEqual(prior.AdditionalWritesLost, in.AdditionalWritesLost) {
82 return ErrLossAlreadyApplied
83 }
84 return fmt.Errorf("%w: declaration id already used by another admission", ErrWrongState)
85 }
86 }
87 if c.Op.Kind == KindServing && h.WakeRetry != nil {
88 r := h.WakeRetry
89 if r.State != "attempting" || r.Attempt == nil || r.Attempt.Session != 0 || r.Attempt.Box != in.Box {
90 return fmt.Errorf("%w: serving writer admission has no matching retry reservation", ErrWrongState)
91 }
92 }
93 if h.Session != in.ObservedSession || h.Generation != in.ObservedGeneration {
94 return fmt.Errorf("%w: writer session or generation moved since it was observed", ErrWrongState)
95 }
96 if in.KeepStamp && (in.Mode != "cold" || h.Stamp <= h.Session) {
97 return fmt.Errorf("%w: keeping a stamp requires cold admission before its first writer", ErrWrongState)
98 }
99 switch in.Mode {
100 case "warm":
101 if !in.PredecessorCertified || in.NewGeneration != "" {
102 return fmt.Errorf("%w: a warm admission requires a certificate and keeps the generation", ErrWrongState)
103 }
104 case "cold":
105 if in.NewGeneration == "" || in.NewGeneration == h.Generation {
106 return fmt.Errorf("%w: a cold recovery needs a new generation", ErrWrongState)
107 }
108 default:
109 return fmt.Errorf("%w: unknown writer admission mode %q", ErrWrongState, in.Mode)
110 }
111
112 switch in.Coverage {
113 case "", "sealed", "clean-return":
114 default:
115 return ErrWrongState
116 }
117 predecessor := AdmissionRecord{Session: h.Session, Box: h.WriterBox, Generation: h.Generation, Region: in.PredecessorRegion}
118 if len(h.Admissions) > 0 {
119 predecessor = h.Admissions[len(h.Admissions)-1]
120 }
121 if in.Mode == "warm" && !regionname.SameRegion(c.Op.Intent["home_region"], in.Region, predecessor.Region) {
122 return fmt.Errorf("%w: a warm admission stays in its predecessor's region", ErrWrongState)
123 }
124 type writerSession struct {
125 box string
126 session uint64
127 }
128 seen := make(map[writerSession]bool, len(in.AdditionalWritesLost)+1)
129 if in.Mode == "cold" && !in.PredecessorCertified && in.Coverage == "" {
130 seen[writerSession{predecessor.Box, predecessor.Session}] = true
131 }
132 for _, loss := range in.AdditionalWritesLost {
133 writer := writerSession{loss.Box, loss.Session}
134 if loss.Box == "" || (loss.Session == 0) != (loss.Generation == "") || seen[writer] {
135 return ErrWrongState
136 }
137 seen[writer] = true
138 }
139 entry := AdmissionRecord{
140 Session: c.Gen + 1, Box: in.Box, Generation: h.Generation,
141 LossID: in.LossID,
142 Predecessor: predecessor.Generation, Mode: in.Mode, Coverage: in.Coverage,
143 Region: in.Region, PredecessorRegion: predecessor.Region,
144 Covered: in.Mode == "warm",
145 AdditionalWritesLost: append([]WritesLost(nil), in.AdditionalWritesLost...),
146 }
147 for _, ob := range c.Obligations {
148 // Only a park of the holder's whole location retires the
149 // predecessor; a scoped park of another location (an old
150 // incarnation's residue, a staging prefix) does not.
151 if ob.Kind == "park-replica" && regionname.SameRegion(c.Op.Intent["home_region"], ob.Region, entry.PredecessorRegion) &&
152 (ob.Park == nil || ob.Park.Location == h.Location) {
153 entry.Covered = true
154 break
155 }
156 }
157 if in.Mode == "cold" {
158 entry.Generation = in.NewGeneration
159 if !in.KeepStamp {
160 h.Stamp = entry.Session
161 }
162 // A predecessor with no writer (a created incarnation's first
163 // admission: writer_box "") acknowledged nothing, so there is
164 // nothing to declare; every declaration names a box (§6.4a).
165 if !in.PredecessorCertified && in.Coverage == "" && predecessor.Box != "" {
166 entry.WritesLost = &WritesLost{Box: predecessor.Box, Generation: predecessor.Generation, Session: predecessor.Session, RemoteMaxTXID: in.RemoteMaxTXID}
167 }
168 }
169 if err := pendingOrigin(c, entry); err != nil {
170 return err
171 }
172 h.Admissions = append(h.Admissions, entry)
173 for len(h.Admissions) > 8 && h.Admissions[0].Covered && admissionLossArchived(h.Admissions[0]) {
174 h.Admissions = h.Admissions[1:]
175 }
176 h.Session, h.Generation, h.WriterBox = entry.Session, entry.Generation, in.Box
177 if in.lossVerified && id != "" {
178 setJournal(c, "loss_admitted."+id, strconv.FormatUint(entry.Session, 10))
179 }
180 return nil
181 }
182}
183
184func PublishGeneration(observedOpID, observedPhase string, observedSession uint64, publishedGeneration, region string, newID func() string) func(*Claim) error {
185 return func(c *Claim) error {
186 if c.Holder == nil || c.Op.ID != observedOpID || c.Op.Phase != observedPhase ||
187 c.Holder.Session != observedSession || c.Holder.Generation != publishedGeneration || region == "" {
188 return ErrWrongState
189 }
190 switch c.Op.Kind {
191 case KindCreating, KindDeleting, KindParking, KindPurging, KindPlacing:
192 return ErrWrongState
193 case KindRelocating:
194 // §6.4a item 5: recognizing the kind grants nothing; only a
195 // phase where a relocating writer session lives may publish.
196 if !SessionPhase(KindRelocating, c.Op.Phase) {
197 return ErrWrongState
198 }
199 }
200
201 h := c.Holder
202 admitted := false
203 for _, record := range h.Admissions {
204 if record.Generation == publishedGeneration {
205 admitted = true
206 break
207 }
208 }
209 if !admitted {
210 return ErrWrongState
211 }
212 activeGeneration := publishedGeneration
213 if activeGeneration == "" {
214 activeGeneration = RootGeneration
215 }
216 op := c.Op.ID
217 if op == "" {
218 op = fmt.Sprintf("session:%d", h.Session)
219 }
220 for i := range h.Admissions {
221 record := &h.Admissions[i]
222 if record.Covered {
223 continue
224 }
225 predecessor := record.Predecessor
226 if predecessor == "" {
227 predecessor = RootGeneration
228 }
229 if predecessor == activeGeneration {
230 continue
231 }
232 if record.PredecessorRegion == "" {
233 record.Covered = true
234 continue
235 }
236 if !regionname.SameRegion(c.Op.Intent["home_region"], record.PredecessorRegion, region) {
237 continue
238 }
239 queued := false
240 for _, ob := range c.Obligations {
241 if ob.Kind == "park-generation" && regionname.SameRegion(c.Op.Intent["home_region"], ob.Region, region) && ob.Generation == predecessor {
242 queued = true
243 break
244 }
245 }
246 if !queued {
247 c.Obligations = append(c.Obligations, Obligation{
248 ID: newID(), Op: op, Kind: "park-generation", Region: region,
249 Generation: predecessor, Retirement: newID(), Owner: "region:" + region,
250 })
251 }
252 record.Covered = true
253 }
254 return nil
255 }
256}
257
258// CoverRegionPredecessors covers predecessors retired by a region's park of
259// the whole location. Callers compose it into the CAS that queues that park
260// (a committed move's source cleanup). It queues no chores itself.
261func CoverRegionPredecessors(observedOpID, observedPhase string, region string) func(*Claim) error {
262 return func(c *Claim) error {
263 if c.Holder == nil || c.Op.ID != observedOpID || c.Op.Phase != observedPhase || region == "" {
264 return ErrWrongState
265 }
266 for i := range c.Holder.Admissions {
267 record := &c.Holder.Admissions[i]
268 if !record.Covered && regionname.SameRegion(c.Op.Intent["home_region"], record.PredecessorRegion, region) {
269 record.Covered = true
270 }
271 }
272 return nil
273 }
274}
275
276// CoverParkedPredecessors covers only the predecessors retired by a sweep.
277// Callers compose it into the CAS that queues the sweep and the CAS that
278// acknowledges it. It queues no chores itself.
279func CoverParkedPredecessors(observedOpID, observedPhase string, region string, generations []string) func(*Claim) error {
280 parked := make(map[string]bool, len(generations))
281 for _, generation := range generations {
282 parked[generation] = true
283 }
284 return func(c *Claim) error {
285 if c.Holder == nil || c.Op.ID != observedOpID || c.Op.Phase != observedPhase || region == "" {
286 return ErrWrongState
287 }
288 for i := range c.Holder.Admissions {
289 record := &c.Holder.Admissions[i]
290 predecessor := record.Predecessor
291 if predecessor == "" {
292 predecessor = RootGeneration
293 }
294 if !record.Covered && regionname.SameRegion(c.Op.Intent["home_region"], record.PredecessorRegion, region) && parked[predecessor] {
295 record.Covered = true
296 }
297 }
298 return nil
299 }
300}
301
302// relocatingAdmission is §4's relocating phase table for a writer admission,
303// as claim Plan 11 accepted it (R1): only op.exec's cold probe, in probing,
304// of an operation that is not stuck. Every other phase admits no writer.
305// The source's admission in starting or releasing needs its admission-fence
306// authority, which no claim code supplies yet, so it stays refused; this
307// check only ever adds refusals to AdmitWriter's own.
308func relocatingAdmission(c *Claim, in Admission) error {
309 if c.Op.Journal["stuck"] != "" {
310 return fmt.Errorf("%w: relocation is stuck", ErrWrongState)
311 }
312 if c.Op.Phase != PhaseRelocatingProbing {
313 return fmt.Errorf("%w: relocating/%s admits no writer", ErrWrongState, c.Op.Phase)
314 }
315 if in.Mode != "cold" || in.Box != c.Op.Exec || in.Coverage != "" {
316 return fmt.Errorf("%w: a relocation probe is op.exec's cold admission", ErrWrongState)
317 }
318 return nil
319}
320
321// relocationProbeOpening is region HA's prepared generation P on top of
322// relocatingAdmission (§5.8, L9, rev 12), and it only adds refusals: the
323// probe admission that opens P installs P itself, and P is opened at most
324// once. The opening is journaled with the admission, so it outlives the
325// admission list's trim. A certified opening must be backed by this
326// operation's journaled release, naming the exact predecessor session, and
327// a pin reaching that release's final position: that is the only way a
328// relocation probe certifies its predecessor (claim Plan 11 grants no
329// relocation coverage of its own, so the release is expressed as the
330// generic PredecessorCertified, never as a new coverage value).
331func relocationProbeOpening(c *Claim, in Admission) error {
332 p, opened := c.Op.Journal["prepared"], preparedOpened(c)
333 if p == "" {
334 // No prepared generation journaled: main's cold probe as claim Plan
335 // 11 accepted it (region HA's ToProbing always journals P first).
336 // With no P there is no release to back a certification, so a
337 // certified relocating probe is refused.
338 if in.PredecessorCertified {
339 return fmt.Errorf("%w: a relocating probe certifies only P's opening", ErrWrongState)
340 }
341 return nil
342 }
343 if (!opened && in.NewGeneration != p) || (opened && in.NewGeneration == p) {
344 return fmt.Errorf("%w: the first probe opens P, and only it", ErrWrongState)
345 }
346 if opened {
347 if in.PredecessorCertified {
348 // The release certifies only the session P's opening succeeds.
349 return fmt.Errorf("%w: only P's opening certifies its predecessor", ErrWrongState)
350 }
351 return nil
352 }
353 if in.PredecessorCertified {
354 var r Release
355 var pin RecoveryPin
356 if !readJournal(c.Op, "release", &r) || !readJournal(c.Op, "pin", &pin) || pin.MaxTXID < r.FinalTXID || pin.FilesSeq < r.ManifestSeq {
357 return fmt.Errorf("%w: a certified opening needs this operation's release and a pin reaching it", ErrWrongState)
358 }
359 h := c.Holder
360 predecessor := AdmissionRecord{Session: h.Session, Box: h.WriterBox, Generation: h.Generation}
361 if len(h.Admissions) > 0 {
362 predecessor = h.Admissions[len(h.Admissions)-1]
363 }
364 if predecessor.Box != r.ReleasedBy || predecessor.Session != r.Session || predecessor.Generation != r.Generation {
365 return fmt.Errorf("%w: a certified opening names another predecessor", ErrWrongState)
366 }
367 }
368 ensureJournal(c)
369 c.Op.Journal["prepared_opened"] = p
370 return nil
371}
372