use std::sync::{Arc, OnceLock};
use quick_cache::sync::Cache;
use serde_json::Value;
use crate::backend::DatabaseBackend;
use crate::error::Result;
use crate::instrumentation;
use crate::ownership::{
ownership_unified_fetch_enabled, OwnershipGateStatus, OwnershipService, RecordOwnershipBundle,
};
type CacheKey = (usize, String, String);
fn cache_key(backend: &Arc<dyn DatabaseBackend>, table: &str, id: &str) -> CacheKey {
let backend_id = Arc::as_ptr(backend).cast::<()>() as usize;
(backend_id, table.to_string(), id.to_string())
}
pub fn read_cache_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
!matches!(
std::env::var("VALENCE_READ_CACHE").as_deref(),
Ok("0") | Ok("false") | Ok("FALSE")
)
})
}
fn read_cache_capacity() -> usize {
static CAPACITY: OnceLock<usize> = OnceLock::new();
*CAPACITY.get_or_init(|| {
std::env::var("VALENCE_READ_CACHE_MAX")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(10_000)
.max(1)
})
}
fn global_cache() -> &'static Cache<CacheKey, RecordOwnershipBundle> {
static CACHE: OnceLock<Cache<CacheKey, RecordOwnershipBundle>> = OnceLock::new();
CACHE.get_or_init(|| Cache::new(read_cache_capacity()))
}
pub async fn get_record_via_cache(
backend: &Arc<dyn DatabaseBackend>,
table: &str,
id: &str,
) -> Result<Option<Value>> {
if backend.engine_id() == crate::KnownEngines::HYBRID_INDRA_SQL {
return backend.get_record(table, id).await;
}
if read_cache_enabled() {
let key = cache_key(backend, table, id);
if let Some(cached) = global_cache().get(&key) {
instrumentation::record_ownership_fetch_mode("cache_hit_row");
return Ok(cached.row);
}
let row = backend.get_record(table, id).await?;
if row.is_some() || ownership_unified_fetch_enabled() {
global_cache().insert(
key,
RecordOwnershipBundle {
row: row.clone(),
ownership_status: OwnershipGateStatus::NotFetched,
},
);
}
return Ok(row);
}
backend.get_record(table, id).await
}
pub async fn get_record_with_ownership_bundle_via_cache(
backend: &Arc<dyn DatabaseBackend>,
table: &str,
id: &str,
valence_model: &str,
v: &crate::runtime::Valence,
) -> Result<RecordOwnershipBundle> {
let hybrid = backend.engine_id() == crate::KnownEngines::HYBRID_INDRA_SQL;
if read_cache_enabled() && !hybrid {
let key = cache_key(backend, table, id);
if let Some(cached) = global_cache().get(&key) {
if cached.ownership_status != OwnershipGateStatus::NotFetched {
instrumentation::record_ownership_fetch_mode("cache_hit");
return Ok(cached);
}
}
}
let bundle = OwnershipService::fetch_record_with_ownership_gate_uncached(
backend,
table,
id,
valence_model,
v,
)
.await?;
instrumentation::record_ownership_fetch_mode(if hybrid { "unified_hybrid" } else { "unified" });
if read_cache_enabled() && !hybrid {
global_cache().insert(cache_key(backend, table, id), bundle.clone());
}
Ok(bundle)
}
pub fn invalidate(table: &str, id: &str) {
let _ = (table, id);
if read_cache_enabled() {
global_cache().clear();
}
}
pub fn invalidate_for_backend(backend: &Arc<dyn DatabaseBackend>, table: &str, id: &str) {
if read_cache_enabled() {
global_cache().remove(&cache_key(backend, table, id));
}
}
#[doc(hidden)]
pub fn clear_for_test() {
global_cache().clear();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_cache_enabled_by_default() {
assert!(read_cache_enabled());
}
#[test]
fn read_cache_capacity_defaults_to_10k() {
assert_eq!(read_cache_capacity(), 10_000);
}
}