#![allow(clippy::significant_drop_tightening)]
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use serde::{Deserialize, Serialize};
use crate::config::StoragePolicy;
mod associative;
mod file_mapped;
mod legacy;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TokenRecord {
pub id: String,
pub label: String,
pub issued_at: i64,
pub expires_at: i64,
pub revoked: bool,
#[serde(default)]
pub ephemeral: bool,
#[serde(default)]
pub sliding_window_seconds: Option<i64>,
#[serde(default)]
pub account: Option<String>,
#[serde(default)]
pub max_requests: Option<u64>,
#[serde(default)]
pub used_requests: u64,
#[serde(default)]
pub max_tokens: Option<u64>,
#[serde(default)]
pub used_tokens: u64,
#[serde(default)]
pub reserved_tokens: u64,
#[serde(default)]
pub rate_limit_per_minute: Option<u64>,
#[serde(default)]
pub rate_window_started_at: i64,
#[serde(default)]
pub rate_window_requests: u64,
#[serde(default)]
pub scope: String,
#[serde(default)]
pub github_repos: Vec<String>,
#[serde(default)]
pub client_kind: Option<String>,
#[serde(default)]
pub principal_id: Option<String>,
}
#[derive(Debug)]
pub enum StorageError {
Io(io::Error),
Codec(String),
Capacity(String),
LockPoisoned,
}
impl std::fmt::Display for StorageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "storage I/O error: {e}"),
Self::Codec(msg) => write!(f, "storage codec error: {msg}"),
Self::Capacity(msg) => write!(f, "storage capacity error: {msg}"),
Self::LockPoisoned => write!(f, "storage lock poisoned"),
}
}
}
impl std::error::Error for StorageError {}
impl From<io::Error> for StorageError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
pub trait TokenStore: Send + Sync {
fn list(&self) -> Result<Vec<TokenRecord>, StorageError>;
fn get(&self, id: &str) -> Result<Option<TokenRecord>, StorageError>;
fn put(&self, record: TokenRecord) -> Result<(), StorageError>;
fn delete(&self, id: &str) -> Result<bool, StorageError>;
fn put_compacting_ephemeral(&self, record: TokenRecord, now: i64) -> Result<(), StorageError> {
for stale in self
.list()?
.into_iter()
.filter(|held| held.ephemeral && (held.revoked || held.expires_at <= now))
{
self.delete(&stale.id)?;
}
self.put(record)
}
fn revoke(&self, id: &str) -> Result<bool, StorageError> {
if let Some(mut rec) = self.get(id)? {
if rec.revoked {
return Ok(false);
}
rec.revoked = true;
self.put(rec)?;
return Ok(true);
}
Ok(false)
}
fn revoked_ids(&self) -> Result<Vec<String>, StorageError> {
Ok(self
.list()?
.into_iter()
.filter(|r| r.revoked)
.map(|r| r.id)
.collect())
}
fn try_consume_request(&self, id: &str) -> Result<bool, StorageError> {
if let Some(mut rec) = self.get(id)? {
if let Some(max) = rec.max_requests
&& rec.used_requests >= max
{
return Ok(false);
}
rec.used_requests = rec.used_requests.saturating_add(1);
self.put(rec)?;
}
Ok(true)
}
fn try_admit_request(&self, id: &str, now: i64) -> Result<RequestAdmission, StorageError> {
self.try_admit_request_reserving(id, now, 0)
}
fn try_admit_request_reserving(
&self,
id: &str,
now: i64,
reserve: u64,
) -> Result<RequestAdmission, StorageError> {
let Some(mut record) = self.get(id)? else {
return Ok(RequestAdmission::Admitted);
};
let admission = admit_request_reserving(Some(&mut record), now, reserve);
if admission == RequestAdmission::Admitted {
self.put(record)?;
}
Ok(admission)
}
fn record_token_usage(&self, id: &str, tokens: u64) -> Result<(), StorageError> {
if let Some(mut record) = self.get(id)? {
record.used_tokens = record.used_tokens.saturating_add(tokens);
self.put(record)?;
}
Ok(())
}
fn settle_token_usage(&self, id: &str, reserved: u64, actual: u64) -> Result<(), StorageError> {
if let Some(mut record) = self.get(id)? {
settle_token_usage(Some(&mut record), reserved, actual);
self.put(record)?;
}
Ok(())
}
fn release_stale_reservations(&self) -> Result<usize, StorageError> {
let mut cleared = 0;
for mut record in self.list()? {
if record.reserved_tokens > 0 {
record.reserved_tokens = 0;
self.put(record)?;
cleared += 1;
}
}
Ok(cleared)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestAdmission {
Admitted,
RequestLimitExceeded,
TokenLimitExceeded,
RateLimitExceeded,
}
#[derive(Default, Clone)]
pub struct MemoryTokenStore {
inner: Arc<RwLock<HashMap<String, TokenRecord>>>,
}
impl MemoryTokenStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl TokenStore for MemoryTokenStore {
fn list(&self) -> Result<Vec<TokenRecord>, StorageError> {
let guard = self.inner.read().map_err(|_| StorageError::LockPoisoned)?;
Ok(guard.values().cloned().collect())
}
fn get(&self, id: &str) -> Result<Option<TokenRecord>, StorageError> {
let guard = self.inner.read().map_err(|_| StorageError::LockPoisoned)?;
Ok(guard.get(id).cloned())
}
fn put(&self, record: TokenRecord) -> Result<(), StorageError> {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
guard.insert(record.id.clone(), record);
Ok(())
}
fn delete(&self, id: &str) -> Result<bool, StorageError> {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
Ok(guard.remove(id).is_some())
}
fn put_compacting_ephemeral(&self, record: TokenRecord, now: i64) -> Result<(), StorageError> {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
compact_ephemeral_records(&mut guard, now);
guard.insert(record.id.clone(), record);
Ok(())
}
fn try_consume_request(&self, id: &str) -> Result<bool, StorageError> {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
Ok(consume_request(guard.get_mut(id)))
}
fn try_admit_request_reserving(
&self,
id: &str,
now: i64,
reserve: u64,
) -> Result<RequestAdmission, StorageError> {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
Ok(admit_request_reserving(guard.get_mut(id), now, reserve))
}
fn record_token_usage(&self, id: &str, tokens: u64) -> Result<(), StorageError> {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
add_token_usage(guard.get_mut(id), tokens);
Ok(())
}
fn settle_token_usage(&self, id: &str, reserved: u64, actual: u64) -> Result<(), StorageError> {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
settle_token_usage(guard.get_mut(id), reserved, actual);
Ok(())
}
}
#[derive(Clone)]
pub struct TextTokenStore {
path: PathBuf,
lock_path: PathBuf,
inner: Arc<RwLock<HashMap<String, TokenRecord>>>,
}
impl TextTokenStore {
pub fn open(path: impl Into<PathBuf>) -> Result<Self, StorageError> {
let path = path.into();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let (records, migrated) = if path.exists() {
let contents = fs::read_to_string(&path)?;
decode_text_records(&contents).map_err(StorageError::Codec)?
} else {
(Vec::new(), false)
};
let map: HashMap<_, _> = records.into_iter().map(|r| (r.id.clone(), r)).collect();
let store = Self {
lock_path: path.with_extension("lock"),
path,
inner: Arc::new(RwLock::new(map)),
};
if migrated {
let guard = store.inner.read().map_err(|_| StorageError::LockPoisoned)?;
store.flush(&guard)?;
}
Ok(store)
}
fn flush(&self, guard: &HashMap<String, TokenRecord>) -> Result<(), StorageError> {
let mut sorted: Vec<&TokenRecord> = guard.values().collect();
sorted.sort_by(|a, b| a.id.cmp(&b.id));
let body = associative::encode_text(sorted.iter().copied());
atomic_write(&self.path, body.as_bytes())
}
fn load_map(&self) -> Result<HashMap<String, TokenRecord>, StorageError> {
if !self.path.exists() {
return Ok(HashMap::new());
}
let contents = fs::read_to_string(&self.path)?;
let (records, _) = decode_text_records(&contents).map_err(StorageError::Codec)?;
Ok(records
.into_iter()
.map(|record| (record.id.clone(), record))
.collect())
}
fn refresh(&self) -> Result<(), StorageError> {
crate::durable_file::with_exclusive_lock(&self.lock_path, || {
let map = self.load_map()?;
*self.inner.write().map_err(|_| StorageError::LockPoisoned)? = map;
Ok(())
})
}
fn mutate<T>(
&self,
operation: impl FnOnce(&mut HashMap<String, TokenRecord>) -> T,
) -> Result<T, StorageError> {
crate::durable_file::with_exclusive_lock(&self.lock_path, || {
let mut guard = self.inner.write().map_err(|_| StorageError::LockPoisoned)?;
*guard = self.load_map()?;
let before = guard.clone();
let result = operation(&mut guard);
if let Err(error) = self.flush(&guard) {
*guard = before;
return Err(error);
}
Ok(result)
})
}
fn replace_all(&self, records: &[TokenRecord]) -> Result<(), StorageError> {
self.mutate(|current| {
current.clear();
current.extend(
records
.iter()
.cloned()
.map(|record| (record.id.clone(), record)),
);
})
}
}
fn decode_text_records(contents: &str) -> Result<(Vec<TokenRecord>, bool), String> {
match associative::decode_text(contents) {
Ok(records) => Ok((records, false)),
Err(associative_error) => match legacy::decode_text(contents) {
Ok(records) => Ok((records, true)),
Err(legacy_error) => Err(format!(
"associative decoder: {}; legacy decoder: {}",
sanitize_codec_error(&associative_error, contents),
sanitize_codec_error(&legacy_error, contents)
)),
},
}
}
fn sanitize_codec_error(error: &str, contents: &str) -> String {
contents
.lines()
.map(str::trim)
.filter(|line| line.len() >= 8)
.fold(error.to_string(), |message, line| {
message.replace(line, "<redacted input>")
})
}
impl TokenStore for TextTokenStore {
fn list(&self) -> Result<Vec<TokenRecord>, StorageError> {
self.refresh()?;
let guard = self.inner.read().map_err(|_| StorageError::LockPoisoned)?;
Ok(guard.values().cloned().collect())
}
fn get(&self, id: &str) -> Result<Option<TokenRecord>, StorageError> {
self.refresh()?;
let guard = self.inner.read().map_err(|_| StorageError::LockPoisoned)?;
Ok(guard.get(id).cloned())
}
fn put(&self, record: TokenRecord) -> Result<(), StorageError> {
self.mutate(|records| {
records.insert(record.id.clone(), record);
})
}
fn delete(&self, id: &str) -> Result<bool, StorageError> {
self.mutate(|records| records.remove(id).is_some())
}
fn put_compacting_ephemeral(&self, record: TokenRecord, now: i64) -> Result<(), StorageError> {
self.mutate(|records| {
compact_ephemeral_records(records, now);
records.insert(record.id.clone(), record);
})
}
fn try_consume_request(&self, id: &str) -> Result<bool, StorageError> {
self.mutate(|records| consume_request(records.get_mut(id)))
}
fn try_admit_request_reserving(
&self,
id: &str,
now: i64,
reserve: u64,
) -> Result<RequestAdmission, StorageError> {
self.mutate(|records| admit_request_reserving(records.get_mut(id), now, reserve))
}
fn record_token_usage(&self, id: &str, tokens: u64) -> Result<(), StorageError> {
self.mutate(|records| add_token_usage(records.get_mut(id), tokens))
}
fn settle_token_usage(&self, id: &str, reserved: u64, actual: u64) -> Result<(), StorageError> {
self.mutate(|records| settle_token_usage(records.get_mut(id), reserved, actual))
}
}
#[path = "storage_budget.rs"]
mod budget;
use budget::{
add_token_usage, admit_request_reserving, consume_request, merge_safer_record,
settle_token_usage,
};
fn compact_ephemeral_records(records: &mut HashMap<String, TokenRecord>, now: i64) {
records.retain(|_, record| !record.ephemeral || (!record.revoked && record.expires_at > now));
}
#[path = "storage_binary.rs"]
mod binary;
pub use binary::BinaryTokenStore;
pub struct DualTokenStore {
pub primary: Arc<dyn TokenStore>,
pub secondary: Arc<dyn TokenStore>,
}
impl TokenStore for DualTokenStore {
fn list(&self) -> Result<Vec<TokenRecord>, StorageError> {
let mut by_id: HashMap<String, TokenRecord> = HashMap::new();
for rec in self.primary.list()? {
by_id.insert(rec.id.clone(), rec);
}
for rec in self.secondary.list()? {
by_id.entry(rec.id.clone()).or_insert(rec);
}
Ok(by_id.into_values().collect())
}
fn get(&self, id: &str) -> Result<Option<TokenRecord>, StorageError> {
if let Some(rec) = self.primary.get(id)? {
return Ok(Some(rec));
}
self.secondary.get(id)
}
fn put(&self, record: TokenRecord) -> Result<(), StorageError> {
self.primary.put(record.clone())?;
self.secondary.put(record)?;
Ok(())
}
fn delete(&self, id: &str) -> Result<bool, StorageError> {
let a = self.primary.delete(id)?;
let b = self.secondary.delete(id)?;
Ok(a || b)
}
fn try_consume_request(&self, id: &str) -> Result<bool, StorageError> {
if !self.primary.try_consume_request(id)? {
return Ok(false);
}
self.secondary.try_consume_request(id)
}
fn try_admit_request_reserving(
&self,
id: &str,
now: i64,
reserve: u64,
) -> Result<RequestAdmission, StorageError> {
let admission = self.primary.try_admit_request_reserving(id, now, reserve)?;
if admission != RequestAdmission::Admitted {
return Ok(admission);
}
self.secondary.try_admit_request_reserving(id, now, reserve)
}
fn record_token_usage(&self, id: &str, tokens: u64) -> Result<(), StorageError> {
self.primary.record_token_usage(id, tokens)?;
self.secondary.record_token_usage(id, tokens)
}
fn settle_token_usage(&self, id: &str, reserved: u64, actual: u64) -> Result<(), StorageError> {
self.primary.settle_token_usage(id, reserved, actual)?;
self.secondary.settle_token_usage(id, reserved, actual)
}
}
struct DurableDualTokenStore {
text: TextTokenStore,
binary: BinaryTokenStore,
lock_path: PathBuf,
journal_path: PathBuf,
}
impl DurableDualTokenStore {
fn open(data_dir: &Path) -> Result<Self, StorageError> {
let store = Self {
text: TextTokenStore::open(data_dir.join("tokens.lino"))?,
binary: BinaryTokenStore::open(data_dir.join("tokens.bin"))?,
lock_path: data_dir.join("tokens.transaction.lock"),
journal_path: data_dir.join("tokens.transaction.json"),
};
if store.journal_path.exists() {
store.with_records(|_| ())?;
}
Ok(store)
}
fn merged_records(&self) -> Result<HashMap<String, TokenRecord>, StorageError> {
let mut records = HashMap::new();
for record in self.text.list()? {
records.insert(record.id.clone(), record);
}
for record in self.binary.list()? {
records
.entry(record.id.clone())
.and_modify(|current| merge_safer_record(current, &record))
.or_insert(record);
}
Ok(records)
}
fn recover(&self) -> Result<(), StorageError> {
if !self.journal_path.exists() {
return Ok(());
}
let source = fs::read_to_string(&self.journal_path)?;
let records: Vec<TokenRecord> = crate::lino_json::decode(&source)
.map_err(|error| StorageError::Codec(format!("transaction journal: {error}")))?;
self.install(&records)
}
fn install(&self, records: &[TokenRecord]) -> Result<(), StorageError> {
self.text.replace_all(records)?;
self.binary.replace_all_if_changed(records)?;
self.drop_journal()
}
fn drop_journal(&self) -> Result<(), StorageError> {
if self.journal_path.exists() {
fs::remove_file(&self.journal_path)?;
if let Some(parent) = self.journal_path.parent() {
crate::durable_file::sync_directory(parent)?;
}
}
Ok(())
}
fn commit(&self, records: &HashMap<String, TokenRecord>) -> Result<(), StorageError> {
let mut records = records.values().cloned().collect::<Vec<_>>();
records.sort_by(|left, right| left.id.cmp(&right.id));
let journal = crate::lino_json::encode(&records)
.map_err(|error| StorageError::Codec(format!("transaction journal: {error}")))?
.into_bytes();
crate::durable_file::atomic_write_owner_only(&self.journal_path, &journal)?;
self.install(&records)
}
fn with_records<T>(
&self,
operation: impl FnOnce(&mut HashMap<String, TokenRecord>) -> T,
) -> Result<T, StorageError> {
crate::durable_file::with_exclusive_lock(&self.lock_path, || {
self.recover()?;
let mut records = self.merged_records()?;
let result = operation(&mut records);
self.commit(&records)?;
Ok(result)
})
}
fn read_records<T>(
&self,
operation: impl FnOnce(&HashMap<String, TokenRecord>) -> T,
) -> Result<T, StorageError> {
crate::durable_file::with_shared_lock(&self.lock_path, || {
self.recover()?;
let records = self.readable_records()?;
Ok(operation(&records))
})
}
fn readable_records(&self) -> Result<HashMap<String, TokenRecord>, StorageError> {
let mut records = HashMap::new();
for record in self.text.list()? {
records.insert(record.id.clone(), record);
}
if records.is_empty() {
return self.merged_records();
}
Ok(records)
}
}
impl TokenStore for DurableDualTokenStore {
fn list(&self) -> Result<Vec<TokenRecord>, StorageError> {
self.read_records(|records| records.values().cloned().collect())
}
fn get(&self, id: &str) -> Result<Option<TokenRecord>, StorageError> {
self.read_records(|records| records.get(id).cloned())
}
fn put(&self, record: TokenRecord) -> Result<(), StorageError> {
self.with_records(|records| {
records.insert(record.id.clone(), record);
})
}
fn delete(&self, id: &str) -> Result<bool, StorageError> {
self.with_records(|records| records.remove(id).is_some())
}
fn put_compacting_ephemeral(&self, record: TokenRecord, now: i64) -> Result<(), StorageError> {
self.with_records(|records| {
compact_ephemeral_records(records, now);
records.insert(record.id.clone(), record);
})
}
fn try_consume_request(&self, id: &str) -> Result<bool, StorageError> {
self.with_records(|records| consume_request(records.get_mut(id)))
}
fn try_admit_request_reserving(
&self,
id: &str,
now: i64,
reserve: u64,
) -> Result<RequestAdmission, StorageError> {
self.with_records(|records| admit_request_reserving(records.get_mut(id), now, reserve))
}
fn record_token_usage(&self, id: &str, tokens: u64) -> Result<(), StorageError> {
self.with_records(|records| add_token_usage(records.get_mut(id), tokens))
}
fn settle_token_usage(&self, id: &str, reserved: u64, actual: u64) -> Result<(), StorageError> {
self.with_records(|records| settle_token_usage(records.get_mut(id), reserved, actual))
}
}
pub fn build_token_store(
policy: StoragePolicy,
data_dir: &Path,
) -> Result<Arc<dyn TokenStore>, StorageError> {
match policy {
StoragePolicy::Memory => Ok(Arc::new(MemoryTokenStore::new())),
StoragePolicy::Text => {
let s = TextTokenStore::open(data_dir.join("tokens.lino"))?;
Ok(Arc::new(s))
}
StoragePolicy::Binary => {
let s = BinaryTokenStore::open(data_dir.join("tokens.bin"))?;
Ok(Arc::new(s))
}
StoragePolicy::Both => Ok(Arc::new(DurableDualTokenStore::open(data_dir)?)),
}
}
pub fn build_token_store_read_only(
policy: StoragePolicy,
data_dir: &Path,
) -> Result<Arc<dyn TokenStore>, StorageError> {
let mut records = HashMap::<String, TokenRecord>::new();
let mut merge = |record: TokenRecord| {
records
.entry(record.id.clone())
.and_modify(|current| merge_safer_record(current, &record))
.or_insert(record);
};
if matches!(policy, StoragePolicy::Text | StoragePolicy::Both) {
let path = data_dir.join("tokens.lino");
if path.is_file() {
let contents = fs::read_to_string(path)?;
let (loaded, _) = decode_text_records(&contents).map_err(StorageError::Codec)?;
loaded.into_iter().for_each(&mut merge);
}
}
if matches!(policy, StoragePolicy::Binary | StoragePolicy::Both) {
let path = data_dir.join("tokens.bin");
if path.is_file() {
let loaded = if legacy::is_binary(&path)? {
legacy::decode_binary(&path)?
} else {
binary::load_records_read_only(&path)?
};
loaded.into_iter().for_each(&mut merge);
}
}
let store = MemoryTokenStore::new();
for record in records.into_values() {
store.put(record)?;
}
Ok(Arc::new(store))
}
fn atomic_write(path: &Path, contents: &[u8]) -> Result<(), StorageError> {
crate::durable_file::atomic_write_owner_only(path, contents).map_err(Into::into)
}
#[cfg(test)]
#[path = "storage_tests.rs"]
mod tests;