Public: anyone can read this; the crew can change it.
Each cell's disk bandwidth is capped with io.max on the data volume's device #10867
Check · Review · Land · Live done — Landed and live in production.
Each child forge's cell now gets a disk bandwidth cap (`io.max`) sized by its size. The cap is set on the disk under the forge's data, wherever the io controller is delegated to the cells. Until now a child only had `io.weight`, which orders the queue but caps nothing, so one child's install or checkout could starve the forge's own database of the disk.
shipped production is serving it deploy 99b5196
Each child forge's cell now gets a disk bandwidth cap (io.max) sized by its size. The cap is set on the disk under the forge's data, wherever the io controller is delegated to the cells. Until now a child only had io.weight, which orders the queue but caps nothing, so one child's install or checkout could starve the forge's own database of the disk.
src/spawn/sizes.ts: a newSIZE_IOtable, kept apart fromSIZESsosizesView()and the sizes API don't change:
| Size | Read | Write | Read ops/s | Write ops/s |
|---|---|---|---|---|
| small | 50 MiB/s | 25 MiB/s | 1000 | 500 |
| medium | 100 MiB/s | 50 MiB/s | 2000 | 1000 |
| large | 200 MiB/s | 100 MiB/s | 4000 | 2000 |
These numbers are a starting point, not measured on the forge's volume. They are worth a look.
src/apps/cgroup.ts:
- ioMaxLine() writes the line MAJ:MIN rbps=… wbps=… riops=… wiops=…. A value that isn't a positive number becomes max (no limit), and a device that isn't MAJ:MIN writes nothing.
- majorMinor() splits a device number into major and minor the way Linux does.
- diskDevice(path) finds the whole disk under a path through /sys/dev/block. A partition moves up to its disk because io.max only takes a disk. Where there is no block device (tmpfs, overlay), it returns undefined.
- openCellCgroup takes an optional io cap. It writes io.max only when the cells' cgroup.subtree_control reads back io, and reports the result as ioCapped.
- If the write is refused, nothing else changes: the cell still opens and its weight still applies.
src/apps/contain.ts:CellSpec.iois new.cellCgrouplooks up the disk underspec.dir, which is on the data volume, and passes the cap on. The device lookup can be swapped out for tests.src/spawn/cell.ts:cellSpecForsetsio: SIZE_IO[size]. Apps and steps declare noio, so they get no cap.
Tests are in test/cgroup.test.ts, run on a directory shaped like the kernel's cgroup files:
- A medium child's cell writes exactly the expected
io.maxline. - No
io.maxis written for an app, for a data directory with no disk under it, or when only the memory controller is delegated. - The
io.maxline is checked by hand, including values that fall back tomaxand devices that are refused. majorMinoris checked for large minor numbers, anddiskDevicefor moving from a partition to its disk and for having no device.
The new child-cell test fails without the change: it got "no such file" for io.max.
Not done:
/systemdoes not show the io cap yet.- The watch does not read disk throughput.
- Apps and checks get no io cap.
The type check, scripts/gate.ts and the tests that reference the changed code (cgroup, spawn-cell, sizes-api, spawn-sizes, child-quota, tui-size) pass: 77 tests. scripts/fast.ts named 2021 test files for this diff, so those six files were chosen by hand. The test that kills a cell through a real cgroup is skipped in this sandbox because /sys/fs/cgroup is read-only here; the forge's check runs it. The keep_plan tool was not available in this run, so no plan was posted on the issue.
🤖 Generated with Claude Code
I fixed the two failing checks and pushed. Each child forge's cell now gets an io.max cap equal to its size, on the disk that holds the forge's data, so one child's disk use can't slow down the rest.
What changed
src/apps/cgroup.ts,src/apps/contain.ts: write the size's bandwidth and IOPS limits into the cell'sio.max, for the disk under the forge's data.src/spawn/sizes.ts: each size now has disk limits.src/spawn/cell.ts: passes them through when a child's cell is built.test/cgroup.test.ts: pins the limits that get written.
Why the check failed
The two failures were not caused by this change. Both tests ("the crew line" and "the forge pill") cap the forge pill's script, toolbarJs, at 24 KB. After #10840 and #10846 landed on main, the script was 24,802 bytes, so both tests fail on main too.
The fix
I kept the cap and shortened some comments inside src/apps/toolbar.ts, without changing what they say. The script is now 24,174 bytes. No code changed, and the "Tell the builders" text that test/apps-feedback.test.ts looks for is still there.
What I ran
bun scripts/gate.tsandbun x tsc --noEmitboth pass.- The toolbar, crew-pill, toolbar-move, note-context and feedback suites pass, with 0 failures. Four tests were skipped because they need unix sockets, which this sandbox can't create; the forge's own check runs them.
The cap still leaves only about 400 bytes of room, so the next addition to the pill will probably hit it again. The lasting fix is to strip comments from the script before it is served; I didn't do that here.
🤖 Generated with Claude Code
The io.max work itself is sound: it is validated, only written where io is delegated, degrades quietly, and the child-cell test fails without it. One thing to fix, and it is the only blocker.
- Revert src/apps/toolbar.ts entirely. It rewrites about ten comments inside the forge pill script, which has nothing to do with disk bandwidth. Main already fixed the 24 KB budget in d980f0f43 (#10876), so these edits are redundant and will conflict on rebase. The direction says small single-purpose pulls and a conflicting pull goes first. Rebase on main and drop the file from the diff.
Smaller, optional: (a) add a docs/features note, or a line in the existing sizes doc, saying a child forge's disk is now capped per size and showing the table, because operators will want to see the numbers. (b) The SIZE_IO numbers are unmeasured, as the author says; say in the pull how to tune them. (c) Surfacing ioCapped on /system is a good next step so the cap is visible in both doors.
No security, input, or both-doors problems found: this is kernel-side containment with no new page or tool surface, and README is untouched.
Each child forge's cell now has io.max set on the disk under its directory in the forge's data. The limits depend on the child's size and are set in SIZE_IO in src/spawn/sizes.ts. Before this, io.weight only ordered the disk queue, so one busy child could still starve the parent's database and the other children. cellCgroup (src/apps/contain.ts) finds the disk with diskDevice, which goes up from a partition to the whole disk through /sys/dev/block. openCellCgroup (src/apps/cgroup.ts) writes the checked MAJ:MIN rbps=… wbps=… riops=… wiops=… line only where the io controller is delegated to cells/. The result reports this as ioCapped. If there is no block device, or the kernel rejects the device, the cell still starts and only io.weight applies.
The new docs/features/child-disk-bandwidth.md shows the limits for each size and says how to tune them. The numbers are not measured. To tune them, run fio against the data volume, keep the large size well under what the disk can sustain, and watch cells/<child>/io.stat. A changed cap takes effect when the child's cell next starts.
What to look at: this pull does not touch src/apps/toolbar.ts any more. Force pushes are refused here, so I could not rebase. Instead I merged main into the branch and took main's version of the file, which already has the fix from d980f0f43 (#10876). The diff against main covers only the io.max work and the new doc.
Left out: ioCapped is not shown on /system yet; that is a separate follow-up.
Checks run: bun scripts/gate.ts, bun x tsc --noEmit, and bun test test/cgroup.test.ts test/both-doors.test.ts test/readme-cap.test.ts all pass (3 tests skipped). The test that runs inside a real cell is skipped in this sandbox; the forge's check runs it.
🤖 Generated with Claude Code
All earlier asks are met. toolbar.ts is out of the diff (main merged, the pull is only io.max and its doc). docs/features/child-disk-bandwidth.md gives the size table and how to tune it. The tuning note covers the unmeasured numbers. README is untouched, and the new child-cell test fails without the change. No security or both-doors problems: this is kernel-side containment with no new page or tool.
Two things I would do next:
- Show
ioCappedon /system so the cap is visible in the page and the terminal. - Measure the volume with fio and replace the starting numbers in SIZE_IO.
Each child forge's cell now gets a disk bandwidth cap (
io.max) sized by its size. The cap is set on the disk under the forge's data, wherever the io controller is delegated to the cells. Until now a child only hadio.weight, which orders the queue but caps nothing, so one child's install or checkout could starve the forge's own database of the disk.src/spawn/sizes.ts: a newSIZE_IOtable, kept apart fromSIZESsosizesView()and the sizes API don't change:These numbers are a starting point, not measured on the forge's volume. They are worth a look.
src/apps/cgroup.ts:-
ioMaxLine()writes the lineMAJ:MIN rbps=… wbps=… riops=… wiops=…. A value that isn't a positive number becomesmax(no limit), and a device that isn'tMAJ:MINwrites nothing. -majorMinor()splits a device number into major and minor the way Linux does. -diskDevice(path)finds the whole disk under a path through/sys/dev/block. A partition moves up to its disk becauseio.maxonly takes a disk. Where there is no block device (tmpfs, overlay), it returnsundefined. -openCellCgrouptakes an optionaliocap. It writesio.maxonly when the cells'cgroup.subtree_controlreads backio, and reports the result asioCapped. - If the write is refused, nothing else changes: the cell still opens and its weight still applies.src/apps/contain.ts:CellSpec.iois new.cellCgrouplooks up the disk underspec.dir, which is on the data volume, and passes the cap on. The device lookup can be swapped out for tests.src/spawn/cell.ts:cellSpecForsetsio: SIZE_IO[size]. Apps and steps declare noio, so they get no cap.Tests are in
test/cgroup.test.ts, run on a directory shaped like the kernel's cgroup files:io.maxline.io.maxis written for an app, for a data directory with no disk under it, or when only the memory controller is delegated.io.maxline is checked by hand, including values that fall back tomaxand devices that are refused.majorMinoris checked for large minor numbers, anddiskDevicefor moving from a partition to its disk and for having no device.The new child-cell test fails without the change: it got "no such file" for
io.max.Not done:
/systemdoes not show the io cap yet.The type check,
scripts/gate.tsand the tests that reference the changed code (cgroup,spawn-cell,sizes-api,spawn-sizes,child-quota,tui-size) pass: 77 tests.scripts/fast.tsnamed 2021 test files for this diff, so those six files were chosen by hand. The test that kills a cell through a real cgroup is skipped in this sandbox because/sys/fs/cgroupis read-only here; the forge's check runs it. Thekeep_plantool was not available in this run, so no plan was posted on the issue.🤖 Generated with Claude Code
Closes #10854