Task files, the files they copy in (byte-identical to the reference on m3a-ref), each area's check record, and a README with the per-task table of what each check exposed. The handoff note is done with. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.7 KiB
M3a task 15: brokerd serve
Branch: m3a (run git switch m3a; git status --short must be empty, otherwise stop)
Commit subject: Add brokerd serve: startup, both sockets, and the expiry thread
Goal
brokerd serve --config <path> [--accept-break] takes the audit lock, checks the log, makes its
two socket directories private, binds broker.sock and admin.sock, and serves each connection
on its own thread, with one more thread expiring approvals every second. The runtime is
Refusing (task 10). The audit lock comes first: it is what proves that a socket file left
behind is stale and not another brokerd's. serve is the only module that starts threads.
Files
- Copy:
crates/brokerd/tests/serve.rs - Create:
crates/brokerd/src/serve.rs - Modify:
crates/brokerd/src/main.rs(it holds the "not implemented" stub; replace all of it),crates/brokerd/src/lib.rs(addpub mod serve;),docs/implementer-log.md
Interfaces
#[derive(Debug)]
pub enum ServeError {
Audit(AuditError), // Display: the AuditError's text
Dir(PathBuf, std::io::Error), // "cannot prepare {path}: {e}"
Socket(PathBuf, std::io::Error), // "cannot listen on {path}: {e}"
} // + std::error::Error
pub struct Started {
pub broker: Arc<Broker>,
pub recovered: bool, // from audit::Opened
pub accepted: Option<proto::ChainFailure>, // from audit::Opened
/* private: tools: UnixListener, admin: UnixListener */
}
pub fn start(cfg: Config, accept_break: bool, runtime: Box<dyn Runtime>,
log: Arc<dyn Fn(&str) + Send + Sync>) -> Result<Started, ServeError>;
impl Started { pub fn run(self) -> std::io::Result<()>; }
start, in this order
audit::Writer::open(&cfg.audit_dir(), accept_break);Err(e)→Err(Audit(e)). Nothing else has happened yet: no directory, no socket.listen(&cfg.broker_socket()), thenlisten(&cfg.admin_socket()); each error returned.Ledger::new(Box::new(opened.writer), StateStore::new(&cfg.state_dir()), log1)andBroker::new(cfg, ledger, runtime, log2), wherelog1andlog2are boxes that call the oneArc(let l = Arc::clone(&log); Box::new(move |line| l(line))).
listen(socket), every exit (the directory is socket.parent(), or / if it has none):
DirBuilder::new().recursive(true).mode(0o700).create(dir);Err→Dir.std::fs::set_permissions(dir, Permissions::from_mode(0o700))always, also when the directory was already there at 0755;Err→Dir.std::fs::remove_file(socket):OkorNotFoundgo on; any otherErr→Socket.UnixListener::bind(socket);Err→Socket.set_permissions(socket, from_mode(0o600));Err→Socket. Return the listener.
run
- Spawn the expiry thread: forever,
sleep(1 s), thenadmin::expire_due(&broker, Timestamp::now()). - Spawn one accept thread per listener. Each does: for every
incoming()stream,stream?, then spawn a thread callingbroker::handle(tools) oradmin::handle(admin) with its ownArcclone. Whenincominggives an error, the accept thread sends thatErron anmpscchannel. runwaits for the first message on that channel and returns it. Either listener failing stops the daemon. (If the channel is closed, returnErr(io::Error::other("a listener thread ended")).) A send after the first has nobody to receive it; ignoring that result is correct, and a comment should say so.
main.rs
Usage is exactly usage: brokerd serve --config <path> [--accept-break]. The first argument must
be serve; then --config <path> exactly once and --accept-break at most once, in any order.
Anything else (no arguments, a missing value, a flag twice, an unknown word) → print the usage to
stderr, exit 2.
Config::load(&path);Err(e)→brokerd: {e}, exit 1.start(cfg, accept_break, Box::new(Refusing), Arc::new(|line: &str| eprintln!("{line}")))(keepcfg.broker_socket()andcfg.admin_socket()first, for step 4).Err(ServeError::Audit(AuditError::NothingToAccept))→brokerd: {e}, exit 2. Any otherErr(e)→brokerd: {e}, exit 1. (AuditError's text already ends with its runbook pointer.)recovered→eprintln!("{RECOVERED_NOTICE}").accepted: Some(f)→audit: accepted the break at {file}:{line}: {what}.brokerd: serving tools on {broker socket} and approvals on {admin socket}.run():Err(e)→brokerd: {e}, exit 1.
Steps
- 1. Copy.
git switch m3a, thencp docs/plans/M3a/files/crates/brokerd/tests/serve.rs crates/brokerd/tests/ - 2. See the test fail.
cargo test -p brokerd --test serve. Expected: it compiles (it runs the binary) and all 9 fail, becausebrokerdstill says "not implemented"; one of them waits ten seconds for sockets that never appear. - 3. Write
serve.rs, addpub mod serve;, rewritemain.rs. Runcargo fmt --all. - 4. See the tests pass.
cargo test -p brokerd --test serve, five times. Expected:9 passedevery time. - 5. Run all of
brokerd.cargo test -p brokerd. Every suite passes. - 6. Run the gate.
make gate. Expected last line:gate: ok. - 7. Log and commit.
git add crates/brokerd docs/implementer-log.md && git commit
Done when
servereports 9 passed five runs in a row, andmake gateprintsgate: ok.
Stop and report if
- A test wants a socket bound, or a directory made, before the audit lock is taken.
- You need
libc,unsafeor a signal handler.brokerdis stopped by being killed; the audit log is safe at any moment because every record is synced before anything acts on it.