rustlavel_cache/redis/
mod.rs1pub mod config;
17pub mod connection;
18pub mod pool;
19pub mod resp;
20
21pub use config::RedisConfig;
22pub use connection::Connection;
23pub use pool::{Pool, PooledConnection};
24pub use resp::Value;
25
26use crate::store::{BoxFuture, Cache, decode, prefixed, record};
27use rustlavel_core::{Error, Json, Result};
28use std::time::Duration;
29
30#[derive(Clone)]
34pub struct RedisStore {
35 pool: Pool,
36 prefix: String,
37}
38
39impl RedisStore {
40 pub fn connect(url: &str) -> Result<Self> {
42 Ok(RedisStore::new(RedisConfig::from_url(url)?, ""))
43 }
44
45 pub fn new(config: RedisConfig, prefix: impl Into<String>) -> Self {
46 RedisStore { pool: Pool::new(config), prefix: prefix.into() }
47 }
48
49 pub fn pool(&self) -> &Pool {
50 &self.pool
51 }
52
53 pub async fn verify(&self) -> Result<()> {
55 self.pool.verify().await
56 }
57
58 pub async fn ping(&self) -> Result<String> {
60 let reply = self.pool.command(&[b"PING"]).await?.into_result()?;
61 reply
62 .as_str()
63 .map(str::to_string)
64 .ok_or_else(|| Error::msg("Redis answered PING with something other than a status"))
65 }
66
67 pub async fn expire(&self, key: &str, seconds: u64) -> Result<bool> {
70 let full = prefixed(&self.prefix, key);
71 let seconds = seconds.to_string();
72 let reply = self
73 .pool
74 .command(&[b"EXPIRE", full.as_bytes(), seconds.as_bytes()])
75 .await?
76 .into_result()?;
77 Ok(reply.as_i64() == Some(1))
78 }
79
80 pub async fn pexpire(&self, key: &str, ttl: Duration) -> Result<bool> {
83 let full = prefixed(&self.prefix, key);
84 let millis = (ttl.as_millis() as u64).max(1).to_string();
85 let reply = self
86 .pool
87 .command(&[b"PEXPIRE", full.as_bytes(), millis.as_bytes()])
88 .await?
89 .into_result()?;
90 Ok(reply.as_i64() == Some(1))
91 }
92
93 pub async fn command(&self, args: &[&[u8]]) -> Result<Value> {
96 self.pool.command(args).await?.into_result()
97 }
98
99 async fn integer(&self, args: &[&[u8]]) -> Result<i64> {
100 let reply = self.pool.command(args).await?.into_result()?;
101 reply.as_i64().ok_or_else(|| {
102 Error::msg(format!("expected an integer reply from Redis, got {reply:?}"))
103 })
104 }
105}
106
107impl Cache for RedisStore {
108 fn driver(&self) -> &'static str {
109 "redis"
110 }
111
112 fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Json>>> {
113 Box::pin(async move {
114 let full = prefixed(&self.prefix, key);
115 let reply = self.pool.command(&[b"GET", full.as_bytes()]).await?.into_result()?;
116
117 let found = reply.as_str().and_then(decode);
119 record(found.is_some(), "redis", key);
120 Ok(found)
121 })
122 }
123
124 fn put<'a>(&'a self, key: &'a str, value: Json, ttl: Duration) -> BoxFuture<'a, Result<()>> {
125 Box::pin(async move {
126 let full = prefixed(&self.prefix, key);
127 if ttl.is_zero() {
128 self.pool.command(&[b"DEL", full.as_bytes()]).await?.into_result()?;
129 return Ok(());
130 }
131
132 let millis = (ttl.as_millis() as u64).max(1).to_string();
135 let payload = value.to_string();
136 self.pool
137 .command(&[b"SET", full.as_bytes(), payload.as_bytes(), b"PX", millis.as_bytes()])
138 .await?
139 .into_result()?;
140 Ok(())
141 })
142 }
143
144 fn forever<'a>(&'a self, key: &'a str, value: Json) -> BoxFuture<'a, Result<()>> {
145 Box::pin(async move {
146 let full = prefixed(&self.prefix, key);
147 let payload = value.to_string();
148 self.pool
149 .command(&[b"SET", full.as_bytes(), payload.as_bytes()])
150 .await?
151 .into_result()?;
152 Ok(())
153 })
154 }
155
156 fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
157 Box::pin(async move {
158 let full = prefixed(&self.prefix, key);
159 Ok(self.integer(&[b"DEL", full.as_bytes()]).await? > 0)
160 })
161 }
162
163 fn flush(&self) -> BoxFuture<'_, Result<()>> {
164 Box::pin(async move {
165 self.pool.command(&[b"FLUSHDB"]).await?.into_result()?;
170 Ok(())
171 })
172 }
173
174 fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
175 Box::pin(async move {
176 let full = prefixed(&self.prefix, key);
177 Ok(self.integer(&[b"EXISTS", full.as_bytes()]).await? > 0)
178 })
179 }
180
181 fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
182 Box::pin(async move {
183 let full = prefixed(&self.prefix, key);
184 let by = by.to_string();
185 self.integer(&[b"INCRBY", full.as_bytes(), by.as_bytes()]).await
186 })
187 }
188
189 fn decrement<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
190 Box::pin(async move {
191 let full = prefixed(&self.prefix, key);
192 let by = by.to_string();
193 self.integer(&[b"DECRBY", full.as_bytes(), by.as_bytes()]).await
194 })
195 }
196
197 fn increment_within<'a>(
198 &'a self,
199 key: &'a str,
200 by: i64,
201 ttl: Duration,
202 ) -> BoxFuture<'a, Result<i64>> {
203 Box::pin(async move {
204 let full = prefixed(&self.prefix, key);
205 let millis = (ttl.as_millis() as u64).max(1).to_string();
206
207 self.pool
213 .command(&[b"SET", full.as_bytes(), b"0", b"PX", millis.as_bytes(), b"NX"])
214 .await?
215 .into_result()?;
216
217 let by = by.to_string();
218 self.integer(&[b"INCRBY", full.as_bytes(), by.as_bytes()]).await
219 })
220 }
221
222 fn ttl<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Duration>>> {
223 Box::pin(async move {
224 let full = prefixed(&self.prefix, key);
225 let millis = self.integer(&[b"PTTL", full.as_bytes()]).await?;
228 Ok((millis >= 0).then(|| Duration::from_millis(millis as u64)))
229 })
230 }
231}