cmd/gitbay-runner/cgroup.go

bd5cf5d7d1f34fa780660fd7562b9ffd9746ee27
gitbay/cmd/gitbay-runner/cgroup.go history · blame · raw

87 lines · 2937 bytes

 1package main
 2
 3import (
 4	"fmt"
 5	"os"
 6	"path/filepath"
 7	"strconv"
 8	"strings"
 9)
10
11// Build limits are cgroup v2 files the runner writes itself. Podman's
12// --memory and --cpus never applied here: under rootless podman with the
13// cgroupfs manager the container starts inside the service's own cgroup
14// and crun cannot create a child, so the flags were accepted and ignored
15// (#188). The runner owns a cgroup per build under its delegated service
16// cgroup instead, writes the limits into it, and starts every podman
17// process for that build from inside it with podman's own cgroup handling
18// off. What follows is the portable half: parsing and the file writes.
19
20// memoryBytes parses podman's memory units — a whole number with an
21// optional b, k, m or g suffix — into bytes.
22func memoryBytes(s string) (int64, error) {
23	if s == "" {
24		return 0, fmt.Errorf("empty memory limit")
25	}
26	num, unit := s, ""
27	if last := s[len(s)-1]; last < '0' || last > '9' {
28		num, unit = s[:len(s)-1], strings.ToLower(s[len(s)-1:])
29	}
30	n, err := strconv.ParseInt(num, 10, 64)
31	if err != nil || n <= 0 {
32		return 0, fmt.Errorf("memory limit %q: want a whole number of b, k, m or g", s)
33	}
34	shift := map[string]uint{"": 0, "b": 0, "k": 10, "m": 20, "g": 30}
35	sh, ok := shift[unit]
36	if !ok {
37		return 0, fmt.Errorf("memory limit %q: unit %q is not b, k, m or g", s, unit)
38	}
39	return n << sh, nil
40}
41
42// cpuMax renders a CPU count, whole or fractional, as cgroup v2's
43// "<quota> <period>" over a 100ms period.
44func cpuMax(s string) (string, error) {
45	const period = 100000
46	f, err := strconv.ParseFloat(s, 64)
47	if err != nil || f <= 0 {
48		return "", fmt.Errorf("cpu limit %q: want a positive number of CPUs", s)
49	}
50	return fmt.Sprintf("%d %d", int64(f*period+0.5), period), nil
51}
52
53// ownCgroupPath reads the cgroup v2 path out of /proc/self/cgroup
54// contents. A v1 hierarchy has more than the one "0::" line and is not
55// something the runner manages.
56func ownCgroupPath(procSelfCgroup string) (string, error) {
57	lines := strings.Split(strings.TrimSpace(procSelfCgroup), "\n")
58	if len(lines) != 1 || !strings.HasPrefix(lines[0], "0::/") {
59		return "", fmt.Errorf("not a cgroup v2 host: /proc/self/cgroup is %q", strings.TrimSpace(procSelfCgroup))
60	}
61	return strings.TrimPrefix(lines[0], "0::"), nil
62}
63
64// writeLimits writes the requested limits into a cgroup directory. An
65// unset limit writes nothing, so the build inherits whatever the unit
66// allows rather than getting "max".
67func writeLimits(dir, memory, cpus string) error {
68	if memory != "" {
69		n, err := memoryBytes(memory)
70		if err != nil {
71			return err
72		}
73		if err := os.WriteFile(filepath.Join(dir, "memory.max"), []byte(strconv.FormatInt(n, 10)), 0o644); err != nil {
74			return err
75		}
76	}
77	if cpus != "" {
78		v, err := cpuMax(cpus)
79		if err != nil {
80			return err
81		}
82		if err := os.WriteFile(filepath.Join(dir, "cpu.max"), []byte(v), 0o644); err != nil {
83			return err
84		}
85	}
86	return nil
87}