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
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "log"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/carlosframework/platform/internal/activator"
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/moves"
16 "github.com/carlosframework/platform/internal/provision"
17)
18
19// The landing's probing phase (moves spec §7.6, §7.7, §7.13; Plan 3 Task 6).
20//
21// restored to probing is the landing's step (movesland.go). At probing the
22// pass makes the probe attempts T11 admitted (H10: the record's
23// CutAttempts, written by MoveAdmit), at most moves.AttemptsFor("landing",
24// n). Each attempt is counted on the record (provision.MoveProbeAttempt)
25// and journalled (Journal.Attempt) before its call, then made through the
26// probe wake (Task 5's activator.Service.ProbeWake behind moveCfg.Probe):
27// one cold admission into the prepared generation (or a fresh one for a
28// later attempt), restored, rendered, published and started by the claim
29// (Group D), up to five starts inside the attempt's 30 s window, bounded by
30// the landing's deadline (H8). The attempt's own journal steps
31// (probe_opened_<k>, probe_start_<k>) are Task 5's movesProbeAttempts';
32// probe_failed_<k> is this file's, written when the attempt is over.
33//
34// Outcomes. A pass: probing to probed, record then claim (the claim's step
35// keeps the admitted writer session, H3); the commit is Task 7's. Task 5's
36// ProbeInputsChanged: its recorded sentence, the configuration rollback. An
37// attempt that is over (ErrProbeFailed, ErrAttemptOver): the next one, if
38// any; none left: the attempts-exhausted rollback; the deadline passed: the
39// deadline's. Rollbacks are marks (MoveMarkRollback); the one conversion to
40// MoveRollBack is the pass's rollback decision (movesrollback.go). Any other
41// error ends the pass and the same attempt is made again on the next.
42//
43// A crash restarts only the probe (§7.13): local and restored are never run
44// again; a fresh worker finds probing and the counted attempt. One cold
45// admission per budgeted attempt (§7.7, fix round 1, ruling 2): an attempt
46// is admitted once Task 5's journalled session binding (probe_session_<s> =
47// k) exists for it, and an admitted attempt is consumed on restart, failed
48// unless it passed. Only a counted attempt with no binding is made again.
49// Fix round 2: the claim is the source of truth for the admission itself.
50// probe_admitting_<k> records the claim's writer before the call; a claim
51// whose writer moved on since admitted under k though Reserve's binding
52// never became durable. Before an admitted attempt with no probe_passed_<k>
53// is consumed, the activator's live entry is asked: this move's, awake,
54// with the claim's writer, it passed, and its pass is journalled then.
55//
56// A pass is journalled (probe_passed_<k>: the admitted session, stamp and
57// generation) before the probing to probed record write (ruling 1). A pass
58// that finds it does not probe again: it checks the claim's holder still
59// carries that writer and the activator's live probe entry is this move's,
60// then retries only the phase writes, record then claim. A writer that is
61// gone consumes the attempt as failed.
62//
63// Unarmed in production: moveCfg.Probe is nil until claim Plan 11's launcher
64// (R10) and Plan 5 Task 9's arming, so the landing waits at restored.
65
66// probeStepFailed marks attempt k over in the op journal; probeStepPassed
67// names attempt k's passing writer (probePassed).
68const (
69 probeStepFailed = "probe_failed_"
70 probeStepPassed = "probe_passed_"
71 probeStepAdmitting = "probe_admitting_"
72)
73
74// probeAdmitting is a probe_admitting_<k> step's value, written before the
75// call: the claim's writer session and admission count the attempt started
76// from. The claim is the source of truth for whether the attempt admitted
77// (fix round 2, ruling 1).
78type probeAdmitting struct {
79 Session uint64 `json:"session"`
80 Admissions int `json:"admissions"`
81}
82
83// probePassed is a probe_passed_<k> step's value: the admitted writer the
84// passing attempt left serving.
85type probePassed struct {
86 Session uint64 `json:"session"`
87 Stamp uint64 `json:"stamp"`
88 Generation string `json:"generation"`
89}
90
91// probeWindowLen is the attempt's window, counted from its first start.
92var probeWindowLen = time.Duration(moves.ProbeSeconds * float64(time.Second))
93
94// MovesRecordAdmittedAttempts is Task 5's Admitted hook (H10): the probe
95// attempts the record's admission priced, refused when it recorded none.
96func MovesRecordAdmittedAttempts(_ context.Context, rec *provision.Record) (int, error) {
97 if rec == nil || rec.Move == nil || rec.Move.CutAttempts < 1 {
98 return 0, errors.New("moves: " + provision.RollbackProbeUnpriced)
99 }
100 return rec.Move.CutAttempts, nil
101}
102
103// attemptSpent reports whether counted attempt k is over: its failure is
104// journalled, it was admitted (attemptAdmittedBy), or its window opened and
105// has passed.
106func (l *landingRun) attemptSpent(j *moves.Journal, k int, h *hostclaim.Holder) bool {
107 if j.Done(probeStepFailed+strconv.Itoa(k)) || l.attemptAdmittedBy(j, k, h) {
108 return true
109 }
110 raw, ok := j.Steps[probeStepOpened+strconv.Itoa(k)]
111 if !ok {
112 return false
113 }
114 opened, err := time.Parse(time.RFC3339Nano, raw)
115 return err == nil && !l.cfg().Now().Before(opened.Add(probeWindowLen))
116}
117
118// attemptAdmitted reports whether a session is journalled as bound to
119// record attempt k (movesProbeAttempts.bindSession).
120func attemptAdmitted(j *moves.Journal, k int) bool {
121 want := strconv.Itoa(k)
122 for step, v := range j.Steps {
123 if strings.HasPrefix(step, probeStepSession) && v == want {
124 return true
125 }
126 }
127 return false
128}
129
130// attemptAdmittedBy reports whether attempt k admitted a writer: Task 5's
131// binding exists for it, or probe_admitting_<k> was written and the claim
132// has since moved on (another writer session, or a newer cold admission on
133// this box). probe_admitting_<k> with the claim unchanged is an attempt that
134// never admitted, and only that may reuse its index.
135func (l *landingRun) attemptAdmittedBy(j *moves.Journal, k int, h *hostclaim.Holder) bool {
136 if attemptAdmitted(j, k) {
137 return true
138 }
139 raw, ok := j.Steps[probeStepAdmitting+strconv.Itoa(k)]
140 if !ok || h == nil {
141 return false
142 }
143 var a probeAdmitting
144 if err := json.Unmarshal([]byte(raw), &a); err != nil {
145 // Unreadable: never risk a second admission under k.
146 return true
147 }
148 if h.Session != a.Session {
149 return true
150 }
151 if len(h.Admissions) > a.Admissions {
152 last := h.Admissions[len(h.Admissions)-1]
153 return last.Box == l.cfg().Box && last.Mode == "cold"
154 }
155 return false
156}
157
158// livePass reports the activator's live probe entry when it is this move's
159// and its writer is the claim's current one: an attempt that passed though
160// its probe_passed_<k> never became durable (fix round 2, ruling 2).
161func (l *landingRun) livePass(h *hostclaim.Holder) (activator.ProbeResult, bool) {
162 live := l.cfg().ProbeLive
163 if live == nil || h == nil || h.WriterBox != l.cfg().Box {
164 return activator.ProbeResult{}, false
165 }
166 op, res, ok := live(l.rec.Host)
167 if !ok || op != l.g.ID || res.Session != h.Session || res.Stamp != h.Stamp || res.Generation != h.Generation {
168 return activator.ProbeResult{}, false
169 }
170 return res, true
171}
172
173// journalPassed writes probe_passed_<k> for the passing writer res.
174func (l *landingRun) journalPassed(k int, res activator.ProbeResult) error {
175 if err := l.fault("before-journal-passed"); err != nil {
176 return err
177 }
178 raw, err := json.Marshal(probePassed{Session: res.Session, Stamp: res.Stamp, Generation: res.Generation})
179 if err != nil {
180 return err
181 }
182 j, err := l.journal()
183 if err != nil {
184 return err
185 }
186 j.Mark(probeStepPassed+strconv.Itoa(k), string(raw))
187 return j.Commit(l.cfg().DataDir)
188}
189
190// failAttempt journals attempt k over.
191func (l *landingRun) failAttempt(k int) error {
192 j, err := l.journal()
193 if err != nil {
194 return err
195 }
196 if j.Done(probeStepFailed + strconv.Itoa(k)) {
197 return nil
198 }
199 j.Mark(probeStepFailed+strconv.Itoa(k), l.cfg().Now().UTC().Format(time.RFC3339Nano))
200 return j.Commit(l.cfg().DataDir)
201}
202
203// passed writes the phase steps of a passing attempt: record, then claim.
204func (l *landingRun) passed(ctx context.Context) (bool, error) {
205 if err := l.fault("before-record-probed"); err != nil {
206 return false, err
207 }
208 return l.advance(ctx, provision.LandProbing, provision.LandProbed, nil)
209}
210
211// passedWriterLives checks a journalled pass against the claim's holder and
212// the activator's live probe entry.
213func (l *landingRun) passedWriterLives(obs hostclaim.Observed, raw string) bool {
214 var p probePassed
215 if err := json.Unmarshal([]byte(raw), &p); err != nil || p.Session == 0 {
216 return false
217 }
218 h := obs.Claim.Holder
219 if h == nil || h.Session != p.Session || h.Stamp != p.Stamp || h.Generation != p.Generation || h.WriterBox != l.cfg().Box {
220 return false
221 }
222 live := l.cfg().ProbeLive
223 if live == nil {
224 return false
225 }
226 op, res, ok := live(l.rec.Host)
227 return ok && op == l.g.ID && res.Session == p.Session && res.Stamp == p.Stamp && res.Generation == p.Generation
228}
229
230// probing makes the landing's probe attempts (see the file comment).
231func (l *landingRun) probing(ctx context.Context) (bool, error) {
232 cfg := l.cfg()
233 obs, err := l.syncClaimPhase(ctx)
234 if err != nil {
235 return false, err
236 }
237 if err := l.requirePhase(obs, provision.LandProbing); err != nil {
238 return false, err
239 }
240 if cfg.Probe == nil {
241 l.w.notice(l.rec.Host+"#probe", "moves: %s: landing %s waits at probing: this box has no probe wake", l.rec.Host, l.g.ID)
242 return false, nil
243 }
244 m := l.rec.Move
245 if m.CutAttempts < 1 {
246 return l.markRollback(ctx, provision.RollbackProbeUnpriced)
247 }
248 n := moves.AttemptsFor(moves.ProbeLanding, m.CutAttempts)
249 j, err := l.journal()
250 if err != nil {
251 return false, err
252 }
253 raw, ok := j.Steps[landStepInstall]
254 if !ok {
255 return false, fmt.Errorf("move %s: probing without an install manifest in the journal", l.g.ID)
256 }
257 var manifest moves.InstallManifest
258 if err := json.Unmarshal([]byte(raw), &manifest); err != nil {
259 return false, fmt.Errorf("move %s: the journalled install manifest: %w", l.g.ID, err)
260 }
261 counted := 0
262 if m.Landing != nil {
263 counted = m.Landing.Attempts
264 }
265 k := counted
266 if k > 0 {
267 if raw, ok := j.Steps[probeStepPassed+strconv.Itoa(k)]; ok {
268 if l.passedWriterLives(obs, raw) {
269 log.Printf("moves: %s: landing %s attempt %d passed; its phase writes are retried", l.rec.Host, l.g.ID, k)
270 return l.passed(ctx)
271 }
272 log.Printf("moves: %s: landing %s attempt %d passed, but its writer is gone; the attempt is consumed", l.rec.Host, l.g.ID, k)
273 if err := l.failAttempt(k); err != nil {
274 return false, err
275 }
276 k++
277 } else if l.attemptSpent(j, k, obs.Claim.Holder) {
278 if !j.Done(probeStepFailed + strconv.Itoa(k)) {
279 if res, ok := l.livePass(obs.Claim.Holder); ok {
280 log.Printf("moves: %s: landing %s attempt %d passed (its live writer is the claim's); its phase writes follow", l.rec.Host, l.g.ID, k)
281 if err := l.journalPassed(k, res); err != nil {
282 return false, err
283 }
284 return l.passed(ctx)
285 }
286 }
287 if err := l.failAttempt(k); err != nil {
288 return false, err
289 }
290 k++
291 }
292 } else {
293 k = 1
294 }
295 for ; k <= n; k++ {
296 if l.pastDeadline() {
297 return l.markRollback(ctx, provision.RollbackLandingDeadline)
298 }
299 if k > counted {
300 ok, err := l.transition(ctx, "probe attempt "+strconv.Itoa(k), provision.MoveProbeAttempt(l.g, k))
301 if !ok {
302 return false, err
303 }
304 counted = k
305 }
306 // The claim's writer before the call: whether the attempt admitted is
307 // the claim's to say after a crash (ruling 1).
308 before, err := l.claim(ctx)
309 if err != nil {
310 return false, err
311 }
312 admitting, err := json.Marshal(probeAdmitting{Session: before.Claim.Holder.Session, Admissions: len(before.Claim.Holder.Admissions)})
313 if err != nil {
314 return false, err
315 }
316 // Under the journal's one lock (moves.UpdateJournal): the probe
317 // owners and the paging rules write this journal too.
318 if err := moves.UpdateJournal(cfg.DataDir, l.rec.Host, l.g.ID, func(j *moves.Journal) error {
319 j.Phase, j.Attempt = provision.LandProbing, k
320 j.Mark(probeStepAdmitting+strconv.Itoa(k), string(admitting))
321 return nil
322 }); err != nil {
323 return false, err
324 }
325 if err := l.fault("before-probe-" + strconv.Itoa(k)); err != nil {
326 return false, err
327 }
328 deadline, _ := landingDeadlineOf(m)
329 if deadline == nil {
330 return false, fmt.Errorf("move %s: the landing has no deadline", l.g.ID)
331 }
332 res, err := cfg.Probe(ctx, activator.ProbeConfig{Host: l.rec.Host, Op: l.g.ID, HoldOp: l.g.ID, Deadline: *deadline, Prepared: &manifest, Inputs: m.Inputs})
333 var changed *ProbeInputsChanged
334 switch {
335 case err == nil:
336 log.Printf("moves: %s: landing %s passed its probe on attempt %d of %d", l.rec.Host, l.g.ID, k, n)
337 if err := l.journalPassed(k, res); err != nil {
338 return false, err
339 }
340 return l.passed(ctx)
341 case errors.As(err, &changed):
342 log.Printf("moves: %s: landing %s probe attempt %d: %v", l.rec.Host, l.g.ID, k, err)
343 return l.markRollback(ctx, changed.Reason)
344 case errors.Is(err, activator.ErrProbeFailed) || errors.Is(err, activator.ErrAttemptOver):
345 log.Printf("moves: %s: landing %s probe attempt %d of %d is over: %v", l.rec.Host, l.g.ID, k, n, err)
346 if err := l.failAttempt(k); err != nil {
347 return false, err
348 }
349 default:
350 return false, fmt.Errorf("probe attempt %d: %w", k, err)
351 }
352 }
353 if l.pastDeadline() {
354 return l.markRollback(ctx, provision.RollbackLandingDeadline)
355 }
356 return l.markRollback(ctx, provision.RollbackProbeAttempts)
357}
358