carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package park
2
3import (
4 "context"
5 "fmt"
6 "reflect"
7 "sort"
8 "strings"
9 "time"
10
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/lease"
13)
14
15// View is a moves-owned, completely read snapshot bound to one claim ETag.
16// GenerationOf is a pure classifier for any exact reference, including a
17// version already deleted by a previous attempt. It does no storage I/O.
18// Pinned includes the exact noncurrent versions needed by the installed root.
19// MarkerKnown means its read succeeded; an absent marker may denote @root.
20type View struct {
21 HomeRegion string
22 // ArchivedIncarnations labels proven retired locations independently of
23 // the moves-sweep proof's incarnation. Missing entries never invent labels.
24 ArchivedIncarnations map[string]string
25 RefusedLocations map[string]error
26 ClaimETag, Region, AuthoritativeRegion string
27 MarkerKnown bool
28 MarkerGeneration string
29 Marker Ref
30 Pinned map[Ref]bool
31 FilePins map[Ref]bool
32 GenerationOf func(Ref) (string, bool)
33}
34type Alert struct{ Location, Reason string }
35type Candidate struct{ Location, Incarnation, Generation string }
36type Decision struct {
37 Candidates []Candidate
38 Alerts []Alert
39}
40
41func generation(s string) string {
42 if s == "" {
43 return hostclaim.RootGeneration
44 }
45 return s
46}
47
48func protected(c *hostclaim.Claim, v View, location string) (map[string]bool, error) {
49 live := map[string]bool{}
50 if c.Holder == nil || c.Op.Kind == hostclaim.KindParking {
51 return live, nil
52 }
53 if v.AuthoritativeRegion == "" {
54 return nil, ErrUnsafe
55 }
56 if !v.sameRegion(v.Region, v.AuthoritativeRegion) {
57 return live, nil
58 }
59 if !v.MarkerKnown || v.GenerationOf == nil {
60 return nil, ErrUnsafe
61 }
62 for r, on := range v.Pinned {
63 if on {
64 g, ok := v.GenerationOf(r)
65 if !ok {
66 return nil, ErrUnsafe
67 }
68 if v.FilePins[r] && strings.HasPrefix(r.Key, c.Holder.Location+"/files/") {
69 continue
70 }
71 live[generation(g)] = true
72 }
73 }
74 h := c.Holder
75 if location != "" && location != h.Location && !strings.HasPrefix(location, h.Location+"/") && !strings.HasPrefix(h.Location, location+"/") {
76 return live, nil
77 }
78 current := generation(h.Generation)
79 marker := generation(v.MarkerGeneration)
80 live[current] = true
81 live[marker] = true
82 if current == marker {
83 return live, nil
84 }
85 // Walk the recorded chronological predecessor chain backwards. Warm
86 // admissions do not form cold unfinished links and cannot bridge a gap.
87 want, region := current, v.AuthoritativeRegion
88 for i := len(h.Admissions) - 1; i >= 0; i-- {
89 a := h.Admissions[i]
90 if generation(a.Generation) != want || !v.sameRegion(a.Region, region) {
91 continue
92 }
93 if a.Mode != "cold" {
94 return nil, ErrUnsafe
95 }
96 pred := generation(a.Predecessor)
97 if pred == want {
98 return nil, ErrUnsafe
99 }
100 want = pred
101 region = a.PredecessorRegion
102 if region == "" || v.sameRegion(region, v.AuthoritativeRegion) {
103 if a.Covered {
104 return nil, ErrUnsafe
105 }
106 live[want] = true
107 }
108 if want == marker {
109 if region != "" && !v.sameRegion(region, v.AuthoritativeRegion) {
110 return nil, ErrUnsafe
111 }
112 return live, nil
113 }
114 }
115 return nil, ErrUnsafe
116}
117
118// LiveGenerations is the region-qualified live generation set the engine
119// protects at location: the holder's generation, the marker's, every
120// generation the pinned versions belong to, and the unfinished cold
121// admission chain walked back to the marker in the authoritative region.
122// It is protected itself, exported so a view that needs the live set (the
123// moves files live set) enumerates the same generations rather than a
124// parallel walk. An error is protected's refusal.
125func LiveGenerations(c *hostclaim.Claim, v View, location string) (map[string]bool, error) {
126 if c == nil {
127 return nil, ErrUnsafe
128 }
129 return protected(c, v, location)
130}
131
132// Decide uses a complete namespace version listing supplied by Discover.
133// LegacyIncarnation is a caller-supplied, evidenced archive label only. It is
134// never used to discover locations, and its absence refuses legacy cleanup.
135func Decide(obs hostclaim.Observed, v View, versions []lease.ObjectVersion, legacyIncarnation string) (Decision, error) {
136 var d Decision
137 if obs.Absent || obs.Claim == nil || obs.ETag == "" || v.ClaimETag != obs.ETag || v.Region == "" {
138 return d, ErrUnsafe
139 }
140 c := obs.Claim
141 if err := c.Validate(); err != nil {
142 return d, err
143 }
144 joining := c.Holder == nil && c.Op.Kind == hostclaim.KindParking && c.Op.Intent["park_sweep"] == "v1"
145 if !c.Free() && c.Op.Kind != hostclaim.KindServing && !joining {
146 return d, nil
147 }
148 for _, o := range c.Obligations {
149 if v.sameRegion(o.Region, v.Region) {
150 return d, nil
151 }
152 }
153 live, liveErr := protected(c, v, "")
154 if liveErr != nil && c.Holder != nil {
155 d.Alerts = append(d.Alerts, Alert{c.Holder.Location, "marker does not match the claim or an unfinished admission run"})
156 }
157 groups := map[Candidate]bool{}
158 refused := map[string]bool{}
159 seen := map[Ref]bool{}
160 for _, x := range versions {
161 loc, err := locationOf(c.Host, x.Key)
162 if err != nil || x.VersionID == "" || seen[ref(x)] {
163 return Decision{}, ErrUnsafe
164 }
165 seen[ref(x)] = true
166 if isLock(c.Host, loc, x.Key) {
167 continue
168 }
169 if refusal := v.RefusedLocations[loc]; refusal != nil && !(c.Holder != nil && loc == c.Holder.Location && v.sameRegion(v.Region, v.AuthoritativeRegion)) {
170 if !refused[loc] {
171 d.Alerts = append(d.Alerts, Alert{loc, refusal.Error()})
172 refused[loc] = true
173 }
174 continue
175 }
176 candidate := Candidate{Location: loc}
177 if tail := strings.TrimPrefix(loc, "instances/"+c.Host+"/"); tail != loc {
178 candidate.Incarnation = tail
179 } else {
180 candidate.Incarnation = legacyIncarnation
181 if loc == "instances/"+c.Host && v.ArchivedIncarnations[loc] != "" {
182 candidate.Incarnation = v.ArchivedIncarnations[loc]
183 }
184 }
185 if c.Holder != nil && loc == c.Holder.Location && v.sameRegion(v.Region, v.AuthoritativeRegion) {
186 if liveErr != nil {
187 continue
188 }
189 if x.Key == v.Marker.Key || isBuildState(c.Holder.Location, x.Key) || v.Pinned[ref(x)] {
190 continue
191 }
192 g, ok := v.GenerationOf(ref(x))
193 if !ok {
194 return Decision{}, ErrUnsafe
195 }
196 g = generation(g)
197 if live[g] {
198 continue
199 }
200 candidate.Generation = g
201 candidate.Incarnation = c.Holder.Incarnation
202 }
203 if !hex64(candidate.Incarnation) {
204 return Decision{}, ErrUnsafe
205 }
206 groups[candidate] = true
207 }
208 for c := range groups {
209 d.Candidates = append(d.Candidates, c)
210 }
211 sort.Slice(d.Candidates, func(i, j int) bool {
212 a, b := d.Candidates[i], d.Candidates[j]
213 if a.Location == b.Location {
214 return a.Generation < b.Generation
215 }
216 return a.Location < b.Location
217 })
218 return d, nil
219}
220
221// Discover expands both namespaces across every version page. This is a
222// conservative superset of delimiter discovery: deriving first child prefixes
223// from the complete version listing also discovers marker-only locations.
224func (e Engine) Discover(ctx context.Context, host string) ([]lease.ObjectVersion, error) {
225 var out []lease.ObjectVersion
226 for _, p := range []string{"instances/" + host + "/", "moves/" + host + "/"} {
227 vs, err := e.Store.ListVersions(ctx, e.Bucket, p)
228 if err != nil {
229 return nil, err
230 }
231 out = append(out, vs...)
232 }
233 return out, nil
234}
235
236func parkedGenerations(c *hostclaim.Claim, v View, chores []hostclaim.Obligation, versions []lease.ObjectVersion) ([]string, error) {
237 if c.Holder == nil || len(c.Holder.Admissions) == 0 {
238 return nil, nil
239 }
240 seen := map[string]bool{}
241 for _, o := range chores {
242 if o.Park.Location != c.Holder.Location || o.Park.Incarnation != c.Holder.Incarnation {
243 continue
244 }
245 for _, x := range versions {
246 // The holder location's build state belongs to no generation
247 // (ruling R-PARK), so it names none parked, whichever region
248 // parked it.
249 if !strings.HasPrefix(x.Key, c.Holder.Location+"/") || isLock(c.Host, o.Park.Location, x.Key) || x.Key == v.Marker.Key || isBuildState(c.Holder.Location, x.Key) {
250 continue
251 }
252 inScope, err := scopeContains(c.Host, o.Park.Location, x.Key)
253 if err != nil {
254 return nil, err
255 }
256 if !inScope {
257 continue
258 }
259 if v.GenerationOf == nil {
260 return nil, ErrUnsafe
261 }
262 g, ok := v.GenerationOf(ref(x))
263 if !ok {
264 return nil, ErrUnsafe
265 }
266 g = generation(g)
267 if o.Kind != "park-generation" || o.Generation == g {
268 seen[g] = true
269 }
270 }
271 }
272 var generations []string
273 for g := range seen {
274 generations = append(generations, g)
275 }
276 sort.Strings(generations)
277 return generations, nil
278}
279
280// Sweep queues every decision in one claim CAS; Run is the common executor.
281// EvidenceAt is genuine prior stop/fence evidence for the residue. A sweep
282// uses listing-time evidence for moves/<host> only after VerifyMovesSweep
283// proves that the newest retired move was fenced or released before listing.
284// Other namespaces still require their original stop/fence evidence.
285func (e Engine) Sweep(ctx context.Context, host, legacyIncarnation string, evidenceAt time.Time, requestLimit time.Duration) (Decision, error) {
286 return e.sweep(ctx, hostclaim.Observed{Host: host}, nil, legacyIncarnation, evidenceAt, requestLimit, false)
287}
288
289// SweepObserved binds retirement evidence to the exact claim observation and
290// complete version census from which it was verified. Any claim or namespace
291// change requires new evidence; it is never folded into the old timestamp.
292func (e Engine) SweepObserved(ctx context.Context, expected hostclaim.Observed, versions []lease.ObjectVersion, legacyIncarnation string, evidenceAt time.Time, requestLimit time.Duration) (Decision, error) {
293 if expected.Host == "" || expected.Absent || expected.Claim == nil || expected.ETag == "" {
294 return Decision{}, ErrUnsafe
295 }
296 return e.sweep(ctx, expected, versions, legacyIncarnation, evidenceAt, requestLimit, true)
297}
298
299func sameCensus(a, b []lease.ObjectVersion) bool {
300 if len(a) != len(b) {
301 return false
302 }
303 indexed := make(map[Ref]lease.ObjectVersion, len(a))
304 for _, version := range a {
305 r := ref(version)
306 if r.Key == "" || r.VersionID == "" {
307 return false
308 }
309 if _, duplicate := indexed[r]; duplicate {
310 return false
311 }
312 indexed[r] = version
313 }
314 seen := make(map[Ref]bool, len(b))
315 for _, version := range b {
316 r := ref(version)
317 want, ok := indexed[r]
318 if !ok || seen[r] || !reflect.DeepEqual(want, version) {
319 return false
320 }
321 seen[r] = true
322 }
323 return true
324}
325
326func (e Engine) sweep(ctx context.Context, expected hostclaim.Observed, expectedVersions []lease.ObjectVersion, legacyIncarnation string, evidenceAt time.Time, requestLimit time.Duration, bound bool) (Decision, error) {
327 var empty Decision
328 if e.View == nil || e.NewID == nil || e.Authorize == nil || e.Now == nil {
329 return empty, ErrUnsafe
330 }
331 host := expected.Host
332 obs, err := hostclaim.Read(ctx, e.Claims, host)
333 if err != nil {
334 return empty, err
335 }
336 if bound && (obs.ETag != expected.ETag || !reflect.DeepEqual(obs.Claim, expected.Claim)) {
337 return empty, ErrChanged
338 }
339 if obs.Absent || obs.Claim == nil {
340 return empty, ErrUnsafe
341 }
342 v, err := e.View(ctx, obs)
343 if err != nil {
344 return empty, err
345 }
346 home, err := BoundHome(e.HomeRegion, obs.Claim.Op.Intent["home_region"])
347 if err != nil {
348 return empty, err
349 }
350 v.HomeRegion = home
351 listingStarted := e.Now()
352 vs, err := e.Discover(ctx, host)
353 listedAt := e.Now()
354 if err != nil {
355 return empty, err
356 }
357 if bound && !sameCensus(vs, expectedVersions) {
358 return empty, ErrChanged
359 }
360 d, err := Decide(obs, v, vs, legacyIncarnation)
361 if err != nil || len(d.Candidates) == 0 {
362 return d, err
363 }
364 var movesEvidence time.Time
365 candidates := d.Candidates[:0]
366 for _, candidate := range d.Candidates {
367 if candidate.Location == "moves/"+host {
368 if e.VerifyMovesSweep == nil || listedAt.Before(listingStarted) {
369 continue
370 }
371 detached, copyErr := moveSweepObservation(obs)
372 if copyErr != nil {
373 return d, copyErr
374 }
375 proof, verifyErr := e.VerifyMovesSweep(ctx, detached, append([]lease.ObjectVersion(nil), vs...))
376 if verifyErr != nil || proof.Check(obs, candidate.Incarnation, listingStarted) != nil {
377 continue
378 }
379 movesEvidence = listedAt
380 }
381 candidates = append(candidates, candidate)
382 }
383 d.Candidates = candidates
384 if len(d.Candidates) == 0 {
385 return d, nil
386 }
387 var op string
388 if obs.Claim.Holder != nil {
389 op = fmt.Sprintf("session:%d", obs.Claim.Holder.Session)
390 } else if obs.Claim.Op.Kind == hostclaim.KindParking {
391 op = obs.Claim.Op.ID
392 } else {
393 op = e.NewID()
394 if !token(op) {
395 return d, ErrUnsafe
396 }
397 }
398 chores := make([]hostclaim.Obligation, 0, len(d.Candidates))
399 for _, x := range d.Candidates {
400 boundAt := evidenceAt
401 if x.Location == "moves/"+host {
402 boundAt = movesEvidence
403 }
404 o := hostclaim.Obligation{ID: e.NewID(), Op: op, Kind: "park-replica", Region: e.Region, Generation: x.Generation, Retirement: e.NewID(), Owner: "region:" + e.Region, Park: &hostclaim.ParkScope{Location: x.Location, Incarnation: x.Incarnation, EvidenceAt: boundAt, RequestLimit: requestLimit}}
405 if x.Generation != "" {
406 o.Kind = "park-generation"
407 }
408 if !token(o.ID) || validateScope(host, o) != nil {
409 return d, ErrUnsafe
410 }
411 if err := e.Authorize(ctx, obs, o); err != nil {
412 return d, err
413 }
414 chores = append(chores, o)
415 }
416 generations, err := parkedGenerations(obs.Claim, v, chores, vs)
417 if err != nil {
418 return d, err
419 }
420 if bound {
421 fresh, discoverErr := e.Discover(ctx, host)
422 if discoverErr != nil {
423 return d, discoverErr
424 }
425 if !sameCensus(fresh, expectedVersions) {
426 return d, ErrChanged
427 }
428 }
429 _, err = hostclaim.Update(ctx, e.Claims, obs, func(c *hostclaim.Claim) error {
430 if c.Holder != nil {
431 if err := hostclaim.CoverParkedPredecessors(obs.Claim.Op.ID, obs.Claim.Op.Phase, e.Region, generations)(c); err != nil {
432 return err
433 }
434 }
435 if c.Free() {
436 c.Op = hostclaim.Op{Kind: hostclaim.KindParking, ID: op, Intent: map[string]string{"park_sweep": "v1"}}
437 }
438 c.Obligations = append(c.Obligations, chores...)
439 return nil
440 })
441 if err != nil {
442 return d, err
443 }
444 for _, o := range chores {
445 if err := e.Run(ctx, Work{host, o.ID, o.Op}); err != nil {
446 return d, fmt.Errorf("sweep %s: %w", o.ID, err)
447 }
448 }
449 return d, nil
450}
451
452// ResidueLocations uses the same namespace and lock classifier as Decide.
453func ResidueLocations(host string, versions []lease.ObjectVersion) ([]string, error) {
454 seen := map[string]bool{}
455 for _, v := range versions {
456 loc, err := locationOf(host, v.Key)
457 if err != nil || v.VersionID == "" {
458 return nil, ErrUnsafe
459 }
460 if !isLock(host, loc, v.Key) {
461 seen[loc] = true
462 }
463 }
464 out := make([]string, 0, len(seen))
465 for loc := range seen {
466 out = append(out, loc)
467 }
468 sort.Strings(out)
469 return out, nil
470}
471