use async_trait::async_trait;
use redb::{ReadableDatabase, ReadableTable, TableDefinition};
use crate::core::{RunId, Spend, StoreError, Timestamp};
use crate::quota::{QuotaError, QuotaStore};
use super::redb::{MAX_STR, RedbStore, be, begin_write};
const RUNNING: TableDefinition<(&str, &str), i64> = TableDefinition::new("quota_running");
const SPENT: TableDefinition<(&str, &str), (u64, u64)> = TableDefinition::new("quota_spent");
const HALTED: TableDefinition<&str, &str> = TableDefinition::new("quota_halted");
#[async_trait]
impl QuotaStore for RedbStore {
async fn reserve(
&self,
run: RunId,
limit: Option<u32>,
at: Timestamp,
) -> Result<(), QuotaError> {
let tenant = self.tenant_name();
let run = run.to_string();
let at = at.unix_timestamp();
let taken: Result<(), u32> = self
.with_db(move |db| {
let w = begin_write(db)?;
let outcome = {
let mut running = w.open_table(RUNNING).map_err(|e| be(&e))?;
let held = running
.get((tenant.as_str(), run.as_str()))
.map_err(|e| be(&e))?
.is_some();
let mut refused = None;
if !held && let Some(limit) = limit {
let mut n = 0u32;
for e in running
.range((tenant.as_str(), "")..=(tenant.as_str(), MAX_STR))
.map_err(|e| be(&e))?
.take(limit as usize)
{
e.map_err(|e| be(&e))?;
n += 1;
}
if n >= limit {
refused = Some(n);
}
}
if let Some(n) = refused {
Err(n)
} else {
running
.insert((tenant.as_str(), run.as_str()), at)
.map_err(|e| be(&e))?;
Ok(())
}
};
w.commit().map_err(|e| be(&e))?;
Ok(outcome)
})
.await?;
taken.map_err(|running| QuotaError::TooManyRuns {
tenant: self.tenant_name(),
running,
})
}
async fn set_halt(&self, reason: Option<&str>) -> Result<(), StoreError> {
let tenant = self.tenant_name();
let reason = reason.map(ToOwned::to_owned);
self.with_db(move |db| {
let w = begin_write(db)?;
{
let mut halted = w.open_table(HALTED).map_err(|e| be(&e))?;
match &reason {
Some(reason) => {
halted
.insert(tenant.as_str(), reason.as_str())
.map_err(|e| be(&e))?;
}
None => {
halted.remove(tenant.as_str()).map_err(|e| be(&e))?;
}
}
}
w.commit().map_err(|e| be(&e))
})
.await
}
async fn halted(&self) -> Result<Option<String>, StoreError> {
let tenant = self.tenant_name();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(halted) = r.open_table(HALTED) else {
return Ok(None);
};
Ok(halted
.get(tenant.as_str())
.map_err(|e| be(&e))?
.map(|v| v.value().to_owned()))
})
.await
}
async fn release(&self, run: RunId) -> Result<(), StoreError> {
let tenant = self.tenant_name();
let run = run.to_string();
self.with_db(move |db| {
let w = begin_write(db)?;
{
let mut running = w.open_table(RUNNING).map_err(|e| be(&e))?;
running
.remove((tenant.as_str(), run.as_str()))
.map_err(|e| be(&e))?;
}
w.commit().map_err(|e| be(&e))?;
Ok(())
})
.await
}
async fn accrue(&self, period: &str, spend: Spend) -> Result<(), StoreError> {
let tenant = self.tenant_name();
let period = period.to_owned();
self.with_db(move |db| {
let w = begin_write(db)?;
{
let mut totals = w.open_table(SPENT).map_err(|e| be(&e))?;
let (tokens, minor) = totals
.get((tenant.as_str(), period.as_str()))
.map_err(|e| be(&e))?
.map_or((0, 0), |v| v.value());
totals
.insert(
(tenant.as_str(), period.as_str()),
(
tokens.saturating_add(spend.tokens),
minor.saturating_add(spend.minor_units),
),
)
.map_err(|e| be(&e))?;
}
w.commit().map_err(|e| be(&e))?;
Ok(())
})
.await
}
async fn spent(&self, period: &str) -> Result<Spend, StoreError> {
let tenant = self.tenant_name();
let period = period.to_owned();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(t) = r.open_table(SPENT) else {
return Ok(Spend::default());
};
let (tokens, minor_units) = t
.get((tenant.as_str(), period.as_str()))
.map_err(|e| be(&e))?
.map_or((0, 0), |v| v.value());
Ok(Spend {
tokens,
minor_units,
})
})
.await
}
async fn running(&self) -> Result<u32, StoreError> {
let tenant = self.tenant_name();
self.with_db(move |db| {
let r = db.begin_read().map_err(|e| be(&e))?;
let Ok(t) = r.open_table(RUNNING) else {
return Ok(0);
};
let mut n = 0u32;
for e in t
.range((tenant.as_str(), "")..=(tenant.as_str(), MAX_STR))
.map_err(|e| be(&e))?
{
e.map_err(|e| be(&e))?;
n += 1;
}
Ok(n)
})
.await
}
}