use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::error::HttpError;
use crate::retry::parse_retry_after_at;
pub const MAX_DEFER: Duration = Duration::from_secs(365 * 24 * 3600);
pub const MAX_PROVIDERS: usize = 4096;
#[derive(Serialize, Deserialize, Default)]
struct Stored {
version: u32,
not_before_ms: BTreeMap<String, u64>,
}
#[derive(Debug, Default)]
pub struct QuotaSchedule {
not_before_ms: BTreeMap<String, u64>,
path: Option<PathBuf>,
}
fn now_ms() -> u64 {
system_ms(SystemTime::now())
}
fn system_ms(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub trait QuotaStore {
fn get_not_before_ms(&self, provider: &str) -> io::Result<Option<u64>>;
fn merge_not_before_ms(&mut self, provider: &str, not_before_ms: u64) -> io::Result<()>;
}
impl QuotaStore for QuotaSchedule {
fn get_not_before_ms(&self, provider: &str) -> io::Result<Option<u64>> {
Ok(self.not_before_ms.get(provider).copied())
}
fn merge_not_before_ms(&mut self, provider: &str, not_before_ms: u64) -> io::Result<()> {
if provider.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"empty provider key",
));
}
if !self.not_before_ms.contains_key(provider) && self.not_before_ms.len() >= MAX_PROVIDERS {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"too many providers in the quota schedule",
));
}
let slot = self.not_before_ms.entry(provider.to_string()).or_insert(0);
if not_before_ms > *slot {
*slot = not_before_ms;
}
self.save()
}
}
pub fn defer_in<S: QuotaStore + ?Sized>(
store: &mut S,
key: &str,
wait: Duration,
now: SystemTime,
) -> io::Result<SystemTime> {
if key.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"empty provider key",
));
}
let until = system_ms(now).saturating_add(wait.min(MAX_DEFER).as_millis() as u64);
let existing = store.get_not_before_ms(key)?.unwrap_or(0);
let merged = existing.max(until);
store.merge_not_before_ms(key, merged)?;
Ok(UNIX_EPOCH + Duration::from_millis(merged))
}
pub fn remaining_in<S: QuotaStore + ?Sized>(
store: &S,
key: &str,
now: SystemTime,
) -> io::Result<Option<Duration>> {
Ok(store.get_not_before_ms(key)?.and_then(|until| {
until
.checked_sub(system_ms(now))
.filter(|d| *d > 0)
.map(Duration::from_millis)
}))
}
impl QuotaSchedule {
pub fn new() -> Self {
Self::default()
}
pub fn open(path: impl Into<PathBuf>) -> io::Result<Self> {
let path = path.into();
let not_before_ms = match std::fs::read(&path) {
Ok(bytes) => {
let stored: Stored = serde_json::from_slice(&bytes)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
stored.not_before_ms
}
Err(e) if e.kind() == io::ErrorKind::NotFound => BTreeMap::new(),
Err(e) => return Err(e),
};
let mut s = Self {
not_before_ms,
path: Some(path),
};
s.prune(now_ms());
Ok(s)
}
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
fn prune(&mut self, now: u64) {
self.not_before_ms.retain(|_, until| *until > now);
}
fn save(&self) -> io::Result<()> {
let Some(path) = &self.path else {
return Ok(());
};
let stored = Stored {
version: 1,
not_before_ms: self.not_before_ms.clone(),
};
let bytes = serde_json::to_vec(&stored).map_err(io::Error::other)?;
let mut tmp = path.clone().into_os_string();
tmp.push(".tmp");
let tmp = PathBuf::from(tmp);
{
use std::io::Write;
let mut f = std::fs::File::create(&tmp)?;
f.write_all(&bytes)?;
f.sync_all()?;
}
std::fs::rename(&tmp, path)
}
pub fn defer(&mut self, provider: &str, wait: Duration) -> io::Result<()> {
self.defer_from(provider, wait, now_ms())
}
fn defer_from(&mut self, provider: &str, wait: Duration, now: u64) -> io::Result<()> {
if provider.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"empty provider key",
));
}
self.prune(now);
if !self.not_before_ms.contains_key(provider) && self.not_before_ms.len() >= MAX_PROVIDERS {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"too many providers in the quota schedule",
));
}
let until = now.saturating_add(wait.min(MAX_DEFER).as_millis() as u64);
let slot = self.not_before_ms.entry(provider.to_string()).or_insert(0);
if until > *slot {
*slot = until;
}
self.save()
}
pub fn defer_at(&mut self, provider: &str, wait: Duration, now: SystemTime) -> io::Result<()> {
self.defer_from(provider, wait, system_ms(now))
}
pub fn remaining_at(&self, provider: &str, now: SystemTime) -> Option<Duration> {
self.remaining_ms(provider, system_ms(now))
}
pub fn defer_for_header_at(
&mut self,
provider: &str,
value: &str,
now: SystemTime,
) -> io::Result<Option<Duration>> {
match parse_retry_after_at(value, now) {
Some(wait) => {
self.defer_at(provider, wait, now)?;
Ok(Some(wait.min(MAX_DEFER)))
}
None => Ok(None),
}
}
pub fn defer_for_header(
&mut self,
provider: &str,
value: &str,
) -> io::Result<Option<Duration>> {
match parse_retry_after_at(value, SystemTime::now()) {
Some(wait) => {
self.defer(provider, wait)?;
Ok(Some(wait.min(MAX_DEFER)))
}
None => Ok(None),
}
}
pub fn defer_for_error(
&mut self,
provider: &str,
err: &HttpError,
) -> io::Result<Option<Duration>> {
let wait = match err {
HttpError::Status {
retry_after: Some(w),
..
} => *w,
_ => return Ok(None),
};
self.defer(provider, wait)?;
Ok(Some(wait.min(MAX_DEFER)))
}
pub fn remaining(&self, provider: &str) -> Option<Duration> {
self.remaining_ms(provider, now_ms())
}
fn remaining_ms(&self, provider: &str, now: u64) -> Option<Duration> {
let until = *self.not_before_ms.get(provider)?;
until
.checked_sub(now)
.filter(|d| *d > 0)
.map(Duration::from_millis)
}
pub fn not_before(&self, provider: &str) -> Option<SystemTime> {
self.remaining(provider).map(|d| SystemTime::now() + d)
}
pub fn clear(&mut self, provider: &str) -> io::Result<()> {
if self.not_before_ms.remove(provider).is_some() {
self.prune(now_ms());
self.save()?;
}
Ok(())
}
pub fn pending(&self) -> Vec<(String, Duration)> {
let now = now_ms();
let mut v: Vec<(String, Duration)> = self
.not_before_ms
.iter()
.filter_map(|(k, until)| {
until
.checked_sub(now)
.filter(|d| *d > 0)
.map(|d| (k.clone(), Duration::from_millis(d)))
})
.collect();
v.sort_by_key(|(_, d)| *d);
v
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn longer_waits_win_and_shorter_never_shorten() {
let mut q = QuotaSchedule::new();
q.defer_from("p", Duration::from_secs(3600), 1_000).unwrap();
q.defer_from("p", Duration::from_secs(10), 1_000).unwrap();
assert_eq!(q.remaining_ms("p", 1_000), Some(Duration::from_secs(3600)));
assert_eq!(q.remaining_ms("p", 1_000 + 3_600_000), None);
assert_eq!(q.remaining_ms("other", 1_000), None);
}
#[test]
fn schedule_survives_a_reopen() {
let dir = std::env::temp_dir().join(format!(
"rightkit-http-quota-{}-{}",
std::process::id(),
now_ms()
));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("quota.json");
{
let mut q = QuotaSchedule::open(&file).unwrap();
q.defer("openai", Duration::from_secs(7200)).unwrap();
}
let q = QuotaSchedule::open(&file).unwrap();
let left = q.remaining("openai").unwrap();
assert!(left > Duration::from_secs(7000) && left <= Duration::from_secs(7200));
std::fs::write(&file, b"not json").unwrap();
assert!(QuotaSchedule::open(&file).is_err());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn header_and_error_forms_are_recorded() {
let mut q = QuotaSchedule::new();
assert_eq!(
q.defer_for_header("a", "120").unwrap(),
Some(Duration::from_secs(120))
);
assert_eq!(q.defer_for_header("a", "garbage").unwrap(), None);
let err = HttpError::Status {
status: 429,
body: String::new(),
retry_after: Some(Duration::from_secs(600)),
attempts: 1,
};
assert_eq!(
q.defer_for_error("b", &err).unwrap(),
Some(Duration::from_secs(600))
);
assert_eq!(q.defer_for_error("c", &HttpError::Cancelled).unwrap(), None);
assert!(q.remaining("c").is_none());
q.clear("a").unwrap();
assert!(q.remaining("a").is_none());
assert!(q.defer("", Duration::from_secs(1)).is_err());
}
#[test]
fn injected_clock_and_store_are_max_merged() {
let now = UNIX_EPOCH + Duration::from_secs(1_000);
let mut q = QuotaSchedule::new();
q.defer_at("acct-1", Duration::from_secs(60), now).unwrap();
q.defer_at("acct-1", Duration::from_secs(10), now).unwrap();
assert_eq!(q.remaining_at("acct-1", now), Some(Duration::from_secs(60)));
assert_eq!(
q.remaining_at("acct-1", now + Duration::from_secs(60)),
None
);
let until = defer_in(&mut q, "acct-2", Duration::from_secs(120), now).unwrap();
assert_eq!(until, now + Duration::from_secs(120));
assert_eq!(
remaining_in(&q, "acct-2", now).unwrap(),
Some(Duration::from_secs(120))
);
assert_eq!(
q.defer_for_header_at("acct-3", "Thu, 01 Jan 1970 00:20:00 GMT", now)
.unwrap(),
Some(Duration::from_secs(200))
);
assert!(defer_in(&mut q, "", Duration::from_secs(1), now).is_err());
}
}