#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
use std::sync::Mutex;
use std::time::Instant;
use super::super::grants::CapabilityQuotas;
use super::super::manifest::SandboxCapability;
use super::CapabilityCall;
#[derive(Debug, Clone)]
pub struct QuotaLedger {
declared: CapabilityQuotas,
kv_reads: u32,
kv_writes: u32,
outbound_calls: u32,
db_reads: u32,
db_writes: u32,
job_enqueues: u32,
calls: u32,
}
impl QuotaLedger {
#[must_use]
pub const fn new(declared: CapabilityQuotas) -> Self {
Self {
declared,
kv_reads: 0,
kv_writes: 0,
outbound_calls: 0,
db_reads: 0,
db_writes: 0,
job_enqueues: 0,
calls: 0,
}
}
#[must_use]
pub const fn declared(&self) -> &CapabilityQuotas {
&self.declared
}
pub const fn charge_call(&mut self) -> Result<(), &'static str> {
if self.calls >= self.declared.calls {
return Err("calls");
}
self.calls = self.calls.saturating_add(1);
Ok(())
}
pub const fn charge_capability(&mut self, call: &CapabilityCall) -> Result<(), &'static str> {
let (counter, ceiling, field) = match call {
CapabilityCall::KvGet { .. } => {
(&mut self.kv_reads, self.declared.kv_reads, "kv_reads")
}
CapabilityCall::KvSet { .. } | CapabilityCall::KvDelete { .. } => {
(&mut self.kv_writes, self.declared.kv_writes, "kv_writes")
}
CapabilityCall::HttpFetch { .. } => (
&mut self.outbound_calls,
self.declared.outbound_calls,
"outbound_calls",
),
CapabilityCall::DbGet { .. } | CapabilityCall::DbQuery { .. } => {
(&mut self.db_reads, self.declared.db_reads, "db_reads")
}
CapabilityCall::DbInsert { .. }
| CapabilityCall::DbUpdate { .. }
| CapabilityCall::DbDelete { .. } => {
(&mut self.db_writes, self.declared.db_writes, "db_writes")
}
CapabilityCall::JobEnqueue { .. } => (
&mut self.job_enqueues,
self.declared.job_enqueues,
"job_enqueues",
),
};
if *counter >= ceiling {
return Err(field);
}
*counter = counter.saturating_add(1);
Ok(())
}
}
#[derive(Debug)]
pub struct CapabilityRateLimiter {
buckets: Vec<Mutex<Bucket>>,
per_second: u32,
}
#[derive(Debug)]
struct Bucket {
tokens: u64,
last: Instant,
}
const SCALE: u64 = 1_000;
impl CapabilityRateLimiter {
#[must_use]
pub fn new(per_second: u32) -> Self {
let full = u64::from(per_second).saturating_mul(SCALE);
Self {
buckets: SandboxCapability::ALL
.iter()
.map(|_| {
Mutex::new(Bucket {
tokens: full,
last: crate::time::ambient_instant(),
})
})
.collect(),
per_second,
}
}
#[must_use]
pub const fn per_second(&self) -> u32 {
self.per_second
}
#[must_use]
pub fn try_take(&self, capability: SandboxCapability) -> bool {
let Some(index) = SandboxCapability::ALL
.iter()
.position(|known| *known == capability)
else {
return false;
};
let Some(bucket) = self.buckets.get(index) else {
return false;
};
let mut bucket = bucket
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let full = u64::from(self.per_second).saturating_mul(SCALE);
let now = crate::time::ambient_instant();
let micros = u64::try_from(now.saturating_duration_since(bucket.last).as_micros())
.unwrap_or(u64::MAX);
let rate = u64::from(self.per_second).saturating_mul(SCALE);
let refill = micros.saturating_mul(rate) / 1_000_000;
bucket.tokens = bucket.tokens.saturating_add(refill).min(full);
let converted = refill
.saturating_mul(1_000_000)
.checked_div(rate)
.unwrap_or(micros)
.min(micros);
bucket.last = bucket
.last
.checked_add(std::time::Duration::from_micros(converted))
.unwrap_or(now);
if bucket.tokens < SCALE {
return false;
}
bucket.tokens = bucket.tokens.saturating_sub(SCALE);
true
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use chrono::{TimeZone, Utc};
use super::{CapabilityRateLimiter, SandboxCapability};
use crate::time::{TickingClock, install_ambient};
#[test]
fn a_bucket_from_a_nested_timeline_refills_on_the_outer_one() {
let epoch = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
let capability = SandboxCapability::ALL[0];
let outer = TickingClock::starting_at(epoch);
let _outer = install_ambient(Arc::new(outer.clone()));
let limiter = CapabilityRateLimiter::new(1);
let inner = TickingClock::starting_at(epoch);
let guard = install_ambient(Arc::new(inner.clone()));
inner.advance(Duration::from_secs(3600));
assert!(limiter.try_take(capability));
assert!(!limiter.try_take(capability), "drained");
drop(guard);
assert!(!limiter.try_take(capability), "no time has passed yet");
outer.advance(Duration::from_secs(1));
assert!(limiter.try_take(capability), "one second refills one token");
}
}