Skip to content
quarry Artifact [415b2fce3c]
Login

Artifact [415b2fce3c]

Artifact 415b2fce3c0d66a479928cac0eaf351991dcd2426fcb5b3026626f9ee26d91bb:


//go:build linux

package children

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"sync/atomic"
	"syscall"
)

// syncGroup and workGroup are the open directories of the cgroups
// children are started in, or -1 when there are none. Sync children
// (zit serve, the sync front) get a group of their own with a floor it
// keeps (memory.min), so a heavy page child that fills the shared
// ceiling is killed in its own group and a concurrent sync is not
// (jva, ^3920).
var syncGroup, workGroup atomic.Int64

func init() {
	syncGroup.Store(-1)
	workGroup.Store(-1)
}

// syncFloor is the memory a sync child keeps however full the page
// children's share is.
const syncFloor = 512 << 20

// ConfineMemory gives every child this process starts a memory ceiling
// of its own, separate from the forge's: a runaway child is killed
// inside its group before it can push the machine into the OOM killer,
// whose pick is then a guess (zit annotate of a 33,677-line file grew
// past 4 GB, zit#325). No page may run the forge out of memory.
//
// It needs a delegated cgroup v2 (systemd Delegate=yes): the unit's own
// group is split into main, where this process and its wrapper move,
// and children, whose memory.max is max bytes. cgroup v2 allows no
// process in a group whose controllers are delegated to its children,
// which is why the process itself moves rather than staying put.
// Anything unavailable is an error to log; children then run as before.
func ConfineMemory(max int64) (string, error) {
	raw, err := os.ReadFile("/proc/self/cgroup")
	if err != nil {
		return "", err
	}
	rel := ""
	for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
		if p, ok := strings.CutPrefix(line, "0::"); ok {
			rel = p
		}
	}
	if rel == "" || rel == "/" {
		return "", fmt.Errorf("no cgroup v2 group of our own (%q)", rel)
	}
	base := filepath.Join("/sys/fs/cgroup", rel)
	if filepath.Base(base) == "main" {
		base = filepath.Dir(base)
	}
	main := filepath.Join(base, "main")
	if err := os.Mkdir(main, 0o755); err != nil && !os.IsExist(err) {
		return "", fmt.Errorf("make %s (is the unit Delegate=yes?): %w", main, err)
	}
	procs, err := os.ReadFile(filepath.Join(base, "cgroup.procs"))
	if err != nil {
		return "", err
	}
	for _, pid := range strings.Fields(string(procs)) {
		if err := os.WriteFile(filepath.Join(main, "cgroup.procs"), []byte(pid), 0); err != nil {
			return "", fmt.Errorf("move %s into %s: %w", pid, main, err)
		}
	}
	if err := os.WriteFile(filepath.Join(base, "cgroup.subtree_control"), []byte("+memory"), 0); err != nil {
		return "", fmt.Errorf("enable memory for %s's children: %w", base, err)
	}
	group := filepath.Join(base, "children")
	if err := os.Mkdir(group, 0o755); err != nil && !os.IsExist(err) {
		return "", err
	}
	if err := os.WriteFile(filepath.Join(group, "memory.max"), []byte(strconv.FormatInt(max, 10)), 0); err != nil {
		return "", err
	}
	_ = os.WriteFile(filepath.Join(group, "memory.swap.max"), []byte("0"), 0)
	for _, name := range []string{"sync", "work"} {
		if err := os.Mkdir(filepath.Join(group, name), 0o755); err != nil && !os.IsExist(err) {
			return "", err
		}
	}
	// A child the last instance left behind (a sync front not yet
	// reaped) would keep children from delegating its memory controller,
	// since cgroup v2 allows that only on a group holding no process.
	if left, err := os.ReadFile(filepath.Join(group, "cgroup.procs")); err == nil {
		for _, pid := range strings.Fields(string(left)) {
			_ = os.WriteFile(filepath.Join(group, "work", "cgroup.procs"), []byte(pid), 0)
		}
	}
	if err := os.WriteFile(filepath.Join(group, "cgroup.subtree_control"), []byte("+memory"), 0); err != nil {
		return "", fmt.Errorf("enable memory for %s's children: %w", group, err)
	}
	open := func(name string, min int64) (int, error) {
		dir := filepath.Join(group, name)
		if min > 0 {
			if err := os.WriteFile(filepath.Join(dir, "memory.min"), []byte(strconv.FormatInt(min, 10)), 0); err != nil {
				return -1, err
			}
		}
		return syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_CLOEXEC, 0)
	}
	sfd, err := open("sync", min(syncFloor, max/4))
	if err != nil {
		return "", err
	}
	wfd, err := open("work", 0)
	if err != nil {
		return "", err
	}
	syncGroup.Store(int64(sfd))
	workGroup.Store(int64(wfd))
	return group, nil
}

// confine starts cmd inside its children group, when there is one: a
// sync child (verb serve) in sync, every other child in work.
func confine(cmd *exec.Cmd, verb string) {
	fd := workGroup.Load()
	if verb == "serve" {
		fd = syncGroup.Load()
	}
	if fd < 0 {
		return
	}
	if cmd.SysProcAttr == nil {
		cmd.SysProcAttr = &syscall.SysProcAttr{}
	}
	cmd.SysProcAttr.UseCgroupFD = true
	cmd.SysProcAttr.CgroupFD = int(fd)
}