Repository navigation
tests: settle the runtime adapter's executions through cgroups - #862
Enrique Saurez (esaurez) wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Fixture cleanup can mask a worker shutdown regression, so the cgroup-removal assertion needs to run before the fixture is dropped.
1 open issue
What changed in this PR
This PR implements the first part of #805 by giving each runtime-adapter execution its own cgroup, allowing settlement to account for descendants that leave the program’s process group.
Changes:
- Creates per-execution cgroups and retains execution slots until cleanup, keeping unsettled executions fenced until shutdown.
- Connects root bootstrap cgroups to the worker while preserving the process-group fallback.
- Adds cgroup lifecycle tests and documents settlement and admission behavior.
| File | Description |
|---|---|
tests/runtime-adapter/tests/guest_session.rs |
Adds cgroup fixtures and descendant-settlement and shutdown tests. |
tests/runtime-adapter/src/worker/executions.rs |
Implements cgroup placement, settlement, slot retention, and cleanup. |
tests/runtime-adapter/src/worker.rs |
Supplies bootstrap cgroups to executions and documents their lifecycle. |
tests/runtime-adapter/README.md |
Describes cgroup behavior, fallback behavior, and test coverage. |
Review effort: Balanced
a40048e to
20d5cdc
Compare
| if binding.execution().status().is_none() { | ||
| self.reaping.push(binding.id()); | ||
| } | ||
| drop(binding); |
There was a problem hiding this comment.
Why does binding need to be drop manually? Are we required to release its resources before the next stament? Could we rely on automatic drop semantics instead?
There was a problem hiding this comment.
No, the explicit drop isn't required. Nothing after it depends on the binding being gone, so the automatic drop at the end of scope behaves the same. The explicit drops only marked where a program that hasn't ended gets killed.
Your 21fd36f already relies on automatic drop. It removes drop(binding); from clear() (line 282 of the version you commented on) and let key = binding.runner().key(); drop(binding); from forget() (lines 386-387), names binding.runner().key() in forget()'s diagnostic instead, and updates forget()'s doc to say that the binding drops last. I checked that the new order is equivalent:
clear(): eachbindingstill drops at the end of its own loop iteration. So every program that hasn't ended, with its process group or its cgroup's processes, is still killed before thecgroup.killloop overfencedand the reaping wait.self.fenced.extend(cgroup)only stores the handle.forget(): the binding now drops after the cgroup's removal. That's safe becausesettledcomes from a reusable outcome, whichBinding::settlereports only after it finds the cgroup unoccupied and callsExecution::release_cgroup(). By then no process or zombie is left in the cgroup, whichExecutionCgroup::removechecks anyway, and the binding's drop no longer touches it. An unsettled execution's cgroup only goes tofenced, and the drop still kills its processes before the stop'sclear()kills and removes what's left.
So your head needs nothing more from me. With 5a071a5 checked out, rustfmt and Clippy in the guest-only configuration pass. I'll leave the thread open for you to resolve.
20d5cdc to
5a071a5
Compare
9f0c445 to
090de0a
Compare
5a071a5 to
ec3af7d
Compare
090de0a to
47f8bcd
Compare
The worker ran each program in a process group of its own, which it killed before it collected the program's status. Nothing proved that a descendant that left the group was gone, so every outcome was fenced, and the worker could admit a new execution while such a descendant still ran. Each execution now gets a cgroup of its own below the workload cgroup, which the worker prepares with ExecutionCgroup::prepare, and which the program joins as it spawns. The execution settles through that cgroup alone, so the other executions keep running, and an execution whose program ends on its own is reusable. An execution keeps its place among the worker's limit until its cgroup is empty and removed. One that does not settle keeps its place, and its cgroup, until the host stops the worker, which ends and removes every cgroup that is left. The root's operations name the cgroups that the bootstrap prepared. A fake root names none where the in-process tests cannot create cgroups, and the programs keep their process groups there, as before. Where the tests can, each fake root has a workload cgroup of its own, and new tests check a descendant in a session of its own, which the settlement ends, and an execution that does not settle, which keeps its place until the stop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
A program that the worker spawned, but whose execution it could not have, may still be joining its cgroup, as its launch has yet to report, and removing the cgroup could then hide the program once it ended, which ExecutionCgroup::remove forbids. Such a start's cgroup now keeps its place, as that of an execution that did not settle, and a stop removes the executions' cgroups only once the worker collected the status of every program that it forgot. The holder in the test of an execution that does not settle now runs in a process group of its own, which its drop kills, so that its child no longer outlives a test that fails before the child joins the cgroup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
Once the in-process tests create cgroups, each fake root holds a workload cgroup of the test's own, which ends what is left in it and removes the cgroups of executions when it drops. Two tests discarded the fake root that a stop returns, so that it dropped before their checks: that the stop removed the cgroup of an execution that did not settle, and that it left no process of a program's group running. Those checks then held whatever the worker did. The tests now keep the fake root until their checks are done. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7ad408a-da3b-45c4-aa02-5d25300b1c90
ec3af7d to
a8cc6e2
Compare

