use std::fmt::Debug;
use async_trait::async_trait;
use crate::core::{RunId, Spend, StoreError, Timestamp};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TenantQuota {
pub max_concurrent_runs: Option<u32>,
pub max_tokens_per_period: Option<u64>,
pub max_minor_units_per_period: Option<u64>,
pub period: Period,
}
impl TenantQuota {
#[must_use]
pub const fn is_unlimited(&self) -> bool {
self.max_concurrent_runs.is_none()
&& self.max_tokens_per_period.is_none()
&& self.max_minor_units_per_period.is_none()
}
#[must_use]
pub const fn bounds_spend(&self) -> bool {
self.max_tokens_per_period.is_some() || self.max_minor_units_per_period.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Period {
#[default]
Monthly,
Daily,
}
impl Period {
#[must_use]
pub fn key_for(self, at: Timestamp) -> String {
let d = at.date();
match self {
Self::Monthly => format!("{:04}-{:02}", d.year(), u8::from(d.month())),
Self::Daily => format!("{:04}-{:02}-{:02}", d.year(), u8::from(d.month()), d.day()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum QuotaError {
#[error(
"tenant '{tenant}' already has {running} runs executing, which is its limit — \
this is back-pressure, not a fault: retry when one finishes"
)]
TooManyRuns { tenant: String, running: u32 },
#[error(
"tenant '{tenant}' has spent {spent} of its {limit} {unit} for period {period} — \
this does not reset until the period does"
)]
SpentOut {
tenant: String,
period: String,
unit: &'static str,
spent: u64,
limit: u64,
},
#[error("tenant '{tenant}' is halted by an operator: {reason}")]
Halted { tenant: String, reason: String },
#[error(
"the quota store could not be reached, and a ceiling that yields under load is not a ceiling: {0}"
)]
Unavailable(String),
}
impl From<StoreError> for QuotaError {
fn from(e: StoreError) -> Self {
Self::Unavailable(e.to_string())
}
}
#[async_trait]
pub trait QuotaStore: Send + Sync + Debug {
async fn reserve(
&self,
run: RunId,
limit: Option<u32>,
at: Timestamp,
) -> Result<(), QuotaError>;
async fn release(&self, run: RunId) -> Result<(), StoreError>;
async fn set_halt(&self, reason: Option<&str>) -> Result<(), StoreError>;
async fn halted(&self) -> Result<Option<String>, StoreError>;
async fn accrue(&self, period: &str, spend: Spend) -> Result<(), StoreError>;
async fn spent(&self, period: &str) -> Result<Spend, StoreError>;
async fn running(&self) -> Result<u32, StoreError>;
}
pub fn check_spend(
tenant: &str,
period: &str,
quota: &TenantQuota,
spent: Spend,
) -> Result<(), QuotaError> {
if let Some(limit) = quota.max_tokens_per_period
&& spent.tokens >= limit
{
return Err(QuotaError::SpentOut {
tenant: tenant.to_owned(),
period: period.to_owned(),
unit: "tokens",
spent: spent.tokens,
limit,
});
}
if let Some(limit) = quota.max_minor_units_per_period
&& spent.minor_units >= limit
{
return Err(QuotaError::SpentOut {
tenant: tenant.to_owned(),
period: period.to_owned(),
unit: "minor units",
spent: spent.minor_units,
limit,
});
}
Ok(())
}