Skip to main content

cachekit/backend/
mod.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use async_trait::async_trait;
5
6use crate::error::BackendError;
7
8// ── HealthStatus ─────────────────────────────────────────────────────────────
9
10/// Describes the health of a backend at a point in time.
11#[derive(Debug, Clone)]
12pub struct HealthStatus {
13    /// Whether the backend is considered healthy.
14    pub is_healthy: bool,
15    /// Round-trip latency of the health check in milliseconds.
16    pub latency_ms: f64,
17    /// Human-readable name for this backend implementation.
18    pub backend_type: String,
19    /// Optional key-value details (pool size, version, etc.).
20    pub details: HashMap<String, String>,
21}
22
23// ── Backend trait ─────────────────────────────────────────────────────────────
24
25/// Async cache backend abstraction.
26///
27/// Implementors must be `Send + Sync` on native targets (unless the `unsync`
28/// feature is enabled). On `wasm32` targets or with `unsync`, `Send` is relaxed
29/// (`?Send`) because the runtime is single-threaded.
30#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
31#[async_trait]
32pub trait Backend: Send + Sync {
33    /// Retrieve the raw bytes stored under `key`, or `None` if absent.
34    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError>;
35
36    /// Store `value` under `key`, optionally expiring after `ttl`.
37    async fn set(
38        &self,
39        key: &str,
40        value: Vec<u8>,
41        ttl: Option<Duration>,
42    ) -> Result<(), BackendError>;
43
44    /// Remove `key` and return `true` if it existed.
45    async fn delete(&self, key: &str) -> Result<bool, BackendError>;
46
47    /// Return `true` if `key` exists without fetching the value.
48    async fn exists(&self, key: &str) -> Result<bool, BackendError>;
49
50    /// Return health/status information for this backend.
51    async fn health(&self) -> Result<HealthStatus, BackendError>;
52
53    /// Expose this backend's [`LockableBackend`] capability, if it has one.
54    ///
55    /// Trait objects (`dyn Backend`) cannot be cross-cast to a sibling trait,
56    /// so backends that support distributed locking opt in by overriding this
57    /// to return `Some(self)`. Used by the client's cold-miss single-flight
58    /// for cross-process fill suppression. Default: `None`.
59    fn as_lockable(&self) -> Option<&dyn LockableBackend> {
60        None
61    }
62}
63
64/// Async cache backend abstraction (`?Send` variant).
65///
66/// Active when compiling for `wasm32` or with the `unsync` feature.
67/// Identical API to the `Send + Sync` variant but without thread-safety bounds.
68#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
69#[async_trait(?Send)]
70pub trait Backend {
71    /// Retrieve the raw bytes stored under `key`, or `None` if absent.
72    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError>;
73
74    /// Store `value` under `key`, optionally expiring after `ttl`.
75    async fn set(
76        &self,
77        key: &str,
78        value: Vec<u8>,
79        ttl: Option<Duration>,
80    ) -> Result<(), BackendError>;
81
82    /// Remove `key` and return `true` if it existed.
83    async fn delete(&self, key: &str) -> Result<bool, BackendError>;
84
85    /// Return `true` if `key` exists without fetching the value.
86    async fn exists(&self, key: &str) -> Result<bool, BackendError>;
87
88    /// Return health/status information for this backend.
89    async fn health(&self) -> Result<HealthStatus, BackendError>;
90
91    /// Expose this backend's [`LockableBackend`] capability, if it has one.
92    ///
93    /// Trait objects (`dyn Backend`) cannot be cross-cast to a sibling trait,
94    /// so backends that support distributed locking opt in by overriding this
95    /// to return `Some(self)`. Used by the client's cold-miss single-flight
96    /// for cross-process fill suppression. Default: `None`.
97    fn as_lockable(&self) -> Option<&dyn LockableBackend> {
98        None
99    }
100}
101
102// ── TtlInspectable ───────────────────────────────────────────────────────────
103
104/// Optional extension for backends that can report the remaining TTL of a key.
105#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
106#[async_trait]
107pub trait TtlInspectable: Backend {
108    /// Return the remaining TTL for `key`, or `None` if the key does not exist
109    /// or has no expiry.
110    async fn ttl(&self, key: &str) -> Result<Option<Duration>, BackendError>;
111
112    /// Refresh the TTL on an existing key. Default: not supported.
113    async fn refresh_ttl(&self, _key: &str, _ttl: Duration) -> Result<bool, BackendError> {
114        Err(BackendError::permanent(
115            "refresh_ttl not supported by this backend",
116        ))
117    }
118}
119
120/// Optional extension for backends that can report the remaining TTL of a key (`?Send` variant).
121#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
122#[async_trait(?Send)]
123pub trait TtlInspectable: Backend {
124    /// Return the remaining TTL for `key`, or `None` if the key does not exist
125    /// or has no expiry.
126    async fn ttl(&self, key: &str) -> Result<Option<Duration>, BackendError>;
127
128    /// Refresh the TTL on an existing key. Default: not supported.
129    async fn refresh_ttl(&self, _key: &str, _ttl: Duration) -> Result<bool, BackendError> {
130        Err(BackendError::permanent(
131            "refresh_ttl not supported by this backend",
132        ))
133    }
134}
135
136// ── LockableBackend ─────────────────────────────────────────────────────────
137
138/// Optional extension for backends that support distributed locking.
139#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
140#[async_trait]
141pub trait LockableBackend: Backend {
142    /// Acquire a distributed lock. Returns lock_id if acquired, None if contested.
143    async fn acquire_lock(
144        &self,
145        key: &str,
146        timeout_ms: u64,
147    ) -> Result<Option<String>, BackendError>;
148    /// Release a distributed lock. Returns true if released.
149    async fn release_lock(&self, key: &str, lock_id: &str) -> Result<bool, BackendError>;
150}
151
152/// Optional extension for backends that support distributed locking (`?Send` variant).
153#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
154#[async_trait(?Send)]
155pub trait LockableBackend: Backend {
156    /// Acquire a distributed lock. Returns lock_id if acquired, None if contested.
157    async fn acquire_lock(
158        &self,
159        key: &str,
160        timeout_ms: u64,
161    ) -> Result<Option<String>, BackendError>;
162    /// Release a distributed lock. Returns true if released.
163    async fn release_lock(&self, key: &str, lock_id: &str) -> Result<bool, BackendError>;
164}
165
166// ── Blocking-pool bridge (file + memcached backends) ─────────────────────────
167
168/// Run sync I/O on tokio's blocking pool so the async executor never stalls.
169#[cfg(all(any(feature = "file", feature = "memcached"), not(feature = "unsync")))]
170pub(crate) async fn run_blocking<T: Send + 'static>(
171    f: impl FnOnce() -> Result<T, crate::error::BackendError> + Send + 'static,
172) -> Result<T, crate::error::BackendError> {
173    tokio::task::spawn_blocking(f).await.map_err(|e| {
174        crate::error::BackendError::permanent(format!("backend blocking task failed: {e}"))
175    })?
176}
177
178/// `unsync` opts into single-threaded runtimes and drops `Send` from
179/// `BackendError`, so results cannot cross `spawn_blocking`. Run the I/O
180/// inline instead — the same sync-in-async trade-off cachekit-py documents.
181#[cfg(all(any(feature = "file", feature = "memcached"), feature = "unsync"))]
182pub(crate) async fn run_blocking<T>(
183    f: impl FnOnce() -> Result<T, crate::error::BackendError>,
184) -> Result<T, crate::error::BackendError> {
185    f()
186}
187
188// ── Feature-gated backend modules ─────────────────────────────────────────────
189
190/// JSON wire bodies for the SaaS lock/TTL endpoints. Compiled under `test`
191/// unconditionally so the wire-contract round-trip tests always run in CI,
192/// even though only the `workers` backend consumes the structs at runtime.
193#[cfg(any(feature = "workers", test))]
194mod saas_wire;
195
196/// HTTP backend for the cachekit.io SaaS API.
197#[cfg(feature = "cachekitio")]
198pub mod cachekitio;
199#[cfg(feature = "cachekitio")]
200mod cachekitio_lock;
201#[cfg(feature = "cachekitio")]
202mod cachekitio_ttl;
203
204/// Redis backend via the [`fred`](https://crates.io/crates/fred) client.
205#[cfg(feature = "redis")]
206pub mod redis;
207
208/// Memcached backend via the [`rust-memcache`](https://crates.io/crates/memcache) client.
209#[cfg(feature = "memcached")]
210pub mod memcached;
211
212/// Local filesystem backend, byte-compatible with cachekit-py's File backend.
213#[cfg(feature = "file")]
214pub mod file;
215
216/// Cloudflare Workers backend using `worker::Fetch`.
217#[cfg(feature = "workers")]
218pub mod workers;