use std::{
sync::{Arc, Mutex, MutexGuard},
time::Duration,
};
use thiserror::Error;
use tokio::{
sync::{futures::Notified, Notify},
time::sleep,
};
use crate::zakura::transport::{Clock, RealClock};
const NANOS_PER_SECOND: u128 = 1_000_000_000;
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
pub(crate) enum RateBudgetConfigError {
#[error("rate budget capacity must be greater than zero")]
ZeroCapacity,
#[error("rate budget refill must be greater than zero")]
ZeroRefill,
}
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
pub(crate) enum RateReservationError {
#[error("requested rate reservation {requested} exceeds capacity {capacity}")]
ExceedsCapacity {
requested: u64,
capacity: u64,
},
#[error("rate reservation is temporarily unavailable for {retry_after:?}")]
TemporarilyUnavailable {
retry_after: Duration,
},
}
impl RateReservationError {
pub(crate) fn retry_after(self) -> Option<Duration> {
match self {
Self::TemporarilyUnavailable { retry_after } => Some(retry_after),
Self::ExceedsCapacity { .. } => None,
}
}
}
#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
#[error("rate spend {spent} exceeds refundable reservation {refundable}")]
pub(crate) struct RateReservationSpendError {
pub(crate) spent: u64,
pub(crate) refundable: u64,
}
#[derive(Clone, Debug)]
pub(crate) struct RateBudget<C: Clock = RealClock> {
inner: Arc<RateBudgetInner<C>>,
}
#[derive(Debug)]
struct RateBudgetInner<C: Clock> {
capacity: u64,
refill_per_second: u64,
clock: C,
state: Mutex<RateState>,
tokens_returned: Notify,
}
#[derive(Debug)]
struct RateState {
available: u64,
refill_remainder: u128,
last_refill: tokio::time::Instant,
}
impl RateBudget<RealClock> {
pub(crate) fn new(
capacity: u64,
refill_per_second: u64,
) -> Result<Self, RateBudgetConfigError> {
Self::with_clock(capacity, refill_per_second, RealClock)
}
pub(crate) async fn wait_for(&self, units: u64) -> Result<(), RateReservationError> {
self.ensure_reservation_fits(units)?;
loop {
let returned = self.inner.tokens_returned.notified();
tokio::pin!(returned);
Notified::enable(returned.as_mut());
let Some(retry_after) = self.time_until_available(units) else {
return Ok(());
};
tokio::select! {
_ = &mut returned => {}
_ = sleep(retry_after) => {}
}
}
}
}
impl<C: Clock> RateBudget<C> {
pub(crate) fn with_clock(
capacity: u64,
refill_per_second: u64,
clock: C,
) -> Result<Self, RateBudgetConfigError> {
if capacity == 0 {
return Err(RateBudgetConfigError::ZeroCapacity);
}
if refill_per_second == 0 {
return Err(RateBudgetConfigError::ZeroRefill);
}
let now = clock.now();
Ok(Self {
inner: Arc::new(RateBudgetInner {
capacity,
refill_per_second,
clock,
state: Mutex::new(RateState {
available: capacity,
refill_remainder: 0,
last_refill: now,
}),
tokens_returned: Notify::new(),
}),
})
}
pub(crate) fn capacity(&self) -> u64 {
self.inner.capacity
}
pub(crate) fn refill_per_second(&self) -> u64 {
self.inner.refill_per_second
}
pub(crate) fn available(&self) -> u64 {
let mut state = self.lock_state();
self.refill(&mut state);
state.available
}
pub(crate) fn try_reserve(
&self,
units: u64,
) -> Result<RateReservation<C>, RateReservationError> {
self.ensure_reservation_fits(units)?;
let mut state = self.lock_state();
self.refill(&mut state);
if state.available < units {
return Err(RateReservationError::TemporarilyUnavailable {
retry_after: self.retry_after(&state, units),
});
}
state.available -= units;
Ok(RateReservation {
budget: self.clone(),
refundable: units,
})
}
fn ensure_reservation_fits(&self, units: u64) -> Result<(), RateReservationError> {
if units > self.capacity() {
return Err(RateReservationError::ExceedsCapacity {
requested: units,
capacity: self.capacity(),
});
}
Ok(())
}
fn time_until_available(&self, units: u64) -> Option<Duration> {
let mut state = self.lock_state();
self.refill(&mut state);
(state.available < units).then(|| self.retry_after(&state, units))
}
fn retry_after(&self, state: &RateState, units: u64) -> Duration {
let deficit = units.saturating_sub(state.available);
let scaled_deficit = u128::from(deficit)
.saturating_mul(NANOS_PER_SECOND)
.saturating_sub(state.refill_remainder);
let refill = u128::from(self.refill_per_second().max(1));
let nanos = scaled_deficit.saturating_add(refill - 1) / refill;
Duration::from_nanos(u64::try_from(nanos).unwrap_or(u64::MAX))
}
fn refill(&self, state: &mut RateState) {
let now = self.inner.clock.now();
let elapsed = now.saturating_duration_since(state.last_refill);
if elapsed.is_zero() || state.available == self.capacity() {
state.last_refill = now;
if state.available == self.capacity() {
state.refill_remainder = 0;
}
return;
}
let produced = elapsed
.as_nanos()
.saturating_mul(u128::from(self.refill_per_second()))
.saturating_add(state.refill_remainder);
let whole_units = u64::try_from(produced / NANOS_PER_SECOND).unwrap_or(u64::MAX);
state.available = self
.capacity()
.min(state.available.saturating_add(whole_units));
state.refill_remainder = if state.available == self.capacity() {
0
} else {
produced % NANOS_PER_SECOND
};
state.last_refill = now;
}
fn return_units(&self, units: u64) {
if units == 0 {
return;
}
let mut state = self.lock_state();
self.refill(&mut state);
state.available = self.capacity().min(state.available.saturating_add(units));
if state.available == self.capacity() {
state.refill_remainder = 0;
}
drop(state);
self.inner.tokens_returned.notify_waiters();
}
fn lock_state(&self) -> MutexGuard<'_, RateState> {
self.inner
.state
.lock()
.expect("rate budget mutex should not be poisoned")
}
}
#[derive(Debug)]
#[must_use = "dropping a provisional rate reservation returns it"]
pub(crate) struct RateReservation<C: Clock = RealClock> {
budget: RateBudget<C>,
refundable: u64,
}
impl<C: Clock> RateReservation<C> {
pub(crate) fn commit(
mut self,
initial_spend: u64,
) -> Result<CommittedRateReservation<C>, RateReservationSpendError> {
if initial_spend > self.refundable {
return Err(RateReservationSpendError {
spent: initial_spend,
refundable: self.refundable,
});
}
self.refundable -= initial_spend;
let refundable = self.refundable;
self.refundable = 0;
Ok(CommittedRateReservation {
budget: self.budget.clone(),
refundable,
})
}
pub(crate) fn reserved(&self) -> u64 {
self.refundable
}
}
impl<C: Clock> Drop for RateReservation<C> {
fn drop(&mut self) {
self.budget.return_units(self.refundable);
self.refundable = 0;
}
}
#[derive(Debug)]
#[must_use = "dropping a committed rate reservation returns its unused units"]
pub(crate) struct CommittedRateReservation<C: Clock = RealClock> {
budget: RateBudget<C>,
refundable: u64,
}
impl<C: Clock> CommittedRateReservation<C> {
pub(crate) fn spend(&mut self, units: u64) -> Result<(), RateReservationSpendError> {
if units > self.refundable {
return Err(RateReservationSpendError {
spent: units,
refundable: self.refundable,
});
}
self.refundable -= units;
Ok(())
}
pub(crate) fn refundable(&self) -> u64 {
self.refundable
}
pub(crate) fn finish(self) {
drop(self);
}
}
impl<C: Clock> Drop for CommittedRateReservation<C> {
fn drop(&mut self) {
self.budget.return_units(self.refundable);
self.refundable = 0;
}
}