use std::sync::Arc;
use super::clock::{Clock, SystemClock};
use super::token_bucket::TokenBucket;
pub struct HierarchicalLimiter {
parent: Arc<TokenBucket>,
children: Vec<Arc<TokenBucket>>,
}
impl HierarchicalLimiter {
pub fn new(
parent_capacity: u64,
parent_rate: f64,
num_children: usize,
child_capacity: u64,
child_rate: f64,
) -> Self {
Self::with_clock_fn(
parent_capacity,
parent_rate,
num_children,
child_capacity,
child_rate,
|| Box::new(SystemClock::new()),
)
}
pub fn with_clock_fn<F>(
parent_capacity: u64,
parent_rate: f64,
num_children: usize,
child_capacity: u64,
child_rate: f64,
mut clock_fn: F,
) -> Self
where
F: FnMut() -> Box<dyn Clock>,
{
let parent = Arc::new(TokenBucket::with_clock(
parent_capacity,
parent_rate,
clock_fn(),
));
let children = (0..num_children.max(1))
.map(|_| {
Arc::new(TokenBucket::with_clock(
child_capacity,
child_rate,
clock_fn(),
))
})
.collect();
Self { parent, children }
}
pub fn try_acquire(&self, child_id: usize, n: u64) -> bool {
let child = match self.children.get(child_id) {
Some(c) => c,
None => return false,
};
if self.parent_available_at_least(n) {
if !child.try_acquire(n) {
return false;
}
if self.parent.try_acquire(n) {
return true;
}
false
} else {
false
}
}
pub fn parent(&self) -> &TokenBucket {
&self.parent
}
pub fn child(&self, child_id: usize) -> Option<&TokenBucket> {
self.children.get(child_id).map(|c| c.as_ref())
}
pub fn num_children(&self) -> usize {
self.children.len()
}
fn parent_available_at_least(&self, n: u64) -> bool {
self.parent.available() >= n
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::features::clock::TestClock;
struct ArcClock(Arc<TestClock>);
impl Clock for ArcClock {
fn now_ns(&self) -> u64 {
self.0.now_ns()
}
}
fn build(
parent_cap: u64,
parent_rate: f64,
children: usize,
child_cap: u64,
child_rate: f64,
) -> (HierarchicalLimiter, Arc<TestClock>) {
let clk = Arc::new(TestClock::new());
let c = clk.clone();
let h = HierarchicalLimiter::with_clock_fn(
parent_cap,
parent_rate,
children,
child_cap,
child_rate,
|| Box::new(ArcClock(c.clone())),
);
(h, clk)
}
#[test]
fn parent_caps_total_across_children() {
let (h, _clk) = build(5, 0.0, 2, 10, 0.0);
let mut granted = 0;
for _ in 0..10 {
if h.try_acquire(0, 1) {
granted += 1;
}
}
for _ in 0..10 {
if h.try_acquire(1, 1) {
granted += 1;
}
}
assert_eq!(granted, 5, "parent must cap total at 5");
}
#[test]
fn child_caps_independent_when_parent_has_budget() {
let (h, _clk) = build(1000, 0.0, 1, 3, 0.0);
for _ in 0..3 {
assert!(h.try_acquire(0, 1));
}
assert!(!h.try_acquire(0, 1), "child capacity exhausted");
}
#[test]
fn unknown_child_id_rejects() {
let (h, _clk) = build(10, 0.0, 1, 5, 0.0);
assert!(!h.try_acquire(99, 1), "out-of-range child id should reject");
}
#[test]
fn refill_after_parent_exhaustion_unblocks_children() {
let (h, clk) = build(5, 50.0, 2, 10, 0.0);
for _ in 0..5 {
assert!(h.try_acquire(0, 1));
}
assert!(!h.try_acquire(1, 1), "parent exhausted");
clk.advance_ms(100);
let mut got = 0;
for _ in 0..10 {
if h.try_acquire(1, 1) {
got += 1;
}
}
assert_eq!(got, 5, "exactly 5 parent tokens refilled (then exhausted)");
}
#[test]
fn batch_acquire_atomic_at_both_levels() {
let (h, _clk) = build(10, 0.0, 1, 10, 0.0);
assert!(h.try_acquire(0, 7));
assert!(!h.try_acquire(0, 5), "would exceed parent capacity now");
assert!(h.try_acquire(0, 3), "exactly 3 left should grant");
assert!(!h.try_acquire(0, 1));
}
#[test]
fn parent_and_child_accessors_expose_underlying_buckets() {
let (h, _clk) = build(7, 0.0, 2, 3, 0.0);
assert_eq!(h.parent().capacity(), 7);
assert_eq!(h.child(0).map(|c| c.capacity()), Some(3));
assert_eq!(h.child(99).map(|c| c.capacity()), None);
assert_eq!(h.num_children(), 2);
}
}