Closes #211. The container boundary was good and the resource the job consumes was
not: a job could fork without a bound, write /tmp until the machine’s RAM was gone,
and fill the disk its workspace sits on, and the runner would only notice when a build
died. The isolation protects the forge’s data; this protects the machine that runs the
code.
What bounds what
| Bound | Default | Why it is that |
|---|---|---|
--container-pids |
2048 | A container’s processes. Zero now means the default, not “no limit”: a bound nobody set is the easiest one to be missing. |
--container-tmpfs |
1G | The job’s /tmp, which is a tmpfs — its size is memory. Passed to service containers too. |
--container-memory |
none (operator) | Per job, and the right number is the machine’s size. A wrong default would appear as an OOM kill in someone’s build. |
--disk-reserve |
2G | Free space the runner keeps under --dir; a job that takes the filesystem below it is stopped while it runs. |
unit MemoryMax |
90% | Backstop under all of it, for a job that gets past its own cgroup. |
Decisions
- The job tree is bounded by noticing, not by a quota. It is a host bind mount,
so the runtime cannot quota it and a filesystem quota needs root and a filesystem
that supports one; a walker over the tree on a timer would cost more than it
protects. One
statfsper second while a command runs, and the log gets what was left and what the reserve is.--diron its own sized filesystem is the hard bound, and the docs say so. - Memory has no default. Every other limit is safe to set blind; this one is a property of the machine, and the failure mode of getting it wrong is an OOM kill that reads like a bug in the workflow.
- No
TasksMax/LimitNPROCon the unit. The container’s own pids limit already applies to what the runner starts, and a unit-level value would have to track--jobsand--container-pids; when it drifted the builds would fail in a way that looks like the workflow’s fault.MemoryMaxis the backstop because a cgroup memory escape is the one that takes the whole machine down. - A workflow still cannot raise any of it. These are runner flags and unit settings, never fields a job can name.
Where it is configured
All three places a runner is documented or created now carry the limits:
packaging/runner.env (the shipped example), the manual steps in
docs/deploy/runner.md, and deploy/cloud-init/runner.yaml via new render
variables RUNNER_MEMORY (4G), RUNNER_PIDS, RUNNER_TMPFS and
RUNNER_DISK_RESERVE, documented with the sizing rule (--jobs × memory below the
MemoryMax backstop).
Testing
ci/container: an unconfigured runtime still gets finite pids and a sized tmpfs; an operator’s values reach both a job and a service container; a tmpfs value with whitespace is refused.ci/agent:diskLowagainst a real filesystem — below the reserve, above it, and the zero that disables it;--container-tmpfsreachescontainer.Config.cmd/vole: a--disk-reservethat is not a size is refused where the flag is read.scripts/check-deploy.pyfails when the rendered runner arguments stop carrying--container-memory,--container-pids,--container-tmpfsor--disk-reserve(verified by removing one and watching it fail).
vole workflow run verify passes (format, vet, build, licenses, govulncheck,
packages, deployment files, links, race suite).