carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "log"
5 "path/filepath"
6 "sort"
7
8 "github.com/carlosframework/platform/internal/release"
9)
10
11// Worker pin publication (zero-downtime deploys spec §4.2, §8.5; rulings
12// R17, R19).
13//
14// A worker recycles onto the build it was exec'd from, its captured serving
15// identity, long after live/<host> and the registry row have moved on. Root's
16// staging and root's version sweep must never overwrite or remove that staged
17// copy, and root is a separate process that cannot see this one's memory, so
18// the activator publishes it: the generations file (release.WriteGenerations,
19// read by root through release.GenerationPins and release.GenerationVersions)
20// lists, for every worker entry it holds with a captured serving build and
21// for every build a spawn is about to exec, that build under the worker's
22// host, by version (when it is a valid version name) and by its staged path
23// (when it has one). It does so whatever CARLOS_SWAP says; this
24// flag-independent writer is the one plan 5 extends.
25//
26// Plan 5 (Task 12) adds every live or pending INSTANCE generation (spec
27// §8.5: current, successor, retiring, spawning): the current generation, a
28// hold's pinned one, a planned wake's build and its fallback, every
29// operation-set member, a failed wake retained until its end is proven, and
30// a socketless attempt's builds. An instance generation is listed with
31// generations on, or whenever it was exec'd from a staged path
32// (genIdentity). Since settings Task 5c every exec-backed wake runs its
33// recorded file by path, flag off included, so a flag-off file names those
34// instances' files too; only a generation started from the live link (a
35// backend that cannot pin a file) stays unlisted, the link being root's
36// pin already.
37//
38// A cold wake (no plan, no pin) captures the live link once after its
39// restore and publishes it as a pending build before it records it
40// (settleWakeIdentity, ruling R-COLDPUBLISH); wake() drops that pending
41// build once the entry's generation names the file.
42//
43// Every staged instance spawn (a planned wake and its fallback, a hold's
44// pinned wake, an overlap successor) publishes the entry state that names
45// its build BEFORE the exec, and does not exec when that write fails
46// (errStagedBuildNotPublished): doWake and doClaimWake before the lease, checkPlan
47// for a replanned build, swapOverlap before its Step 3a. The entry state is
48// set first (e.plan, e.pin, the operation set) and then written, rather than
49// recorded as a pending build: that state already lives as long as the
50// spawn's process may, so nothing has to be dropped when the spawn installs.
51//
52// A hold is the exception (zdd-readiness Task 13, Astra R1 T12, R3 §5 item
53// 9). It stops its incumbent before its pinned entry exists, and from that
54// stop until the pinned wake publishes nothing serves and nothing names the
55// hold's build, so root could sweep or restage it under the wake about to
56// run it. swapHold records the build as a pending generation and writes the
57// file BEFORE the stop, and drops the record when it returns: by then the
58// pinned entry, or a retained failed wake, names the build for as long as a
59// process of it may run.
60//
61// The file is written at Start, and before the first worker spawn (so it exists, even
62// empty, before any worker runs), before every staged spawn's exec, naming
63// the build about to run (startWorker's before-exec hook, ruling R19 item 3),
64// after every spawn or respawn installs its captured identity in place of
65// that pending one, and after every worker stop removes one. A write that
66// fails is logged and marks the publication dirty, and the sweep retries it.
67// A failed write before an exec refuses that spawn; elsewhere it blocks
68// nothing, because root reads a publication that did not finish as an
69// incomplete pin set (release.ReadGenerations) and then neither restages nor
70// sweeps.
71
72// pendingGeneration names one build a spawn is about to exec, recorded by
73// startWorker's before-exec hook until the spawn installs or fails. 0 is
74// none.
75type pendingGeneration uint64
76
77type pendingBuild struct {
78 host string
79 id release.StagedIdentity
80}
81
82// addPendingGeneration records id as a build host's spawn is about to exec,
83// and returns its name. Never call it with a.mu held.
84func (a *Service) addPendingGeneration(host string, id release.StagedIdentity) pendingGeneration {
85 a.mu.Lock()
86 defer a.mu.Unlock()
87 return a.addPendingGenerationLocked(host, id)
88}
89
90// addPendingGenerationLocked is addPendingGeneration for a caller that holds
91// a.mu: a hold registers its build in the same critical section as its
92// admission (swapHold).
93func (a *Service) addPendingGenerationLocked(host string, id release.StagedIdentity) pendingGeneration {
94 if a.genPending == nil {
95 a.genPending = map[pendingGeneration]pendingBuild{}
96 }
97 a.genPendingSeq++
98 k := pendingGeneration(a.genPendingSeq)
99 a.genPending[k] = pendingBuild{host: host, id: id}
100 return k
101}
102
103// dropPendingGenerationLocked forgets a pending build: its spawn failed, its
104// child was shed, or its captured identity has been installed on the entry.
105// 0 is a no-op. The caller holds a.mu.
106func (a *Service) dropPendingGenerationLocked(k pendingGeneration) {
107 if k != 0 {
108 delete(a.genPending, k)
109 }
110}
111
112// generationsSnapshotLocked is what the generations file should say now: for
113// every entry (waking, awake, mid-swap, mid-respawn or stopping, since its
114// processes may still run) what entryGenerationsLocked names, the build of
115// every retained failed wake (unprovenWakes), and every pending build, each
116// listed under its host by version when the version is a valid version name,
117// and by staged path (made absolute) when it has one. A build with neither
118// names nothing root could restage or sweep. Each host's lists are sorted and
119// hold each entry once. The caller holds a.mu.
120func (a *Service) generationsSnapshotLocked() release.Generations {
121 hosts, paths := map[string]map[string]bool{}, map[string]map[string]bool{}
122 add := func(host string, id release.StagedIdentity) {
123 if v := id.Version; v != "" && release.ValidVersion(v) {
124 if hosts[host] == nil {
125 hosts[host] = map[string]bool{}
126 }
127 hosts[host][v] = true
128 }
129 if p := id.StagedPath; p != "" {
130 // A relative path is published as the exec resolved it, from
131 // this process's working directory; if even that fails, the
132 // relative path stays and WriteGenerations refuses it, so the
133 // spawn does not happen.
134 if abs, err := filepath.Abs(p); err == nil {
135 p = abs
136 }
137 if paths[host] == nil {
138 paths[host] = map[string]bool{}
139 }
140 paths[host][p] = true
141 }
142 }
143 for host, e := range a.entries {
144 a.entryGenerationsLocked(e, func(id release.StagedIdentity) { add(host, id) })
145 }
146 // A failed wake retained until its termination is proven: its entry is
147 // gone, but its process may still run the build (F12; spec §4.2, §8.5).
148 // A worker's is its captured identity, an instance's its generation.
149 for host, u := range a.unprovenWakes {
150 add(host, u.build)
151 if id, ok := a.genIdentity(u.gen); ok {
152 add(host, id)
153 }
154 }
155 for _, pb := range a.genPending {
156 add(pb.host, pb.id)
157 }
158 return release.Generations{Hosts: sortedLists(hosts), Paths: sortedLists(paths)}
159}
160
161// entryGenerationsLocked calls add with every build e's processes run or may
162// be about to run (spec §8.5):
163// - a worker's captured serving build;
164// - the current generation, and every operation-set member's (successor,
165// retiring, spawning), and the build of a shed worker child the set (or,
166// with generations off, the entry) retains (final review F12, R2-1);
167// - a hold's pinned generation, from the moment its waking entry exists;
168// - a planned wake's build and its fallback, from the moment the plan is
169// stored, so both are on disk before either execs;
170// - a socketless attempt's build and fallback (Task 11);
171// - the generation every run not yet proven settled was admitted
172// against (merge ruling M2).
173//
174// The plan's builds stay listed while the entry lives, as e.plan does
175// (afterWake reads it after the wake): a few more pins than the processes
176// strictly need, never fewer. The caller holds a.mu.
177func (a *Service) entryGenerationsLocked(e *actEntry, add func(release.StagedIdentity)) {
178 gen := func(g Gen) {
179 if id, ok := a.genIdentity(g); ok {
180 add(id)
181 }
182 }
183 if e.worker {
184 add(e.workerServing)
185 }
186 gen(e.gen)
187 if e.pin != nil {
188 gen(*e.pin)
189 }
190 if e.plan.planned() {
191 add(e.plan.Boot)
192 if e.plan.Fallback != nil {
193 add(*e.plan.Fallback)
194 }
195 }
196 for _, m := range e.ops.members {
197 add(m.build)
198 gen(m.gen)
199 }
200 if e.shed != nil {
201 add(e.shed.build)
202 }
203 for _, v := range e.attemptBuilds {
204 add(release.StagedIdentity{Version: v})
205 }
206 // A run's generation, until the run is proven settled (merge ruling
207 // M2): its command execs that build, and may outlive the generation's
208 // own process once a swap retires it.
209 for _, h := range cmdHoldsLocked(e) {
210 gen(h.gen)
211 }
212}
213
214// genIdentity is g as the generations file names it, and whether it is
215// listed at all. A generation exec'd from a staged path is listed whatever
216// the switch says. Otherwise it is listed only with generations on: with
217// them off an instance's generation is the live link's build, which root
218// pins as the row's Version already, and plan 4's flag-off file never
219// named it.
220func (a *Service) genIdentity(g Gen) (release.StagedIdentity, bool) {
221 if g.StagedPath == "" && (!a.gens || g.Build == "") {
222 return release.StagedIdentity{}, false
223 }
224 return release.StagedIdentity{Version: g.Build, StagedPath: g.StagedPath}, true
225}
226
227// publishesLocked reports whether e names anything in the generations file,
228// so a change that removes e must rewrite it. The caller holds a.mu.
229func (a *Service) publishesLocked(e *actEntry) bool {
230 named := false
231 a.entryGenerationsLocked(e, func(id release.StagedIdentity) {
232 if id.StagedPath != "" || release.ValidVersion(id.Version) {
233 named = true
234 }
235 })
236 return named
237}
238
239// publishBeforeExec writes the generations file before a staged instance
240// spawn's exec (spec §4.2, §8.5; the index's generations-file contract).
241// The caller has already put the build about to run on its entry (e.plan,
242// e.pin, the operation set), so the snapshot names it. When the write fails
243// the caller does not exec: root reads only what the file names, and could
244// sweep or restage a build it does not name under the process. The refusal
245// is errStagedBuildNotPublished, which classifyStep reads as abandonment,
246// never a failure of the build. The write error itself goes to the operator
247// log (persistGenerations), not into the returned error, which may reach a
248// member's wake event (as the fence's wording, memberSafeWakeReason). Never
249// call it with a.mu held.
250func (a *Service) publishBeforeExec() error {
251 if err := a.persistGenerations(); err != nil {
252 return errStagedBuildNotPublished
253 }
254 return nil
255}
256
257func sortedLists(m map[string]map[string]bool) map[string][]string {
258 out := make(map[string][]string, len(m))
259 for k, set := range m {
260 l := make([]string, 0, len(set))
261 for v := range set {
262 l = append(l, v)
263 }
264 sort.Strings(l)
265 out[k] = l
266 }
267 return out
268}
269
270// persistGenerations writes the generations file from a snapshot of the
271// entries. Never call it with a.mu held: it takes genMu first, then a.mu only
272// for the snapshot, and does the file I/O off a.mu. genMu serialises the
273// writers, and each one snapshots after taking it, so an older snapshot can
274// never land over a newer one. A no-op when GenerationsFile is empty.
275func (a *Service) persistGenerations() error {
276 if a.GenerationsFile == "" {
277 return nil
278 }
279 a.genMu.Lock()
280 defer a.genMu.Unlock()
281 return a.persistGenerationsGenLocked()
282}
283
284// persistGenerationsGenLocked is persistGenerations' body. The caller holds
285// genMu and not a.mu.
286func (a *Service) persistGenerationsGenLocked() error {
287 a.mu.Lock()
288 g := a.generationsSnapshotLocked()
289 a.mu.Unlock()
290 if err := release.WriteGenerations(a.GenerationsFile, g); err != nil {
291 a.genDirty = true
292 log.Printf("activator: publish generations to %s: %v; the sweep retries", a.GenerationsFile, err)
293 return err
294 }
295 a.genWritten = true
296 a.genDirty = false
297 return nil
298}
299
300// publishGenerationsBeforeSpawn makes sure the generations file has been
301// written once before a worker is spawned. A failure is logged and left to
302// the sweep; this write does not decide the spawn, the staged spawn's own
303// before-exec publication does (startWorker). Never call it with a.mu held.
304func (a *Service) publishGenerationsBeforeSpawn() {
305 if a.GenerationsFile == "" {
306 return
307 }
308 a.genMu.Lock()
309 defer a.genMu.Unlock()
310 if a.genWritten {
311 return
312 }
313 _ = a.persistGenerationsGenLocked()
314}
315
316// retryGenerations rewrites the generations file if the last write failed.
317// The sweep calls it after releasing a.mu.
318func (a *Service) retryGenerations() {
319 if a.GenerationsFile == "" {
320 return
321 }
322 a.genMu.Lock()
323 defer a.genMu.Unlock()
324 if !a.genDirty {
325 return
326 }
327 _ = a.persistGenerationsGenLocked()
328}
329