Stacked on #878, PR 8 of #822, whose other PRs have merged into
dev: #863 → #867 → #869 → #865 → #864 → #872 → #875. This PR retargets todevonce #878 merges.Part of #805: PR 1 of 3, a cgroup per execution.
What changed
The runtime adapter's worker ran each program in a process group of its own, which it killed before it collected the program's status. Nothing proved that a descendant that left the group was gone, so every outcome was fenced, and the worker could admit a new execution while such a descendant still ran.
execution-N, prepared withExecutionCgroup::prepare, which the program joins as it spawns (Command::cgroup(cgroup.placement()?)).Options::cgroup(&cgroup, Scope::Execution), which leaves the other executions running. An execution whose program ends on its own is now reusable.worker::MAX_EXECUTIONSuntil its cgroup is empty and the worker removed it. One that does not settle keeps its place and its cgroup until the host stops the worker, which ends and removes every cgroup that is left.RootOperations::cgroupsnames the cgroups that the bootstrap prepared, in which the worker runs its executions once the root is ready; under runtime adapter: run cold sandboxes end to end on KVM, MSHV and WHP #822's order, it runs none before the host started the sandbox. Where the in-process tests cannot create cgroups, the fake root names none, and the programs keep their process groups, as before.Tests
In
tests/guest_session.rs, where the runner lets the tests create cgroups (root, in a cgroup v2 hierarchy with thepidscontroller), each fake root gets a workload cgroup of its own, and every execution test runs with cgroups, its outcomes reusable. Elsewhere, the tests run with process groups, as before. New tests, with cgroups:Validation
This branch is rebased onto #878's current head (47f8bcd), the maintainer's re-commit of #878 on
devafter #875, #907, #908 and #911 merged; the OpenVMM gitlink stays 2b681564. The rebase had no conflicts, andgit range-diffshows the three commits' patches unchanged (3855029 → 878906a, a012812 → 229c58d, ec3af7d → a8cc6e2).check-nvx-rsin its three configurations, with rustfmt, Clippy with warnings denied, the build, the tests, andcargo +1.89 check(the MSRV): protocol-only, host-only, and the guest-onlyx86_64-unknown-linux-muslconfiguration; andscripts/check_markdown_links.py;guest_sessiontests; then 10 runs ofguest_sessionas root, with cgroups, and 10 unprivileged, with process groups, and 3 runs of the library's tests, all passing, with no cgroup left behind;host_session, all passing;v0.1.0-dev.c2ace076c60aartifacts (OpenVMM 2b681564): on WHP, locally, all 35 layered scenarios, the 3 restart tests and the 3 step tests pass; on MSHV, on a Linux test host, as root, with OpenVMM's--cpu-profile host, under a short temporary directory, which keeps the VMs' control sockets' paths withinSUN_LEN, so do all of them. KVM is left to CI. This PR adds no VM scenario; PR 3 of runtime adapter: settle executions through cgroups and run them in a VM #805 adds those that run executions.