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
3// The moves retirement evidence provider (moves Plan 5 Task 5; rulings K10,
4// M7 and M8 of the Plan 5 ledger). The claim's regional worker
5// (hostbox.RegionalWorker) sweeps instances/<host>/ and moves/<host>/ at rest.
6// Before it sweeps it asks this provider twice:
7//
8// - VerifyRetirement, once per sweep, for the incarnation label of every
9// candidate outside an incarnation directory and for the evidence time of
10// every instances/<host> candidate. Decide labels the moves/<host>
11// candidate with the same incarnation (park/sweep.go), and the engine's
12// proof Check requires the proof to name it, so both answers come from one
13// proof.
14// - VerifyMovesSweep, per moves/<host> candidate (park/move_sweep.go). The
15// engine then stamps the listing's end as that chore's evidence, which is
16// valid only with the precondition proof (K10): the claim shows no move
17// op, and the newest retired move's close is earlier than the listing.
18//
19// The proof comes from the record's LastMove (M8, fix round 1 ruling 2): the
20// durable move id, outcome and close time, bound to the holder's
21// incarnation. It holds only when:
22//
23// - the claim is serving, so no move op is live on the host;
24// - the record is the holder's own and carries no live move;
25// - LastMove names a move closed at a real time before this clock;
26// - every moves/<host>/<op>/ object is that move's own, or was written
27// before its close.
28//
29// The claim's released_move journal is not required: any later serving op
30// replaces it (CompleteMetadataPlacement, for one). With M4's record-first
31// close, LastMove is written before the claim's release, so it is the
32// serving claim, with no move op, that proves the release finished.
33// LastMove.At follows every park of the move's lanes, and so every staging
34// horizon. A zero time is never evidence.
35//
36// VerifyRetirement answers for less than the proof covers (M7; fix round 1
37// ruling 1). The worker binds its one time to every instances/<host>
38// candidate, so it refuses unless the close is genuine evidence for the
39// residue at the live location:
40//
41// - at the live location the close covers only generations the move
42// retired: those superseded at or before the move's own admission. A
43// generation admitted from that admission on was written past the
44// close, and ordinary retirement owns it.
45//
46// Residue outside the holder's live location is not judged here (Q4). The
47// claim's census places it: ClaimRetirementCensus labels a qualified path by
48// its own identity, and the claim's view refuses unmapped bare history per
49// location (productionClaimRegional.View), so a refusal of it here would
50// only repeat theirs and would skip the lane sweep as well.
51//
52// The provider is registered by RegisterProductionMovesParks, whose view
53// stays closed until movesWakeSourceRegistered opens; the claim reaches
54// VerifyMovesSweep by type assertion (claimassembly.go).
55
56import (
57 "context"
58 "encoding/hex"
59 "errors"
60 "fmt"
61 "strings"
62 "time"
63
64 "github.com/carlosframework/platform/internal/hostclaim"
65 "github.com/carlosframework/platform/internal/hostwrite"
66 "github.com/carlosframework/platform/internal/lease"
67 "github.com/carlosframework/platform/internal/park"
68 "github.com/carlosframework/platform/internal/provision"
69)
70
71// ErrMovesSweepRefused is what every MovesSweepRefusal unwraps to.
72var ErrMovesSweepRefused = errors.New("host: the moves sweep is refused")
73
74// MovesSweepRefusal is the provider's refusal of one observation. Each one
75// skips that pass's sweep, and the next pass asks again. Anomaly marks a
76// refusal that no ordinary move produces: the history contradicts itself, or
77// something wrote where no known move could. M8 asks for one page per
78// anomaly; the provider sends none itself, and its wiring (Task 9a) pages.
79// Reason is for the agent's log, never shown to a user.
80type MovesSweepRefusal struct {
81 Host, Reason string
82 Anomaly bool
83}
84
85func (e *MovesSweepRefusal) Error() string {
86 return "host: the moves sweep of " + e.Host + " is refused: " + e.Reason
87}
88
89func (e *MovesSweepRefusal) Unwrap() error { return ErrMovesSweepRefused }
90
91// ErrNotMovesEvidence is what every NotMovesEvidence unwraps to.
92var ErrNotMovesEvidence = errors.New("host: the move's close is not evidence for this residue")
93
94// NotMovesEvidence is VerifyRetirement's refusal of residue at the live
95// location that the move did not retire: a generation written past the
96// move's close, or an object no generation owns. The close is not its
97// evidence, and ordinary retirement owns it. The worker binds one evidence
98// time to every instances/<host> candidate, so the provider cannot abstain
99// for one generation; this refusal skips the pass's sweep while such
100// residue stands. Reason is for the agent's log, never shown to a user.
101type NotMovesEvidence struct {
102 Host, Generation, Reason string
103}
104
105func (e *NotMovesEvidence) Error() string {
106 return "host: the move's close is not evidence for " + e.Host + " generation " + e.Generation + ": " + e.Reason
107}
108
109func (e *NotMovesEvidence) Unwrap() error { return ErrNotMovesEvidence }
110
111// movesRetirementEvidence implements hostbox.RegionalEvidence and
112// park.MoveSweepVerifier over the host's provisioning record.
113type movesRetirementEvidence struct {
114 Records MovesRecordReader
115 Now func() time.Time
116}
117
118// movesRetired is the proof both methods answer from.
119type movesRetired struct {
120 host, incarnation, location, moveID string
121 at time.Time
122 holder hostclaim.Holder
123}
124
125func (e movesRetirementEvidence) VerifyMovesSweep(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (park.MoveSweepProof, error) {
126 r, err := e.retired(ctx, o, versions)
127 if err != nil {
128 return park.MoveSweepProof{}, err
129 }
130 return park.MoveSweepProof{Host: r.host, ClaimETag: o.ETag, MoveID: r.moveID, Incarnation: r.incarnation, RetiredAt: r.at}, nil
131}
132
133func (e movesRetirementEvidence) VerifyRetirement(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (string, time.Time, error) {
134 r, err := e.retired(ctx, o, versions)
135 if err != nil {
136 return "", time.Time{}, err
137 }
138 if err := r.retiredGenerations(versions); err != nil {
139 return "", time.Time{}, err
140 }
141 return r.incarnation, r.at, nil
142}
143
144// retiredGenerations refuses unless the close is evidence for every object
145// at the live location that the engine could make a candidate (fix round 1,
146// ruling 1). The marker key and the holder's current generation are the
147// live installation's. Any other generation must be one the move retired:
148// the claim must still hold the move's own admission (a prepared landing
149// admits the generation named after the move, movesprobeowners.go), the
150// generation must not be admitted at or after it (admissions are in claim
151// order and carry no time), and every version of it must predate the close.
152// A rolled-back or aborted move, or a landing that admitted a fresh name,
153// leaves no such admission, so no generation is vouched for. Objects are
154// classified as the landing's view classifies them (landingGenerationOf),
155// after the lock objects the engine itself skips (movesParkLock, fix round
156// 2): the writer's lease stands at every live location. The build state
157// beside it (movesHolderBuildState, ruling R-PARK) is the location's own,
158// the marker's twin, and needs no vouching either.
159func (r movesRetired) retiredGenerations(versions []lease.ObjectVersion) error {
160 h := r.holder
161 refuse := func(gen, format string, args ...any) error {
162 return &NotMovesEvidence{Host: r.host, Generation: gen, Reason: fmt.Sprintf(format, args...)}
163 }
164 moveAdmission := -1
165 for i := len(h.Admissions) - 1; i >= 0; i-- {
166 if h.Admissions[i].Generation == r.moveID {
167 moveAdmission = i
168 break
169 }
170 }
171 afterTheMove := map[string]bool{}
172 if moveAdmission >= 0 {
173 for _, a := range h.Admissions[moveAdmission:] {
174 afterTheMove[a.Generation] = true
175 }
176 }
177 for _, v := range versions {
178 if loc, ok := movesInstancesLocation(r.host, v.Key); !ok || loc != r.location || movesParkLock(r.host, v.Key) || movesHolderBuildState(r.location, v.Key) {
179 continue
180 }
181 gen, ok := landingGenerationOf(r.location, v.Key)
182 switch {
183 case !ok:
184 return refuse("", "%s belongs to no generation", v.Key)
185 case gen == landingMarkerGeneration || gen == h.Generation:
186 continue
187 case moveAdmission < 0:
188 return refuse(gen, "the claim holds no admission of %s to date it by", r.moveID)
189 case afterTheMove[gen]:
190 return refuse(gen, "it was admitted after %s's admission, so written past the close", r.moveID)
191 case v.LastModified.IsZero() || !v.LastModified.Before(r.at):
192 return refuse(gen, "%s was written after %s closed", v.Key, r.moveID)
193 }
194 }
195 return nil
196}
197
198// movesInstancesLocation is the location park.Decide files an
199// instances/<host>/ key under (park's locationOf): a 64-hex first segment
200// with a child is its own incarnation location, and everything else is the
201// bare instances/<host>. ok is false outside instances/<host>/.
202func movesInstancesLocation(host, key string) (string, bool) {
203 base := "instances/" + host
204 tail, ok := strings.CutPrefix(key, base+"/")
205 if !ok {
206 return "", false
207 }
208 first, _, child := strings.Cut(tail, "/")
209 if _, err := hex.DecodeString(first); child && err == nil && len(first) == 64 {
210 return base + "/" + first, true
211 }
212 return base, true
213}
214
215// retired proves the newest retired move from the claim observation, the
216// holder's record and the complete census, or refuses.
217func (e movesRetirementEvidence) retired(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (movesRetired, error) {
218 if ctx == nil || e.Records == nil || e.Now == nil {
219 return movesRetired{}, hostwrite.ErrUnavailable
220 }
221 if o.Absent || o.Claim == nil || o.ETag == "" || o.Host == "" || o.Claim.Host != o.Host {
222 return movesRetired{}, hostwrite.ErrUnavailable
223 }
224 c, host := o.Claim, o.Host
225 refuse := func(anomaly bool, format string, args ...any) (movesRetired, error) {
226 return movesRetired{}, &MovesSweepRefusal{Host: host, Reason: fmt.Sprintf(format, args...), Anomaly: anomaly}
227 }
228 if c.Holder == nil {
229 // A free or joining claim names no record, so no move history. A free
230 // claim's lanes are the claim's own proof (claimFreeMoveProof), which
231 // the claim asks first; a joining parking claim reaches here. It is an
232 // ordinary state, so it is a refusal, quiet in the agent's log, and
233 // not an anomaly (Q4). Once the claim has a holder again the next
234 // pass proves the move.
235 return refuse(false, "the claim has no holder, so no record to prove a closed move from")
236 }
237 if c.Op.Kind != hostclaim.KindServing {
238 return refuse(false, "the claim is under a %s op", c.Op.Kind)
239 }
240 h := c.Holder
241 if h.Family != "instance" || h.Key != provision.RecordKey(h.Account, h.App, host) {
242 return refuse(false, "the holder is not an instance record")
243 }
244 // A moved holder is at the bare location, the activator's own lease
245 // (ruling T5-lease, movescut.go writerLease); a scoped holder is at its
246 // incarnation's. Anything else is a location the census cannot place.
247 location := strings.TrimSuffix(h.Location, "/")
248 if location != "instances/"+host && location != "instances/"+host+"/"+h.Incarnation {
249 return refuse(true, "the holder is at %s, neither the bare nor the incarnation location", h.Location)
250 }
251 rec, err := e.Records(ctx, h.Account, h.App, host)
252 if err != nil {
253 return movesRetired{}, err
254 }
255 if rec == nil {
256 return refuse(true, "the holder's record is missing")
257 }
258 inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
259 if err != nil || rec.Account != h.Account || rec.App != h.App || rec.Host != host || inc != h.Incarnation {
260 return refuse(true, "the record is not the holder's incarnation")
261 }
262 if rec.Move != nil {
263 return refuse(false, "the record carries the live move %s", rec.Move.ID)
264 }
265 last := rec.LastMove
266 if last == nil || last.ID == "" {
267 return refuse(false, "the record has closed no move")
268 }
269 if last.At.IsZero() || !last.At.Before(e.Now()) {
270 return refuse(true, "the close of %s has no time before now", last.ID)
271 }
272 movesRoot := "moves/" + host + "/"
273 for _, v := range versions {
274 if strings.HasPrefix(v.Key, "instances/"+host+"/") {
275 continue
276 }
277 tail, ok := strings.CutPrefix(v.Key, movesRoot)
278 if !ok {
279 return refuse(true, "the census holds %s, outside this host", v.Key)
280 }
281 if movesParkLock(host, v.Key) {
282 continue // the moves/<host> park's own lease
283 }
284 op, _, inLane := strings.Cut(tail, "/")
285 if !inLane {
286 return refuse(true, "%s is outside every lane", v.Key)
287 }
288 if op == last.ID {
289 continue
290 }
291 if v.LastModified.IsZero() || !v.LastModified.Before(last.At) {
292 return refuse(true, "lane %s was written after %s closed", op, last.ID)
293 }
294 }
295 return movesRetired{host: host, incarnation: h.Incarnation, location: location, moveID: last.ID, at: last.At.UTC(), holder: *h}, nil
296}
297
298// movesParkLock mirrors the park engine's lock predicate (park's isLock over
299// locationOf, both unexported): a key is a lock exactly when it is the
300// lock.json at the root of its own park location, instances/<host>,
301// instances/<host>/<incarnation> or moves/<host>. That is the writer's
302// lease at a holder location (lease.Acquire) and a park's own lease.
303// Decide never makes it a candidate, so the close need not vouch for it.
304// TestMovesParkLockMatchesTheEngine keeps the two in step through
305// park.Decide.
306func movesParkLock(host, key string) bool {
307 loc, ok := movesInstancesLocation(host, key)
308 if !ok {
309 if !strings.HasPrefix(key, "moves/"+host+"/") {
310 return false
311 }
312 loc = "moves/" + host
313 }
314 return key == loc+"/lock.json"
315}
316