Skip to main content

sova_redis/
fake.rs

1//! In-memory Redis for tests and demos (`Redis::fake()`).
2
3use crate::error::RedisError;
4use crate::messaging::RedisMessage;
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant};
8use tokio::sync::broadcast;
9
10const MAX_DB: u8 = 15;
11
12#[derive(Clone)]
13struct Entry {
14    value: String,
15    expires: Option<Instant>,
16}
17
18#[derive(Default)]
19struct Db {
20    keys: HashMap<String, Entry>,
21}
22
23struct Inner {
24    dbs: [Db; 16],
25    channels: HashMap<String, broadcast::Sender<Vec<u8>>>,
26}
27
28impl Default for Inner {
29    fn default() -> Self {
30        Self {
31            dbs: std::array::from_fn(|_| Db::default()),
32            channels: HashMap::new(),
33        }
34    }
35}
36
37/// Shared in-memory Redis (16 DBs, strings, pub/sub, SCAN).
38#[derive(Clone, Default)]
39pub struct FakeRedis {
40    inner: Arc<Mutex<Inner>>,
41}
42
43impl FakeRedis {
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    fn purge_expired(db: &mut Db) {
49        let now = Instant::now();
50        db.keys.retain(|_, e| e.expires.is_none_or(|t| t > now));
51    }
52
53    fn db(inner: &mut Inner, db: u8) -> &mut Db {
54        &mut inner.dbs[db.min(MAX_DB) as usize]
55    }
56
57    pub fn select(&self, db: u8) -> Result<(), RedisError> {
58        let _ = db.min(MAX_DB);
59        Ok(())
60    }
61
62    pub fn get(&self, db: u8, key: &str) -> Result<Option<String>, RedisError> {
63        let mut g = self.inner.lock().unwrap();
64        let db = Self::db(&mut g, db);
65        Self::purge_expired(db);
66        Ok(db.keys.get(key).map(|e| e.value.clone()))
67    }
68
69    pub fn set(&self, db: u8, key: &str, value: &str) -> Result<(), RedisError> {
70        let mut g = self.inner.lock().unwrap();
71        let db = Self::db(&mut g, db);
72        db.keys.insert(
73            key.to_string(),
74            Entry {
75                value: value.to_string(),
76                expires: None,
77            },
78        );
79        Ok(())
80    }
81
82    pub fn setex(&self, db: u8, key: &str, ttl_secs: u64, value: &str) -> Result<(), RedisError> {
83        let mut g = self.inner.lock().unwrap();
84        let db = Self::db(&mut g, db);
85        db.keys.insert(
86            key.to_string(),
87            Entry {
88                value: value.to_string(),
89                expires: Some(Instant::now() + Duration::from_secs(ttl_secs)),
90            },
91        );
92        Ok(())
93    }
94
95    pub fn del(&self, db: u8, key: &str) -> Result<i64, RedisError> {
96        let mut g = self.inner.lock().unwrap();
97        let db = Self::db(&mut g, db);
98        Ok(if db.keys.remove(key).is_some() { 1 } else { 0 })
99    }
100
101    pub fn ttl(&self, db: u8, key: &str) -> Result<i64, RedisError> {
102        let mut g = self.inner.lock().unwrap();
103        let db = Self::db(&mut g, db);
104        Self::purge_expired(db);
105        match db.keys.get(key) {
106            None => Ok(-2),
107            Some(e) => match e.expires {
108                None => Ok(-1),
109                Some(t) => {
110                    let left = t.saturating_duration_since(Instant::now());
111                    Ok(left.as_secs() as i64)
112                }
113            },
114        }
115    }
116
117    pub fn key_type(&self, db: u8, key: &str) -> Result<String, RedisError> {
118        let mut g = self.inner.lock().unwrap();
119        let db = Self::db(&mut g, db);
120        Self::purge_expired(db);
121        Ok(if db.keys.contains_key(key) {
122            "string".into()
123        } else {
124            "none".into()
125        })
126    }
127
128    pub fn scan(
129        &self,
130        db: u8,
131        cursor: u64,
132        pattern: &str,
133        count: usize,
134    ) -> Result<(u64, Vec<String>), RedisError> {
135        let mut g = self.inner.lock().unwrap();
136        let db = Self::db(&mut g, db);
137        Self::purge_expired(db);
138        let mut keys: Vec<String> = db
139            .keys
140            .keys()
141            .filter(|k| glob_match(pattern, k))
142            .cloned()
143            .collect();
144        keys.sort();
145        let start = cursor as usize;
146        let end = (start + count).min(keys.len());
147        let slice = keys[start..end].to_vec();
148        let next = if end >= keys.len() { 0 } else { end as u64 };
149        Ok((next, slice))
150    }
151
152    pub fn publish(&self, channel: &str, message: &[u8]) -> Result<i64, RedisError> {
153        let mut g = self.inner.lock().unwrap();
154        let tx = g
155            .channels
156            .entry(channel.to_string())
157            .or_insert_with(|| broadcast::channel(64).0);
158        Ok(tx.send(message.to_vec()).unwrap_or(0) as i64)
159    }
160
161    pub fn subscribe(&self, channel: &str) -> FakeSubscriber {
162        let mut g = self.inner.lock().unwrap();
163        let tx = g
164            .channels
165            .entry(channel.to_string())
166            .or_insert_with(|| broadcast::channel(64).0);
167        FakeSubscriber {
168            rx: tx.subscribe(),
169            channel: channel.to_string(),
170        }
171    }
172}
173
174pub struct FakeSubscriber {
175    rx: broadcast::Receiver<Vec<u8>>,
176    channel: String,
177}
178
179impl FakeSubscriber {
180    pub async fn next(&mut self) -> Option<RedisMessage> {
181        loop {
182            match self.rx.recv().await {
183                Ok(payload) => {
184                    return Some(RedisMessage {
185                        channel: self.channel.clone(),
186                        payload,
187                        pattern: None,
188                    });
189                }
190                Err(broadcast::error::RecvError::Lagged(_)) => continue,
191                Err(broadcast::error::RecvError::Closed) => return None,
192            }
193        }
194    }
195}
196
197fn glob_match(pattern: &str, key: &str) -> bool {
198    if pattern == "*" {
199        return true;
200    }
201    if let Some(prefix) = pattern.strip_suffix('*') {
202        return key.starts_with(prefix);
203    }
204    pattern == key
205}