Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 15 additions & 35 deletions crates/busbar-kernel/src/teller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,20 +248,18 @@ pub struct UnitCtx {
pub kernel_verb_only: bool,
}

/// The kernel's running total of what a unit has spent, in nano-units.
/// A running count a test rig keeps of what its unit spent, in nano-units.
///
/// The hold itself lives inside the cell where nothing can borrow it mutably, so the loop counts
/// here as the unit accrues and applies the total to the hold at the exit, where the hold is out of
/// the cell and owned. An accrual is never lost by this: the total is atomic and the exit reads it
/// after the compare-and-set that took the hold.
/// NOT a money figure the exit reads. The exit spends and settles the hold by the settlement
/// writer's one `amount` (TODO L1689); a second figure here could only disagree with it. No
/// production leg accrues onto this.
#[derive(Debug, Default)]
pub struct AccrualMeter {
spent: AtomicU64,
headroom: AtomicU64,
}

impl AccrualMeter {
/// A meter reading zero, with no headroom offered.
/// A meter reading zero.
pub fn new() -> Self {
AccrualMeter::default()
}
Expand All @@ -275,25 +273,6 @@ impl AccrualMeter {
pub fn total(&self) -> u64 {
self.spent.load(Ordering::Acquire)
}

/// Say how far the unit's reservation may still be grown, in nano-units.
///
/// The loop cannot work this out: it is what the principal's slice has left in the window, and
/// only the plane's own leg holds the chain that answers. So the leg offers it here, while it is
/// running, and the exit reads it when it applies the accrual to the hold. A leg that offers
/// nothing gets the safe answer — the reservation does not grow and the excess is carried —
/// which is a unit that still runs and still posts, never one that is refused.
///
/// Offered rather than added: the last word wins, because the figure is a reading of the window
/// and not a quantity that accumulates.
pub fn offer_headroom(&self, nanos: u64) {
self.headroom.store(nanos, Ordering::Release);
}

/// What the leg said the reservation may still grow by.
pub fn headroom(&self) -> u64 {
self.headroom.load(Ordering::Acquire)
}
}

/// The settlement writer the exit, the sweep and recovery all settle by. Pure data and pure
Expand Down Expand Up @@ -437,8 +416,8 @@ pub trait Units {
///
/// Route is the one step of the ten that dials, sends and relays, so it is the one step whose answer
/// is not already in hand when the loop asks for it. Meter folds what Route observed and the exit
/// settles what the meter counted; neither waits on anything, so neither is here and neither needs
/// to be. This trait is that single seam, stated: one method, one future, one place the loop yields.
/// settles what the settlement writer reported; neither waits on anything, so neither is here and
/// neither needs to be. This trait is that single seam, stated: one method, one future, one place the loop yields.
///
/// A plane whose Route answers in place does not implement this at all — [`run_unit`] supplies a leg
/// that is ready on its first poll, over that plane's own [`Units::route`], and the loop body it
Expand Down Expand Up @@ -545,7 +524,7 @@ pub struct Run<'r> {
pub gauge: &'r ConcurrencyGauge,
/// The counts the node balances.
pub canary: &'r Canary,
/// What the unit has spent so far.
/// A test rig's spend count. The exit does not read it: it settles by the one reported figure.
pub meter: &'r AccrualMeter,
}

Expand Down Expand Up @@ -1313,12 +1292,13 @@ pub fn exit<U: Units>(
let Written { amount, flags, fee } = settle_written(&outcome, &evidence);
let drawn = requests_drawn(ctx.origin, evidence.upstream_candidate);
let requests = requests_settled(reached_admitted, drawn);
// What the unit spent while it ran is applied to the hold here, where the hold is
// owned. The spend lands in full: past the end of the reservation it grows out of
// whatever headroom the leg offered while it ran, and whatever nothing can back is
// carried out as an overdraft. There is no arm on this path that refuses — value was
// delivered, so the only question left is which column it lands in.
let _spend = hold.spend(run.meter.total(), run.meter.headroom());
// ONE FIGURE (TODO L1689): the hold is spent by the same `amount` the posting settles,
// so the overdraft counter and the settled figure can never disagree. Spending one
// number and settling another posted an OVERDRAFT on an end that settled inside its
// reservation. The spend lands in full and whatever nothing backs is carried out as an
// overdraft — no arm on this path refuses, value was delivered. Headroom is zero: no
// leg draws a slice at the exit, so the reservation does not grow here.
let _spend = hold.spend(amount, 0);
let class = evidence.class.unwrap_or(KERNEL_ACCRUAL_CLASS);
let lines = vec![UsageLine {
class,
Expand Down
35 changes: 35 additions & 0 deletions crates/busbar-kernel/tests/loop_battery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,41 @@ fn running_past_the_reservation_posts_the_overdraft_rather_than_refusing() {
}
}

/// ONE FIGURE SETTLES THE HOLD (TODO L1689). The exit spent the hold by the rig's meter and settled
/// it by the reported `amount`: two numbers, so an end that settled 80 inside a reservation of 100
/// posted an OVERDRAFT of 50 the meter alone ran up. The hold is now spent by the figure it settles
/// at, so a second count — whatever it reads — cannot move the posting.
#[test]
fn the_hold_is_spent_by_the_figure_it_settles_at_not_by_a_second_count() {
let kernel = Kernel::new();
let units = TestUnits {
door: Door::Own(100),
spend: 150,
evidence: Evidence {
reported: Some(80),
..Evidence::default()
},
..TestUnits::default()
};
let cell = cell(&kernel);
let canary = Canary::new();
match run(&units, &kernel, &cell, &canary) {
Ended::Settled { end, .. } => {
let posted = end.posted().expect("value was delivered, so it posts");
assert_eq!(posted.reserved(), 100);
assert_eq!(posted.settled(), 80);
assert_eq!(posted.released(), 20);
assert_eq!(
posted.overdraft(),
0,
"settled inside its reservation, so nothing went unbacked"
);
assert!(!posted.flags().contains(PostingFlags::OVERDRAFT));
}
other => panic!("expected a settled end, got {other:?}"),
}
}

#[test]
fn the_leases_go_back_on_every_end_whatever_it_was() {
let kernel = Kernel::new();
Expand Down
5 changes: 3 additions & 2 deletions crates/busbar/src/root/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,11 @@ pub fn run<U: Units>(
},
);

if let Ended::Settled { requests, fee, .. } = &ended {
if let Ended::Settled { end, requests, fee } = &ended {
ledger.post(RecordingRow {
principal: principal.to_string(),
amount: meter.total(),
// What the posting settled, not a second count beside it (TODO L1689).
amount: end.posted().map_or(0, |posted| posted.settled()),
requests: *requests,
fee: *fee,
});
Expand Down