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>
4.1 KiB
M3a task 11: the pending-approval table
Branch: m3a (run git switch m3a; git status --short must be empty, otherwise stop)
Commit subject: Add the table of pending approvals
Goal
A call an ask grant matched waits in brokerd::approvals::Table until someone answers it:
bxctl approve, bxctl refuse, the expiry thread, or the waiting thread itself when loopd has
gone. They can race. One rule settles every race: whoever takes the entry out of the table
answers it, and everyone else finds it gone. So there is no "look, then remove": take is one
step under the lock. The table is in memory only.
Files
- Copy:
crates/brokerd/tests/approvals.rs - Create:
crates/brokerd/src/approvals.rs - Modify:
crates/brokerd/src/lib.rs(addpub mod approvals;),docs/implementer-log.md
Interfaces
use std::collections::BTreeMap;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Mutex, MutexGuard};
use proto::{DenyReason, PendingApproval, Timestamp};
use crate::policy::{Ask, Decision};
/// What the waiting thread is told. Boxed: clippy's `large_enum_variant` rejects it unboxed.
#[derive(Debug)]
pub enum Verdict { Run(Box<Decision>), Denied(DenyReason) }
#[derive(Debug)]
pub struct Entry { pub info: PendingApproval, pub ask: Ask, pub reply: Sender<Verdict> }
#[derive(Debug, Default)]
pub struct Table { entries: Mutex<BTreeMap<u64, Entry>> }
impl Table {
pub fn new() -> Table;
pub fn insert(&self, info: PendingApproval, ask: Ask) -> Receiver<Verdict>;
pub fn take(&self, id: u64) -> Option<Entry>;
pub fn take_expired(&self, now: Timestamp) -> Vec<Entry>;
pub fn list(&self) -> Vec<PendingApproval>;
}
PendingApproval is proto's (task 02); its approval field is the id. Ask is not Clone,
which is why take returns the entry itself.
Verified in the std docs: std::sync::mpsc::channel::<T>() -> (Sender<T>, Receiver<T>);
Sender::send(&self, t) -> Result<(), SendError<T>>, an error only when the receiver is gone;
Mutex::lock() -> LockResult<MutexGuard<T>>, and a poisoned lock's guard is
poisoned.into_inner(); BTreeMap::remove(&mut self, &k) -> Option<V>; values() iterates in
key order.
Rules
- One private helper takes the lock, and every method uses it:
self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner()). Nothing in this module can leave the map half-changed, so a lock poisoned by a panic elsewhere is taken over as it is (the ledger lock of task 12 is different; do not copy this there). insert: make a channel, insertEntry { info, ask, reply: sender }underinfo.approval, return the receiver. Ids come from the audit log'sseqand never repeat.take:removeunder the lock and return it.Nonewhen the id is not there.take_expired: under one lock, collect the ids of the entries withnow >= expires(exactly atexpiresis expired), remove each, and return the entries in id order. Taking the lock once per id would let anapproveslip in between; the tests race the two.list: theinfoof every entry, cloned, in id order.
No method sends on reply: whoever took the entry does that (tasks 13 and 14).
Steps
- 1. Copy.
git switch m3a, thencp docs/plans/M3a/files/crates/brokerd/tests/approvals.rs crates/brokerd/tests/ - 2. See the test fail.
cargo test -p brokerd --test approvals. Expected: it does not compile. - 3. Write
approvals.rsand addpub mod approvals;tolib.rs.cargo fmt --all. - 4. See the tests pass.
cargo test -p brokerd --test approvals, five times. Expected:7 passedevery time. Two tests race threads a hundred times each; a failure now and then means a method looks and removes under two separate locks. - 5. Run the gate.
make gate. Expected last line:gate: ok. - 6. Log and commit.
git add crates/brokerd docs/implementer-log.md && git commit
Done when
approvalsreports 7 passed five runs in a row, andmake gateprintsgate: ok.
Stop and report if
- A test needs the table to write anything to disk, or to answer an entry itself.