use std::time::Duration;
use crabka_metadata::MetadataImage;
use super::buckets::QuotaBuckets;
use super::lookup::lookup_quota_with_key;
#[must_use]
pub fn consume_request_quota(
image: &MetadataImage,
buckets: &QuotaBuckets,
principal: &str,
client_id: &str,
elapsed_micros: u64,
) -> Duration {
if elapsed_micros == 0 {
return Duration::ZERO;
}
let Some((entity_key, rate_pct)) =
lookup_quota_with_key(image, principal, client_id, "request_percentage")
else {
return Duration::ZERO;
};
if rate_pct <= 0.0 {
return Duration::ZERO;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let rate_micros_per_sec = (rate_pct * 10_000.0) as u64;
if rate_micros_per_sec == 0 {
return Duration::ZERO;
}
let bucket = buckets.get_or_create("request_percentage", &entity_key, rate_micros_per_sec);
let granted = bucket.try_consume(elapsed_micros);
if granted >= elapsed_micros {
return Duration::ZERO;
}
let overage_micros = elapsed_micros - granted;
let delay_micros = overage_micros.saturating_mul(1_000_000) / rate_micros_per_sec;
Duration::from_micros(delay_micros).min(Duration::from_secs(1))
}
#[cfg(test)]
mod tests {
use super::*;
use assert2::assert;
use crabka_metadata::{ClientQuotaRecord, MetadataRecord, QuotaEntity};
fn img_with_quota(entity: Vec<(&str, Option<&str>)>, rate: f64) -> MetadataImage {
let mut img = MetadataImage::new(uuid::Uuid::nil());
img.apply(&MetadataRecord::V1ClientQuota(ClientQuotaRecord {
entity: entity
.into_iter()
.map(|(t, n)| QuotaEntity {
entity_type: t.into(),
entity_name: n.map(Into::into),
})
.collect(),
config_key: "request_percentage".into(),
config_value: Some(rate),
}));
img
}
#[test]
fn zero_elapsed_returns_zero_delay() {
let img = img_with_quota(vec![("user", Some("alice"))], 100.0);
let buckets = QuotaBuckets::new();
assert!(consume_request_quota(&img, &buckets, "alice", "", 0) == Duration::ZERO);
}
#[test]
fn no_quota_returns_zero_delay() {
let img = MetadataImage::new(uuid::Uuid::nil());
let buckets = QuotaBuckets::new();
assert!(consume_request_quota(&img, &buckets, "alice", "", 5_000) == Duration::ZERO);
}
#[test]
fn under_budget_returns_zero_delay() {
let img = img_with_quota(vec![("user", Some("alice"))], 100.0);
let buckets = QuotaBuckets::new();
assert!(consume_request_quota(&img, &buckets, "alice", "", 5_000) == Duration::ZERO);
}
#[test]
fn overage_returns_capped_delay() {
let img = img_with_quota(vec![("user", Some("alice"))], 0.001);
let buckets = QuotaBuckets::new();
let delay = consume_request_quota(&img, &buckets, "alice", "", 1_000_000);
assert!(delay == Duration::from_secs(1));
}
}