use core::cmp;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BurstPolicy {
Standard,
UploadTight,
}
#[derive(Debug, Clone)]
pub struct TokenBucket {
capacity: u64,
tokens: u64,
rate_bps: u64,
policy: BurstPolicy,
last: u64,
}
impl TokenBucket {
pub fn new(rate_bps: u64, now: u64) -> Self {
Self::with_policy(rate_bps, BurstPolicy::Standard, now)
}
pub fn new_upload(rate_bps: u64, now: u64) -> Self {
Self::with_policy(rate_bps, BurstPolicy::UploadTight, now)
}
fn with_policy(rate_bps: u64, policy: BurstPolicy, now: u64) -> Self {
let capacity = burst_capacity(rate_bps, policy);
TokenBucket {
capacity,
tokens: capacity,
rate_bps,
policy,
last: now,
}
}
pub fn set_rate(&mut self, rate_bps: u64, now: u64) {
self.refill(now);
self.capacity = burst_capacity(rate_bps, self.policy);
self.tokens = cmp::min(self.tokens, self.capacity);
self.rate_bps = rate_bps;
}
pub fn rate_bps(&self) -> u64 {
self.rate_bps
}
pub fn is_unlimited(&self) -> bool {
self.rate_bps == 0
}
pub fn capacity(&self) -> u64 {
self.capacity
}
pub fn available(&mut self, now: u64) -> u64 {
if self.rate_bps == 0 {
return u64::MAX;
}
self.refill(now);
self.tokens
}
pub fn consume(&mut self, want: u64, now: u64) -> u64 {
if self.rate_bps == 0 {
return want;
}
self.refill(now);
let take = cmp::min(want, self.tokens);
self.tokens -= take;
take
}
fn refill(&mut self, now: u64) {
if self.rate_bps == 0 || now <= self.last {
return;
}
let dt = now - self.last;
let add = dt.saturating_mul(self.rate_bps) / 1000;
self.tokens = cmp::min(self.capacity, self.tokens.saturating_add(add));
self.last = now;
}
}
pub fn upload_tolerance_half_pct(rate_bps: u64) -> u64 {
let kb = rate_bps / 1024;
const BASE: u64 = 20; const FLOOR: u64 = 2; if kb < 100 {
return BASE;
}
BASE.saturating_sub((kb - 100) / 50).max(FLOOR)
}
fn burst_capacity(rate_bps: u64, policy: BurstPolicy) -> u64 {
if rate_bps == 0 {
return u64::MAX;
}
match policy {
BurstPolicy::Standard => rate_bps.clamp(64 * 1024, 8 * 1024 * 1024),
BurstPolicy::UploadTight => {
let tol = upload_tolerance_half_pct(rate_bps);
let burst = rate_bps.saturating_mul(tol) / 200;
burst.clamp(4 * 1024, 1024 * 1024)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unlimited_always_serves() {
let mut b = TokenBucket::new(0, 0);
assert!(b.is_unlimited());
assert_eq!(b.available(1_000), u64::MAX);
assert_eq!(b.consume(u64::MAX, 2_000), u64::MAX);
}
#[test]
fn limited_consumes_and_refills() {
let mut b = TokenBucket::new(1000, 0); assert!(!b.is_unlimited());
let burst = b.available(0);
assert!(burst >= 64 * 1024);
let taken = b.consume(burst, 0);
assert_eq!(taken, burst);
assert_eq!(b.available(0), 0);
assert!(b.available(1000) >= 1000);
let c = b.consume(100_000, 1000);
assert_eq!(c, 1000);
assert_eq!(b.available(1000), 0);
}
#[test]
fn set_rate_rebounds_burst() {
let mut b = TokenBucket::new(10 * 1024 * 1024, 0);
let big = b.available(0);
assert_eq!(big, 8 * 1024 * 1024); b.set_rate(0, 0);
assert!(b.is_unlimited());
assert_eq!(b.available(0), u64::MAX);
}
#[test]
fn upload_tolerance_matches_the_spec() {
assert_eq!(upload_tolerance_half_pct(100 * 1024), 20); assert_eq!(upload_tolerance_half_pct(200 * 1024), 18); assert_eq!(upload_tolerance_half_pct(300 * 1024), 16); assert_eq!(upload_tolerance_half_pct(1024 * 1024), 2); assert_eq!(upload_tolerance_half_pct(4096 * 1024), 2); }
#[test]
fn upload_burst_keeps_any_one_second_window_within_tolerance() {
for (rate, tol) in [
(100 * 1024, 20u64), (200 * 1024, 18), (1024 * 1024, 2), (50 * 1024, 20), ] {
let b = TokenBucket::new_upload(rate, 0);
let cap = b.capacity();
let max_window = rate.saturating_add(cap);
let ceiling = rate.saturating_add(rate.saturating_mul(tol) / 200);
assert!(
max_window <= ceiling,
"rate {rate} tol {tol}: max window {max_window} exceeds ceiling {ceiling}"
);
}
}
#[test]
fn upload_bucket_is_tighter_than_standard() {
let u = TokenBucket::new_upload(100 * 1024, 0);
let s = TokenBucket::new(100 * 1024, 0);
assert!(u.capacity() < s.capacity());
assert!(u.capacity() <= 10 * 1024); }
#[test]
fn set_rate_preserves_upload_policy() {
let mut b = TokenBucket::new_upload(100 * 1024, 0);
b.set_rate(200 * 1024, 0);
assert_eq!(b.capacity(), 18_432);
b.set_rate(1024 * 1024, 0);
assert_eq!(b.capacity(), 1024 * 1024 * 2 / 200);
}
#[test]
fn draining_the_bucket_makes_refill_the_rate_authority() {
let mut b = TokenBucket::new(100 * 1024, 0);
let budget = b.consume(u64::MAX, 0);
assert_eq!(budget, 100 * 1024);
assert_eq!(b.available(0), 0, "bucket drained to zero");
let refilled = b.available(100);
assert!(
(10_000..=10_300).contains(&refilled),
"refill after 100 ms must be ≈ rate×0.1, got {refilled}"
);
assert_eq!(b.consume(u64::MAX, 100), refilled);
}
}