use std::sync::atomic::{AtomicU64, Ordering};
const BPS_SCALE: u64 = 10_000;
const PERCENT_STEP: f64 = 0.01;
const PERCENT_STEP_EPSILON: f64 = 1e-4;
#[derive(Debug)]
pub struct HedgeBudget {
requests: AtomicU64,
hedges: AtomicU64,
bps: u64,
}
impl HedgeBudget {
pub fn try_new(percent: f64) -> Result<Self, String> {
Ok(Self {
requests: AtomicU64::new(0),
hedges: AtomicU64::new(0),
bps: percent_to_bps(percent)?,
})
}
pub fn note_request(&self) {
self.requests.fetch_add(1, Ordering::Release);
}
pub fn revert_admission(&self) {
let mut hedges = self.hedges.load(Ordering::Relaxed);
while hedges > 0 {
match self.hedges.compare_exchange_weak(
hedges,
hedges.saturating_sub(1),
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => return,
Err(observed) => hedges = observed,
}
}
}
pub fn try_admit(&self) -> bool {
if self.bps == 0 {
return false;
}
let mut hedges = self.hedges.load(Ordering::Relaxed);
loop {
let requests = self.requests.load(Ordering::Acquire);
let used = hedges.saturating_mul(BPS_SCALE);
let allowed = requests.saturating_mul(self.bps);
if used >= allowed {
return false;
}
match self.hedges.compare_exchange_weak(
hedges,
hedges.saturating_add(1),
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => return true,
Err(observed) => hedges = observed,
}
}
}
#[cfg(test)]
fn hedges(&self) -> u64 {
self.hedges.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn bps(&self) -> u64 {
self.bps
}
}
#[must_use]
pub fn status_is_success(status: u16) -> bool {
(200..500).contains(&status)
}
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the percent is finite and on a 0.01 boundary before the cast"
)]
fn percent_to_bps(percent: f64) -> Result<u64, String> {
if !percent.is_finite() {
return Err(format!("hedge_policy: budget_percent must be finite, got {percent}"));
}
if !(0.0..=100.0).contains(&percent) {
return Err(format!(
"hedge_policy: budget_percent must be in 0.0..=100.0, got {percent}"
));
}
let steps = percent / PERCENT_STEP;
let rounded = steps.round();
if !rounded.is_finite() || (steps - rounded).abs() > PERCENT_STEP_EPSILON {
return Err(format!(
"hedge_policy: budget_percent must be a multiple of 0.01, got {percent}"
));
}
if rounded >= 10_000.0 {
return Ok(BPS_SCALE);
}
if rounded <= 0.0 {
return Ok(0);
}
Ok(rounded as u64)
}
#[cfg(test)]
#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
#[allow(clippy::unwrap_used, clippy::expect_used, reason = "tests")]
mod tests {
use super::*;
#[test]
fn zero_percent_admits_nothing() {
let budget = HedgeBudget::try_new(0.0).unwrap();
budget.note_request();
assert!(!budget.try_admit(), "0% must not admit a hedge");
assert_eq!(budget.hedges(), 0);
}
#[test]
fn first_request_can_hedge_when_percent_is_positive() {
let budget = HedgeBudget::try_new(10.0).unwrap();
budget.note_request();
assert!(budget.try_admit(), "the first request starts under the ratio");
assert!(!budget.try_admit(), "a second copy on the same request exceeds 10%");
}
#[test]
fn ten_percent_sustains_one_hedge_per_ten_requests() {
let budget = HedgeBudget::try_new(10.0).unwrap();
let mut admitted = 0_u32;
for _ in 0..20 {
budget.note_request();
if budget.try_admit() {
admitted = admitted.saturating_add(1);
}
}
assert_eq!(admitted, 2, "20 requests at 10% admit 2 hedges, got {admitted}");
}
#[test]
fn one_hundred_percent_allows_one_hedge_per_request() {
let budget = HedgeBudget::try_new(100.0).unwrap();
for _ in 0..5 {
budget.note_request();
assert!(budget.try_admit(), "100% allows one hedge per counted request");
assert!(!budget.try_admit(), "100% does not allow two hedges on one request");
}
assert_eq!(budget.hedges(), 5);
}
#[test]
fn reverted_admission_can_be_used_again() {
let budget = HedgeBudget::try_new(10.0).unwrap();
budget.note_request();
assert!(budget.try_admit(), "the first copy is under 10%");
budget.revert_admission();
assert_eq!(budget.hedges(), 0, "a copy that was not sent is not charged");
assert!(budget.try_admit(), "the reverted slot is available");
assert!(!budget.try_admit(), "only one copy is under 10% for one request");
}
#[test]
fn rejects_non_finite_and_finer_than_one_basis_point() {
let non_finite = HedgeBudget::try_new(f64::NAN).unwrap_err();
assert!(non_finite.contains("finite"), "{non_finite}");
let half = HedgeBudget::try_new(0.005).unwrap_err();
assert!(half.contains("multiple of 0.01"), "{half}");
let tenth = HedgeBudget::try_new(0.001).unwrap_err();
assert!(tenth.contains("multiple of 0.01"), "{tenth}");
let one = HedgeBudget::try_new(0.01).unwrap();
assert_eq!(one.bps(), 1, "0.01% is one basis point");
}
#[test]
fn success_statuses_are_non_5xx_final_responses() {
assert!(status_is_success(200));
assert!(status_is_success(204));
assert!(status_is_success(302));
assert!(status_is_success(404));
assert!(status_is_success(429));
assert!(!status_is_success(500));
assert!(!status_is_success(503));
assert!(!status_is_success(101), "a 1xx is not a final response");
assert!(!status_is_success(100));
}
}