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 "log"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 "github.com/carlosframework/platform/internal/registry"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// versionSweepGraceEnv overrides release.DefaultSweepGrace, as a Go duration.
16// An escape hatch for a box that ships far faster than the fleet (or an
17// operator who wants a longer rollback window on one), not something any unit
18// sets: the default is the fleet's answer.
19const versionSweepGraceEnv = "CARLOS_VERSIONS_GRACE"
20
21func versionSweepGrace(getenv func(string) string) time.Duration {
22 raw := getenv(versionSweepGraceEnv)
23 if raw == "" {
24 return release.DefaultSweepGrace
25 }
26 d, err := time.ParseDuration(raw)
27 if err != nil || d < 0 {
28 log.Printf("adopt: %s=%q is not a non-negative duration — sweeping with the default %s", versionSweepGraceEnv, raw, release.DefaultSweepGrace)
29 return release.DefaultSweepGrace
30 }
31 return d
32}
33
34// routeLister is what the sweep needs of the registry: the rows, unfiltered.
35// Deliberately NOT release.Adoptable or any awake/canary/TTL filter — those
36// answer "should this route move to a new version", and the question here is
37// "is this version still spoken for", which a hibernating route, an expired
38// canary and a route mid-rollback all answer yes to.
39type routeLister interface {
40 List() ([]registry.Route, error)
41}
42
43// sweepStagedVersions evicts the staged version trees nothing on this box
44// still names (discussion #111).
45//
46// genPath is the generations file (release.GenerationsPath of the data dir).
47//
48// It never fails the pass. Every failure path here declines to delete and
49// returns: an unreadable live dir, an unlistable registry or an incomplete
50// generations file means the pin set is incomplete, and an incomplete pin set is how a running app loses its
51// binary. Freeing disk an hour later is cheap; deleting a live version is not.
52func sweepStagedVersions(reg routeLister, optDir, genPath string, grace time.Duration, now time.Time, sources ...release.PinSource) {
53 pinned, err := release.LivePins(optDir)
54 if err != nil {
55 log.Printf("adopt: staged-version sweep skipped: %v", err)
56 return
57 }
58 live := len(pinned)
59 routes, err := reg.List()
60 if err != nil {
61 log.Printf("adopt: staged-version sweep skipped: list routes: %v", err)
62 return
63 }
64 for _, r := range routes {
65 // PrevVersion is a pin with no grace attached: it is the rollback
66 // target, and a rollback is wanted precisely when a box has been
67 // running the new build long enough for the grace to have expired.
68 if r.Version != "" {
69 pinned[r.Version] = true
70 }
71 if r.PrevVersion != "" {
72 pinned[r.PrevVersion] = true
73 }
74 // GoodVersion likewise (spec §8.5): it is what a failed first wake
75 // falls back to, however long ago it last served.
76 if r.GoodVersion != "" {
77 pinned[r.GoodVersion] = true
78 }
79 }
80 // The builds the instances' settings and build states name (ruling
81 // R-T9-STORE): a held instance's recovery onto last_executed runs its
82 // staged copy here (EvidenceStaged), so that copy is spoken for however
83 // long ago it last ran.
84 settings, err := settingsPins(routes)
85 if err != nil {
86 log.Printf("adopt: staged-version sweep skipped: %v", err)
87 return
88 }
89 for v := range settings {
90 pinned[v] = true
91 }
92 // Every live or pending generation the agent published (spec §8.5,
93 // ruling R19 item 1): a worker's captured build stays pinned after the
94 // live link and the row have both moved on. Neither file present is no
95 // generations; a file that does not read or whose publication did not
96 // finish is an incomplete pin set.
97 gens, err := release.GenerationVersions(genPath, optDir)
98 if err != nil {
99 log.Printf("adopt: staged-version sweep skipped: generations: %v", err)
100 return
101 }
102 for v := range gens {
103 pinned[v] = true
104 }
105 for _, source := range sources {
106 paths, err := source.StagedPins(context.Background(), optDir)
107 if err != nil {
108 log.Printf("adopt: staged-version sweep skipped: generations: %v", err)
109 return
110 }
111 for path := range paths {
112 rel, err := filepath.Rel(filepath.Join(optDir, "versions"), path)
113 version, _, _ := strings.Cut(rel, string(filepath.Separator))
114 if err != nil || !filepath.IsLocal(rel) || !release.ValidVersion(version) {
115 log.Printf("adopt: staged-version sweep skipped: generations: %v", os.ErrPermission)
116 return
117 }
118 pinned[version] = true
119 }
120 }
121 res, err := release.SweepStaged(optDir, pinned, grace, now)
122 if err != nil {
123 log.Printf("adopt: staged-version sweep: %v", err)
124 return
125 }
126 for _, e := range res.Errs {
127 log.Printf("adopt: staged-version sweep: %v", e)
128 }
129 if len(res.Removed) == 0 {
130 // One line an hour on a box with nothing to do is worth having: it is
131 // the difference between "the sweep found nothing" and "the sweep is
132 // not running", which is the question the next full disk will ask.
133 log.Printf("adopt: staged-version sweep: %d staged, %d pinned (%d live), %d within the %s grace, none removed",
134 res.Scanned, res.Pinned, live, res.Young, grace)
135 return
136 }
137 log.Printf("adopt: staged-version sweep: removed %d of %d staged versions (%d bytes), %d pinned (%d live), %d within the %s grace: %v",
138 len(res.Removed), res.Scanned, res.Bytes, res.Pinned, live, res.Young, grace, res.Removed)
139}
140
141// sweepStagedVersionsPass is the RunAdopt entry point: the grace from the
142// environment, the clock from the wall.
143func sweepStagedVersionsPass(reg routeLister, sources ...release.PinSource) {
144 sweepStagedVersions(reg, release.OptDir(), release.GenerationsPath(dataDir()), versionSweepGrace(os.Getenv), time.Now(), sources...)
145}
146
147// settingsPins is every build the rows' update settings and build-state
148// copies name: a pin, last_executed and a resolved request's T
149// (release.SettingsRefs, the reading the restaging pin set uses too). A copy
150// that kept its last known state after a failed refresh still names it. Any
151// row whose references are incomplete makes the whole set incomplete, so the
152// sweep deletes nothing: a column that does not parse, or a held instance
153// (settings other than default) whose build state is not read yet or whose
154// last refresh failed (ReadErr; Astra round 1 finding 6), whose
155// last_executed may name a build only its staged copy here still holds.
156// Version trees hold every app, so this refusal is box-wide. Only a row
157// whose copy the agent refreshes counts (release.BuildStateKept): one
158// keepsBuildState never selects (a named unit, the platform's own row)
159// stays unread forever and must not stop the sweep for good.
160func settingsPins(routes []registry.Route) (map[string]bool, error) {
161 out := map[string]bool{}
162 for _, r := range routes {
163 refs, err := release.SettingsRefs(r)
164 if err != nil {
165 return nil, err
166 }
167 for _, v := range refs {
168 out[v] = true
169 }
170 }
171 return out, nil
172}
173