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;