use super::clock::AdmissionClock;
use otel_arrow_dfe_config::policy::RateLimiterPolicy;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BucketOutcome {
WithinLimit,
OverLimit {
retry_after_nanos: u64,
},
Oversized,
}
#[derive(Debug)]
pub struct RateBucket {
allow: u64,
interval_nanos: u64,
burst: u64,
burst_window_nanos: u64,
clock: AdmissionClock,
theoretical_arrival_nanos: AtomicU64,
}
impl RateBucket {
#[must_use]
pub fn with_clock(policy: &RateLimiterPolicy, clock: AdmissionClock) -> Self {
let allow = policy.token_bucket.allow;
let interval_nanos =
u64::try_from(policy.token_bucket.interval.as_nanos()).unwrap_or(u64::MAX);
let burst = policy.burst_or_allow();
let burst_window_nanos = if burst == 0 || allow == 0 || interval_nanos == 0 {
0
} else {
Self::nanos_for_rate(allow, interval_nanos, burst)
};
let start = clock.now_nanos();
Self {
allow,
interval_nanos,
burst,
burst_window_nanos,
clock,
theoretical_arrival_nanos: AtomicU64::new(start),
}
}
fn nanos_for_units(&self, units: u64) -> u64 {
Self::nanos_for_rate(self.allow, self.interval_nanos, units)
}
fn nanos_for_rate(allow: u64, interval_nanos: u64, units: u64) -> u64 {
if units == 0 {
return 0;
}
if allow == 0 || interval_nanos == 0 {
return u64::MAX;
}
let nanos = (u128::from(units) * u128::from(interval_nanos)).div_ceil(u128::from(allow));
u64::try_from(nanos).unwrap_or(u64::MAX)
}
fn next_theoretical_arrival(current: u64, now: u64, cost: u64, debt_limit: u64) -> u64 {
current
.max(now)
.saturating_add(cost)
.min(debt_limit)
.max(current)
}
#[must_use]
pub fn check_units(&self, units: u64) -> BucketOutcome {
self.apply_units(units, false)
}
#[must_use]
pub fn observe_units(&self, units: u64) -> BucketOutcome {
self.apply_units(units, true)
}
fn apply_units(&self, units: u64, charge_over_limit: bool) -> BucketOutcome {
if units == 0 {
return BucketOutcome::WithinLimit;
}
let cost = self.nanos_for_units(units);
let oversized = units > self.burst;
loop {
let now = self.clock.now_nanos();
let burst_window = self.burst_window_nanos;
let limit = now.saturating_add(burst_window);
let debt_limit = limit.saturating_add(burst_window);
let current = self.theoretical_arrival_nanos.load(Ordering::Acquire);
let candidate = current.max(now).saturating_add(cost);
let over_limit = oversized || candidate > limit;
let retry_after_nanos = candidate.saturating_sub(limit);
if over_limit && !charge_over_limit {
return if oversized {
BucketOutcome::Oversized
} else {
BucketOutcome::OverLimit { retry_after_nanos }
};
}
let next = Self::next_theoretical_arrival(current, now, cost, debt_limit);
if self
.theoretical_arrival_nanos
.compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return if oversized {
BucketOutcome::Oversized
} else if over_limit {
BucketOutcome::OverLimit { retry_after_nanos }
} else {
BucketOutcome::WithinLimit
};
}
}
}
#[must_use]
pub fn is_exhausted(&self) -> bool {
self.retry_after_nanos(1).is_some()
}
#[must_use]
pub fn retry_after_nanos(&self, units: u64) -> Option<u64> {
if units == 0 {
return None;
}
if units > self.burst {
return None;
}
let now = self.clock.now_nanos();
let limit = now.saturating_add(self.burst_window_nanos);
let current = self.theoretical_arrival_nanos.load(Ordering::Acquire);
let candidate = current.max(now).saturating_add(self.nanos_for_units(units));
(candidate > limit).then(|| candidate.saturating_sub(limit))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::admission::clock::ManualClock;
use otel_arrow_dfe_config::policy::{
RateLimitAggregation, RateLimitEnforcement, RateLimitPressure, RateLimitUnit,
TokenBucketPolicy,
};
use std::sync::Arc;
use std::time::Duration;
fn policy(allow: u64, burst: Option<u64>) -> RateLimiterPolicy {
RateLimiterPolicy {
enforcement: RateLimitEnforcement::Enforce,
aggregation: RateLimitAggregation::ReceiverInstance,
unit: RateLimitUnit::RequestBytes,
pressure: RateLimitPressure::Soft,
token_bucket: TokenBucketPolicy {
allow,
interval: Duration::from_secs(1),
burst,
},
}
}
fn manual_bucket(allow: u64, burst: Option<u64>) -> (RateBucket, Arc<ManualClock>) {
let clock = Arc::new(ManualClock::new(0));
let bucket = RateBucket::with_clock(
&policy(allow, burst),
AdmissionClock::Manual(Arc::clone(&clock)),
);
(bucket, clock)
}
#[test]
fn admits_exactly_burst_capacity_before_refusing() {
let (bucket, _clock) = manual_bucket(10, Some(10));
for index in 0..10 {
assert_eq!(
bucket.check_units(1),
BucketOutcome::WithinLimit,
"charge {index} should fit in burst"
);
}
assert!(matches!(
bucket.check_units(1),
BucketOutcome::OverLimit { .. }
));
}
#[test]
fn refills_at_the_configured_rate() {
let (bucket, clock) = manual_bucket(10, Some(10));
for _ in 0..10 {
let _ = bucket.check_units(1);
}
assert_eq!(
bucket.check_units(1),
BucketOutcome::OverLimit {
retry_after_nanos: 100_000_000
}
);
clock.advance(100_000_000);
assert_eq!(bucket.check_units(1), BucketOutcome::WithinLimit);
assert!(matches!(
bucket.check_units(1),
BucketOutcome::OverLimit { .. }
));
}
#[test]
fn weighted_refusal_reports_its_earliest_conforming_delay() {
let (bucket, clock) = manual_bucket(10, Some(10));
for _ in 0..8 {
assert_eq!(bucket.check_units(1), BucketOutcome::WithinLimit);
}
assert_eq!(
bucket.check_units(5),
BucketOutcome::OverLimit {
retry_after_nanos: 300_000_000
}
);
assert_eq!(bucket.retry_after_nanos(5), Some(300_000_000));
clock.advance(299_999_999);
assert!(matches!(
bucket.check_units(5),
BucketOutcome::OverLimit { .. }
));
clock.advance(1);
assert_eq!(bucket.check_units(5), BucketOutcome::WithinLimit);
}
#[test]
fn charge_larger_than_burst_is_reported_as_oversized() {
let (bucket, _clock) = manual_bucket(10, Some(10));
assert_eq!(bucket.check_units(11), BucketOutcome::Oversized);
}
#[test]
fn refused_charge_does_not_consume_capacity() {
let (bucket, clock) = manual_bucket(10, Some(10));
for _ in 0..10 {
let _ = bucket.check_units(1);
}
for _ in 0..50 {
assert!(matches!(
bucket.check_units(1),
BucketOutcome::OverLimit { .. }
));
}
clock.advance(100_000_000);
assert_eq!(
bucket.check_units(1),
BucketOutcome::WithinLimit,
"50 refusals must not have delayed recovery"
);
}
#[test]
fn observed_debt_is_bounded_by_one_extra_burst_window() {
let (bucket, clock) = manual_bucket(10, Some(10));
for _ in 0..10_000 {
let _ = bucket.observe_units(1);
}
clock.advance(2_000_000_000);
assert_eq!(bucket.check_units(1), BucketOutcome::WithinLimit);
}
#[test]
fn maximum_observed_debt_recovers_at_two_burst_windows() {
let (bucket, clock) = manual_bucket(1, Some(1));
for _ in 0..10 {
let _ = bucket.observe_units(1);
}
assert_eq!(
bucket.check_units(1),
BucketOutcome::OverLimit {
retry_after_nanos: 2_000_000_000
}
);
clock.advance(1_999_999_999);
assert_eq!(
bucket.check_units(1),
BucketOutcome::OverLimit {
retry_after_nanos: 1
}
);
clock.advance(1);
assert_eq!(bucket.check_units(1), BucketOutcome::WithinLimit);
}
#[test]
fn stale_debt_bound_never_moves_the_bucket_backwards() {
let current = 1_000;
let stale_debt_limit = 900;
assert_eq!(
RateBucket::next_theoretical_arrival(current, 100, 10, stale_debt_limit),
current
);
}
#[test]
fn exhaustion_probe_is_read_only() {
let (bucket, clock) = manual_bucket(10, Some(10));
assert!(!bucket.is_exhausted());
for _ in 0..10 {
let _ = bucket.check_units(1);
}
assert!(bucket.is_exhausted());
assert!(bucket.is_exhausted(), "probing must not change state");
clock.advance(100_000_000);
assert!(!bucket.is_exhausted());
assert_eq!(
bucket.check_units(1),
BucketOutcome::WithinLimit,
"probes must not have consumed the refilled unit"
);
}
#[test]
fn zero_unit_charge_always_conforms() {
let (bucket, _clock) = manual_bucket(10, Some(10));
for _ in 0..20 {
let _ = bucket.observe_units(1);
}
assert_eq!(bucket.check_units(0), BucketOutcome::WithinLimit);
assert_eq!(bucket.observe_units(0), BucketOutcome::WithinLimit);
assert_eq!(bucket.retry_after_nanos(0), None);
}
#[test]
fn concurrent_charges_never_exceed_burst_capacity() {
let clock = Arc::new(ManualClock::new(0));
let bucket = Arc::new(RateBucket::with_clock(
&policy(64, Some(64)),
AdmissionClock::Manual(Arc::clone(&clock)),
));
let admitted: usize = std::thread::scope(|scope| {
let handles: Vec<_> = (0..8)
.map(|_| {
let bucket = Arc::clone(&bucket);
scope.spawn(move || {
(0..100)
.filter(|_| bucket.check_units(1) == BucketOutcome::WithinLimit)
.count()
})
})
.collect();
handles
.into_iter()
.map(|handle| handle.join().expect("charging thread"))
.sum()
});
assert_eq!(
admitted, 64,
"burst capacity must be exact under contention"
);
}
#[test]
fn zero_allowance_refuses_all_positive_charges() {
let (bucket, _clock) = manual_bucket(0, Some(0));
assert!(matches!(
bucket.check_units(1),
BucketOutcome::OverLimit { .. }
));
assert_eq!(bucket.check_units(0), BucketOutcome::WithinLimit);
}
#[test]
fn burst_headroom_exceeds_steady_state_allowance() {
let (bucket, _clock) = manual_bucket(10, Some(25));
for index in 0..25 {
assert_eq!(
bucket.check_units(1),
BucketOutcome::WithinLimit,
"charge {index} should fit in the larger burst"
);
}
assert!(matches!(
bucket.check_units(1),
BucketOutcome::OverLimit { .. }
));
}
}