use dashmap::DashMap;
use parking_lot::Mutex;
use crate::error::LimiteronError;
use super::traits::Limiter;
#[derive(Debug)]
struct HtbBucket {
capacity: u64,
refill_rate: u64,
state: Mutex<BucketState>,
}
#[derive(Debug)]
struct BucketState {
tokens: f64,
last_refill: std::time::Instant,
}
impl HtbBucket {
fn new(capacity: u64, refill_rate: u64) -> Self {
Self {
capacity,
refill_rate,
state: Mutex::new(BucketState {
tokens: capacity as f64,
last_refill: std::time::Instant::now(),
}),
}
}
fn refill_and_available(&self) -> u64 {
let mut st = self.state.lock();
let elapsed = st.last_refill.elapsed().as_secs_f64();
if elapsed > 0.0 && self.refill_rate > 0 {
st.tokens = (st.tokens + elapsed * self.refill_rate as f64).min(self.capacity as f64);
st.last_refill = std::time::Instant::now();
}
st.tokens as u64
}
fn available(&self) -> u64 {
self.refill_and_available()
}
fn try_take(&self, amount: u64) -> u64 {
let mut st = self.state.lock();
let elapsed = st.last_refill.elapsed().as_secs_f64();
if elapsed > 0.0 && self.refill_rate > 0 {
st.tokens = (st.tokens + elapsed * self.refill_rate as f64).min(self.capacity as f64);
st.last_refill = std::time::Instant::now();
}
let taken = st.tokens.min(amount as f64).max(0.0) as u64;
st.tokens -= taken as f64;
taken
}
fn give(&self, amount: u64) {
let mut st = self.state.lock();
st.tokens = (st.tokens + amount as f64).min(self.capacity as f64);
}
}
pub struct HierarchicalTokenBucket {
root: std::sync::Arc<HtbBucket>,
children: DashMap<String, ClassNode>,
}
#[derive(Clone)]
struct ClassNode {
bucket: std::sync::Arc<HtbBucket>,
children: std::sync::Arc<DashMap<String, ClassNode>>,
}
impl Default for HierarchicalTokenBucket {
fn default() -> Self {
Self::new(1_000_000, 1_000_000)
}
}
impl HierarchicalTokenBucket {
pub fn new(root_capacity: u64, root_refill_rate: u64) -> Self {
Self {
root: std::sync::Arc::new(HtbBucket::new(root_capacity, root_refill_rate)),
children: DashMap::new(),
}
}
pub fn root_capacity(&self) -> u64 {
self.root.capacity
}
pub fn add_class(
&self,
path: &[&str],
capacity: u64,
refill_rate: u64,
) -> Result<(), LimiteronError> {
if path.is_empty() {
return Err(LimiteronError::ConfigError(
"HTB: class path cannot be empty (root is implicit)".to_string(),
));
}
let mut current: Option<ClassNode> = None;
for (i, seg) in path.iter().enumerate() {
let is_last = i == path.len() - 1;
let map: &DashMap<String, ClassNode> = match ¤t {
Some(n) => &n.children,
None => &self.children,
};
let existing: Option<ClassNode> = map.get(*seg).map(|e| e.value().clone());
match existing {
Some(next) => {
if is_last {
map.insert((*seg).to_string(), Self::make_node(capacity, refill_rate));
return Ok(());
}
current = Some(next);
}
None => {
if is_last {
map.insert((*seg).to_string(), Self::make_node(capacity, refill_rate));
return Ok(());
}
return Err(LimiteronError::ConfigError(format!(
"HTB: parent class '{seg}' not registered yet (path must be added top-down)"
)));
}
}
}
Ok(())
}
fn make_node(capacity: u64, refill_rate: u64) -> ClassNode {
ClassNode {
bucket: std::sync::Arc::new(HtbBucket::new(capacity, refill_rate)),
children: std::sync::Arc::new(DashMap::new()),
}
}
fn find(&self, path: &[&str]) -> Option<ClassNode> {
let mut node = ClassNode {
bucket: self.root.clone(),
children: std::sync::Arc::new(self.children.clone()),
};
for seg in path {
let next = node.children.get(*seg)?.value().clone();
node = next;
}
Some(node)
}
pub async fn allow(&self, class_path: &[&str], cost: u64) -> Result<bool, LimiteronError> {
if cost == 0 {
return Err(LimiteronError::ConfigError(
"HTB cost cannot be zero".to_string(),
));
}
let mut chain: Vec<std::sync::Arc<HtbBucket>> = Vec::with_capacity(class_path.len() + 1);
let mut node = ClassNode {
bucket: self.root.clone(),
children: std::sync::Arc::new(self.children.clone()),
};
chain.push(node.bucket.clone());
for seg in class_path {
let next: Option<ClassNode> = node.children.get(*seg).map(|e| e.value().clone());
match next {
Some(n) => {
chain.push(n.bucket.clone());
node = n;
}
None => {
return Err(LimiteronError::ConfigError(format!(
"HTB: class '{seg}' not registered"
)));
}
}
}
let leaf = chain[chain.len() - 1].clone();
let taken = leaf.try_take(cost);
let deficit = cost - taken;
if deficit == 0 {
return Ok(true);
}
let mut remaining = deficit;
let mut borrowed: Vec<(std::sync::Arc<HtbBucket>, u64)> = Vec::new();
for and in chain[..chain.len() - 1].iter().rev() {
if remaining == 0 {
break;
}
let got = and.try_take(remaining);
if got > 0 {
borrowed.push((and.clone(), got));
remaining -= got;
}
}
if remaining > 0 {
for (and, got) in &borrowed {
and.give(*got);
}
leaf.give(taken);
return Ok(false);
}
Ok(true)
}
pub fn available(&self, class_path: &[&str]) -> Result<u64, LimiteronError> {
self.find(class_path)
.map(|n| n.bucket.available())
.ok_or_else(|| {
LimiteronError::ConfigError("HTB: class path not registered".to_string())
})
}
}
#[async_trait::async_trait]
impl Limiter for HierarchicalTokenBucket {
async fn allow(&self, cost: u64) -> Result<bool, LimiteronError> {
self.allow(&[], cost).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_t615_htb_consumes_own_tokens_first() {
let htb = HierarchicalTokenBucket::new(1000, 0);
htb.add_class(&["api"], 600, 0).unwrap();
htb.add_class(&["api", "premium"], 100, 0).unwrap();
assert!(htb.allow(&["api", "premium"], 40).await.unwrap());
assert_eq!(htb.available(&["api", "premium"]).unwrap(), 60);
assert_eq!(htb.available(&["api"]).unwrap(), 600, "父桶未被触碰");
assert_eq!(htb.available(&[]).unwrap(), 1000, "根桶未被触碰");
}
#[tokio::test]
async fn test_t615_htb_borrows_from_parent() {
let htb = HierarchicalTokenBucket::new(1000, 0);
htb.add_class(&["api"], 50, 0).unwrap();
htb.add_class(&["api", "premium"], 10, 0).unwrap();
assert!(htb.allow(&["api", "premium"], 10).await.unwrap());
assert_eq!(htb.available(&["api", "premium"]).unwrap(), 0);
assert!(htb.allow(&["api", "premium"], 5).await.unwrap());
assert_eq!(htb.available(&["api"]).unwrap(), 45, "父桶被借走 5");
}
#[tokio::test]
async fn test_t615_htb_all_or_nothing_denial() {
let htb = HierarchicalTokenBucket::new(5, 0);
htb.add_class(&["api"], 5, 0).unwrap();
htb.add_class(&["api", "premium"], 10, 0).unwrap();
assert!(htb.allow(&["api", "premium"], 10).await.unwrap());
assert!(!htb.allow(&["api", "premium"], 30).await.unwrap());
assert_eq!(htb.available(&["api"]).unwrap(), 5);
assert_eq!(htb.available(&[]).unwrap(), 5);
}
#[tokio::test]
async fn test_t615_htb_sibling_isolation() {
let htb = HierarchicalTokenBucket::new(1000, 0);
htb.add_class(&["api"], 100, 0).unwrap();
htb.add_class(&["api", "a"], 10, 0).unwrap();
htb.add_class(&["api", "b"], 10, 0).unwrap();
assert!(htb.allow(&["api", "a"], 10).await.unwrap()); assert!(htb.allow(&["api", "a"], 5).await.unwrap());
assert_eq!(
htb.available(&["api", "b"]).unwrap(),
10,
"兄弟桶 b 的令牌不得被 a 的借用触碰"
);
}
#[tokio::test]
async fn test_t615_htb_multi_level_borrow_order() {
let htb = HierarchicalTokenBucket::new(10, 0);
htb.add_class(&["api"], 10, 0).unwrap();
htb.add_class(&["api", "premium"], 10, 0).unwrap();
assert!(htb.allow(&["api", "premium"], 25).await.unwrap());
assert_eq!(htb.available(&["api", "premium"]).unwrap(), 0);
assert_eq!(htb.available(&["api"]).unwrap(), 0);
assert_eq!(htb.available(&[]).unwrap(), 5);
assert!(!htb.allow(&["api", "premium"], 10).await.unwrap());
assert!(htb.allow(&["api", "premium"], 5).await.unwrap());
assert_eq!(htb.available(&[]).unwrap(), 0);
}
#[tokio::test]
async fn test_t615_htb_unregistered_path_is_error() {
let htb = HierarchicalTokenBucket::new(100, 100);
let err = htb.allow(&["ghost"], 1).await.unwrap_err();
assert!(matches!(err, LimiteronError::ConfigError(_)));
assert!(
htb.add_class(&["a", "ghost"], 10, 10).is_err(),
"须先注册父类"
);
}
#[tokio::test]
async fn test_t615_htb_refills_over_time() {
let htb = HierarchicalTokenBucket::new(100, 0);
htb.add_class(&["api"], 10, 100).unwrap();
assert!(htb.allow(&["api"], 10).await.unwrap());
assert_eq!(htb.available(&["api"]).unwrap(), 0);
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
assert_eq!(
htb.available(&["api"]).unwrap(),
10,
"150ms @ 100/s → 至少 10 个令牌(封顶容量)"
);
}
#[tokio::test]
async fn test_t615_htb_zero_cost_rejected() {
let htb = HierarchicalTokenBucket::new(100, 100);
assert!(htb.allow(&[], 0).await.is_err());
}
#[tokio::test]
async fn test_t615_htb_concurrent_allow_never_overdraws() {
let htb = std::sync::Arc::new(HierarchicalTokenBucket::new(10, 0));
let mut set = tokio::task::JoinSet::new();
for _ in 0..100 {
let htb = htb.clone();
set.spawn(async move { htb.allow(&[], 1).await.unwrap() });
}
let mut granted = 0usize;
while let Some(result) = set.join_next().await {
if result.unwrap() {
granted += 1;
}
}
assert_eq!(granted, 10, "预算 10:并发下不得超发");
assert_eq!(htb.available(&[]).unwrap(), 0);
}
#[tokio::test]
async fn test_t615_htb_concurrent_borrow_all_or_nothing() {
let htb = std::sync::Arc::new(HierarchicalTokenBucket::new(5, 0));
htb.add_class(&["api"], 5, 0).unwrap();
htb.add_class(&["api", "premium"], 10, 0).unwrap();
assert!(htb.allow(&["api", "premium"], 10).await.unwrap());
let mut set = tokio::task::JoinSet::new();
for _ in 0..2 {
let htb = htb.clone();
set.spawn(async move { htb.allow(&["api", "premium"], 6).await.unwrap() });
}
let mut granted = 0usize;
while let Some(result) = set.join_next().await {
if result.unwrap() {
granted += 1;
}
}
assert_eq!(granted, 1, "可借预算仅 10 < 需求 12:两路并发只能成功一笔");
assert_eq!(htb.available(&["api"]).unwrap(), 0);
assert_eq!(htb.available(&[]).unwrap(), 4);
}
#[tokio::test]
async fn test_t615_htb_limiter_trait_bridge() {
let htb = HierarchicalTokenBucket::new(5, 5);
Limiter::allow(&htb, 5).await.unwrap();
assert!(!Limiter::allow(&htb, 1).await.unwrap());
}
}