use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use redb::{Database, ReadableDatabase, ReadableTable, ReadableTableMetadata, TableDefinition};
use super::context::ContextMetadata;
use super::types::{CacheStatus, QueryResponse, SearchResult};
const ENTRIES_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("cache_entries");
const HEALTH_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("upstream_health");
const META_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("metadata");
#[derive(Debug)]
pub enum PersistenceError {
DatabaseOpen(String),
Serialize(String),
Deserialize(String),
Write(String),
Read(String),
Flush(String),
}
impl std::fmt::Display for PersistenceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DatabaseOpen(e) => write!(f, "Failed to open database: {}", e),
Self::Serialize(msg) => write!(f, "Serialization error: {}", msg),
Self::Deserialize(msg) => write!(f, "Deserialization error: {}", msg),
Self::Write(e) => write!(f, "Database write error: {}", e),
Self::Read(e) => write!(f, "Database read error: {}", e),
Self::Flush(e) => write!(f, "Database flush error: {}", e),
}
}
}
impl std::error::Error for PersistenceError {}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PersistedEntry {
pub query: String,
pub upstream_id: String,
pub response: PersistedResponse,
pub cached_at_ms: u64,
pub extended_count: u32,
pub content_hash: [u8; 32],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PersistedResponse {
pub results: Vec<PersistedResult>,
pub cache_status: String,
pub took_ms: u64,
pub generated_at: Option<u64>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PersistedResult {
pub id: String,
pub content: String,
pub score: f32,
pub metadata: Option<serde_json::Value>,
}
impl From<&QueryResponse> for PersistedResponse {
fn from(response: &QueryResponse) -> Self {
Self {
results: response
.results
.iter()
.map(|r| PersistedResult {
id: r.id.clone(),
content: r.content.clone(),
score: r.score,
metadata: r.metadata.clone(),
})
.collect(),
cache_status: match response.cache_status {
CacheStatus::Hit => "hit".to_string(),
CacheStatus::Miss => "miss".to_string(),
CacheStatus::Stale => "stale".to_string(),
CacheStatus::Frozen => "frozen".to_string(),
},
took_ms: response.took_ms,
generated_at: response.generated_at,
}
}
}
impl From<&PersistedResponse> for QueryResponse {
fn from(persisted: &PersistedResponse) -> Self {
Self {
results: persisted
.results
.iter()
.map(|r| SearchResult {
id: r.id.clone(),
content: r.content.clone(),
score: r.score,
metadata: r.metadata.clone(),
upstream_id: None,
})
.collect(),
cache_status: match persisted.cache_status.as_str() {
"hit" => CacheStatus::Hit,
"miss" => CacheStatus::Miss,
"stale" => CacheStatus::Stale,
"frozen" => CacheStatus::Frozen,
_ => CacheStatus::Miss,
},
took_ms: persisted.took_ms,
generated_at: persisted.generated_at,
miss_reason: None,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PersistedUpstreamHealth {
pub upstream_id: String,
pub status: String,
pub consecutive_failures: u32,
pub consecutive_successes: u32,
pub last_success_ms: Option<u64>,
pub last_failure_ms: Option<u64>,
}
pub struct PersistentCache {
db: Database,
}
impl PersistentCache {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, PersistenceError> {
let db =
Database::create(path).map_err(|e| PersistenceError::DatabaseOpen(e.to_string()))?;
let write_txn = db
.begin_write()
.map_err(|e| PersistenceError::DatabaseOpen(e.to_string()))?;
let _ = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::DatabaseOpen(e.to_string()))?;
let _ = write_txn
.open_table(HEALTH_TABLE)
.map_err(|e| PersistenceError::DatabaseOpen(e.to_string()))?;
let _ = write_txn
.open_table(META_TABLE)
.map_err(|e| PersistenceError::DatabaseOpen(e.to_string()))?;
write_txn
.commit()
.map_err(|e| PersistenceError::DatabaseOpen(e.to_string()))?;
Ok(Self { db })
}
pub fn store_entry(
&self,
query_hash: &[u8; 32],
entry: &PersistedEntry,
) -> Result<(), PersistenceError> {
let value =
serde_json::to_vec(entry).map_err(|e| PersistenceError::Serialize(e.to_string()))?;
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
table
.insert(query_hash.as_slice(), value.as_slice())
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
pub fn load_entry(
&self,
query_hash: &[u8; 32],
) -> Result<Option<PersistedEntry>, PersistenceError> {
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
match table
.get(query_hash.as_slice())
.map_err(|e| PersistenceError::Read(e.to_string()))?
{
Some(value) => {
let entry: PersistedEntry = serde_json::from_slice(value.value())
.map_err(|e| PersistenceError::Deserialize(e.to_string()))?;
Ok(Some(entry))
}
None => Ok(None),
}
}
pub fn remove_entry(&self, query_hash: &[u8; 32]) -> Result<(), PersistenceError> {
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
let _ = table
.remove(query_hash.as_slice())
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
fn context_key(context_id: &str, query_hash: &[u8; 32]) -> Vec<u8> {
#[allow(clippy::arithmetic_side_effects)]
let mut key = Vec::with_capacity(4 + context_id.len() + 1 + 32);
key.extend_from_slice(b"ctx:");
key.extend_from_slice(context_id.as_bytes());
key.push(b':');
key.extend_from_slice(query_hash);
key
}
pub fn store_entry_for_context(
&self,
context_id: &str,
query_hash: &[u8; 32],
entry: &PersistedEntry,
) -> Result<(), PersistenceError> {
let key = Self::context_key(context_id, query_hash);
let value =
serde_json::to_vec(entry).map_err(|e| PersistenceError::Serialize(e.to_string()))?;
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
table
.insert(key.as_slice(), value.as_slice())
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
pub fn load_entry_for_context(
&self,
context_id: &str,
query_hash: &[u8; 32],
) -> Result<Option<PersistedEntry>, PersistenceError> {
let key = Self::context_key(context_id, query_hash);
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
match table
.get(key.as_slice())
.map_err(|e| PersistenceError::Read(e.to_string()))?
{
Some(value) => {
let entry: PersistedEntry = serde_json::from_slice(value.value())
.map_err(|e| PersistenceError::Deserialize(e.to_string()))?;
Ok(Some(entry))
}
None => Ok(None),
}
}
pub fn remove_entry_for_context(
&self,
context_id: &str,
query_hash: &[u8; 32],
) -> Result<(), PersistenceError> {
let key = Self::context_key(context_id, query_hash);
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
let _ = table
.remove(key.as_slice())
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
pub fn entries_for_context(&self, context_id: &str) -> Result<Vec<[u8; 32]>, PersistenceError> {
let prefix = format!("ctx:{}:", context_id);
let prefix_bytes = prefix.as_bytes();
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let mut hashes = Vec::new();
let iter = table
.iter()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
for item in iter {
let (key, _) = item.map_err(|e| PersistenceError::Read(e.to_string()))?;
let key_bytes = key.value();
if key_bytes.starts_with(prefix_bytes) {
let hash_start = key_bytes.len().saturating_sub(32);
let hash_end = key_bytes.len();
if hash_start >= prefix_bytes.len() && hash_start.saturating_add(32) == hash_end {
let mut hash = [0u8; 32];
if let Some(slice) = key_bytes.get(hash_start..) {
hash.copy_from_slice(slice);
hashes.push(hash);
}
}
}
}
Ok(hashes)
}
pub fn entry_count_for_context(&self, context_id: &str) -> usize {
self.entries_for_context(context_id)
.map(|v| v.len())
.unwrap_or(0)
}
pub fn clear_context(&self, context_id: &str) -> Result<usize, PersistenceError> {
let prefix = format!("ctx:{}:", context_id);
let prefix_bytes = prefix.as_bytes();
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let mut keys_to_remove: Vec<Vec<u8>> = Vec::new();
let iter = table
.iter()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
for item in iter {
let (key, _) = item.map_err(|e| PersistenceError::Read(e.to_string()))?;
if key.value().starts_with(prefix_bytes) {
keys_to_remove.push(key.value().to_vec());
}
}
drop(table);
drop(read_txn);
let count = keys_to_remove.len();
if count > 0 {
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
for key in &keys_to_remove {
let _ = table
.remove(key.as_slice())
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
Ok(count)
}
pub fn all_entry_hashes(&self) -> Result<Vec<[u8; 32]>, PersistenceError> {
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let mut hashes = Vec::new();
let iter = table
.iter()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
for item in iter {
let (key, _) = item.map_err(|e| PersistenceError::Read(e.to_string()))?;
let key_bytes = key.value();
if key_bytes.len() == 32 {
let mut hash = [0u8; 32];
hash.copy_from_slice(key_bytes);
hashes.push(hash);
}
}
Ok(hashes)
}
pub fn entry_count(&self) -> usize {
let read_txn = match self.db.begin_read() {
Ok(txn) => txn,
Err(_) => return 0,
};
let table = match read_txn.open_table(ENTRIES_TABLE) {
Ok(t) => t,
Err(_) => return 0,
};
table.len().unwrap_or(0) as usize
}
pub fn store_health(
&self,
upstream_id: &str,
health: &PersistedUpstreamHealth,
) -> Result<(), PersistenceError> {
let value =
serde_json::to_vec(health).map_err(|e| PersistenceError::Serialize(e.to_string()))?;
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(HEALTH_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
table
.insert(upstream_id, value.as_slice())
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
pub fn load_health(
&self,
upstream_id: &str,
) -> Result<Option<PersistedUpstreamHealth>, PersistenceError> {
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(HEALTH_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
match table
.get(upstream_id)
.map_err(|e| PersistenceError::Read(e.to_string()))?
{
Some(value) => {
let health: PersistedUpstreamHealth = serde_json::from_slice(value.value())
.map_err(|e| PersistenceError::Deserialize(e.to_string()))?;
Ok(Some(health))
}
None => Ok(None),
}
}
pub fn all_health_states(&self) -> Result<Vec<PersistedUpstreamHealth>, PersistenceError> {
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(HEALTH_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let mut states = Vec::new();
let iter = table
.iter()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
for item in iter {
let (_, value) = item.map_err(|e| PersistenceError::Read(e.to_string()))?;
let health: PersistedUpstreamHealth = serde_json::from_slice(value.value())
.map_err(|e| PersistenceError::Deserialize(e.to_string()))?;
states.push(health);
}
Ok(states)
}
pub fn store_meta(&self, key: &str, value: &[u8]) -> Result<(), PersistenceError> {
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(META_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
table
.insert(key, value)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
pub fn load_meta(&self, key: &str) -> Result<Option<Vec<u8>>, PersistenceError> {
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(META_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
match table
.get(key)
.map_err(|e| PersistenceError::Read(e.to_string()))?
{
Some(value) => Ok(Some(value.value().to_vec())),
None => Ok(None),
}
}
pub fn record_flush(&self) -> Result<(), PersistenceError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
self.store_meta("last_flush_ms", &now.to_le_bytes())
}
pub fn last_flush_time(&self) -> Result<Option<u64>, PersistenceError> {
match self.load_meta("last_flush_ms")? {
Some(bytes) if bytes.len() == 8 => {
let mut arr = [0u8; 8];
arr.copy_from_slice(&bytes);
Ok(Some(u64::from_le_bytes(arr)))
}
_ => Ok(None),
}
}
pub fn flush(&self) -> Result<(), PersistenceError> {
self.record_flush()?;
Ok(())
}
pub fn clear_entries(&self) -> Result<usize, PersistenceError> {
let count = self.entry_count();
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
write_txn
.delete_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
let _ = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(count)
}
pub fn clear_all(&self) -> Result<(), PersistenceError> {
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
write_txn
.delete_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
write_txn
.delete_table(HEALTH_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
write_txn
.delete_table(META_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
let _ = write_txn
.open_table(ENTRIES_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
let _ = write_txn
.open_table(HEALTH_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
let _ = write_txn
.open_table(META_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
pub fn size_on_disk(&self) -> u64 {
0
}
pub fn store_context(&self, metadata: &ContextMetadata) -> Result<(), PersistenceError> {
let key = format!("context:{}", metadata.id);
let value =
serde_json::to_vec(metadata).map_err(|e| PersistenceError::Serialize(e.to_string()))?;
self.store_meta(&key, &value)
}
pub fn load_context(
&self,
context_id: &str,
) -> Result<Option<ContextMetadata>, PersistenceError> {
let key = format!("context:{}", context_id);
match self.load_meta(&key)? {
Some(bytes) => {
let metadata: ContextMetadata = serde_json::from_slice(&bytes)
.map_err(|e| PersistenceError::Deserialize(e.to_string()))?;
Ok(Some(metadata))
}
None => Ok(None),
}
}
pub fn all_contexts(&self) -> Result<Vec<ContextMetadata>, PersistenceError> {
let read_txn = self
.db
.begin_read()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let table = read_txn
.open_table(META_TABLE)
.map_err(|e| PersistenceError::Read(e.to_string()))?;
let mut contexts = Vec::new();
let iter = table
.iter()
.map_err(|e| PersistenceError::Read(e.to_string()))?;
for item in iter {
let (key, value) = item.map_err(|e| PersistenceError::Read(e.to_string()))?;
if key.value().starts_with("context:") {
let metadata: ContextMetadata = serde_json::from_slice(value.value())
.map_err(|e| PersistenceError::Deserialize(e.to_string()))?;
contexts.push(metadata);
}
}
Ok(contexts)
}
pub fn remove_context(&self, context_id: &str) -> Result<(), PersistenceError> {
let key = format!("context:{}", context_id);
let write_txn = self
.db
.begin_write()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
{
let mut table = write_txn
.open_table(META_TABLE)
.map_err(|e| PersistenceError::Write(e.to_string()))?;
let _ = table
.remove(key.as_str())
.map_err(|e| PersistenceError::Write(e.to_string()))?;
}
write_txn
.commit()
.map_err(|e| PersistenceError::Write(e.to_string()))?;
Ok(())
}
pub fn context_count(&self) -> usize {
self.all_contexts().map(|v| v.len()).unwrap_or(0)
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct PersistenceStats {
pub entry_count: usize,
pub health_record_count: usize,
pub size_on_disk: u64,
pub last_flush_ms: Option<u64>,
}
impl PersistentCache {
pub fn stats(&self) -> PersistenceStats {
let health_count = {
let read_txn = self.db.begin_read().ok();
read_txn
.and_then(|txn| {
txn.open_table(HEALTH_TABLE)
.ok()
.map(|t| t.len().unwrap_or(0) as usize)
})
.unwrap_or(0)
};
PersistenceStats {
entry_count: self.entry_count(),
health_record_count: health_count,
size_on_disk: self.size_on_disk(),
last_flush_ms: self.last_flush_time().ok().flatten(),
}
}
}
pub fn current_time_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
#[path = "tests/persistence_tests.rs"]
mod tests;