M3b plan: task 11 starts from a compiling skeleton of container.rs
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>
This commit is contained in:
@@ -13,8 +13,10 @@ proxy is task 12; in this task every call runs its container with no egress dire
|
|||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
- Copy: `crates/brokerd/tests/support/fake_podman.rs`, `crates/brokerd/tests/container.rs`
|
- Copy: `crates/brokerd/tests/support/fake_podman.rs`, `crates/brokerd/tests/container.rs`, and
|
||||||
- Create: `crates/brokerd/src/container.rs`
|
the **skeleton** `crates/brokerd/src/container.rs` (every signature, the constants, and `todo!()`
|
||||||
|
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`
|
- Modify: `crates/brokerd/src/lib.rs` (`pub mod container;`), `docs/implementer-log.md`
|
||||||
|
|
||||||
## Interfaces
|
## Interfaces
|
||||||
@@ -88,13 +90,30 @@ the `serial()` lock first, because writing a script and running it at once races
|
|||||||
forks ("Text file busy"). The time-limit test's fake ends with `exec sleep 30`, so killing the
|
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.
|
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, run
|
||||||
|
`cargo check -p brokerd` and 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
|
## Steps
|
||||||
|
|
||||||
- [ ] **1. Copy.** `git switch m3b`, then
|
- [ ] **1. Copy.** `git switch m3b`, then
|
||||||
`cp docs/plans/M3b/files/crates/brokerd/tests/support/fake_podman.rs crates/brokerd/tests/support/`
|
`cp docs/plans/M3b/files/crates/brokerd/tests/support/fake_podman.rs crates/brokerd/tests/support/`
|
||||||
and `cp docs/plans/M3b/files/crates/brokerd/tests/container.rs crates/brokerd/tests/`
|
and `cp docs/plans/M3b/files/crates/brokerd/tests/container.rs crates/brokerd/tests/`
|
||||||
- [ ] **2. See it fail.** `cargo test -p brokerd --test container`. Expected: it does not compile.
|
and `cp docs/plans/M3b/files/crates/brokerd/src/container.rs crates/brokerd/src/`, then add
|
||||||
- [ ] **3. Write `container.rs`.** Run `cargo fmt --all`.
|
`pub mod container;` to `crates/brokerd/src/lib.rs` after `pub mod config;`.
|
||||||
|
- [ ] **2. See it fail.** `cargo test -p brokerd --test container`. Expected: it compiles (with
|
||||||
|
warnings about unused variables) and all 11 fail on `todo!()`.
|
||||||
|
- [ ] **3. Fill in `container.rs`** as in "How to work". Delete the skeleton paragraph from the
|
||||||
|
module doc. Run `cargo fmt --all`.
|
||||||
- [ ] **4. See it pass.** `cargo test -p brokerd --test container`. Expected: 11 passed. Run it ten
|
- [ ] **4. See it pass.** `cargo test -p brokerd --test container`. Expected: 11 passed. Run it ten
|
||||||
times; it must pass every time.
|
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
|
- [ ] **5. Walk the table.** Point at the line of your code for each row, and check that no row
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ At the end: `make gate` prints `gate: ok` with about 638 tests.
|
|||||||
`egress_dir` for task 12 but gave nothing that read it, so clippy's `dead_code` would fail the
|
`egress_dir` for task 12 but gave nothing that read it, so clippy's `dead_code` would fail the
|
||||||
gate and `#[allow]` is forbidden: a task that could not be written as described (tip T16). The
|
gate and `#[allow]` is forbidden: a task that could not be written as described (tip T16). The
|
||||||
reference had a public `egress_dir()` getter; the task now gives it. Resume from task 11.
|
reference had a public `egress_dir()` getter; the task now gives it. Resume from task 11.
|
||||||
|
- 2026-09-23, task 11 again: the second session also ended with nothing written, cut off while
|
||||||
|
deliberating over API details the compiler would have settled. The task now hands over a
|
||||||
|
compiling skeleton of `container.rs` (signatures, constants, the steps as comments, `todo!()`
|
||||||
|
bodies), checked against the given tests (11 red), and says to fill one function at a time with
|
||||||
|
`cargo check` between. Resume from task 11.
|
||||||
|
|
||||||
## Running it
|
## Running it
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//! The Podman runtime: one fresh container per call, with the limits of `[runner]`. Whatever the
|
||||||
|
//! tool prints is the result's content; every failure is a fixed sentence, and what Podman itself
|
||||||
|
//! said goes only to `brokerd`'s log. M3b spec, section 6.
|
||||||
|
//!
|
||||||
|
//! SKELETON from task 11: replace every `todo!()`, one function at a time, running
|
||||||
|
//! `cargo check -p brokerd` after each. Then delete this paragraph.
|
||||||
|
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Child, Command, ExitStatus, Stdio};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::config::Runner;
|
||||||
|
use crate::podman;
|
||||||
|
use crate::runner::{RunError, RunOutput, RunSpec, Runtime};
|
||||||
|
|
||||||
|
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";
|
||||||
|
/// How often a running container is checked.
|
||||||
|
pub const POLL: Duration = Duration::from_millis(50);
|
||||||
|
/// How much of Podman's standard error is kept for the log.
|
||||||
|
pub const STDERR_KEPT: usize = 4096;
|
||||||
|
|
||||||
|
/// A log sink. Call it as `(self.log)("a line")`.
|
||||||
|
pub type Log = Arc<dyn Fn(&str) + Send + Sync>;
|
||||||
|
|
||||||
|
pub struct Podman {
|
||||||
|
runner: Runner,
|
||||||
|
egress_dir: PathBuf,
|
||||||
|
log: Log,
|
||||||
|
next: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Podman {
|
||||||
|
/// `egress_dir` is `Config::egress_dir()`; task 12 uses it.
|
||||||
|
pub fn new(runner: Runner, egress_dir: PathBuf, log: Log) -> Podman {
|
||||||
|
todo!("store the four fields; `next` starts at 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where `http_fetch` calls get their directories. Public, so the field counts as read.
|
||||||
|
pub fn egress_dir(&self) -> &Path {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run one short `podman` command (`kill`, `rm -f`) whose result only matters for the log.
|
||||||
|
pub(crate) fn podman(&self, args: &[&str]) {
|
||||||
|
todo!("`.status()` with the three standard streams null; log a line if it is not a success")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for `child` until `limit` has passed since `started`. `Some(status)` if it ended;
|
||||||
|
/// `None` if it ran too long, after `podman kill <name>`, `podman rm -f <name>`,
|
||||||
|
/// `child.kill()` and `child.wait()` (step 5).
|
||||||
|
fn wait(&self, child: &mut Child, name: &str, started: Instant, limit: Duration) -> Option<ExitStatus> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runtime for Podman {
|
||||||
|
fn run(&self, spec: &RunSpec) -> Result<RunOutput, RunError> {
|
||||||
|
// 1. `n` and `name`.
|
||||||
|
// 2. `args`, `input`, `limit`.
|
||||||
|
// 3. Spawn podman with all three standard streams piped; a failure is Unavailable.
|
||||||
|
// 4. Three threads: write `input` then drop stdin; `read_capped` stdout with the cap;
|
||||||
|
// `read_capped` stderr with STDERR_KEPT.
|
||||||
|
// 5. `self.wait(…)`.
|
||||||
|
// 6. Join the three threads (`join().ok()`, `unwrap_or_default()`).
|
||||||
|
// 7. The answer, by the table in the task.
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything `from` gives, keeping the first `cap` bytes; `true` if there was more. Reads on past
|
||||||
|
/// the cap, so the writer is never blocked or broken by a closed pipe. No indexing: `buf.get(..n)`.
|
||||||
|
fn read_capped(from: impl Read, cap: usize) -> (Vec<u8>, bool) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user