use std::collections::HashMap;
use std::time::Duration;
use async_trait::async_trait;
use crate::error::BackendError;
use crate::metrics::MetricsProvider;
#[derive(Debug, Clone)]
pub struct HealthStatus {
pub is_healthy: bool,
pub latency_ms: f64,
pub backend_type: String,
pub details: HashMap<String, String>,
}
#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
#[async_trait]
pub trait Backend: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError>;
async fn set(
&self,
key: &str,
value: Vec<u8>,
ttl: Option<Duration>,
) -> Result<(), BackendError>;
async fn delete(&self, key: &str) -> Result<bool, BackendError>;
async fn exists(&self, key: &str) -> Result<bool, BackendError>;
async fn health(&self) -> Result<HealthStatus, BackendError>;
fn as_lockable(&self) -> Option<&dyn LockableBackend> {
None
}
fn attach_metrics(&self, _provider: MetricsProvider) {}
}
#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
#[async_trait(?Send)]
pub trait Backend {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError>;
async fn set(
&self,
key: &str,
value: Vec<u8>,
ttl: Option<Duration>,
) -> Result<(), BackendError>;
async fn delete(&self, key: &str) -> Result<bool, BackendError>;
async fn exists(&self, key: &str) -> Result<bool, BackendError>;
async fn health(&self) -> Result<HealthStatus, BackendError>;
fn as_lockable(&self) -> Option<&dyn LockableBackend> {
None
}
fn attach_metrics(&self, _provider: MetricsProvider) {}
}
#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
#[async_trait]
pub trait TtlInspectable: Backend {
async fn ttl(&self, key: &str) -> Result<Option<Duration>, BackendError>;
async fn refresh_ttl(&self, _key: &str, _ttl: Duration) -> Result<bool, BackendError> {
Err(BackendError::permanent(
"refresh_ttl not supported by this backend",
))
}
}
#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
#[async_trait(?Send)]
pub trait TtlInspectable: Backend {
async fn ttl(&self, key: &str) -> Result<Option<Duration>, BackendError>;
async fn refresh_ttl(&self, _key: &str, _ttl: Duration) -> Result<bool, BackendError> {
Err(BackendError::permanent(
"refresh_ttl not supported by this backend",
))
}
}
#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
#[async_trait]
pub trait LockableBackend: Backend {
async fn acquire_lock(
&self,
key: &str,
timeout_ms: u64,
) -> Result<Option<String>, BackendError>;
async fn release_lock(&self, key: &str, lock_id: &str) -> Result<bool, BackendError>;
}
#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
#[async_trait(?Send)]
pub trait LockableBackend: Backend {
async fn acquire_lock(
&self,
key: &str,
timeout_ms: u64,
) -> Result<Option<String>, BackendError>;
async fn release_lock(&self, key: &str, lock_id: &str) -> Result<bool, BackendError>;
}
#[cfg(all(any(feature = "file", feature = "memcached"), not(feature = "unsync")))]
pub(crate) async fn run_blocking<T: Send + 'static>(
f: impl FnOnce() -> Result<T, crate::error::BackendError> + Send + 'static,
) -> Result<T, crate::error::BackendError> {
tokio::task::spawn_blocking(f).await.map_err(|e| {
crate::error::BackendError::permanent(format!("backend blocking task failed: {e}"))
})?
}
#[cfg(all(any(feature = "file", feature = "memcached"), feature = "unsync"))]
pub(crate) async fn run_blocking<T>(
f: impl FnOnce() -> Result<T, crate::error::BackendError>,
) -> Result<T, crate::error::BackendError> {
f()
}
#[cfg(any(feature = "cachekitio", feature = "workers", test))]
pub(crate) fn encode_key(key: &str) -> Result<std::borrow::Cow<'_, str>, BackendError> {
let encoded = urlencoding::encode(key);
if matches!(encoded.as_ref(), "." | ".." | "health" | "ttl" | "lock") {
return Err(BackendError::permanent(
"cache key must not be a reserved path segment (`.`, `..`, `health`, \
`ttl`, `lock`): the client URL parser or the SaaS router would route \
it off the `/v1/cache/{key}` path (CWE-22), so it cannot be \
addressed on the wire",
));
}
Ok(encoded)
}
#[cfg(any(feature = "workers", test))]
mod saas_wire;
#[cfg(feature = "cachekitio")]
pub mod cachekitio;
#[cfg(feature = "cachekitio")]
mod cachekitio_lock;
#[cfg(feature = "cachekitio")]
mod cachekitio_ttl;
#[cfg(feature = "redis")]
pub mod redis;
#[cfg(feature = "memcached")]
pub mod memcached;
#[cfg(feature = "file")]
pub mod file;
#[cfg(feature = "workers")]
pub mod workers;
#[cfg(test)]
#[allow(clippy::expect_used)] mod encode_key_tests {
use super::encode_key;
const RESERVED_SEGMENTS: &[&str] = &[".", "..", "health", "ttl", "lock"];
const SAFE_VECTORS: &[&str] = &[
"a:..",
"..a",
"a..",
".hidden",
"default:../../admin",
"x/../../health",
"healthy",
"HEALTH",
"ttls",
"unlock",
"ns:default:func:m.f:args:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:",
"a b",
"k?x=1#f",
"100%",
"f(x)!*'",
"",
];
#[test]
fn reserved_segments_are_rejected() {
for k in RESERVED_SEGMENTS {
assert!(
encode_key(k).is_err(),
"reserved segment {k:?} must be rejected"
);
}
}
#[test]
fn safe_keys_are_byte_identical_to_urlencoding() {
for k in SAFE_VECTORS {
let enc = encode_key(k).expect("safe key must encode");
assert_eq!(
enc,
urlencoding::encode(k),
"encode_key diverged from urlencoding for {k:?}"
);
}
}
#[test]
fn safe_keys_decode_once_back_to_the_original() {
for k in SAFE_VECTORS {
let enc = encode_key(k).expect("safe key must encode");
let decoded = urlencoding::decode(&enc).expect("single decode succeeds");
assert_eq!(decoded, *k, "decode-once round-trip changed {k:?}");
}
}
}