Two sessions ended with nothing written, each out of room while planning the whole file in one turn. The skeleton has the signatures, the constants and the steps as comments; the task says to fill one function at a time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7.2 KiB
M3b task 11: the Podman runtime
Branch: m3b (run git switch m3b; git status --short must be empty, otherwise stop)
Commit subject: brokerd: the Podman runtime
Goal
Podman implements Runtime: each call runs in a fresh container, its arguments go on standard
input, its output is kept up to a cap, and it is stopped at its time limit. Whatever the tool
prints is the result's content, labelled by the grant; every failure is a fixed sentence, and
what Podman itself said goes only to brokerd's log. Spec section 6, "One call". http_fetch's
proxy is task 12; in this task every call runs its container with no egress directory.
Files
- Copy:
crates/brokerd/tests/support/fake_podman.rs,crates/brokerd/tests/container.rs, and the skeletoncrates/brokerd/src/container.rs(every signature, the constants, andtodo!()bodies with the steps as comments) - Fill in:
crates/brokerd/src/container.rs - Modify:
crates/brokerd/src/lib.rs(pub mod container;),docs/implementer-log.md
Interfaces
pub const RUNBOOK: &str = "see docs/runbook.md#runner-unavailable";
pub const COULD_NOT_RUN: &str = "the tool could not run";
pub const CANNOT_START: &str = "the tool runner could not start the container";
pub const KILLED: &str = "the tool was stopped: it ran out of memory or was killed";
pub const TIMED_OUT: &str = "the tool ran past its time limit";
pub const UNEXPECTED: &str = "the tool failed with an unexpected status";
pub const POLL: Duration = Duration::from_millis(50);
pub const STDERR_KEPT: usize = 4096;
pub type Log = Arc<dyn Fn(&str) + Send + Sync>;
pub struct Podman { /* runner: Runner, egress_dir: PathBuf, log: Log, next: AtomicU64 — private */ }
impl Podman {
/// `egress_dir` is `Config::egress_dir()`; task 12 uses it.
pub fn new(runner: Runner, egress_dir: PathBuf, log: Log) -> Podman;
/// Where `http_fetch` calls get their directories. Nothing in this task calls it; it is public,
/// so the field counts as read and clippy's `dead_code` does not fire. Do not add `#[allow]`.
pub fn egress_dir(&self) -> &Path;
}
impl Runtime for Podman {
fn run(&self, spec: &RunSpec) -> Result<RunOutput, RunError>;
}
run: every step and exit
n = next.fetch_add(1, SeqCst);name = podman::container_name(spec.session(), spec.call(), n). The first call of aPodmanis number 0.args = podman::tool_args(spec, &runner, &name, None);input = spec.arguments().canonical_json();limit = runner.time_limit(spec.tool()).- Spawn
Command::new(&runner.podman).args(args)with standard input, output and error all piped. A failure → logbrokerd: cannot start {podman path}: {e}+"\n"+RUNBOOK, and returnErr(RunError::Unavailable(CANNOT_START)). - Three threads, so no pipe can block another: one writes
inputto standard input and then drops it (closing it); one reads standard output, keeping the firstoutput_capbytes and reading on past the cap, throwing the rest away, and remembering that there was more; one reads standard error the same way, keepingSTDERR_KEPTbytes. (Stopping to read would leave the tool blocked on a full pipe; closing the pipe would make Podman fail. Both give the wrong answer.) Write the reading loop once, as a private function returning(Vec<u8>, bool). - Wait with a loop:
child.try_wait(); if it has ended, go to 6. Iflimithas passed since the spawn, stop the container: runpodman kill <name>, thenpodman rm -f <name>(each with.status(), standard streams null; log a line if one does not succeed, and go on), thenchild.kill()andchild.wait(). Otherwise sleepPOLLand try again. - Join the three threads (a thread that panicked counts as empty output).
- The answer:
| How it ended | Answer | Log |
|---|---|---|
| past the time limit (step 5) | Err(RunError::Failed(TIMED_OUT)) |
a line naming the container |
| exit 0 or 1 | Ok(RunOutput { content, truncated }), content = the kept output decoded with from_utf8_lossy |
— |
| exit 2 | Err(RunError::Failed(COULD_NOT_RUN)) |
the kept standard error |
| exit 125, 126 or 127 | Err(RunError::Unavailable(CANNOT_START)) |
the kept standard error, then "\n" + RUNBOOK |
| exit 137 | Err(RunError::Failed(KILLED)) |
— |
| anything else, including killed by a signal | Err(RunError::Failed(UNEXPECTED)) |
the status and the kept standard error |
The tool's output never goes into a RunError: only the six constants do.
Also add pub(crate) fn podman(&self, args: &[&str]), the helper step 5 uses to run a short
podman command and log if it fails; task 12 uses it too.
About the given tests
The tests run brokerd's real runner::run against a fake podman: a shell script that records
every call's arguments in a file and then does what the test says. As in task 06, every test takes
the serial() lock first, because writing a script and running it at once races with other tests'
forks ("Text file busy"). The time-limit test's fake ends with exec sleep 30, so killing the
process kills the sleep and nothing keeps the pipes open.
How to work (read this first)
Two earlier sessions on this task ended with nothing written: each tried to plan the whole file in one turn and ran out of room while still deciding. So:
- Start from the skeleton. It compiles. Replace the
todo!()s one function at a time, in this order:read_capped,new,egress_dir,podman,wait,run. After each, runcargo check -p brokerdand fix what it says before going on. - Let the compiler answer API questions. If you are unsure whether something compiles (how to
call the log, which trait a type has), write it and run
cargo check. The log is called as(self.log)(&line). - Keep each turn short: write, check, next. Do not restate the task to yourself.
Steps
- 1. Copy.
git switch m3b, thencp docs/plans/M3b/files/crates/brokerd/tests/support/fake_podman.rs crates/brokerd/tests/support/andcp docs/plans/M3b/files/crates/brokerd/tests/container.rs crates/brokerd/tests/andcp docs/plans/M3b/files/crates/brokerd/src/container.rs crates/brokerd/src/, then addpub mod container;tocrates/brokerd/src/lib.rsafterpub mod config;. - 2. See it fail.
cargo test -p brokerd --test container. Expected: it compiles (with warnings about unused variables) and all 11 fail ontodo!(). - 3. Fill in
container.rsas in "How to work". Delete the skeleton paragraph from the module doc. Runcargo fmt --all. - 4. See it pass.
cargo test -p brokerd --test container. Expected: 11 passed. Run it ten times; it must pass every time. - 5. Walk the table. Point at the line of your code for each row, and check that no row
puts output into a
RunError. - 6. Run the gate.
make gate. Expected last line:gate: ok. - 7. Log and commit.
git add crates/brokerd docs/implementer-log.md Cargo.lock && git commit
Done when
cargo test -p brokerd --test containerreports 11 passed ten times running;make gateprintsgate: ok.
Stop and report if
- A test hangs, or fails only sometimes: report which, and how often, rather than add sleeps.
- A test wants tool output inside a failure message.