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
54/// Async cache backend abstraction (`?Send` variant).
55///
56/// Active when compiling for `wasm32` or with the `unsync` feature.
57/// Identical API to the `Send + Sync` variant but without thread-safety bounds.
58#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
59#[async_trait(?Send)]
60pub trait Backend {
61 /// Retrieve the raw bytes stored under `key`, or `None` if absent.
62 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, BackendError>;
63
64 /// Store `value` under `key`, optionally expiring after `ttl`.
65 async fn set(
66 &self,
67 key: &str,
68 value: Vec<u8>,
69 ttl: Option<Duration>,
70 ) -> Result<(), BackendError>;
71
72 /// Remove `key` and return `true` if it existed.
73 async fn delete(&self, key: &str) -> Result<bool, BackendError>;
74
75 /// Return `true` if `key` exists without fetching the value.
76 async fn exists(&self, key: &str) -> Result<bool, BackendError>;
77
78 /// Return health/status information for this backend.
79 async fn health(&self) -> Result<HealthStatus, BackendError>;
80}
81
82// ── TtlInspectable ───────────────────────────────────────────────────────────
83
84/// Optional extension for backends that can report the remaining TTL of a key.
85#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
86#[async_trait]
87pub trait TtlInspectable: Backend {
88 /// Return the remaining TTL for `key`, or `None` if the key does not exist
89 /// or has no expiry.
90 async fn ttl(&self, key: &str) -> Result<Option<Duration>, BackendError>;
91
92 /// Refresh the TTL on an existing key. Default: not supported.
93 async fn refresh_ttl(&self, _key: &str, _ttl: Duration) -> Result<bool, BackendError> {
94 Err(BackendError::permanent(
95 "refresh_ttl not supported by this backend",
96 ))
97 }
98}
99
100/// Optional extension for backends that can report the remaining TTL of a key (`?Send` variant).
101#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
102#[async_trait(?Send)]
103pub trait TtlInspectable: Backend {
104 /// Return the remaining TTL for `key`, or `None` if the key does not exist
105 /// or has no expiry.
106 async fn ttl(&self, key: &str) -> Result<Option<Duration>, BackendError>;
107
108 /// Refresh the TTL on an existing key. Default: not supported.
109 async fn refresh_ttl(&self, _key: &str, _ttl: Duration) -> Result<bool, BackendError> {
110 Err(BackendError::permanent(
111 "refresh_ttl not supported by this backend",
112 ))
113 }
114}
115
116// ── LockableBackend ─────────────────────────────────────────────────────────
117
118/// Optional extension for backends that support distributed locking.
119#[cfg(not(any(target_arch = "wasm32", feature = "unsync")))]
120#[async_trait]
121pub trait LockableBackend: Backend {
122 /// Acquire a distributed lock. Returns lock_id if acquired, None if contested.
123 async fn acquire_lock(
124 &self,
125 key: &str,
126 timeout_ms: u64,
127 ) -> Result<Option<String>, BackendError>;
128 /// Release a distributed lock. Returns true if released.
129 async fn release_lock(&self, key: &str, lock_id: &str) -> Result<bool, BackendError>;
130}
131
132/// Optional extension for backends that support distributed locking (`?Send` variant).
133#[cfg(any(target_arch = "wasm32", feature = "unsync"))]
134#[async_trait(?Send)]
135pub trait LockableBackend: Backend {
136 /// Acquire a distributed lock. Returns lock_id if acquired, None if contested.
137 async fn acquire_lock(
138 &self,
139 key: &str,
140 timeout_ms: u64,
141 ) -> Result<Option<String>, BackendError>;
142 /// Release a distributed lock. Returns true if released.
143 async fn release_lock(&self, key: &str, lock_id: &str) -> Result<bool, BackendError>;
144}
145
146// ── Blocking-pool bridge (file + memcached backends) ─────────────────────────
147
148/// Run sync I/O on tokio's blocking pool so the async executor never stalls.
149#[cfg(all(any(feature = "file", feature = "memcached"), not(feature = "unsync")))]
150pub(crate) async fn run_blocking<T: Send + 'static>(
151 f: impl FnOnce() -> Result<T, crate::error::BackendError> + Send + 'static,
152) -> Result<T, crate::error::BackendError> {
153 tokio::task::spawn_blocking(f).await.map_err(|e| {
154 crate::error::BackendError::permanent(format!("backend blocking task failed: {e}"))
155 })?
156}
157
158/// `unsync` opts into single-threaded runtimes and drops `Send` from
159/// `BackendError`, so results cannot cross `spawn_blocking`. Run the I/O
160/// inline instead — the same sync-in-async trade-off cachekit-py documents.
161#[cfg(all(any(feature = "file", feature = "memcached"), feature = "unsync"))]
162pub(crate) async fn run_blocking<T>(
163 f: impl FnOnce() -> Result<T, crate::error::BackendError>,
164) -> Result<T, crate::error::BackendError> {
165 f()
166}
167
168// ── Feature-gated backend modules ─────────────────────────────────────────────
169
170/// JSON wire bodies for the SaaS lock/TTL endpoints. Compiled under `test`
171/// unconditionally so the wire-contract round-trip tests always run in CI,
172/// even though only the `workers` backend consumes the structs at runtime.
173#[cfg(any(feature = "workers", test))]
174mod saas_wire;
175
176/// HTTP backend for the cachekit.io SaaS API.
177#[cfg(feature = "cachekitio")]
178pub mod cachekitio;
179#[cfg(feature = "cachekitio")]
180mod cachekitio_lock;
181#[cfg(feature = "cachekitio")]
182mod cachekitio_ttl;
183
184/// Redis backend via the [`fred`](https://crates.io/crates/fred) client.
185#[cfg(feature = "redis")]
186pub mod redis;
187
188/// Memcached backend via the [`rust-memcache`](https://crates.io/crates/memcache) client.
189#[cfg(feature = "memcached")]
190pub mod memcached;
191
192/// Local filesystem backend, byte-compatible with cachekit-py's File backend.
193#[cfg(feature = "file")]
194pub mod file;
195
196/// Cloudflare Workers backend using `worker::Fetch`.
197#[cfg(feature = "workers")]
198pub mod workers;