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 "errors"
6 "fmt"
7 "time"
8
9 "github.com/carlosframework/platform/internal/activator"
10 "github.com/carlosframework/platform/internal/hostbox"
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/moves"
13 "github.com/carlosframework/platform/internal/provision"
14 "github.com/carlosframework/platform/internal/registry"
15 "github.com/carlosframework/platform/internal/release"
16)
17
18// The source's recovery probe adapter (moves §7.8 source probing; claim
19// Plan 10 J2). It is the production recoveryProbe hook: movesrecover.go's
20// probing calls it once the record says probing and the claim is this
21// rollback's restoring op at probing with this box executing.
22//
23// One call is one probe-shaped claim wake, in this order:
24//
25// 1. the request is checked against the record (this rollback at probing,
26// nine attempts, the coverage and generation of its return; a declared
27// return, whose record cites the target's declared loss or fence, asks
28// no coverage, Q6) and the claim is read fresh (restoring/probing, this
29// op, this rollback, this box executing, the record's incarnation, a
30// clean return's src_gen, the target's loss the record cites);
31// 2. the inputs the probe will render are selected (the box's current
32// inputs, as the cut reads them) and recorded through req.Record, which
33// is provision.MoveRecordRecoveryInputs under the move and rollback ids,
34// legal only at probing. A clean return records here: it has no
35// configured phase. A dirty return records again, replacing what its
36// install recorded at configured, because this render is the probe's.
37// A failed record write stops the attempt: nothing is rendered or run;
38// 3. the activator is asked for a probe-shaped claim wake (StartPolicy
39// Probe, carrying exactly the recorded inputs). That wake is the claim's
40// Group D path unchanged: Prerequisites and admitsOperation admit a
41// recovery probe only at probing with the moves C1 owners bound; Admit,
42// the pre-launch record and Prepare bind OperationAttempts and the
43// role-cgroup probe readiness to the admitted session; the moves Inputs
44// owner selects and materializes (the held render) the recorded inputs,
45// and moves' policy admits only those (MovesClaimPolicy).
46//
47// The adapter writes nothing besides req.Record. The restore, install and
48// evidence remain the moves owners'; the nine-attempt budget is journalled
49// by moves' OperationAttempts, which refuses a start past it with
50// ErrRecoveryProbeExhausted.
51
52// ClaimRecoveryProbeWake is the activator's recovery probe entry: one
53// claim wake of rt under p (p.Probe set), through ClaimRuntime's
54// Prerequisites, Admit and Prepare and the activator's own start loop. It
55// returns nil once a start was healthy (OperationAttempts.Healthy accepted
56// it). Production binds Service.RecoveryProbe, which retains the process
57// in the activator and refuses missing claim-runtime dependencies.
58type ClaimRecoveryProbeWake func(ctx context.Context, rt registry.Route, p activator.StartPolicy) error
59
60// ErrRecoveryProbeExhausted is the moves attempt journal's refusal once the
61// recovery's nine starts are spent. The recovery stops for an operator at
62// probing (StuckRecoveryProbeAttempts).
63var ErrRecoveryProbeExhausted = errors.New("moves: the recovery probe's attempts are spent")
64
65// errRecoveryProbeRequest is a request that does not match its record: a
66// caller bug, never retried into a wake.
67var errRecoveryProbeRequest = errors.New("moves: the recovery probe request does not match its record")
68
69// errRecoveryInputsNoRelease: the selected inputs name no release, which the
70// record transition would refuse; the adapter refuses first.
71var errRecoveryInputsNoRelease = errors.New("moves: the recovery probe's inputs name no release")
72
73// recoveryProbeStartsPerWake is the starts one probe wake may run. The
74// activator allows at most five per wake; the recovery's nine are a budget
75// across wakes, journalled by moves.
76const recoveryProbeStartsPerWake = 3
77
78// recoveryProbeWindow is the probe's readiness window from its first
79// app start (§7.7's 30 s).
80const recoveryProbeWindow = 30 * time.Second
81
82// recoveryProbeAdapter is the source's recovery probe. Claims is the
83// captured CARLOS_HOST_CLAIM; Route reads this box's registry row; Current
84// selects the inputs the probe will render; Wake is the activator's entry.
85type recoveryProbeAdapter struct {
86 Box string
87 Claims bool
88 Route func(host string) (*registry.Route, error)
89 Current func(ctx context.Context, s release.Store, rec provision.Record) (provision.MoveInputs, error)
90 Wake ClaimRecoveryProbeWake
91 Ready func() bool
92}
93
94// armRecoveryProbe binds the production recoveryProbe hook: the adapter
95// over this worker's configuration, read when the probe runs. wake is the
96// activator's recovery probe entry; tests may leave it nil to prove refusal.
97func armRecoveryProbe(cfg *moveCfg, w *movesWorker, wake ClaimRecoveryProbeWake, ready func() bool) {
98 cfg.recoveryProbe = func(ctx context.Context, s release.Store, rec provision.Record, req recoveryProbeRequest) (recoveryProbeOutcome, error) {
99 c := w.cfg
100 a := recoveryProbeAdapter{Box: c.Box, Claims: c.claimRecords, Route: c.Route, Current: w.recoveryInputs, Wake: wake, Ready: ready}
101 return a.probe(ctx, s, rec, req)
102 }
103}
104
105// probe runs one recovery probe wake for rec (see the file comment).
106func (a recoveryProbeAdapter) probe(ctx context.Context, s release.Store, rec provision.Record, req recoveryProbeRequest) (recoveryProbeOutcome, error) {
107 var missing []string
108 if !a.Claims {
109 missing = append(missing, "CARLOS_HOST_CLAIM Group D recovery admission")
110 }
111 if a.Wake == nil || (a.Ready != nil && !a.Ready()) {
112 missing = append(missing, "activator recovery probe entry")
113 }
114 if a.Box == "" || a.Route == nil || a.Current == nil {
115 missing = append(missing, "moves recovery probe route and inputs")
116 }
117 if len(missing) > 0 {
118 return recoveryProbeWaiting, &ClaimDependencyError{Contracts: missing}
119 }
120 if err := recoveryProbeMatches(rec, req); err != nil {
121 return recoveryProbeWaiting, err
122 }
123 obs, err := hostclaim.Read(ctx, s, rec.Host)
124 if err != nil {
125 return recoveryProbeWaiting, fmt.Errorf("read the host claim: %w", err)
126 }
127 if err := a.admits(obs, rec, req); err != nil {
128 return recoveryProbeWaiting, err
129 }
130 rt, err := a.Route(rec.Host)
131 if err != nil {
132 return recoveryProbeWaiting, fmt.Errorf("read the registry row: %w", err)
133 }
134 if rt == nil || rt.Host != rec.Host {
135 return recoveryProbeWaiting, fmt.Errorf("%w: %s has no registry row on this box", hostbox.ErrChanged, rec.Host)
136 }
137 in, err := a.Current(ctx, s, rec)
138 if err != nil {
139 return recoveryProbeWaiting, fmt.Errorf("select the recovery inputs: %w", err)
140 }
141 if in.Release == "" {
142 return recoveryProbeWaiting, errRecoveryInputsNoRelease
143 }
144 // The record precedes every render and launch of this wake.
145 if err := req.Record(ctx, in); err != nil {
146 return recoveryProbeWaiting, fmt.Errorf("record the recovery inputs: %w", err)
147 }
148 recorded := in
149 recorded.BundleETags = make(map[string]string, len(in.BundleETags))
150 for k, v := range in.BundleETags {
151 recorded.BundleETags[k] = v
152 }
153 err = a.Wake(ctx, *rt, activator.StartPolicy{RecoveryFence: moveFenceGeneration(rec.Move), MaxStarts: recoveryProbeStartsPerWake, Window: recoveryProbeWindow, Probe: true, Inputs: &recorded})
154 switch {
155 case err == nil:
156 return recoveryProbeAnswered, nil
157 case errors.Is(err, ErrRecoveryProbeExhausted):
158 return recoveryProbeExhausted, nil
159 case errors.Is(err, moves.ErrAttemptOver), errors.Is(err, hostbox.ErrChanged):
160 // This wake's window lapsed or its admission no longer holds: the
161 // next pass probes again, and only the probe.
162 return recoveryProbeWaiting, nil
163 }
164 return recoveryProbeWaiting, fmt.Errorf("the recovery probe wake: %w", err)
165}
166
167// recoveryProbeMatches checks the request against its record: this
168// rollback, rolling back with the source at probing, the nine-attempt
169// budget and the coverage and generation of its return.
170func recoveryProbeMatches(rec provision.Record, req recoveryProbeRequest) error {
171 m := rec.Move
172 if m != nil && m.ID != "" && m.State == provision.MoveReverting {
173 if m.ReopenedAt != nil || req.RollbackID != "" || req.Coverage != "" || req.Generation != "" || !req.Clean || req.Declared || req.Attempts != recoveryProbeAttempts || req.Record == nil {
174 return errRecoveryProbeRequest
175 }
176 return nil
177 }
178 if m == nil || m.ID == "" || m.State != provision.MoveRollingBack || m.Rollback == nil || m.Rollback.ID != req.RollbackID || m.Rollback.SourcePhase != provision.RbSourceProbing {
179 return fmt.Errorf("%w: not rollback %s at probing", errRecoveryProbeRequest, req.RollbackID)
180 }
181 if req.Record == nil || req.Attempts != recoveryProbeAttempts || req.Clean == m.Rollback.Dirty {
182 return fmt.Errorf("%w: budget %d, clean %v", errRecoveryProbeRequest, req.Attempts, req.Clean)
183 }
184 // The declared kind (Q6): a return whose record cites a declared loss
185 // or a fence of the target is clean and asks no coverage.
186 declared := m.Rollback.LossRef != ""
187 switch {
188 case req.Declared != declared:
189 return fmt.Errorf("%w: the request's declared return is not the record's", errRecoveryProbeRequest)
190 // SourceGeneration may be "" (whole-branch fixes, item 4): the claim's
191 // legacy encoding of a source whose sessions predate admission records,
192 // which recoverRun.probing passes through from the journal and the
193 // owners normalise as hostclaim.RootGeneration; admits holds it to the
194 // claim's.
195 case declared && (!req.Clean || req.Coverage != "" || req.Generation != ""):
196 return fmt.Errorf("%w: a declared return admits a fresh generation restored from src_gen, uncovered", errRecoveryProbeRequest)
197 case declared:
198 case req.Clean:
199 if req.Coverage != recoveryCoverageClean || req.Generation != "" {
200 return fmt.Errorf("%w: a clean return admits a fresh generation restored from src_gen", errRecoveryProbeRequest)
201 }
202 case req.Coverage != recoveryCoverageSealed || req.Generation != req.RollbackID:
203 return fmt.Errorf("%w: a dirty return admits its sealed return generation", errRecoveryProbeRequest)
204 }
205 return nil
206}
207
208// admits checks the fresh claim: this move's restoring op for this
209// rollback, at probing, executed by this box under the record's
210// incarnation, naming the request's src_gen. Anything else waits.
211func (a recoveryProbeAdapter) admits(obs hostclaim.Observed, rec provision.Record, req recoveryProbeRequest) error {
212 c := obs.Claim
213 if obs.Absent || c == nil || c.Holder == nil || c.Op.ID != rec.Move.ID || c.Op.Phase != hostclaim.PhaseProbing || c.Op.Exec != a.Box || c.Holder.Incarnation != rec.Move.Incarnation {
214 return hostbox.ErrChanged
215 }
216 if rec.Move.State == provision.MoveReverting {
217 if c.Op.Kind != hostclaim.KindReverting || c.Op.Journal[hostclaim.JournalRevPhase] != hostclaim.PhaseProbing || c.Holder.Generation != req.SourceGeneration {
218 return hostbox.ErrChanged
219 }
220 return nil
221 }
222 if c.Op.Kind != hostclaim.KindRestoring || c.Op.Intent["rollback"] != req.RollbackID {
223 return hostbox.ErrChanged
224 }
225 // The fresh claim must still agree with the record on the target's
226 // loss, and with the request's kind (Q6).
227 declared, err := recoveryDeclared(c, rec.Move.Rollback.LossRef)
228 if err != nil {
229 return err
230 }
231 if declared != req.Declared {
232 return fmt.Errorf("%w: the claim's loss of the target is not the request's", hostbox.ErrChanged)
233 }
234 // The claim's src_gen and the request's, normalised as the owners
235 // normalise them (selectedGeneration: "" is the legacy root), must agree.
236 if selectedGeneration(c.Op.Journal[hostclaim.JournalSrcGen]) != selectedGeneration(req.SourceGeneration) {
237 return fmt.Errorf("%w: the claim's src_gen is not the request's", hostbox.ErrChanged)
238 }
239 return nil
240}
241