carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "time"
5
6 "github.com/carlosframework/platform/internal/registry"
7)
8
9// TargetReason is why Target chose what it did; plan 5 renders each.
10type TargetReason string
11
12const (
13 ReasonAdopt TargetReason = "adopt"
14 ReasonPin TargetReason = "pin"
15 ReasonRequest TargetReason = "request"
16 ReasonManual TargetReason = "manual"
17 ReasonMajor TargetReason = "major"
18 ReasonUnknown TargetReason = "unknown"
19 ReasonNew TargetReason = "new"
20 ReasonBaking TargetReason = "baking"
21 ReasonFailed TargetReason = "failed"
22 ReasonUnauthorized TargetReason = "unauthorized"
23)
24
25// Evidence is what authorizes an install (settings spec §4.1).
26type Evidence int
27
28const (
29 EvidencePointer Evidence = iota // the signed head (and its plan-1 digest): today's adoption only
30 EvidenceRecord // a signed promotion record with a matching manifest digest
31 EvidenceStaged // the box's own staged, verified copy (adoptHost's fallback from Record)
32)
33
34// TargetInput is everything §4.3 decides on, read by the caller so Target
35// stays pure.
36type TargetInput struct {
37 Route registry.Route
38 Pointer *ChannelPointer // the verified head; nil when unresolved
39 HeadTag string // the head's semver tag, "" when it has none
40 Settings UpdateSettings
41 Build BuildStateCopy
42 Consumed *Consumed
43 Installed string
44 HeadHeld string // Hold(...) for the head on this box: "" = eligible
45 Failed func(version string) bool // the zero-downtime failure records for this host
46}
47
48// TargetDecision is root's one answer per route per pass.
49type TargetDecision struct {
50 Version string // "" installs nothing
51 Reason TargetReason
52 Evidence Evidence
53 Bake bool // the caller applies today's Hold to Version
54 Resolve *ResolvedRequest // record before staging: this pass resolves R to T
55 Fail uint64 // record request Fail as completed, failed
56 Detail string // the build or number a reason names
57}
58
59// Target is settings spec §4.3, rules in order, with the zero-downtime
60// failure records outranking them all. The console never calls it; root is
61// the only caller that installs (§4).
62func Target(in TargetInput) TargetDecision {
63 u := in.Settings
64 head := ""
65 if in.Pointer != nil {
66 head = in.Pointer.Version
67 }
68 failed := func(v string) bool { return v != "" && in.Failed != nil && in.Failed(v) }
69 state := in.Build.State
70 wm := Watermark(in.Consumed, state)
71
72 // Request bookkeeping comes first, before any selection rule, pins
73 // included (§4.3 rule 2: "resolved at the start of every pass"; Astra I3
74 // #6). A pending R is resolved onto the head eligible now; a resolved R
75 // whose T failed completes failed. A pin then decides what runs, and the
76 // resolution waits, so clearing the pin later installs this T, not a
77 // head that moved on. A completed request never enters here again.
78 //
79 // None of it happens while the build state is unread (!Build.Read(): never
80 // read, or its latest refresh failed and the copy may be stale): the
81 // watermark then sees only the consumed file, or an old copy's, so a
82 // request that already completed would look pending (R-TARGET-UNKNOWN).
83 // The pin, held and adoption rules decide as before, with no resolution
84 // and no failure completion.
85 var (
86 resolve *ResolvedRequest
87 failedReq uint64
88 reqTarget string // a resolved T, not failed, above the watermark
89 )
90 read := in.Build.Read()
91 if read {
92 if u.InstallRequest > wm {
93 if res := resolvedFor(in.Consumed, state, u.InstallRequest); res != nil {
94 if failed(res.Target) {
95 failedReq = res.ID // completes failed, once; the rules below decide what runs
96 } else {
97 reqTarget = res.Target
98 }
99 } else if head != "" && (in.HeadHeld == "" || u.InstallFastTrack) {
100 // The eligible head is the request's T. When this box holds that
101 // head as failed the request is resolved and completed failed in
102 // the same pass and installs nothing (R-TARGET-FAILEDHEAD); the
103 // later rules decide what runs.
104 resolve = &ResolvedRequest{ID: u.InstallRequest, Target: head}
105 if failed(head) {
106 failedReq = u.InstallRequest
107 } else {
108 reqTarget = head
109 }
110 }
111 // Unresolved only while the head is baking here: it blocks nothing.
112 }
113 // A completed request whose target failed after it first ran (a later
114 // health, settle or hold failure) is recorded completed-failed, once,
115 // without reopening it (Astra I1 #10, §4.3 rule 2); also before the pin.
116 if failedReq == 0 && u.InstallRequest != 0 && u.InstallRequest <= wm && !completedFailed(in.Consumed, state, wm) {
117 if res := resolvedFor(in.Consumed, state, wm); res != nil && failed(res.Target) {
118 failedReq = res.ID
119 }
120 }
121 }
122 carry := func(d TargetDecision) TargetDecision {
123 d.Resolve, d.Fail = resolve, failedReq
124 return d
125 }
126
127 // Rule 1: a pin. A person chose it, so bake does not apply.
128 if u.Pin != "" {
129 if failed(u.Pin) {
130 return carry(TargetDecision{Reason: ReasonFailed, Detail: u.Pin})
131 }
132 return carry(TargetDecision{Version: u.Pin, Reason: ReasonPin, Evidence: EvidenceRecord, Detail: u.Pin})
133 }
134
135 // Rule 2: a resolved request above the watermark (this pass's resolution
136 // included).
137 if reqTarget != "" {
138 ev := EvidenceRecord
139 if reqTarget == head {
140 ev = EvidencePointer
141 }
142 return carry(TargetDecision{Version: reqTarget, Reason: ReasonRequest, Evidence: ev, Detail: reqTarget})
143 }
144
145 // Rule 3: held.
146 majorHeld := u.Major != nil && !tagInMajor(in.HeadTag, *u.Major)
147 if u.Updates == UpdatesManual || majorHeld {
148 reason := ReasonManual
149 if u.Updates != UpdatesManual {
150 reason = ReasonMajor
151 }
152 var d TargetDecision
153 switch {
154 case !read:
155 d = TargetDecision{Reason: ReasonUnknown}
156 case state != nil && state.LastExecuted != "":
157 d = TargetDecision{Version: state.LastExecuted, Reason: reason, Evidence: EvidenceRecord, Detail: state.LastExecuted}
158 case in.Installed != "":
159 d = TargetDecision{Version: in.Installed, Reason: reason, Evidence: EvidenceRecord, Detail: in.Installed}
160 case in.Build.Replica:
161 d = TargetDecision{Reason: ReasonUnknown}
162 case head != "" && !failed(head):
163 d = TargetDecision{Version: head, Reason: ReasonNew, Evidence: EvidencePointer, Bake: true, Detail: head}
164 case head == "":
165 d = TargetDecision{Reason: ReasonNew} // new, and no readable head to give it: nothing yet
166 default:
167 d = TargetDecision{Reason: ReasonFailed, Detail: head}
168 }
169 if failed(d.Version) {
170 d = TargetDecision{Reason: ReasonFailed, Detail: d.Version}
171 }
172 return carry(d)
173 }
174
175 // Rule 4: today's adoption.
176 if head == "" {
177 return carry(TargetDecision{})
178 }
179 if failed(head) {
180 return carry(TargetDecision{Reason: ReasonFailed, Detail: head})
181 }
182 return carry(TargetDecision{Version: head, Reason: ReasonAdopt, Evidence: EvidencePointer, Bake: true, Detail: head})
183}
184
185// completedFailed reports whether either source already records request id
186// as completed, failed.
187func completedFailed(c *Consumed, b *BuildState, id uint64) bool {
188 return (c != nil && c.Completed == id && c.CompletedFailed) || (b != nil && b.Completed == id && b.CompletedFailed)
189}
190
191// resolvedFor is R's T from either source (§4.3 rule 2: "locally or in the
192// build state"), or nil. When both resolve the same R to different builds
193// the build state's wins (ruling R-RESOLUTION-STATE-WINS): it travels with
194// the data, and the pre-exec write completes R only when its T runs. A box
195// that resolved R and lost the instance to a move before any wake keeps an
196// older resolution in its own file; the destination resolved R again into
197// the build state. Following the file there would run a T that never
198// completes R.
199func resolvedFor(c *Consumed, b *BuildState, id uint64) *ResolvedRequest {
200 if b != nil && b.Resolved != nil && b.Resolved.ID == id {
201 return b.Resolved
202 }
203 if c != nil && c.Resolved != nil && c.Resolved.ID == id {
204 return c.Resolved
205 }
206 return nil
207}
208
209// tagInMajor: a head whose semver tag is absent or has another major is
210// held (§3.1).
211func tagInMajor(tag string, major int) bool {
212 v, ok := ParseSemVer(tag)
213 return ok && v.Major == major
214}
215
216// NextConsumed is root's consumed-file update for one decision (settings
217// spec §4.3 rule 2; ruling R-MERGE): in is what Target decided on, and
218// in.Consumed the file as read. root's pass writes the result before it
219// stages anything; the simulation calls the same function. A file
220// ForeignTo in.Route (another tenant's) is not carried: the record starts
221// empty (ruling R26).
222//
223// - dec.Fail completes that request as failed: above the file's watermark
224// it becomes the new watermark; equal to it (T failed after it first
225// ran, Astra I1 #10) the outcome changes and the request stays closed;
226// below it, nothing.
227// - dec.Resolve records {R, T}. With R-TARGET-FAILEDHEAD a decision can
228// resolve R and complete it failed in the same write. A file resolving
229// the same R as a read build state, to another T, is rewritten to the
230// build state's T (R-RESOLUTION-STATE-WINS).
231// - Baking names the head a pending, unresolved request waits on, beside
232// the decision's reason, never in its place (Astra I1 #8): Reason is what
233// the held wake check reads. Only with the build state read
234// (in.Build.Read()), as Target does no request bookkeeping without it.
235// - Desired, Reason and Detail are the decision; Settings names the
236// update_settings column it was made for (in.Route.UpdateSettings,
237// ruling R-HELD-PURE); Account and App name the route's tenant (ruling
238// R22); At is the writer's.
239func NextConsumed(in TargetInput, dec TargetDecision) Consumed {
240 next := Consumed{Host: in.Route.Host}
241 // Another tenant's file passes on nothing (ruling R26): the record starts
242 // empty, whatever the caller read. An untagged file is carried as today.
243 if in.Consumed != nil && !in.Consumed.ForeignTo(in.Route) {
244 next = *in.Consumed
245 next.Host = in.Route.Host
246 next.Resolved = copyResolved(in.Consumed.Resolved)
247 }
248 switch {
249 case dec.Fail > next.Completed:
250 next.Completed, next.CompletedFailed = dec.Fail, true
251 case dec.Fail != 0 && dec.Fail == next.Completed:
252 next.CompletedFailed = true
253 }
254 if dec.Resolve != nil {
255 next.Resolved = copyResolved(dec.Resolve)
256 }
257 u, state := in.Settings, in.Build.State
258 // A file that resolves the same request as the build state, to another
259 // build, takes the build state's T (ruling R-RESOLUTION-STATE-WINS), so
260 // the box's own record stops disagreeing with what Target followed. Only
261 // with the build state read: an unread copy says nothing.
262 if in.Build.Read() && state != nil && state.Resolved != nil && next.Resolved != nil &&
263 next.Resolved.ID == state.Resolved.ID && next.Resolved.Target != state.Resolved.Target {
264 next.Resolved = copyResolved(state.Resolved)
265 }
266 next.Baking = ""
267 if in.Build.Read() && in.Pointer != nil && in.HeadHeld != "" && !u.InstallFastTrack && dec.Resolve == nil &&
268 u.InstallRequest > Watermark(&next, state) && resolvedFor(&next, state, u.InstallRequest) == nil {
269 next.Baking = in.Pointer.Version
270 }
271 next.Desired, next.Reason, next.Detail, next.At = dec.Version, string(dec.Reason), dec.Detail, time.Time{}
272 // The column this decision was made for (ruling R-HELD-PURE): the held
273 // wake check reads Reason only while the column is still this one.
274 next.Settings = SettingsDigest(in.Route.UpdateSettings)
275 // The tenant it was made for (Astra r2 N1, ruling R22), always the
276 // route's: a file read from a previous occupant of this hostname never
277 // passes its tenant on.
278 next.Account, next.App = RouteAccount(in.Route), in.Route.App
279 return next
280}
281