use super::{
BudgetPacer, CHUNK_BYTES, CHUNK_GROUP_BYTES, DownstreamFrontier, MIN_DRAW_WINDOW,
PULL_WINDOW_FLOOR, PaceDecision, PaceState, Pacer, RampPacer, VERIFY_LAG_BYTES, WindowPacer,
};
use alloy::primitives::U256;
use std::assert_matches;
fn healthy() -> PaceState {
PaceState {
cleared_bytes: 16 * 1024,
requested_bytes: 1_000_000,
remaining_deposit: U256::from(1_000u64),
next_voucher_cost: U256::from(10u64),
pulled_frontier: 0,
gap_remaining: 1_000_000,
downstream: DownstreamFrontier::default(),
}
}
#[test]
fn deposit_covers_the_next_voucher_so_draw() {
let s = healthy();
assert_eq!(
BudgetPacer::new().decide(&s),
PaceDecision::Draw {
up_to_bytes: s.requested_bytes - s.cleared_bytes
}
);
}
#[test]
fn fully_cleared_range_is_done() {
let mut s = healthy();
s.cleared_bytes = s.requested_bytes;
assert_eq!(BudgetPacer::new().decide(&s), PaceDecision::Done);
s.cleared_bytes = s.requested_bytes + 5;
assert_eq!(BudgetPacer::new().decide(&s), PaceDecision::Done);
}
#[test]
fn a_deposit_short_of_the_next_voucher_refuses_instead_of_topping_up() {
let mut s = healthy();
s.remaining_deposit = U256::from(4u64);
s.next_voucher_cost = U256::from(10u64);
assert_eq!(BudgetPacer::new().decide(&s), PaceDecision::Refuse);
}
#[test]
fn a_deposit_that_exactly_covers_the_next_voucher_draws() {
let mut s = healthy();
s.remaining_deposit = U256::from(10u64);
s.next_voucher_cost = U256::from(10u64);
assert_matches!(BudgetPacer::new().decide(&s), PaceDecision::Draw { .. });
}
#[test]
fn the_first_draw_before_any_quote_always_proceeds() {
let mut s = healthy();
s.remaining_deposit = U256::ZERO;
s.next_voucher_cost = U256::ZERO;
assert_matches!(BudgetPacer::new().decide(&s), PaceDecision::Draw { .. });
}
#[test]
fn window_room_clamps_the_draw() {
let mut s = healthy();
s.pulled_frontier = 5 * CHUNK_GROUP_BYTES;
s.downstream.served_paid = 2 * CHUNK_GROUP_BYTES;
assert_eq!(
BudgetPacer::new().decide(&s),
PaceDecision::Draw {
up_to_bytes: s.requested_bytes - s.cleared_bytes
},
"sanity: BudgetPacer would draw far more than the window room"
);
assert_eq!(
WindowPacer::new(4 * CHUNK_GROUP_BYTES).decide(&s),
PaceDecision::Draw {
up_to_bytes: CHUNK_GROUP_BYTES
}
);
}
#[test]
fn window_room_floors_to_whole_groups() {
let mut s = healthy();
s.pulled_frontier = 5 * CHUNK_GROUP_BYTES;
s.downstream.served_paid = 2 * CHUNK_GROUP_BYTES;
assert_eq!(
WindowPacer::new(3 * CHUNK_GROUP_BYTES + 5_000).decide(&s),
PaceDecision::Wait
);
assert_eq!(
WindowPacer::new(4 * CHUNK_GROUP_BYTES + 1).decide(&s),
PaceDecision::Draw {
up_to_bytes: CHUNK_GROUP_BYTES
}
);
}
fn big_window_state(ahead: u64) -> (WindowPacer, PaceState) {
const WINDOW: u64 = 64 * 1024 * 1024;
let mut s = healthy();
s.requested_bytes = 1 << 40;
s.gap_remaining = 1 << 40;
s.pulled_frontier = 100 * CHUNK_BYTES + ahead;
s.downstream.served_paid = 100 * CHUNK_BYTES;
(WindowPacer::new(WINDOW), s)
}
#[test]
fn a_ramped_window_waits_until_half_the_window_opens() {
const WINDOW: u64 = 64 * 1024 * 1024;
let (pacer, s) = big_window_state(WINDOW - 4 * CHUNK_GROUP_BYTES);
assert_eq!(pacer.decide(&s), PaceDecision::WaitForMinDraw);
let (pacer, s) = big_window_state(WINDOW / 2 + CHUNK_GROUP_BYTES);
assert_eq!(pacer.decide(&s), PaceDecision::WaitForMinDraw);
let (pacer, s) = big_window_state(WINDOW / 2);
assert_eq!(
pacer.decide(&s),
PaceDecision::Draw {
up_to_bytes: WINDOW / 2
}
);
}
#[test]
fn min_draw_is_clamped_to_the_gap_remainder() {
let (pacer, mut s) = big_window_state(64 * 1024 * 1024 - 3 * CHUNK_GROUP_BYTES);
s.gap_remaining = 2 * CHUNK_GROUP_BYTES + 100;
assert_matches!(pacer.decide(&s), PaceDecision::Draw { .. });
let (pacer, mut s) = big_window_state(64 * 1024 * 1024 - 2 * CHUNK_GROUP_BYTES);
s.gap_remaining = 2 * CHUNK_GROUP_BYTES + 100;
assert_eq!(pacer.decide(&s), PaceDecision::WaitForMinDraw);
}
#[test]
fn serve_demand_bypasses_the_min_draw() {
let (pacer, mut s) = big_window_state(64 * 1024 * 1024 - 2 * CHUNK_GROUP_BYTES);
s.downstream.serve_demand = s.pulled_frontier + 1;
assert_eq!(
pacer.decide(&s),
PaceDecision::Draw {
up_to_bytes: PULL_WINDOW_FLOOR
}
);
}
#[test]
fn min_draw_is_off_below_the_min_draw_window() {
let window = MIN_DRAW_WINDOW - CHUNK_GROUP_BYTES;
let mut s = healthy();
s.requested_bytes = 1 << 40;
s.gap_remaining = 1 << 40;
s.pulled_frontier = 100 * CHUNK_BYTES + window - CHUNK_GROUP_BYTES;
s.downstream.served_paid = 100 * CHUNK_BYTES;
assert_eq!(
WindowPacer::new(window).decide(&s),
PaceDecision::Draw {
up_to_bytes: CHUNK_GROUP_BYTES
}
);
s.pulled_frontier = 100 * CHUNK_BYTES + MIN_DRAW_WINDOW - CHUNK_GROUP_BYTES;
assert_eq!(
WindowPacer::new(MIN_DRAW_WINDOW).decide(&s),
PaceDecision::WaitForMinDraw
);
}
#[test]
fn half_the_window_always_opens_once_the_client_catches_up() {
let lag = CHUNK_BYTES + 2 * CHUNK_GROUP_BYTES;
let mut window = MIN_DRAW_WINDOW;
while window <= 64 * 1024 * 1024 {
let mut s = healthy();
s.requested_bytes = 1 << 40;
s.gap_remaining = 1 << 40;
s.pulled_frontier = 100 * CHUNK_BYTES + lag;
s.downstream.served_paid = 100 * CHUNK_BYTES;
assert_matches!(
WindowPacer::new(window).decide(&s),
PaceDecision::Draw { .. },
"window {window} must draw with a caught-up client"
);
window += CHUNK_GROUP_BYTES;
}
}
#[test]
fn window_full_waits() {
let mut s = healthy();
s.pulled_frontier = 5 * CHUNK_GROUP_BYTES;
s.downstream.served_paid = 2 * CHUNK_GROUP_BYTES;
assert_eq!(
WindowPacer::new(3 * CHUNK_GROUP_BYTES).decide(&s),
PaceDecision::Wait
);
}
#[test]
fn serve_demand_at_a_full_window_draws_one_floor() {
let mut s = healthy();
s.requested_bytes = 64 * CHUNK_BYTES;
s.pulled_frontier = 5 * CHUNK_GROUP_BYTES;
s.downstream.served_paid = 2 * CHUNK_GROUP_BYTES;
for demand in [5 * CHUNK_GROUP_BYTES + 1, 6 * CHUNK_GROUP_BYTES] {
s.downstream.serve_demand = demand;
assert_eq!(
WindowPacer::new(3 * CHUNK_GROUP_BYTES).decide(&s),
PaceDecision::Draw {
up_to_bytes: PULL_WINDOW_FLOOR
},
"demand {demand}"
);
}
}
#[test]
fn serve_demand_far_past_the_pull_is_ignored() {
let mut s = healthy();
s.pulled_frontier = 5 * CHUNK_GROUP_BYTES;
s.downstream.served_paid = 2 * CHUNK_GROUP_BYTES;
s.downstream.serve_demand = 6 * CHUNK_GROUP_BYTES + 1;
assert_eq!(
WindowPacer::new(3 * CHUNK_GROUP_BYTES).decide(&s),
PaceDecision::Wait
);
s.downstream.serve_demand = 1 << 30;
assert_eq!(
WindowPacer::new(3 * CHUNK_GROUP_BYTES).decide(&s),
PaceDecision::Wait
);
}
#[test]
fn serve_demand_already_pulled_still_waits() {
let mut s = healthy();
s.pulled_frontier = 5 * CHUNK_GROUP_BYTES;
s.downstream.served_paid = 2 * CHUNK_GROUP_BYTES;
s.downstream.serve_demand = 5 * CHUNK_GROUP_BYTES;
assert_eq!(
WindowPacer::new(3 * CHUNK_GROUP_BYTES).decide(&s),
PaceDecision::Wait
);
}
#[test]
fn serve_demand_inside_a_wider_window_changes_nothing() {
let mut s = healthy();
s.requested_bytes = 64 * CHUNK_BYTES;
s.pulled_frontier = 2 * CHUNK_GROUP_BYTES;
s.downstream.served_paid = 2 * CHUNK_GROUP_BYTES;
s.downstream.serve_demand = 3 * CHUNK_GROUP_BYTES;
assert_eq!(
WindowPacer::new(4 * CHUNK_BYTES).decide(&s),
PaceDecision::Draw {
up_to_bytes: 4 * CHUNK_BYTES
}
);
}
#[test]
fn window_pacer_passes_through_done_and_refuse() {
let mut s = healthy();
s.cleared_bytes = s.requested_bytes;
s.pulled_frontier = 1_000_000;
s.downstream.served_paid = 0;
assert_eq!(WindowPacer::new(10).decide(&s), PaceDecision::Done);
assert_eq!(BudgetPacer::new().decide(&s), PaceDecision::Done);
let mut s = healthy();
s.remaining_deposit = U256::from(4u64);
s.next_voucher_cost = U256::from(10u64);
s.pulled_frontier = 1_000_000;
s.downstream.served_paid = 0;
assert_eq!(WindowPacer::new(10).decide(&s), PaceDecision::Refuse);
assert_eq!(BudgetPacer::new().decide(&s), PaceDecision::Refuse);
}
#[test]
fn ramp_pacer_paces_pull_on_the_ramped_window() {
let floor = 4 * CHUNK_GROUP_BYTES;
let pacer = RampPacer {
divisor: 2,
floor,
credit_max: 64 * CHUNK_GROUP_BYTES,
paid_base: 0,
paid_carried: 0,
};
let mut s = healthy();
s.downstream.served_paid = 0;
s.pulled_frontier = floor; assert_eq!(pacer.decide(&s), PaceDecision::Wait);
}
#[test]
fn ramp_pacer_widens_the_pull_window_as_served_paid_advances() {
let floor = 4 * CHUNK_GROUP_BYTES;
let pacer = RampPacer {
divisor: 2,
floor,
credit_max: 64 * CHUNK_GROUP_BYTES,
paid_base: 0,
paid_carried: 0,
};
let mut s = healthy();
s.downstream.served_paid = 32 * CHUNK_GROUP_BYTES;
s.pulled_frontier = floor;
assert_matches!(pacer.decide(&s), PaceDecision::Draw { .. });
}
#[test]
fn ramp_pacer_measures_paid_from_the_session_start() {
let floor = 4 * CHUNK_GROUP_BYTES;
let start = 32 * CHUNK_GROUP_BYTES;
let pacer = RampPacer {
divisor: 2,
floor,
credit_max: 64 * CHUNK_GROUP_BYTES,
paid_base: start,
paid_carried: 0,
};
let mut s = healthy();
s.downstream.served_paid = start;
s.pulled_frontier = start + floor;
assert_eq!(pacer.decide(&s), PaceDecision::Wait);
s.downstream.served_paid = start + 32 * CHUNK_GROUP_BYTES;
assert_matches!(pacer.decide(&s), PaceDecision::Draw { .. });
}
#[test]
fn ramp_pacer_adds_the_carried_credit_to_the_streams_own_payment() {
let floor = 4 * CHUNK_GROUP_BYTES;
let start = 8 * CHUNK_GROUP_BYTES;
let pacer = RampPacer {
divisor: 2,
floor,
credit_max: 64 * CHUNK_GROUP_BYTES,
paid_base: start,
paid_carried: 32 * CHUNK_GROUP_BYTES,
};
let mut s = healthy();
s.downstream.served_paid = start;
s.pulled_frontier = start + floor;
assert_matches!(pacer.decide(&s), PaceDecision::Draw { .. });
s.pulled_frontier = start + 16 * CHUNK_GROUP_BYTES;
assert_eq!(pacer.decide(&s), PaceDecision::Wait);
s.downstream.served_paid = start + 16 * CHUNK_GROUP_BYTES;
s.pulled_frontier = start + 16 * CHUNK_GROUP_BYTES + 24 * CHUNK_GROUP_BYTES;
assert_eq!(pacer.decide(&s), PaceDecision::Wait);
s.pulled_frontier -= CHUNK_GROUP_BYTES;
assert_matches!(pacer.decide(&s), PaceDecision::Draw { .. });
}
#[test]
fn the_pull_window_floor_clears_one_chunk_and_a_prefetch_after_both_roundings() {
let survives = PULL_WINDOW_FLOOR - 2 * CHUNK_GROUP_BYTES;
let needed = CHUNK_BYTES + VERIFY_LAG_BYTES + CHUNK_GROUP_BYTES;
assert!(
survives >= needed,
"a {PULL_WINDOW_FLOOR}-byte floor leaves only {survives} bytes after both \
roundings, short of the {needed} bytes the client must draw — one \
{CHUNK_BYTES}-byte chunk plus the {VERIFY_LAG_BYTES}-byte leaf that \
verifies its boundary to complete a payment, plus the \
{CHUNK_GROUP_BYTES}-byte group the serve leg prefetches past a shut \
window. One side or the other would park forever"
);
}
#[test]
fn at_the_floor_a_pull_one_chunk_ahead_may_still_draw() {
let pacer = RampPacer {
divisor: 2,
floor: PULL_WINDOW_FLOOR,
credit_max: 64 * CHUNK_BYTES,
paid_base: 0,
paid_carried: 0,
};
let mut s = healthy();
s.requested_bytes = 64 * CHUNK_BYTES;
s.downstream.served_paid = 0;
s.pulled_frontier = CHUNK_BYTES;
assert_matches!(
pacer.decide(&s),
PaceDecision::Draw { .. },
"the pull must be able to feed the client past the chunk boundary it \
pays at, or neither side can move"
);
}