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 "log"
7 "os"
8 "path/filepath"
9 "runtime"
10 "sync"
11 "sync/atomic"
12 "time"
13
14 "github.com/carlosframework/platform/internal/activator"
15 "github.com/carlosframework/platform/internal/appconfig"
16 "github.com/carlosframework/platform/internal/cgroup"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/rollout"
20 "github.com/carlosframework/platform/internal/status"
21)
22
23// The reason codes RolloutReport.Unavailable carries (zero-downtime deploys
24// spec §15, §8.4). They are codes, not member-facing text: plan 6 words
25// them.
26const (
27 // The flag is on but the box cannot contain generations, so the
28 // scheduler is not armed and every leg stays on today's paths (swapArming).
29 rolloutNoDelegation = "no-cgroup-delegation"
30 rolloutNoGenControl = "no-generation-control"
31 rolloutGenerationsOff = "generations-off"
32 // The flag is on but host claims are on: claims-backed launches cannot
33 // yet carry a swap's recovery choice, its execution evidence, the
34 // generations file or the good build (#272, "claims-on swaps"), so the
35 // box declines and stays on today's paths.
36 rolloutHostClaims = "host-claims-on"
37 // The flag is on and the box has delegation, but a test placement into
38 // a scratch cgroup through the shim failed (placementCheck): generation
39 // starts could not be placed inside their limits (#272, the second
40 // dev-box check), so the box declines and stays on today's paths.
41 rolloutPlacementFailed = "cgroup-placement-failed"
42 // The flag is on and generations are on, but the scheduler's required
43 // state is unavailable, so every managed start is refused
44 // (refuseManaged).
45 rolloutNoStore = "no-deploy-store"
46 rolloutNoReleaseKey = "no-release-key"
47 rolloutBookUnreadable = "failure-book-unreadable"
48)
49
50// refuseTexts is the wake refusal each refuseManaged code carries. It reaches
51// a member through the activator's "wake failed:" event, so it names the
52// missing fact and never the raw error, which goes to the agent's log. The
53// unreadable book's text is the scheduler's own (rollout Task 5 refuses a
54// wake whose intent it cannot write with it), shared rather than copied.
55var refuseTexts = map[string]string{
56 rolloutNoStore: "this box cannot start instances right now: the deploy store is unavailable",
57 rolloutNoReleaseKey: "this box cannot start instances right now: the release key is unavailable",
58 rolloutBookUnreadable: rollout.BookUnreadableRefusal,
59}
60
61var rolloutMgr atomic.Pointer[rollout.Manager]
62
63// rolloutMaterialize is the exec backend's Materialize hook, handed to the
64// scheduler so a cold managed host is installed before its wake is planned
65// (Task 10). runAgent sets it before armRollout; nil refuses such a wake.
66var rolloutMaterialize func(ctx context.Context, host string) error
67
68// armedRollout is the box's rollout manager, nil unless CARLOS_SWAP=overlap
69// armed one. Every leg that would bounce an exec child asks it first.
70func armedRollout() *rollout.Manager { return rolloutMgr.Load() }
71
72// statusNudge asks the status loop for an early tick: a rollout transition
73// should reach status/boxes/<label>.json in seconds, not at the next
74// five-minute tick. Never sent to with the flag off. Capacity one, and every
75// send is non-blocking, so a burst of transitions is one nudge.
76var statusNudge = make(chan struct{}, 1)
77
78// nudgeStatus is the scheduler's OnChange: a non-blocking send that
79// coalesces, as Deps.OnChange requires. It never waits for the status loop.
80func nudgeStatus() {
81 select {
82 case statusNudge <- struct{}{}:
83 default:
84 }
85}
86
87// rolloutDown is why CARLOS_SWAP=overlap is set but the scheduler could not
88// arm while generations are on, or why a box that declined the flag cannot
89// read the failure records it must honour: managed starts are refused and
90// the bounce legs stand down (refuseManaged). Nil when it armed, when the
91// flag is off, or when the box declined the flag (rolloutDeclined) with a
92// readable failure book.
93var rolloutDown atomic.Pointer[string]
94
95// rolloutDeclined is why CARLOS_SWAP=overlap is set but the box stayed on
96// today's paths: it cannot contain generations, has host claims on, or
97// failed its arm-time test placement (swapArming). Unlike
98// rolloutDown nothing is refused and no leg changes, except where failure
99// records an earlier armed run left hold a build for an instance
100// (honourDeclinedFailures, final review F6). The reason is published in the
101// box's status.
102var rolloutDeclined atomic.Pointer[string]
103
104// rolloutUnavailable is rolloutDown's reason, "" when there is none. A leg
105// that bounces exec children stands down while it is set.
106func rolloutUnavailable() string {
107 if p := rolloutDown.Load(); p != nil {
108 return *p
109 }
110 return ""
111}
112
113// rolloutUnarmed is rolloutDeclined's reason, "" when there is none.
114func rolloutUnarmed() string {
115 if p := rolloutDeclined.Load(); p != nil {
116 return *p
117 }
118 return ""
119}
120
121func declineRollout(code string) {
122 c := code
123 rolloutDeclined.Store(&c)
124}
125
126// swapDelegation is what this box lacks to contain generations, "" when
127// nothing. A flat or foreign row's generation children live under the
128// agent's delegated cgroup subtree (eb.Cgroups): without it a spawn still
129// runs, unplaced, but KillGen and the parent raise answer
130// cgroup.ErrNoDelegation, so an overlap would be abandoned and a failed first
131// wake's kill would be unproven. A nested row's generations are root's
132// carlos-instance-gen@ units, reached through the instance control channel,
133// which must speak the generation half (activator.GenControl); whether root
134// can run them is asked per wake by the backend's nested gate (eb.NestedOK)
135// and per call by the channel itself, and a failed StopGen is an unproven
136// kill the activator already keeps the lease for. So the box-wide question
137// for nested rows is only whether the channel has the generation verbs.
138func swapDelegation(eb *activator.ExecBackend) string {
139 if eb == nil || eb.Cgroups == nil {
140 return rolloutNoDelegation
141 }
142 if _, ok := eb.Control.(activator.GenControl); !ok {
143 return rolloutNoGenControl
144 }
145 return ""
146}
147
148// placementCheck is swapArming's arm-time placement check: one real shim
149// placement into a scratch child of the delegated subtree
150// (cgroup.Manager.VerifyPlacement). VerifyPlacement bounds itself, the
151// shim's waits included, at 4 s, so a decline lands well inside the 20 s
152// `carlos system update` gives the restarted edge (systemGateWait). A seam
153// host tests replace, since a temp-dir root can never pass it.
154var placementCheck = func(m *cgroup.Manager) error {
155 return m.VerifyPlacement(context.Background())
156}
157
158// swapArming decides, before the activator exists, whether CARLOS_SWAP=overlap
159// turns generations on (spec §15; plan 3's review: missing delegation must
160// fail closed, CODEX-FINDINGS-ZDD-IMPL-3.md:138). With the flag off it is
161// false and says nothing. With the flag on but host claims on, or no usable
162// containment, it is false too (claims-on boxes get no swaps, the owner's
163// ruling, #272): generations, generation containment and the scheduler all stay
164// off, every leg keeps today's paths but for the failure records armRollout
165// honours (honourDeclinedFailures), and the refusal is logged loudly and
166// published as a reason code in the box's status (rolloutStatusProbe). With
167// containment present it asks for one real placement last (placementCheck),
168// and declines the same way when it fails: an armed box whose generation
169// starts can never be placed fails every one of them (#272).
170func swapArming(getenv func(string) string, eb *activator.ExecBackend) bool {
171 if !rollout.Enabled(getenv) {
172 return false
173 }
174 if eb != nil && eb.Cfg.HostWrites.Enabled {
175 log.Printf("agent: REFUSING %s=overlap (%s): host claims are on, and claims-backed launches cannot yet keep a swap's "+
176 "recovery choice, execution evidence, generations file or good build, so this box stays on today's stop-then-wake paths",
177 rollout.SwapEnv, rolloutHostClaims)
178 declineRollout(rolloutHostClaims)
179 return false
180 }
181 if why := swapDelegation(eb); why != "" {
182 log.Printf("agent: REFUSING %s=overlap (%s): generation containment needs the agent's delegated cgroup subtree "+
183 "and the control channel's generation units, so this box stays on today's stop-then-wake paths", rollout.SwapEnv, why)
184 declineRollout(why)
185 return false
186 }
187 if err := placementCheck(eb.Cgroups); err != nil {
188 log.Printf("agent: REFUSING %s=overlap (%s): a test placement into a scratch cgroup failed (%v), so generations could not be started inside their limits; this box stays on today's stop-then-wake paths", rollout.SwapEnv, rolloutPlacementFailed, err)
189 declineRollout(rolloutPlacementFailed)
190 return false
191 }
192 return true
193}
194
195// refuseManaged is the flag on with the scheduler's required state
196// unavailable (spec §6.4, §8.3, §8.4). The legacy paths are NOT a fallback:
197// a wake from the live link could exec a build the unreadable failure book
198// holds down, including a hold build after its migration failed. So every
199// managed wake is refused before the lease, and an unmanaged one (a named
200// unit, a unit-backed row) wakes as today. The bounce-based legs stand down
201// too (fastConvergeLoop, adoptBounceFn, restartRequestLeg,
202// syncExecSerialized): a stop whose next wake is refused would only take an
203// instance down. So do the activator's own two moves onto the live link: the
204// sweep's convergence stop of an awake instance (OnDrift answers it with
205// nothing, as the armed scheduler answers an unmanaged host) and a worker's
206// recycle converging onto the live link (CoalesceRecycle present and false:
207// the recycle respawns the worker's captured build). The reason code is
208// published in the box's status (rolloutStatusProbe) until the agent
209// restarts with the state readable.
210func refuseManaged(svc *activator.Service, reason string) {
211 log.Printf("agent: %s=overlap but %s: refusing every managed start, and every stop and move onto a new build, until the agent restarts with it readable",
212 rollout.SwapEnv, reason)
213 r := reason
214 rolloutDown.Store(&r)
215 text := refuseTexts[reason]
216 if text == "" {
217 text = refuseNoSwaps
218 }
219 svc.Planner = func(_ context.Context, rt registry.Route) activator.WakePlan {
220 if !rollout.Managed(rt) {
221 return activator.WakePlan{}
222 }
223 return activator.WakePlan{Refuse: text}
224 }
225 svc.OnDrift = func(string) {}
226 svc.CoalesceRecycle = func(string) bool { return false }
227}
228
229// servingWriter is OnWorkerServing's registry write, off the publisher's
230// path. The activator delivers each worker's serving identity under its
231// box-wide servingMu, inside the respawn and before the respawn is marked
232// done, so a slow registry write there would delay every host's publication,
233// a stop and a lease release behind it. publish only records the newest
234// version per host and returns; one drain goroutine at a time writes them.
235// Deliveries arrive in incarnation order (servingMu), each overwrites the
236// host's pending value, and the drain writes a host's values one after
237// another, so the last write for a host is always the newest identity it
238// was handed: an older incarnation's version can never land last. A
239// coalesced intermediate version is never written, which only skips a value
240// that was already stale.
241type servingWriter struct {
242 set func(host, ver string) error
243 mu sync.Mutex
244 pending map[string]string
245 running bool
246}
247
248func newServingWriter(set func(host, ver string) error) *servingWriter {
249 return &servingWriter{set: set, pending: map[string]string{}}
250}
251
252func (w *servingWriter) publish(host, ver string) {
253 w.mu.Lock()
254 w.pending[host] = ver
255 start := !w.running
256 w.running = true
257 w.mu.Unlock()
258 if start {
259 go w.drain()
260 }
261}
262
263func (w *servingWriter) drain() {
264 for {
265 w.mu.Lock()
266 host, ver, found := "", "", false
267 for h, v := range w.pending {
268 host, ver, found = h, v, true
269 break
270 }
271 if !found {
272 w.running = false
273 w.mu.Unlock()
274 return
275 }
276 delete(w.pending, host)
277 w.mu.Unlock()
278 if err := w.set(host, ver); err != nil {
279 log.Printf("agent: %s: record the worker's serving build %q: %v", host, ver, err)
280 }
281 }
282}
283
284// armAlways wires what the activator needs whatever CARLOS_SWAP says: a
285// worker's serving identity (an empty version included) becomes the
286// registry's ServingVersion, which `carlos sidecars ls` reads (spec §7.9,
287// Task 11). The write goes through a servingWriter so the callback is cheap.
288// The generations file is not set here: runAgent sets it flag-independently
289// (plan 4 R17).
290func armAlways(svc *activator.Service, reg *registry.Registry, dd string) {
291 w := newServingWriter(reg.SetServingVersion)
292 svc.OnWorkerServing = func(host string, id release.StagedIdentity) { w.publish(host, id.Version) }
293}
294
295// rolloutPolicy is the scheduler's policy loader (spec §4.1): the app's
296// default config bundle. appconfig.Get answers an error for every store or
297// decode failure, and only a confirmed missing object (os.ErrNotExist) as an
298// empty manifest, which rollout.PolicyFromManifest reads exactly as nil:
299// the defaults. A failed read leaves the policy cache with its last value,
300// or hold for an app it has never read, so a bucket failure can never
301// become an overlap.
302func rolloutPolicy(store release.Store) func(ctx context.Context, acct, app string) (*appconfig.Manifest, error) {
303 return func(ctx context.Context, acct, app string) (*appconfig.Manifest, error) {
304 return appconfig.Get(ctx, store, acct, app)
305 }
306}
307
308// touchStageMarker writes the stage-request marker, as stageRequester does.
309// A failed write costs latency; carlos-adopt.timer is the floor. Task 15's
310// stage relay uses it too.
311func touchStageMarker(dd string) {
312 if err := os.WriteFile(filepath.Join(dd, stageRequestMarker), []byte(time.Now().UTC().Format(time.RFC3339)+"\n"), 0o644); err != nil {
313 log.Printf("agent: stage relay: %v", err)
314 }
315}
316
317// armRollout builds the scheduler when CARLOS_SWAP=overlap (spec §15) and
318// wires it into svc, which must not have started yet. With the flag off it
319// returns nil and touches nothing. With the flag on but svc built without
320// generations (swapArming declined the flag) it returns nil and touches
321// nothing either: a manager is never armed over a Service that cannot run
322// its attempts. It does honour the failure records an earlier armed run left
323// (honourDeclinedFailures, final review F6): an infrastructure prerequisite
324// failing is not an operator clearing a failure hold. With the flag on but
325// no deploy store, no public key or an
326// unreadable failure book, it returns nil and refuses every managed start
327// (refuseManaged): each of those is a fact the scheduler cannot do without
328// (the canonical demand, the retry signal, the records), and today's paths
329// would ignore the failure records.
330func armRollout(ctx context.Context, getenv func(string) string, svc *activator.Service, reg *registry.Registry, dd string,
331 sink activator.EventSink, openStore func(context.Context) (release.Store, error)) *rollout.Manager {
332 if svc == nil || !rollout.Enabled(getenv) {
333 return nil
334 }
335 if !svc.GenerationsOn() {
336 if rolloutUnarmed() == "" {
337 log.Printf("agent: REFUSING %s=overlap: the activator was built without generations", rollout.SwapEnv)
338 declineRollout(rolloutGenerationsOff)
339 }
340 honourDeclinedFailures(svc, reg, dd, release.OptDir())
341 return nil
342 }
343 store, err := openStore(ctx)
344 if err != nil {
345 log.Printf("agent: %s=overlap: deploy store: %v", rollout.SwapEnv, err)
346 refuseManaged(svc, rolloutNoStore)
347 return nil
348 }
349 pub, err := release.ParsePublicKey(getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
350 if err != nil {
351 log.Printf("agent: %s=overlap: release key: %v", rollout.SwapEnv, err)
352 refuseManaged(svc, rolloutNoReleaseKey)
353 return nil
354 }
355 book, err := rollout.OpenFailureBook(dd)
356 if err != nil {
357 log.Printf("agent: %s=overlap: failure book: %v", rollout.SwapEnv, err)
358 refuseManaged(svc, rolloutBookUnreadable)
359 return nil
360 }
361 if n := book.Reconciled(); n > 0 {
362 // Executions an earlier agent began and never saw recorded (Task 5,
363 // spec §8.4): their intents are failure records now, saved before
364 // anything runs (the Flush below). A box that cannot save them must
365 // not run either: its next restart would find the intents again, and
366 // nothing it recorded meanwhile would be kept.
367 log.Printf("agent: %s=overlap: %d execution(s) an earlier agent left unrecorded are now failure records", rollout.SwapEnv, n)
368 }
369 // The book is saved before anything runs: an open may owe a save (a new
370 // book's identity, a book an earlier agent wrote given a new one,
371 // reconciled intents), and until it lands status publishes no
372 // generation and no retry grant clears anything (the crash-epoch hard
373 // limit in rollout.FailureBook's doc). A box that cannot save refuses
374 // managed starts, as it does for reconciliation.
375 if err := book.Flush(); err != nil {
376 log.Printf("agent: %s=overlap: save the failure book after opening it: %v", rollout.SwapEnv, err)
377 refuseManaged(svc, rolloutBookUnreadable)
378 return nil
379 }
380 platform := runtime.GOOS + "-" + runtime.GOARCH
381 m := rollout.NewManager(ctx, rollout.Deps{
382 Act: svc, Routes: reg, Book: book, OptDir: release.OptDir(), DataDir: dd, Box: BoxLabel(),
383 Demand: func(ctx context.Context, acct, app, ver, path string) (string, string, error) {
384 return release.CanonicalDemand(ctx, store, acct, app, ver, platform, path)
385 },
386 Policy: rolloutPolicy(store),
387 // The raw signed read (PF4): only its Version and PromotedAt are
388 // used, to clear failure records.
389 Pointer: func(ctx context.Context, acct, app, channel string) (*release.ChannelPointer, error) {
390 return release.Channel(ctx, store, pub, acct, app, channel)
391 },
392 Events: sink,
393 Materialize: rolloutMaterialize, // the exec backend's hook, set by runAgent
394 Touch: func() { touchStageMarker(dd) },
395 OnChange: nudgeStatus,
396 })
397 svc.Planner = m.Plan
398 svc.OnGen = m.OnGen
399 svc.OnDrift = m.Nudge
400 svc.CoalesceRecycle = m.CoalesceRecycle
401 if routes, err := reg.List(); err == nil {
402 m.Bootstrap(routes) // spec §8.5: awake, healthy instances record their serving build
403 m.Pass(routes)
404 }
405 rolloutMgr.Store(m)
406 log.Printf("agent: overlap swaps armed (%s=overlap)", rollout.SwapEnv)
407 return m
408}
409
410// rolloutLegFor is the converge leg with the flag on (spec §10): the agent
411// pass of spec §6.1, every adoptedConvergeEvery, in place of convergeAdopted.
412func rolloutLegFor(m *rollout.Manager, reg *registry.Registry) func() {
413 return func() {
414 routes, err := reg.List()
415 if err != nil {
416 return
417 }
418 m.Pass(routes)
419 }
420}
421
422// fastConvergeLeg is the leg fastConvergeLoop runs every
423// adoptedConvergeEvery: the scheduler's pass when it is armed, none while
424// managed starts are refused (legacy convergence would stop instances whose
425// next wake is refused), and today's convergeAdopted otherwise, the flag off
426// or declined included (declined with failure records, it passes by a held
427// host: convergeLegFor).
428func fastConvergeLeg(svc *activator.Service, reg *registry.Registry) func() {
429 if m := armedRollout(); m != nil {
430 return rolloutLegFor(m, reg)
431 }
432 if rolloutUnavailable() != "" {
433 return nil
434 }
435 return convergeLegFor(svc, reg)
436}
437
438// rolloutConfigCallbacks is config sync's callbacks when the scheduler, or
439// the failure records, have a say, and false when they have none (the flag
440// off, or declined with no records), in which case syncExecSerialized runs
441// today's SyncExec unchanged.
442//
443// Armed and refusing, the pass splits as the scheduler does
444// (appconfig.Callbacks.Managed, final review F7): an exec route the scheduler
445// does not manage gets today's restart (legs.restart, svc.Bounce) and today's
446// ack, never a no-op that claims a restart. A managed route:
447//
448// Armed: the scheduler restarts it, paced, and a revision is acknowledged
449// only once it is active (spec §8.1, §10); appconfig calls Delivered after
450// the file is written (syncExecHostClaimed), for a managed native sidecar
451// too, whose config attempt is the scheduler's. A sleeper's ack is
452// Hibernating (configTrack).
453//
454// Refusing: delivered and acknowledged at delivery, but no restart, because
455// the wake after it would be refused. An AWAKE managed host (legs.awake says
456// which) still runs its old config, so its restart answers an error and the
457// ack carries RestartError with Restarted false rather than claiming a
458// restart that did not happen (spec §8.1). A sleeper reads the file on its
459// next wake, so its ack says delivered on wake (appconfig.ErrDeliveredOnWake
460// sets Hibernating, the unit pass's marker).
461//
462// Declined with records: today's restart and ack for every host but one a
463// record holds (failureHolds.configCallbacks).
464func rolloutConfigCallbacks(ctx context.Context, store release.Store, legs configLegs) (appconfig.Callbacks, bool) {
465 if m := armedRollout(); m != nil {
466 return appconfig.Callbacks{
467 Restart: func(string) error { return nil },
468 Delivered: m.ConfigDelivered,
469 Ack: m.DeferAck(configAckWriter(store, "agent")),
470 Managed: rollout.Managed,
471 Unmanaged: legs.restart,
472 }, true
473 }
474 if rolloutUnavailable() != "" {
475 restart := func(host string) error {
476 if legs.awake != nil && legs.awake(host) {
477 return errors.New(rolloutRestartRefused)
478 }
479 return appconfig.ErrDeliveredOnWake
480 }
481 return appconfig.Callbacks{Restart: restart, Ack: configAckFunc(ctx, store, "agent"),
482 Managed: rollout.Managed, Unmanaged: legs.restart}, true
483 }
484 if h := heldHolds(); h != nil {
485 return h.configCallbacks(configAckFunc(ctx, store, "agent"), legs), true
486 }
487 return appconfig.Callbacks{}, false
488}
489
490// feedRestarts is the restart leg's exec half with the flag on (spec §10):
491// each moved app's exec instances and sidecars, and each narrowed request's
492// hosts, get a restart attempt identified by the request (the app-wide
493// generation's etag, or the newest narrowed request's id). The root half,
494// named units and unit backing, is unchanged.
495func feedRestarts(routes []registry.Route, moved [][2]string, scoped []scopedRestart, gen map[[2]string]string, feed func(host, id string) bool) int {
496 want := map[[2]string]bool{}
497 for _, k := range moved {
498 want[k] = true
499 }
500 n := 0
501 for _, rt := range routes {
502 k := [2]string{release.RouteAccount(rt), rt.App}
503 if !want[k] || !restartEligible(rt) || !execBounceable(rt) {
504 continue
505 }
506 if feed(rt.Host, "gen:"+gen[k]) {
507 n++
508 }
509 }
510 for _, w := range scoped {
511 if len(w.requests) == 0 {
512 continue
513 }
514 id := "scope:" + w.requests[len(w.requests)-1].ID
515 for _, host := range scopedHosts(routes, w.app, w.requests, execBounceable) {
516 if feed(host, id) {
517 n++
518 }
519 }
520 }
521 return n
522}
523
524// configLegs is what config sync's callbacks need from the activator.
525type configLegs struct {
526 // awake reports a managed instance the activator has a live entry for
527 // (managedAwake). Nil: nothing is awake.
528 awake func(host string) bool
529 // restart is today's restart, bounce(svc): what config sync does to an
530 // exec instance with the flag off. Nil: no restart.
531 restart func(host string) error
532}
533
534// rolloutStatusProbe feeds status.Probes.Rollout: nil with the flag off, the
535// scheduler's report when it is armed, and only the reason code when the
536// flag is on but the scheduler is not armed (refusing, or declined). A
537// declined box's report keeps the decline code and says what it does with
538// the failure records (FailureBook): honoured, unreadable (every managed
539// start refused), or nothing when there are none. Honoured, it also names
540// each instance the book holds, with the build holding it and that
541// restriction's epoch (failureHolds.report; Task 10, plan review #5), so
542// the console can sign a retry for it.
543func rolloutStatusProbe() *status.RolloutReport { return rootRetryStatus(legacyRolloutStatusProbe()) }
544
545func legacyRolloutStatusProbe() *status.RolloutReport {
546 m := armedRollout()
547 if m == nil {
548 if why := rolloutUnarmed(); why != "" {
549 r := &status.RolloutReport{Unavailable: why, FailureBook: rolloutUnavailable()}
550 if h := heldHolds(); h != nil {
551 r.FailureBook = rolloutBookHonoured
552 r.Hosts = h.report()
553 }
554 return r
555 }
556 if why := rolloutUnavailable(); why != "" {
557 return &status.RolloutReport{Unavailable: why}
558 }
559 return nil
560 }
561 r := m.Status()
562 return &r
563}
564
565// rolloutRestartRefused is the RestartError an awake managed host's config
566// ack carries while managed starts are refused. It is fixed text: the cause
567// goes to the agent's log, never to the member (plan 6 words it).
568const rolloutRestartRefused = "restart refused: this box cannot restart instances right now"
569
570// managedAwake reports whether host is a managed exec instance the activator
571// has a live entry for (waking, awake or stopping, as Owns counts them), so
572// one whose process may still hold its old config. A host that is not in the
573// registry, not managed, or not owned is not awake; nor is anything with no
574// activator.
575func managedAwake(svc *activator.Service, reg *registry.Registry) func(host string) bool {
576 return func(host string) bool {
577 if svc == nil || reg == nil || !svc.Owns(host) {
578 return false
579 }
580 rt, err := reg.Get(host)
581 return err == nil && rt != nil && rollout.Managed(*rt)
582 }
583}
584