use std::sync::Mutex;
use std::time::Instant;
pub struct Budget {
per_minute: Option<u32>,
state: Mutex<State>,
}
struct State {
allowance: f64,
refilled_at: Instant,
}
impl Budget {
pub fn afford(&self) -> Result<(), crate::error::Error> {
match self.spend() {
None => Ok(()),
Some(retry_after) => Err(crate::error::Error::LookupBudgetSpent { retry_after }),
}
}
pub fn new(per_minute: Option<u32>) -> Self {
match per_minute {
Some(per_minute) => tracing::info!(
per_minute,
"this server will not ask the forge more often than this, counting only lookups \
the permission cache could not answer"
),
None => tracing::warn!(
"LFSX_AUTH_LOOKUP_BUDGET=0, so there is no ceiling on forge lookups: a caller \
sending a different token every request can spend this server's standing with \
the forge, and every repository shares it"
),
}
Self {
per_minute,
state: Mutex::new(State {
allowance: per_minute.unwrap_or_default().into(),
refilled_at: Instant::now(),
}),
}
}
pub fn spend(&self) -> Option<u64> {
let capacity = f64::from(self.per_minute?);
let mut state = self.state.lock().expect("forge lookup budget");
let now = Instant::now();
let earned = now.duration_since(state.refilled_at).as_secs_f64() * capacity / 60.0;
state.allowance = (state.allowance + earned).min(capacity);
state.refilled_at = now;
if state.allowance >= 1.0 {
state.allowance -= 1.0;
return None;
}
let wait = (1.0 - state.allowance) * 60.0 / capacity;
Some((wait.ceil() as u64).max(1))
}
}
#[cfg(test)]
mod tests;