| 1 | package instancectl |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "net/http" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strconv" |
| 10 | "strings" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/cgroup" |
| 14 | "github.com/carlosframework/platform/internal/provision" |
| 15 | ) |
| 16 | |
| 17 | // Generation units (zero-downtime deploys spec §7.6, plan 2 D1). The |
| 18 | // template carlos-instance-gen@<iid>:<n>.service cannot key its slice and |
| 19 | // data mounts by itself: systemd has no specifier for part of an instance |
| 20 | // string. So before each start this service writes a runtime drop-in with |
| 21 | // everything the template leaves out, checks that systemd took it |
| 22 | // (reloading first only when it has not), and only then starts the unit. |
| 23 | |
| 24 | // ErrGenUnproven is StartGen's answer when the start failed after systemd |
| 25 | // may have started the unit and the stop that followed did not prove the |
| 26 | // unit's cgroup empty. The generation may still be running: its files are |
| 27 | // kept, and the caller must not release anything it owns for it. |
| 28 | var ErrGenUnproven = errors.New("instance control: generation termination unproven") |
| 29 | |
| 30 | // statusGenUnproven is the status that carries ErrGenUnproven. No other |
| 31 | // verb answers it, so the client can tell it from an ordinary 502. |
| 32 | const statusGenUnproven = http.StatusInternalServerError |
| 33 | |
| 34 | // DropInName is the drop-in's file name inside <unit>.d. |
| 35 | const DropInName = "50-carlos-generation.conf" |
| 36 | |
| 37 | // GenDropIn is the drop-in's text. The paths are the templates' literal |
| 38 | // ones (/data/instances, /run/carlos/instances, /run/carlos-control/exe). |
| 39 | // The memory lines are the generation's own tier (spec §7.3). The |
| 40 | // template's OOMPolicy=kill gives the unit's cgroup memory.oom.group=1. |
| 41 | func GenDropIn(host string, n uint64, tier cgroup.Limit) string { |
| 42 | iid := provision.InstanceID(host) |
| 43 | key := GenKey(host, n) |
| 44 | max := strconv.FormatInt(tier.MemoryMax, 10) |
| 45 | return "# Written by carlos instance-control for one generation; removed when it stops (zero-downtime plan 2).\n" + |
| 46 | "[Service]\n" + |
| 47 | "Slice=" + SliceName(host) + "\n" + |
| 48 | "BindPaths=/data/instances/" + iid + " /run/carlos/instances/" + iid + "\n" + |
| 49 | "ReadWritePaths=/data/instances/" + iid + " /run/carlos/instances/" + iid + "\n" + |
| 50 | "WorkingDirectory=/data/instances/" + iid + "\n" + |
| 51 | "BindReadOnlyPaths=/run/carlos-control/exe/" + key + "/app:/opt/carlos/app /run/carlos-control/exe/" + key + "/live:/opt/carlos/live\n" + |
| 52 | "StandardOutput=file:" + GenLogSocket(host, n) + "\n" + |
| 53 | "MemoryMax=" + max + "\n" + |
| 54 | "MemoryHigh=" + strconv.FormatInt(cgroup.HighOf(tier.MemoryMax), 10) + "\n" + |
| 55 | "MemorySwapMax=" + max + "\n" |
| 56 | } |
| 57 | |
| 58 | // writeGenDropIn writes the drop-in atomically: a temp file systemd never |
| 59 | // reads (only *.conf is read), renamed into place. |
| 60 | func (s *Server) writeGenDropIn(host string, n uint64, tier cgroup.Limit) error { |
| 61 | dir := filepath.Join(s.DropInDir, GenUnitName(host, n)+".d") |
| 62 | if err := os.MkdirAll(dir, 0o755); err != nil { |
| 63 | return err |
| 64 | } |
| 65 | tmp := filepath.Join(dir, "."+DropInName+".tmp") |
| 66 | if err := os.WriteFile(tmp, []byte(GenDropIn(host, n, tier)), 0o644); err != nil { |
| 67 | return err |
| 68 | } |
| 69 | return os.Rename(tmp, filepath.Join(dir, DropInName)) |
| 70 | } |
| 71 | |
| 72 | // ensureGenExe lays out generation n's view of its build under ExeRoot, |
| 73 | // keyed by GenKey, as ensureHostExe does for the original template. Its |
| 74 | // target is the host's live link, as today. Plan 3 binds the attempt's |
| 75 | // staged path instead (spec §7.5 step 3). |
| 76 | func (s *Server) ensureGenExe(host string, n uint64) error { |
| 77 | return ensureExe(s.ExeRoot, GenKey(host, n), filepath.Join(LiveDir, host), host) |
| 78 | } |
| 79 | |
| 80 | // removeGenFiles removes a generation unit's drop-in and executable layout, |
| 81 | // by the unit's name as systemd reports it. Best-effort: a leftover is a |
| 82 | // few bytes in /run, never a running unit, so most callers drop the error. |
| 83 | // The sweep logs it, so a leftover it cannot remove says why (Task 15). |
| 84 | func (s *Server) removeGenFiles(unit string) error { |
| 85 | esc, n, ok := ParseGenUnitName(unit) |
| 86 | if !ok { |
| 87 | return nil |
| 88 | } |
| 89 | var errs []error |
| 90 | if s.DropInDir != "" { |
| 91 | errs = append(errs, os.RemoveAll(filepath.Join(s.DropInDir, unit+".d"))) |
| 92 | } |
| 93 | iid, ok := Unescape(esc) |
| 94 | if !ok || iid == "" || strings.ContainsAny(iid, "/\x00") || iid == "." || iid == ".." { |
| 95 | return errors.Join(errs...) |
| 96 | } |
| 97 | if s.ExeRoot != "" { |
| 98 | errs = append(errs, os.RemoveAll(filepath.Join(s.ExeRoot, GenKeyOf(iid, n)))) |
| 99 | } |
| 100 | return errors.Join(errs...) |
| 101 | } |
| 102 | |
| 103 | // slicePopulated reports whether anything runs in host's slice. Absent is |
| 104 | // no. Unreadable is yes, so a raised limit is never lowered on a guess. |
| 105 | func (s *Server) slicePopulated(host string) bool { |
| 106 | b, err := os.ReadFile(filepath.Join(s.CgroupRoot, parentSlices, SliceName(host), "cgroup.events")) |
| 107 | if errors.Is(err, os.ErrNotExist) { |
| 108 | return false |
| 109 | } |
| 110 | if err != nil { |
| 111 | return true |
| 112 | } |
| 113 | return !strings.Contains(string(b), "populated 0") |
| 114 | } |
| 115 | |
| 116 | // genView is startGen's one look at systemd, a single show of the unit and |
| 117 | // its host slice. applied reports whether systemd's view of unit is the |
| 118 | // drop-in just written (GenDropIn). The unit's name fixes the host and |
| 119 | // generation, and so every line of the drop-in but the tier's: the slice |
| 120 | // says systemd read a drop-in at all, and the memory lines say it was this |
| 121 | // tier's. NeedDaemonReload=no says no file changed since systemd loaded |
| 122 | // the unit, though only by mtime, which is why the tier is checked as well |
| 123 | // (Astra review: a reused, still-loaded generation and a clock that went |
| 124 | // back). tierSet reports whether the slice already carries every value |
| 125 | // setTier would give it, so a cold start can skip that call (wake latency, |
| 126 | // pass 2c). Any failure to answer is a no to both. |
| 127 | func (s *Server) genView(ctx context.Context, host, unit string, tier cgroup.Limit) (slice string, applied, tierSet bool) { |
| 128 | out, err := s.Systemctl(ctx, "show", "-p", "Slice,NeedDaemonReload,MemoryMax,MemoryHigh,MemorySwapMax,CPUWeight,CPUQuotaPerSecUSec,IOWeight", unit, SliceName(host)) |
| 129 | if err != nil { |
| 130 | return "", false, false |
| 131 | } |
| 132 | blocks := strings.SplitN(strings.TrimLeft(out, "\n"), "\n\n", 2) |
| 133 | m := parseShow(blocks[0]) |
| 134 | max := strconv.FormatInt(tier.MemoryMax, 10) |
| 135 | high := strconv.FormatInt(cgroup.HighOf(tier.MemoryMax), 10) |
| 136 | applied = m["Slice"] == SliceName(host) && m["NeedDaemonReload"] == "no" && |
| 137 | m["MemoryMax"] == max && m["MemorySwapMax"] == max && m["MemoryHigh"] == high |
| 138 | if len(blocks) == 2 { |
| 139 | sl := parseShow(blocks[1]) |
| 140 | sh := cgroup.SharesOf(tier) |
| 141 | tierSet = sl["MemoryMax"] == max && sl["MemorySwapMax"] == max && sl["MemoryHigh"] == high && |
| 142 | sl["CPUWeight"] == strconv.Itoa(sh.CPUWeight) && sl["IOWeight"] == strconv.Itoa(sh.IOWeight) && |
| 143 | sl["CPUQuotaPerSecUSec"] == cpuQuotaShown(sh.CPUQuotaPercent) |
| 144 | } |
| 145 | return m["Slice"], applied, tierSet |
| 146 | } |
| 147 | |
| 148 | // cpuQuotaShown is CPUQuota=<pct>% as systemctl show prints its |
| 149 | // CPUQuotaPerSecUSec (systemd 257): 500ms, 1s, 1.500000s. A format this |
| 150 | // gets wrong only costs the set-property call it would have saved. |
| 151 | func cpuQuotaShown(pct int) string { |
| 152 | usec := int64(pct) * 10000 |
| 153 | switch { |
| 154 | case usec%1_000_000 == 0: |
| 155 | return strconv.FormatInt(usec/1_000_000, 10) + "s" |
| 156 | case usec < 1_000_000: |
| 157 | return strconv.FormatInt(usec/1000, 10) + "ms" |
| 158 | default: |
| 159 | return strconv.FormatFloat(float64(usec)/1e6, 'f', 6, 64) + "s" |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | // startGen is /start with a generation. The host slice is the parent (spec |
| 164 | // §7.3): a cold start sets it to the tier. A start that finds the slice |
| 165 | // populated leaves it alone, because a swap raised it with /parent before |
| 166 | // the successor's start. |
| 167 | func (s *Server) startGen(w http.ResponseWriter, r *http.Request, req request) { |
| 168 | ctx := r.Context() |
| 169 | unit := GenUnitName(req.host, req.gen) |
| 170 | if s.DropInDir == "" { |
| 171 | http.Error(w, "not starting "+unit+": generation units are not configured on this service", http.StatusNotImplemented) |
| 172 | return |
| 173 | } |
| 174 | s.markGenStarting(unit, true) |
| 175 | defer s.markGenStarting(unit, false) |
| 176 | // Timed for the start's log line: a wake waits on all of this. |
| 177 | began, reload := time.Now(), "skipped" |
| 178 | // A refusal for the disk writes nothing. An exe layout that fails |
| 179 | // half-way may have made its directory, so that failure cleans up too. |
| 180 | // A staged executable is validated before anything is laid out |
| 181 | // (zero-downtime plan 3, Task 9). |
| 182 | if s.refuseStagedExe(w, req, unit) { |
| 183 | return |
| 184 | } |
| 185 | checked := time.Now() |
| 186 | if s.refuseStart(w, req.host, unit, func() error { |
| 187 | err := s.ensureGenExeAt(req.host, req.gen, req.exe) |
| 188 | if err != nil { |
| 189 | s.removeGenFiles(unit) |
| 190 | } |
| 191 | return err |
| 192 | }) { |
| 193 | return |
| 194 | } |
| 195 | fail := func(status int, msg string) { |
| 196 | s.removeGenFiles(unit) |
| 197 | http.Error(w, msg, status) |
| 198 | } |
| 199 | laidOut := time.Now() |
| 200 | if err := s.writeGenDropIn(req.host, req.gen, req.tier); err != nil { |
| 201 | fail(http.StatusBadGateway, "drop-in: "+err.Error()) |
| 202 | return |
| 203 | } |
| 204 | written := time.Now() |
| 205 | // The reload is only for a unit systemd already holds loaded from older |
| 206 | // files. A generation's unit is new and an inactive one is unloaded |
| 207 | // straight away, so the next reference reads the drop-in fresh: the |
| 208 | // common case needs no reload, which on a small box was a third of the |
| 209 | // whole wake. The check is the same either way (genDropInApplied). |
| 210 | slice, applied, tierSet := s.genView(ctx, req.host, unit, req.tier) |
| 211 | if !applied { |
| 212 | reloadStart := time.Now() |
| 213 | if out, err := s.Systemctl(ctx, "daemon-reload"); err != nil { |
| 214 | fail(http.StatusBadGateway, "daemon-reload: "+strings.TrimSpace(out+" "+err.Error())) |
| 215 | return |
| 216 | } |
| 217 | if slice, applied, tierSet = s.genView(ctx, req.host, unit, req.tier); !applied { |
| 218 | fail(http.StatusBadGateway, "not starting "+unit+": its drop-in did not apply (slice "+strconv.Quote(slice)+")") |
| 219 | return |
| 220 | } |
| 221 | reload = time.Since(reloadStart).Round(time.Millisecond).String() |
| 222 | } |
| 223 | if !s.slicePopulated(req.host) && !tierSet { |
| 224 | if err := s.setTier(ctx, req.host, req.tier); err != nil { |
| 225 | fail(http.StatusBadGateway, "set-property: "+err.Error()) |
| 226 | return |
| 227 | } |
| 228 | } |
| 229 | // From the start on, systemd may have started the unit, whatever the |
| 230 | // answer says. A failure past this point kills it and proves it empty |
| 231 | // before its files go; one that cannot prove that keeps the files and |
| 232 | // says so (statusGenUnproven). |
| 233 | startAt := time.Now() |
| 234 | if out, err := s.Systemctl(ctx, "start", unit); err != nil { |
| 235 | s.abandonGen(ctx, w, req.host, unit, "start: "+strings.TrimSpace(out+" "+err.Error())) |
| 236 | return |
| 237 | } |
| 238 | inv, err := s.Systemctl(ctx, "show", "-p", "InvocationID", "--value", unit) |
| 239 | if err != nil { |
| 240 | s.abandonGen(ctx, w, req.host, unit, "show: "+err.Error()) |
| 241 | return |
| 242 | } |
| 243 | ms := func(d time.Duration) time.Duration { return d.Round(time.Millisecond) } |
| 244 | s.logf("instance-control: %s: started %s memory.max=%d (prepared in %s: staged exe %s, layout %s, drop-in %s, check and tier %s; reload %s, start %s)", req.host, unit, req.tier.MemoryMax, |
| 245 | ms(startAt.Sub(began)), ms(checked.Sub(began)), ms(laidOut.Sub(checked)), ms(written.Sub(laidOut)), ms(startAt.Sub(written)), reload, ms(time.Since(startAt))) |
| 246 | writeJSON(w, http.StatusOK, StartResponse{InvocationID: strings.TrimSpace(inv)}) |
| 247 | } |
| 248 | |
| 249 | // abandonGen answers a generation start that failed after systemctl start |
| 250 | // was issued. The unit may be running, so it is killed and waited on, on a |
| 251 | // context the client's hang-up cannot cancel. Only a stop that proved the |
| 252 | // unit's cgroup empty removes its files; otherwise they stay, and the |
| 253 | // answer is statusGenUnproven so the client knows the generation may live. |
| 254 | func (s *Server) abandonGen(ctx context.Context, w http.ResponseWriter, host, unit, msg string) { |
| 255 | dctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), DeadlineControl+s.waitBudget()) |
| 256 | defer cancel() |
| 257 | if err := s.killGen(dctx, host, unit); err != nil { |
| 258 | s.logf("instance-control: %s: abandon %s: termination unproven, files kept: %v", host, unit, err) |
| 259 | http.Error(w, msg+"; termination unproven: "+err.Error(), statusGenUnproven) |
| 260 | return |
| 261 | } |
| 262 | s.resetGenFailed(dctx, unit) |
| 263 | s.removeGenFiles(unit) |
| 264 | http.Error(w, msg, http.StatusBadGateway) |
| 265 | } |
| 266 | |
| 267 | // resetGenFailed clears a stopped generation unit's failed state, so a |
| 268 | // generation that died on its own does not stay in systemctl --failed |
| 269 | // once the control service has stopped it. It runs only after a stop or |
| 270 | // kill that proved the unit gone. Best-effort: the stop already succeeded, |
| 271 | // so a unit that is no longer loaded is ignored and any other failure is |
| 272 | // logged, never turned into a failed stop. |
| 273 | func (s *Server) resetGenFailed(ctx context.Context, unit string) { |
| 274 | if out, err := s.Systemctl(ctx, "reset-failed", unit); err != nil && !strings.Contains(out+err.Error(), "not loaded") { |
| 275 | s.logf("instance-control: reset-failed %s: %v", unit, err) |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | // killGen kills a generation unit outright, stops it, and waits until its |
| 280 | // cgroup is empty (spec §8.2 step 1). nil is the proof that nothing of it |
| 281 | // runs. A kill that finds nothing to kill is not a failure: the stop and |
| 282 | // the wait are the proof. |
| 283 | func (s *Server) killGen(ctx context.Context, host, unit string) error { |
| 284 | if out, err := s.Systemctl(ctx, "kill", "--signal=SIGKILL", unit); err != nil && !strings.Contains(out+err.Error(), "not loaded") { |
| 285 | s.logf("instance-control: %s: kill %s: %v", host, unit, err) |
| 286 | } |
| 287 | if out, err := s.Systemctl(ctx, "stop", unit); err != nil { |
| 288 | return errors.New("stop: " + strings.TrimSpace(out+" "+err.Error())) |
| 289 | } |
| 290 | wctx, cancel := context.WithTimeout(ctx, s.waitBudget()) |
| 291 | defer cancel() |
| 292 | if err := WaitCgroupEmpty(wctx, s.CgroupRoot, SliceName(host), unit, 50*time.Millisecond); err != nil { |
| 293 | return errors.New("stop: " + err.Error()) |
| 294 | } |
| 295 | return nil |
| 296 | } |
| 297 | |
| 298 | // stopGen is /stop with a generation. With kill=1 the generation is killed |
| 299 | // outright (spec §8.2 step 1) and waited on until its cgroup is empty. |
| 300 | // Either way the stop acts on that generation's unit only, and its files |
| 301 | // go once it has stopped. |
| 302 | func (s *Server) stopGen(w http.ResponseWriter, r *http.Request, req request) { |
| 303 | ctx := r.Context() |
| 304 | unit := GenUnitName(req.host, req.gen) |
| 305 | if req.kill { |
| 306 | if err := s.killGen(ctx, req.host, unit); err != nil { |
| 307 | http.Error(w, err.Error(), http.StatusBadGateway) |
| 308 | return |
| 309 | } |
| 310 | } else if out, err := s.Systemctl(ctx, "stop", unit); err != nil { |
| 311 | http.Error(w, "stop: "+strings.TrimSpace(out+" "+err.Error()), http.StatusBadGateway) |
| 312 | return |
| 313 | } |
| 314 | s.resetGenFailed(ctx, unit) |
| 315 | s.removeGenFiles(unit) |
| 316 | w.WriteHeader(http.StatusNoContent) |
| 317 | } |
| 318 | |
| 319 | func (s *Server) markGenStarting(unit string, on bool) { |
| 320 | s.genMu.Lock() |
| 321 | defer s.genMu.Unlock() |
| 322 | if !on { |
| 323 | delete(s.genStarting, unit) |
| 324 | return |
| 325 | } |
| 326 | if s.genStarting == nil { |
| 327 | s.genStarting = map[string]bool{} |
| 328 | } |
| 329 | s.genStarting[unit] = true |
| 330 | } |
| 331 | |
| 332 | // sweepGenLeftovers is the sweep's recovery for generation units that are |
| 333 | // gone but kept their files: their stop's answer was lost, or the agent |
| 334 | // that would have finalised them restarted. Such a unit is inactive, so |
| 335 | // the sweep had nothing to stop. Each drop-in directory under DropInDir is |
| 336 | // checked: a unit the sweep could not stop is skipped, as is one a start |
| 337 | // is preparing. The rest lose their files only when systemd reports them |
| 338 | // inactive or failed AND their cgroup is empty. No daemon-reload follows: |
| 339 | // stopGen removes files the same way, and a generation's name is never |
| 340 | // started again. |
| 341 | // |
| 342 | // Each generation executable directory under ExeRoot (<iid>@<n>) is |
| 343 | // checked the same way, so a generation that kept only its exe layout is |
| 344 | // found too: its drop-in went and the exe's removal failed, which |
| 345 | // removeGenFiles ignores (Task 15, Astra IMPL-2-R2 residual 4). A host's |
| 346 | // own directory (<iid>, no '@') is never a generation key and is never |
| 347 | // touched. On a box that never ran generations, ExeRoot holds only host |
| 348 | // directories and this finds nothing. |
| 349 | func (s *Server) sweepGenLeftovers(ctx context.Context, failed map[string]string) { |
| 350 | var units []string |
| 351 | seen := map[string]bool{} |
| 352 | add := func(unit string) { |
| 353 | if _, stuck := failed[unit]; stuck || seen[unit] { |
| 354 | return |
| 355 | } |
| 356 | seen[unit] = true |
| 357 | units = append(units, unit) |
| 358 | } |
| 359 | for _, e := range s.readSweepDir(s.DropInDir) { |
| 360 | unit, ok := strings.CutSuffix(e.Name(), ".d") |
| 361 | if !ok || !e.IsDir() { |
| 362 | continue |
| 363 | } |
| 364 | if _, _, ok := ParseGenUnitName(unit); !ok { |
| 365 | continue |
| 366 | } |
| 367 | add(unit) |
| 368 | } |
| 369 | for _, e := range s.readSweepDir(s.ExeRoot) { |
| 370 | if !e.IsDir() { |
| 371 | continue |
| 372 | } |
| 373 | if unit, ok := genUnitOfExeKey(e.Name()); ok { |
| 374 | add(unit) |
| 375 | } |
| 376 | } |
| 377 | for _, unit := range units { |
| 378 | s.sweepGenLeftover(ctx, unit) |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | // readSweepDir lists dir for sweepGenLeftovers. An unset or absent |
| 383 | // directory has nothing to sweep; any other failure is logged. |
| 384 | func (s *Server) readSweepDir(dir string) []os.DirEntry { |
| 385 | if dir == "" { |
| 386 | return nil |
| 387 | } |
| 388 | entries, err := os.ReadDir(dir) |
| 389 | if err != nil && !errors.Is(err, os.ErrNotExist) { |
| 390 | s.logf("instance-control: sweep: %s: %v", dir, err) |
| 391 | } |
| 392 | return entries |
| 393 | } |
| 394 | |
| 395 | // genUnitOfExeKey names the generation unit whose executable directory is |
| 396 | // key. It answers only when removeGenFiles, given that unit, removes |
| 397 | // exactly key, so the sweep can never remove some other directory. |
| 398 | func genUnitOfExeKey(key string) (string, bool) { |
| 399 | iid, n, ok := ParseGenKey(key) |
| 400 | if !ok { |
| 401 | return "", false |
| 402 | } |
| 403 | unit := GenUnitNameOf(iid, n) |
| 404 | esc, m, ok := ParseGenUnitName(unit) |
| 405 | if !ok || m != n { |
| 406 | return "", false |
| 407 | } |
| 408 | if back, ok := Unescape(esc); !ok || GenKeyOf(back, n) != key { |
| 409 | return "", false |
| 410 | } |
| 411 | return unit, true |
| 412 | } |
| 413 | |
| 414 | func (s *Server) sweepGenLeftover(ctx context.Context, unit string) { |
| 415 | // Held across the check and the removal, so a start of the same unit |
| 416 | // cannot write fresh files in between. |
| 417 | s.genMu.Lock() |
| 418 | defer s.genMu.Unlock() |
| 419 | if s.genStarting[unit] { |
| 420 | return |
| 421 | } |
| 422 | if state := s.showValue(ctx, "ActiveState", unit); state != "inactive" && state != "failed" { |
| 423 | return |
| 424 | } |
| 425 | _, esc, _ := ParseUnitName(unit) |
| 426 | // One look at the cgroup, no wait: a populated one is not swept. |
| 427 | wctx, cancel := context.WithTimeout(ctx, time.Millisecond) |
| 428 | defer cancel() |
| 429 | if err := WaitCgroupEmpty(wctx, s.CgroupRoot, "carlos-instances-"+esc+".slice", unit, time.Millisecond); err != nil { |
| 430 | return |
| 431 | } |
| 432 | if err := s.removeGenFiles(unit); err != nil { |
| 433 | s.logf("instance-control: sweep: could not remove the files of %s: %v", unit, err) |
| 434 | return |
| 435 | } |
| 436 | s.logf("instance-control: sweep: removed the files of %s", unit) |
| 437 | } |
| 438 | |
| 439 | // handleParent sets the host slice's memory: the generation parent's |
| 440 | // backstop (spec §7.3). The swap raises it before the successor starts and |
| 441 | // restores it once the retiring generation has stopped. CPU and IO shares |
| 442 | // stay at one tier (plan 2 D6). |
| 443 | func (s *Server) handleParent(w http.ResponseWriter, r *http.Request) { |
| 444 | q := r.URL.Query() |
| 445 | host := q.Get("host") |
| 446 | if !provision.ValidHostShape(host) { |
| 447 | http.Error(w, "host "+strconv.Quote(host)+" is not a host", http.StatusBadRequest) |
| 448 | return |
| 449 | } |
| 450 | n, err := strconv.ParseInt(q.Get("memory_max"), 10, 64) |
| 451 | if err != nil || !cgroup.ValidParentLimit(cgroup.Limit{MemoryMax: n}) { |
| 452 | http.Error(w, "memory_max "+strconv.Quote(q.Get("memory_max"))+" is not one tier or two", http.StatusBadRequest) |
| 453 | return |
| 454 | } |
| 455 | max := strconv.FormatInt(n, 10) |
| 456 | if out, err := s.Systemctl(r.Context(), "set-property", "--runtime", SliceName(host), |
| 457 | "MemoryMax="+max, "MemoryHigh="+strconv.FormatInt(cgroup.HighOf(n), 10), "MemorySwapMax="+max); err != nil { |
| 458 | http.Error(w, "set-property: "+strings.TrimSpace(out+" "+err.Error()), http.StatusBadGateway) |
| 459 | return |
| 460 | } |
| 461 | w.WriteHeader(http.StatusNoContent) |
| 462 | } |
| 463 | |