mod common;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use crate::common::{FailingBackend, MockBackend};
use cachekit::backend::{Backend, HealthStatus};
use cachekit::client::SharedBackend;
use cachekit::error::BackendError;
use cachekit::metrics::MetricsProvider;
use cachekit::{CacheKit, L1Stats};
#[derive(Default, Clone)]
struct RecordingBackend {
store: MockBackend,
attached: Arc<Mutex<Option<MetricsProvider>>>,
}
impl RecordingBackend {
fn new_with_handle() -> (SharedBackend, Self) {
let backend = Self::default();
let handle = backend.clone();
#[cfg(not(feature = "unsync"))]
let shared: SharedBackend = Arc::new(backend);
#[cfg(feature = "unsync")]
let shared: SharedBackend = std::rc::Rc::new(backend);
(shared, handle)
}
fn attached(&self) -> MetricsProvider {
self.attached
.lock()
.expect("attached lock")
.clone()
.expect("the client attached a provider at build time")
}
}
#[cfg_attr(not(feature = "unsync"), async_trait)]
#[cfg_attr(feature = "unsync", async_trait(?Send))]
impl Backend for RecordingBackend {
fn attach_metrics(&self, provider: MetricsProvider) {
*self.attached.lock().expect("attached lock") = Some(provider);
}
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError> {
self.store.get(key).await
}
async fn set(
&self,
key: &str,
value: Vec<u8>,
ttl: Option<Duration>,
) -> Result<(), BackendError> {
self.store.set(key, value, ttl).await
}
async fn delete(&self, key: &str) -> Result<bool, BackendError> {
self.store.delete(key).await
}
async fn exists(&self, key: &str) -> Result<bool, BackendError> {
self.store.exists(key).await
}
async fn health(&self) -> Result<HealthStatus, BackendError> {
Ok(HealthStatus {
is_healthy: true,
latency_ms: 0.0,
backend_type: "recording".to_owned(),
details: HashMap::new(),
})
}
}
fn reads(stats: L1Stats) -> u64 {
stats.l1_hits + stats.l2_hits + stats.misses
}
#[tokio::test]
async fn stats_count_every_read_by_the_layer_that_served_it() {
let (shared, _store) = MockBackend::new_with_handle();
let writer = CacheKit::builder()
.backend(shared.clone())
.no_l1()
.build()
.expect("writer builds");
let reader = CacheKit::builder()
.backend(shared)
.default_ttl(Duration::from_secs(60))
.build()
.expect("reader builds");
assert_eq!(
reader.stats(),
L1Stats {
l1_enabled: cfg!(feature = "l1"),
..L1Stats::default()
},
"fresh client: zero everywhere"
);
let miss: Option<u32> = reader.get("k").await.expect("get");
assert_eq!(miss, None);
writer.set("k", &7u32).await.expect("set");
let hit: Option<u32> = reader.get("k").await.expect("get");
assert_eq!(hit, Some(7));
let hit: Option<u32> = reader.get("k").await.expect("get");
assert_eq!(hit, Some(7));
let stats = reader.stats();
assert_eq!(stats.misses, 1);
assert_eq!(reads(stats), 3);
if cfg!(feature = "l1") {
assert_eq!((stats.l2_hits, stats.l1_hits), (1, 1));
assert_eq!(reader.l1_entry_count(), Some(1));
} else {
assert_eq!((stats.l2_hits, stats.l1_hits), (2, 0));
assert_eq!(reader.l1_entry_count(), None);
}
assert_eq!(reads(writer.stats()), 0, "the writer never read");
assert!(!writer.stats().l1_enabled);
assert_eq!(writer.l1_entry_count(), None);
}
#[tokio::test]
async fn clones_share_one_set_of_counters() {
let client = CacheKit::builder()
.backend(MockBackend::shared())
.build()
.expect("client builds");
let clone = client.clone();
let _: Option<u32> = clone.get("k").await.expect("get");
assert_eq!(client.stats().misses, 1);
assert_eq!(client.stats(), clone.stats());
}
#[tokio::test]
async fn exists_is_not_a_read_but_a_served_read_that_fails_to_deserialize_is() {
let client = CacheKit::builder()
.backend(MockBackend::shared())
.build()
.expect("client builds");
client.set("k", &1u32).await.expect("set");
assert!(client.exists("k").await.expect("exists"));
assert_eq!(reads(client.stats()), 0, "exists() is not a value read");
client
.get::<String>("k")
.await
.expect_err("u32 payload is not a String");
assert_eq!(reads(client.stats()), 1, "{:?}", client.stats());
}
#[tokio::test]
async fn reads_that_fail_at_the_backend_are_not_counted() {
let client = CacheKit::builder()
.backend(FailingBackend::shared())
.build()
.expect("client builds");
client.get::<u32>("k").await.expect_err("backend is down");
assert_eq!(reads(client.stats()), 0, "{:?}", client.stats());
}
#[tokio::test]
async fn builder_attaches_a_live_provider_over_the_client_counters() {
let (shared, handle) = RecordingBackend::new_with_handle();
let client = CacheKit::builder()
.backend(shared)
.build()
.expect("client builds");
let provider = handle.attached();
assert_eq!(provider(), Some(client.stats()), "zero reads yet");
let _: Option<u32> = client.get("k").await.expect("get");
client.set("k", &1u32).await.expect("set");
let _: Option<u32> = client.get("k").await.expect("get");
let seen = provider().expect("provider reports a snapshot");
assert_eq!(
seen,
client.stats(),
"the provider is a live view, not a copy"
);
assert_eq!(seen.misses, 1);
assert_eq!(reads(seen), 2);
assert_eq!(seen.l1_enabled, cfg!(feature = "l1"));
let clone = client.clone();
drop(client);
assert!(
provider().is_some(),
"a live clone keeps the counters alive"
);
drop(clone);
assert_eq!(provider(), None);
}
#[tokio::test]
async fn provider_reports_l1_disabled_for_a_no_l1_client() {
let (shared, handle) = RecordingBackend::new_with_handle();
let _client = CacheKit::builder()
.backend(shared)
.no_l1()
.build()
.expect("client builds");
let snapshot = handle.attached()().expect("provider reports a snapshot");
assert!(!snapshot.l1_enabled);
}
#[cfg(all(feature = "reliability", not(target_arch = "wasm32")))]
#[tokio::test]
async fn provider_reaches_the_raw_backend_under_the_reliability_stack() {
let (shared, handle) = RecordingBackend::new_with_handle();
let client = CacheKit::builder()
.backend(shared)
.reliability(cachekit::ReliabilityConfig::default())
.build()
.expect("client builds");
let _: Option<u32> = client.get("k").await.expect("get");
let seen = handle.attached()().expect("provider reports a snapshot");
assert_eq!(seen.misses, 1);
assert_eq!(seen, client.stats());
}