14 moves the pipe handling out of container.rs (a pure move, replayed on its own); 15 starts threads with Builder and bounds output collection with a 2 s grace period; 16 escapes container errors in the log and fixes two texts; 17 fixes toolkit's thread start, casts and the egress-proxy form. Each checked against a reference, which is not kept. Tips T24 to T26 from this run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
6.3 KiB
M3b task 15: no thread panics, and a grace period for the output
Branch: m3b (run git switch m3b; git status --short must be empty, otherwise stop)
Commit subject: brokerd: start pipe threads safely and collect output within a grace period
Goal
Two defects from the M3b review, both in pipes.rs (task 14):
- Finding 3.
std::thread::spawnpanics when the system cannot make a thread. The panic drops theChildwithout stopping the container, which then runs on without its time limit. Usestd::thread::Builder, which returns an error instead, and stop the container on that error. - Finding 4.
Io::finishjoins the readers with no limit. Each reader stops only at its pipe's end, so if any other process still holds a pipe,brokerdwaits for it: the call's time limit does not bound it. After the container has ended, give the output a grace period of 2 s, then abandon what has not ended.
Files
- Copy:
crates/brokerd/tests/container_grace.rs - Modify:
crates/brokerd/src/pipes.rs,crates/brokerd/src/container.rs,docs/implementer-log.md
pipes.rs, new shape
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::thread::Builder;
use std::time::{Duration, Instant};
/// How long, after the container has ended, its output may take to reach its end.
pub const GRACE: Duration = Duration::from_secs(2);
/// What came back.
pub(crate) struct Finished {
pub out: Vec<u8>,
pub truncated: bool,
pub err: String,
/// A pipe had not ended by the grace deadline: something outside the container holds it.
pub open: bool,
}
pub(crate) struct Io {
stdout: Option<Receiver<(Vec<u8>, bool)>>,
stderr: Option<Receiver<(Vec<u8>, bool)>>,
}
impl Io {
pub(crate) fn start(child: &mut Child, input: String, cap: usize, err_cap: usize)
-> std::io::Result<Io>;
pub(crate) fn finish(self, grace: Duration) -> Finished;
}
pub(crate) fn read_capped(from: impl Read, cap: usize) -> (Vec<u8>, bool); // as now
Write each function in turn, running cargo check -p brokerd after each.
Io::start
- If
child.stdin.take()isSome(stdin): start a thread withBuilder::new().spawn(…)that writesinputwithwrite_all(ignore its error) and then dropsstdin. Do not keep its handle and never join it: it ends when the input is written or the pipe breaks. A spawn error → return it (?). - For standard output with
cap, and then standard error witherr_cap: if the pipe isSome, make a channel (mpsc::channel()), start a thread withBuilder::new().spawn(…)that runsread_capped(pipe, cap)andsends the result (ignore a send error), and keep theReceiver. A spawn error → return it. Write a small private functionfn reader(pipe: impl Read + Send + 'static, cap: usize) -> std::io::Result<Receiver<(Vec<u8>, bool)>>and use it for both. Ok(Io { stdout, stderr }).
Io::finish(grace)
One deadline for both readers: let until = Instant::now() + grace;. For each receiver (standard
output first), a small private function collect(rx, until) -> (Vec<u8>, bool, bool):
None(no pipe) →(empty, false, false).rx.recv_timeout(until.saturating_duration_since(Instant::now())):Ok((bytes, truncated))→(bytes, truncated, false);Err(RecvTimeoutError::Timeout)→(empty, false, true): open;Err(RecvTimeoutError::Disconnected)(the reader panicked) →(empty, false, false).
Return Finished { out, truncated, err: String::from_utf8_lossy(&err_bytes).into_owned(), open: stdout_open || stderr_open }.
read_capped
Unchanged in what it does, but no indexing and no subtraction that could wrap: keep
let take = n.min(cap.saturating_sub(kept.len()));, copy with
kept.extend_from_slice(chunk.get(..take).unwrap_or_default());, and set truncated when
take < n. Retry ErrorKind::Interrupted; stop on any other error.
container.rs
- A new constant after
TIMED_OUT:pub const OUTPUT_OPEN: &str = "the tool left its output open"; use crate::pipes::{GRACE, Io};- In
run_container, replace the lines fromlet io = Io::start(…)to the end of the function with exactly this:
let io = match Io::start(&mut child, input, cap, STDERR_KEPT) {
Ok(io) => io,
Err(e) => {
// The container may be running: stop it before answering.
self.podman(&["kill", name]);
self.podman(&["rm", "-f", name]);
let _ = child.kill();
let _ = child.wait();
(self.log)(&format!("brokerd: cannot start a thread for {name}: {e}\n{RUNBOOK}"));
return Err(RunError::Unavailable(CANNOT_START.to_string()));
}
};
let status = self.wait(&mut child, name, started, limit);
let done = io.finish(GRACE);
if done.open && status.is_some() {
(self.log)(&format!(
"brokerd: {name} ended but something still holds its output; it was abandoned"
));
return Err(RunError::Failed(OUTPUT_OPEN.to_string()));
}
self.answer(name, status, done.out, done.truncated, &done.err)
When the time limit was passed (status is None), an open pipe changes nothing: the answer is
still TIMED_OUT, from answer.
Also: grep -n "thread::spawn" crates/brokerd/src/ must print nothing when you are done.
Steps
- 1. Copy.
git switch m3b, thencp docs/plans/M3b/files/crates/brokerd/tests/container_grace.rs crates/brokerd/tests/ - 2. See it fail.
cargo test -p brokerd --test container_grace. Expected: it does not compile (OUTPUT_OPENandpipes::GRACEdo not exist). - 3. Write
pipes.rs, one function at a time, thencontainer.rs. Runcargo fmt --all. - 4. See it pass.
cargo test -p brokerd --test container_grace. Expected: 4 passed, in about 4 s (two tests wait out the grace period on purpose). Thencargo test -p brokerd --test container --test container_egress: 11 and 6 passed. Run all three five times. - 5. Run the gate.
make gate. Expected last line:gate: ok. - 6. Log and commit.
git add crates/brokerd docs/implementer-log.md Cargo.lock && git commit
Done when
- The three suites pass five times running; no
thread::spawnincrates/brokerd/src/;make gateprintsgate: ok.
Stop and report if
- A test takes much longer than stated, or passes only sometimes.