Skip to main content

sova_store/
lib.rs

1//! Byte-oriented key-value store for Sova plugins (sessions, cache, CSRF, rate-limit).
2//!
3//! Trait is stable (memory + file + sql + redis backends).
4//! Enable feature `unstable-store` for backwards-compatible feature flags.
5//! **Not in sova-core** — wire with `app.state(store.namespace("sess"))`.
6
7mod cache;
8#[cfg(feature = "store-crypto")]
9mod encrypted;
10#[cfg(feature = "file")]
11mod file;
12#[cfg(feature = "redis")]
13mod redis;
14#[cfg(feature = "sql")]
15mod sql;
16
17use bytes::Bytes;
18use std::collections::HashMap;
19use std::collections::hash_map::DefaultHasher;
20use std::future::Future;
21use std::hash::{Hash, Hasher};
22use std::pin::Pin;
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25use tokio::sync::Mutex;
26
27pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
28
29pub use cache::{Cache, CacheError};
30
31pub trait KvStore: Send + Sync + 'static {
32    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<Bytes>>;
33    fn set<'a>(&'a self, key: &'a str, val: Bytes, ttl: Option<Duration>) -> BoxFuture<'a, ()>;
34    fn remove<'a>(&'a self, key: &'a str) -> BoxFuture<'a, ()>;
35    /// Atomic increment; creates the key at `by` when missing. Returns new value.
36    fn incr<'a>(&'a self, key: &'a str, by: i64, ttl: Option<Duration>) -> BoxFuture<'a, u64>;
37    /// Remove keys starting with `prefix`. Returns how many were removed.
38    fn clear_prefix<'a>(&'a self, prefix: &'a str) -> BoxFuture<'a, u64>;
39}
40
41pub fn namespace(store: Arc<dyn KvStore>, name: &str) -> Namespace {
42    Namespace {
43        store,
44        prefix: format!("{name}:"),
45    }
46}
47
48/// App-wide store handle — `app.state(AppStore::memory())` or via facade `SharedStore` plugin.
49/// Session / meta / rate-limit can reuse namespaced views of the same backend.
50#[derive(Clone)]
51pub struct AppStore {
52    pub inner: Arc<dyn KvStore>,
53}
54
55impl AppStore {
56    pub fn new(store: Arc<dyn KvStore>) -> Self {
57        Self { inner: store }
58    }
59
60    pub fn memory() -> Self {
61        Self::new(Arc::new(MemoryStore::new()))
62    }
63
64    pub fn namespaced(&self, name: &str) -> Arc<dyn KvStore> {
65        Arc::new(namespace(Arc::clone(&self.inner), name))
66    }
67}
68
69#[cfg(feature = "store-crypto")]
70pub use encrypted::{encrypted, encrypted_ns, AppKey, Encrypted};
71
72#[cfg(feature = "file")]
73pub use file::{Durability, FileStore};
74
75#[cfg(feature = "redis")]
76pub use redis::RedisStore;
77
78#[cfg(feature = "sql")]
79pub use sql::SqlStore;
80
81/// Scoped handle — prefixes all keys; [`clear_prefix`](KvStore::clear_prefix) stays inside.
82#[derive(Clone)]
83pub struct Namespace {
84    store: Arc<dyn KvStore>,
85    prefix: String,
86}
87
88impl Namespace {
89    fn full(&self, key: &str) -> String {
90        format!("{}{key}", self.prefix)
91    }
92}
93
94impl KvStore for Namespace {
95    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<Bytes>> {
96        let k = self.full(key);
97        Box::pin(async move { self.store.get(&k).await })
98    }
99
100    fn set<'a>(&'a self, key: &'a str, val: Bytes, ttl: Option<Duration>) -> BoxFuture<'a, ()> {
101        let k = self.full(key);
102        Box::pin(async move { self.store.set(&k, val, ttl).await })
103    }
104
105    fn remove<'a>(&'a self, key: &'a str) -> BoxFuture<'a, ()> {
106        let k = self.full(key);
107        Box::pin(async move { self.store.remove(&k).await })
108    }
109
110    fn incr<'a>(&'a self, key: &'a str, by: i64, ttl: Option<Duration>) -> BoxFuture<'a, u64> {
111        let k = self.full(key);
112        Box::pin(async move { self.store.incr(&k, by, ttl).await })
113    }
114
115    fn clear_prefix<'a>(&'a self, prefix: &'a str) -> BoxFuture<'a, u64> {
116        let p = self.full(prefix);
117        Box::pin(async move { self.store.clear_prefix(&p).await })
118    }
119}
120
121struct Entry {
122    val: Bytes,
123    exp: Option<Instant>,
124}
125
126type ShardMap = Arc<Mutex<HashMap<String, Entry>>>;
127
128/// In-process KvStore (sharded Mutex maps + TTL).
129#[derive(Clone)]
130pub struct MemoryStore {
131    shards: Arc<[ShardMap]>,
132}
133
134impl Default for MemoryStore {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140impl MemoryStore {
141    /// Default shard count: `max(1, available_parallelism * 2)`.
142    pub fn new() -> Self {
143        let n = std::thread::available_parallelism()
144            .map(|p| p.get() * 2)
145            .unwrap_or(2)
146            .max(1);
147        Self::with_shards(n)
148    }
149
150    /// Explicit shard count (minimum 1).
151    pub fn with_shards(n: usize) -> Self {
152        let n = n.max(1);
153        let shards: Vec<_> = (0..n)
154            .map(|_| Arc::new(Mutex::new(HashMap::new())))
155            .collect();
156        Self {
157            shards: shards.into(),
158        }
159    }
160
161    fn shard(&self, key: &str) -> &ShardMap {
162        &self.shards[shard_index(key, self.shards.len())]
163    }
164
165    fn alive(e: &Entry, now: Instant) -> bool {
166        e.exp.map(|t| t > now).unwrap_or(true)
167    }
168}
169
170fn shard_index(key: &str, n: usize) -> usize {
171    let mut h = DefaultHasher::new();
172    key.hash(&mut h);
173    (h.finish() as usize) % n
174}
175
176impl KvStore for MemoryStore {
177    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<Bytes>> {
178        let shard = Arc::clone(self.shard(key));
179        let key = key.to_string();
180        Box::pin(async move {
181            let mut map = shard.lock().await;
182            let now = Instant::now();
183            match map.get(&key) {
184                Some(e) if Self::alive(e, now) => Some(e.val.clone()),
185                Some(_) => {
186                    map.remove(&key);
187                    None
188                }
189                None => None,
190            }
191        })
192    }
193
194    fn set<'a>(&'a self, key: &'a str, val: Bytes, ttl: Option<Duration>) -> BoxFuture<'a, ()> {
195        let shard = Arc::clone(self.shard(key));
196        let key = key.to_string();
197        Box::pin(async move {
198            let mut map = shard.lock().await;
199            map.insert(
200                key,
201                Entry {
202                    val,
203                    exp: ttl.map(|d| Instant::now() + d),
204                },
205            );
206        })
207    }
208
209    fn remove<'a>(&'a self, key: &'a str) -> BoxFuture<'a, ()> {
210        let shard = Arc::clone(self.shard(key));
211        let key = key.to_string();
212        Box::pin(async move {
213            shard.lock().await.remove(&key);
214        })
215    }
216
217    fn incr<'a>(&'a self, key: &'a str, by: i64, ttl: Option<Duration>) -> BoxFuture<'a, u64> {
218        let shard = Arc::clone(self.shard(key));
219        let key = key.to_string();
220        Box::pin(async move {
221            let mut map = shard.lock().await;
222            let now = Instant::now();
223            let cur = match map.get(&key) {
224                Some(e) if Self::alive(e, now) => {
225                    let s = std::str::from_utf8(&e.val).unwrap_or("0");
226                    s.parse::<i64>().unwrap_or(0)
227                }
228                _ => 0,
229            };
230            let next = (cur + by).max(0) as u64;
231            map.insert(
232                key,
233                Entry {
234                    val: Bytes::from(next.to_string()),
235                    exp: ttl.map(|d| now + d),
236                },
237            );
238            next
239        })
240    }
241
242    fn clear_prefix<'a>(&'a self, prefix: &'a str) -> BoxFuture<'a, u64> {
243        let prefix = prefix.to_string();
244        let shards: Vec<_> = self.shards.iter().cloned().collect();
245        Box::pin(async move {
246            let mut total = 0u64;
247            for shard in shards {
248                let mut map = shard.lock().await;
249                let keys: Vec<_> = map
250                    .keys()
251                    .filter(|k| k.starts_with(&prefix))
252                    .cloned()
253                    .collect();
254                total += keys.len() as u64;
255                for k in keys {
256                    map.remove(&k);
257                }
258            }
259            total
260        })
261    }
262}
263
264/// Shared conformance suite for any [`KvStore`].
265pub mod conformance {
266    use super::*;
267    use std::sync::Arc;
268
269    pub async fn run(store: Arc<dyn KvStore>) {
270        get_set_ttl(store.clone()).await;
271        namespace_isolation(store.clone()).await;
272        incr_atomic(store.clone()).await;
273        clear_prefix_scoped(store).await;
274    }
275
276    async fn get_set_ttl(store: Arc<dyn KvStore>) {
277        store
278            .set("a", Bytes::from_static(b"1"), Some(Duration::from_millis(50)))
279            .await;
280        assert_eq!(store.get("a").await.as_deref(), Some(b"1".as_slice()));
281        tokio::time::sleep(Duration::from_millis(80)).await;
282        assert!(store.get("a").await.is_none());
283    }
284
285    async fn namespace_isolation(store: Arc<dyn KvStore>) {
286        let a = namespace(store.clone(), "a");
287        let b = namespace(store.clone(), "b");
288        a.set("k", Bytes::from_static(b"A"), None).await;
289        b.set("k", Bytes::from_static(b"B"), None).await;
290        assert_eq!(a.get("k").await.as_deref(), Some(b"A".as_slice()));
291        assert_eq!(b.get("k").await.as_deref(), Some(b"B".as_slice()));
292        a.clear_prefix("").await;
293        assert!(a.get("k").await.is_none());
294        assert_eq!(b.get("k").await.as_deref(), Some(b"B".as_slice()));
295    }
296
297    async fn incr_atomic(store: Arc<dyn KvStore>) {
298        store.remove("c").await;
299        let mut handles = Vec::new();
300        for _ in 0..50 {
301            let s = store.clone();
302            handles.push(tokio::spawn(async move {
303                s.incr("c", 1, None).await;
304            }));
305        }
306        for h in handles {
307            h.await.unwrap();
308        }
309        assert_eq!(store.get("c").await.unwrap().as_ref(), b"50");
310    }
311
312    async fn clear_prefix_scoped(store: Arc<dyn KvStore>) {
313        store.set("p:1", Bytes::from_static(b"x"), None).await;
314        store.set("p:2", Bytes::from_static(b"y"), None).await;
315        store.set("q:1", Bytes::from_static(b"z"), None).await;
316        assert_eq!(store.clear_prefix("p:").await, 2);
317        assert!(store.get("p:1").await.is_none());
318        assert_eq!(store.get("q:1").await.as_deref(), Some(b"z".as_slice()));
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
327    async fn memory_conformance() {
328        conformance::run(Arc::new(MemoryStore::new())).await;
329    }
330
331    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
332    async fn sharded_clear_prefix_across_shards() {
333        let store = MemoryStore::with_shards(4);
334        for i in 0..32 {
335            store
336                .set(
337                    &format!("shard:{i}"),
338                    Bytes::from_static(b"v"),
339                    None,
340                )
341                .await;
342        }
343        store
344            .set("other:1", Bytes::from_static(b"z"), None)
345            .await;
346        assert_eq!(store.clear_prefix("shard:").await, 32);
347        assert!(store.get("shard:0").await.is_none());
348        assert_eq!(
349            store.get("other:1").await.as_deref(),
350            Some(b"z".as_slice())
351        );
352    }
353
354    #[tokio::test]
355    async fn cache_remember_memory() {
356        let store = AppStore::memory();
357        let cache = store.cache();
358        let v = cache
359            .remember("k", None, || async { Ok::<_, CacheError>(42u32) })
360            .await
361            .unwrap();
362        assert_eq!(v, 42);
363        let hit = cache.get_json::<u32>("k").await;
364        assert_eq!(hit, Some(42));
365    }
366}