Skip to main content

sova_redis/
plugin.rs

1use crate::fake::FakeRedis;
2use crate::pool::RedisPool;
3use sova_core::{App, Error, Plugin};
4
5/// Redis / Valkey pool plugin (`REDIS_URL`).
6pub struct Redis {
7    mode: Mode,
8}
9
10enum Mode {
11    Url(String),
12    Fake(FakeRedis),
13}
14
15impl Redis {
16    pub fn from_env() -> Self {
17        let url = std::env::var("REDIS_URL").unwrap_or_default();
18        Self {
19            mode: Mode::Url(url),
20        }
21    }
22
23    pub fn url(mut self, url: impl Into<String>) -> Self {
24        self.mode = Mode::Url(url.into());
25        self
26    }
27
28    /// In-memory Redis for tests and demos (no TCP).
29    pub fn fake(fake: FakeRedis) -> Self {
30        Self {
31            mode: Mode::Fake(fake),
32        }
33    }
34}
35
36impl Plugin for Redis {
37    fn id(&self) -> &'static str {
38        "redis"
39    }
40
41    fn meta(&self) -> sova_core::PluginMeta {
42        sova_core::PluginMeta::new("Redis")
43            .description(
44                "Shared Redis/Valkey connection for KvStore, tasks, cache, pub/sub, list queues",
45            )
46            .version(env!("CARGO_PKG_VERSION"))
47    }
48
49    fn install(mut self, app: &mut App) {
50        if let Mode::Fake(fake) = self.mode {
51            let pool = RedisPool::new();
52            pool.set_fake(fake);
53            app.state(pool);
54            return;
55        }
56
57        // Env wins, then builder `.url()`, then `[redis] url` in toml.
58        if let Mode::Url(url) = &mut self.mode {
59            if let Ok(u) = std::env::var("REDIS_URL") {
60                if !u.is_empty() {
61                    *url = u;
62                }
63            }
64            if url.is_empty() {
65                if let Some(u) = app
66                    .config_doc()
67                    .and_then(|d| d.section("redis"))
68                    .and_then(|s| s.get("url").and_then(|v| v.as_str()).map(str::to_string))
69                {
70                    *url = u;
71                }
72            }
73        }
74
75        let Mode::Url(url) = self.mode else {
76            return;
77        };
78
79        if url.is_empty() {
80            app.on_startup(|_state| async {
81                Err(Error::Internal(
82                    "redis url is empty; set REDIS_URL or [redis] url in sova.toml".into(),
83                ))
84            });
85            return;
86        }
87
88        let pool = RedisPool::new();
89        app.state(pool.clone());
90
91        let url_start = url.clone();
92        let pool_start = pool.clone();
93        app.on_startup(move |_state| {
94            let url = url_start.clone();
95            let pool = pool_start.clone();
96            async move {
97                pool.set_url(url.clone());
98                let conn = RedisPool::connect(&url)
99                    .await
100                    .map_err(|e| Error::Internal(format!("redis connect: {e}")))?;
101                pool.set(conn);
102                Ok(())
103            }
104        });
105
106        let pool_stop = pool.clone();
107        app.on_shutdown(move || {
108            let pool = pool_stop.clone();
109            async move {
110                pool.clear();
111            }
112        });
113
114        let pool_check = pool.clone();
115        app.register_check("redis", move |_state| {
116            let pool = pool_check.clone();
117            async move {
118                let mut conn = pool.get().map_err(Error::from)?;
119                let _: String = redis::cmd("PING")
120                    .query_async(&mut conn)
121                    .await
122                    .map_err(|e| Error::Internal(format!("redis ping: {e}")))?;
123                Ok(())
124            }
125        });
126    }
127}