use std::any::TypeId;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use crate::kit::{AsyncAutoBuilder, AsyncKit, ModuleMeta};
use super::capability::CacheStore;
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error("cache config error: {0}")]
Config(String),
#[error("cache build failed: {0}")]
BuildFailed(String),
}
pub const DEFAULT_ENTRY_MAX_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub capacity: u64,
pub entry_max_bytes: usize,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
capacity: 10_000,
entry_max_bytes: DEFAULT_ENTRY_MAX_BYTES,
}
}
}
impl CacheConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_capacity(mut self, capacity: u64) -> Self {
self.capacity = capacity;
self
}
#[must_use]
pub fn with_entry_max_bytes(mut self, bytes: usize) -> Self {
self.entry_max_bytes = bytes;
self
}
}
pub struct CacheModule;
impl ModuleMeta for CacheModule {
const NAME: &'static str = "cache";
fn dependencies() -> &'static [(&'static str, TypeId)] {
&[]
}
}
impl AsyncAutoBuilder for CacheModule {
type Capability = Arc<dyn CacheStore>;
type Error = CacheError;
fn build<'a>(
kit: &'a AsyncKit,
) -> Pin<Box<dyn Future<Output = Result<Self::Capability, Self::Error>> + Send + 'a>> {
Box::pin(async move {
let config = kit
.config::<CacheConfig>()
.map_err(|e| CacheError::Config(e.to_string()))?;
Self::build_cap(&config).await
})
}
}
impl CacheModule {
pub(crate) async fn build_cap(config: &CacheConfig) -> Result<Arc<dyn CacheStore>, CacheError> {
let cache = oxcache::Cache::builder()
.capacity(config.capacity)
.sync_mode(true)
.build()
.await
.map_err(|e| CacheError::BuildFailed(e.to_string()))?;
Ok(Arc::new(OxcacheStore::new(cache, config.entry_max_bytes)))
}
}
struct OxcacheStore {
inner: oxcache::Cache<String, Vec<u8>>,
entry_max_bytes: usize,
}
impl OxcacheStore {
fn new(cache: oxcache::Cache<String, Vec<u8>>, entry_max_bytes: usize) -> Self {
Self {
inner: cache,
entry_max_bytes,
}
}
}
impl CacheStore for OxcacheStore {
fn get(&self, key: &str) -> Option<Vec<u8>> {
match self.inner.get_bytes_sync(key) {
Ok(val) => val,
Err(e) => {
tracing::warn!(key = %key, error = %e, "cache get failed");
None
}
}
}
fn set(&self, key: &str, val: Vec<u8>) {
if val.len() > self.entry_max_bytes {
tracing::warn!(
key = %key,
entry_size = val.len(),
max = self.entry_max_bytes,
"cache set rejected: entry exceeds entry_max_bytes"
);
return;
}
if let Err(e) = self.inner.set_bytes_sync(key, val, None) {
tracing::warn!(key = %key, error = %e, "cache set failed");
}
}
fn invalidate_all(&self) {
if let Err(e) = self.inner.clear_sync() {
tracing::warn!(error = %e, "cache invalidate_all failed");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kit::{AsyncKit, CacheModule};
async fn build_cache() -> Arc<dyn CacheStore> {
CacheModule::build_cap(&CacheConfig::default())
.await
.expect("CacheModule::build_cap")
}
#[test]
fn cache_config_default_capacity() {
let config = CacheConfig::default();
assert_eq!(config.capacity, 10_000);
}
#[test]
fn cache_config_with_capacity_overrides_default() {
let config = CacheConfig::new().with_capacity(5_000);
assert_eq!(config.capacity, 5_000);
}
#[test]
fn cache_config_default_entry_max_bytes_is_64kib() {
let config = CacheConfig::default();
assert_eq!(config.entry_max_bytes, 64 * 1024);
assert_eq!(config.entry_max_bytes, DEFAULT_ENTRY_MAX_BYTES);
}
#[test]
fn cache_config_with_entry_max_bytes_overrides_default() {
let config = CacheConfig::new().with_entry_max_bytes(128 * 1024);
assert_eq!(config.entry_max_bytes, 128 * 1024);
}
#[test]
fn cache_config_builder_chains() {
let config = CacheConfig::new()
.with_capacity(2_000)
.with_entry_max_bytes(32 * 1024);
assert_eq!(config.capacity, 2_000);
assert_eq!(config.entry_max_bytes, 32 * 1024);
}
#[tokio::test(flavor = "multi_thread")]
async fn build_returns_send_sync_capability() {
let cap = build_cache().await;
fn _assert_send_sync<T: Send + Sync>(_: &T) {}
_assert_send_sync(&cap);
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_get_miss_returns_none() {
let cap = build_cache().await;
assert!(cap.get("nonexistent").is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_then_get_returns_value() {
let cap = build_cache().await;
cap.set("key1", b"value1".to_vec());
assert_eq!(cap.get("key1"), Some(b"value1".to_vec()));
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_overwrites_existing() {
let cap = build_cache().await;
cap.set("k", b"v1".to_vec());
cap.set("k", b"v2".to_vec());
assert_eq!(cap.get("k"), Some(b"v2".to_vec()));
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_invalidate_all_clears_entries() {
let cap = build_cache().await;
cap.set("k1", b"v1".to_vec());
cap.set("k2", b"v2".to_vec());
cap.invalidate_all();
assert!(cap.get("k1").is_none());
assert!(cap.get("k2").is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_empty_value() {
let cap = build_cache().await;
let empty: Vec<u8> = vec![];
cap.set("empty", empty.clone());
assert_eq!(cap.get("empty"), Some(empty));
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_large_value() {
let cap = build_cache_with_entry_max(200 * 1024).await;
let large = vec![0xAB; 1024 * 100]; cap.set("large", large.clone());
assert_eq!(cap.get("large"), Some(large));
}
async fn build_cache_with_entry_max(bytes: usize) -> Arc<dyn CacheStore> {
let config = CacheConfig::default().with_entry_max_bytes(bytes);
CacheModule::build_cap(&config)
.await
.expect("CacheModule::build_cap with custom entry_max_bytes")
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_at_entry_max_bytes_accepted() {
let cap = build_cache_with_entry_max(1024).await;
let val = vec![0xCD; 1024];
cap.set("at_max", val.clone());
assert_eq!(cap.get("at_max"), Some(val));
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_above_entry_max_bytes_rejected() {
let cap = build_cache_with_entry_max(1024).await;
let oversized = vec![0xEF; 1025];
cap.set("oversized", oversized.clone());
assert!(
cap.get("oversized").is_none(),
"oversized entry must be rejected"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_oversized_does_not_evict_existing() {
let cap = build_cache_with_entry_max(1024).await;
cap.set("k", b"small".to_vec());
cap.set("k", vec![0xAA; 2048]); assert_eq!(cap.get("k"), Some(b"small".to_vec()));
}
#[tokio::test(flavor = "multi_thread")]
async fn capability_set_default_64kib_rejects_100kib_entry() {
let cap = build_cache().await;
let blob = vec![0xAA; 100 * 1024];
cap.set("ast_blob", blob);
assert!(
cap.get("ast_blob").is_none(),
"100 KiB entry must be rejected by default 64 KiB cap"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn kit_registration_flow() {
let mut kit = AsyncKit::new();
kit.set_config(CacheConfig::default());
kit.register::<CacheModule>()
.expect("register::<CacheModule>");
let kit = kit.build().await.expect("build");
assert!(kit.contains::<CacheModule>(), "CacheModule missing");
let cache = kit
.require::<CacheModule>()
.expect("require::<CacheModule>");
cache.set("k", b"v".to_vec());
assert_eq!(cache.get("k"), Some(b"v".to_vec()));
}
}