rustlavel_cache/redis/
connection.rs1use super::config::RedisConfig;
9use super::resp::{self, Value};
10use rustlavel_core::{Error, Result};
11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12use tokio::net::TcpStream;
13
14const READ_CHUNK: usize = 8 * 1024;
16
17pub struct Connection {
18 stream: TcpStream,
19 buffer: Vec<u8>,
23 consumed: usize,
26 config: RedisConfig,
27 broken: bool,
31}
32
33impl Connection {
34 pub async fn connect(config: &RedisConfig) -> Result<Connection> {
36 let address = config.address();
37
38 let stream = tokio::time::timeout(config.connect_timeout, TcpStream::connect(&address))
39 .await
40 .map_err(|_| {
41 Error::msg(format!("timed out connecting to {address}. Is Redis running and reachable?"))
42 })?
43 .map_err(|e| Error::msg(format!("cannot connect to {}: {e}", config.redacted_url())))?;
44
45 let _ = stream.set_nodelay(true);
48
49 let mut connection = Connection {
50 stream,
51 buffer: Vec::with_capacity(READ_CHUNK),
52 consumed: 0,
53 config: config.clone(),
54 broken: false,
55 };
56
57 connection.handshake().await?;
58 Ok(connection)
59 }
60
61 pub fn is_broken(&self) -> bool {
62 self.broken
63 }
64
65 async fn handshake(&mut self) -> Result<()> {
66 let username = self.config.username.clone();
69 let password = self.config.password.clone();
70
71 if !password.is_empty() {
72 let reply = if username.is_empty() {
75 self.command(&[b"AUTH", password.as_bytes()]).await?
76 } else {
77 self.command(&[b"AUTH", username.as_bytes(), password.as_bytes()]).await?
78 };
79
80 if let Value::Error(message) = reply {
81 return Err(Error::msg(format!(
83 "Redis rejected authentication for {}: {message}",
84 self.config.redacted_url()
85 )));
86 }
87 }
88
89 if self.config.database != 0 {
90 let db = self.config.database.to_string();
91 self.command(&[b"SELECT", db.as_bytes()]).await?.into_result().map_err(|e| {
92 Error::msg(format!("cannot SELECT database {}: {e}", self.config.database))
93 })?;
94 }
95
96 Ok(())
97 }
98
99 pub async fn command(&mut self, args: &[&[u8]]) -> Result<Value> {
105 let request = resp::encode_command(args);
106
107 let write = self.stream.write_all(&request);
108 match tokio::time::timeout(self.config.command_timeout, write).await {
109 Ok(Ok(())) => {}
110 Ok(Err(e)) => {
111 self.broken = true;
112 return Err(Error::msg(format!("cannot write to Redis: {e}")));
113 }
114 Err(_) => {
115 self.broken = true;
116 return Err(Error::msg("timed out sending a command to Redis"));
117 }
118 }
119
120 self.read_reply().await
121 }
122
123 async fn read_reply(&mut self) -> Result<Value> {
124 loop {
125 match resp::decode(&self.buffer[self.consumed..]) {
128 Ok(Some((value, used))) => {
129 self.consumed += used;
130 if self.consumed == self.buffer.len() {
131 self.buffer.clear();
132 self.consumed = 0;
133 }
134 return Ok(value);
135 }
136 Ok(None) => {}
137 Err(e) => {
138 self.broken = true;
140 return Err(e);
141 }
142 }
143
144 if self.consumed > 0 {
147 self.buffer.drain(..self.consumed);
148 self.consumed = 0;
149 }
150
151 let start = self.buffer.len();
152 self.buffer.resize(start + READ_CHUNK, 0);
153
154 let read = self.stream.read(&mut self.buffer[start..]);
155 let count = match tokio::time::timeout(self.config.command_timeout, read).await {
156 Ok(Ok(count)) => count,
157 Ok(Err(e)) => {
158 self.buffer.truncate(start);
159 self.broken = true;
160 return Err(Error::msg(format!("cannot read from Redis: {e}")));
161 }
162 Err(_) => {
163 self.buffer.truncate(start);
164 self.broken = true;
165 return Err(Error::msg("timed out waiting for a reply from Redis"));
166 }
167 };
168
169 self.buffer.truncate(start + count);
170
171 if count == 0 {
172 self.broken = true;
173 return Err(Error::msg(format!(
174 "Redis at {} closed the connection mid-reply",
175 self.config.redacted_url()
176 )));
177 }
178 }
179 }
180
181 pub async fn close(mut self) {
184 let _ = self.stream.write_all(&resp::encode_command(&[b"QUIT"])).await;
185 let _ = self.stream.shutdown().await;
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use std::time::Duration;
193 use tokio::net::TcpListener;
194
195 async fn fake_server(script: Vec<Vec<u8>>) -> (RedisConfig, tokio::task::JoinHandle<Vec<u8>>) {
199 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
200 let port = listener.local_addr().unwrap().port();
201
202 let handle = tokio::spawn(async move {
203 let (mut socket, _) = listener.accept().await.expect("a client");
204 let mut received = Vec::new();
205
206 for reply in script {
207 let mut chunk = vec![0u8; 1024];
208 match socket.read(&mut chunk).await {
209 Ok(0) | Err(_) => break,
210 Ok(count) => received.extend_from_slice(&chunk[..count]),
211 }
212 if socket.write_all(&reply).await.is_err() {
213 break;
214 }
215 }
216 received
217 });
218
219 let config = RedisConfig {
220 port,
221 connect_timeout: Duration::from_secs(2),
222 command_timeout: Duration::from_secs(2),
223 ..RedisConfig::default()
224 };
225 (config, handle)
226 }
227
228 #[tokio::test]
229 async fn a_command_is_written_as_resp_and_its_reply_decoded() {
230 let (config, server) = fake_server(vec![b"+PONG\r\n".to_vec()]).await;
231 let mut connection = Connection::connect(&config).await.unwrap();
232
233 assert_eq!(connection.command(&[b"PING"]).await.unwrap(), Value::Simple("PONG".into()));
234
235 drop(connection);
236 assert_eq!(server.await.unwrap(), b"*1\r\n$4\r\nPING\r\n".to_vec());
237 }
238
239 #[tokio::test]
240 async fn the_handshake_sends_auth_and_select_before_anything_else() {
241 let (mut config, server) =
242 fake_server(vec![b"+OK\r\n".to_vec(), b"+OK\r\n".to_vec(), b":1\r\n".to_vec()]).await;
243 config.password = "hunter2".into();
244 config.database = 4;
245
246 let mut connection = Connection::connect(&config).await.unwrap();
247 connection.command(&[b"EXISTS", b"k"]).await.unwrap();
248 drop(connection);
249
250 let sent = server.await.unwrap();
251 let expected = [
252 resp::encode_command(&[b"AUTH", b"hunter2"]),
253 resp::encode_command(&[b"SELECT", b"4"]),
254 resp::encode_command(&[b"EXISTS", b"k"]),
255 ]
256 .concat();
257 assert_eq!(sent, expected);
258 }
259
260 #[tokio::test]
261 async fn a_rejected_password_is_reported_without_echoing_it() {
262 let (mut config, _server) = fake_server(vec![b"-WRONGPASS invalid password\r\n".to_vec()]).await;
263 config.password = "hunter2".into();
264
265 let error = match Connection::connect(&config).await {
266 Ok(_) => panic!("connecting should have failed"),
267 Err(e) => e.to_string(),
268 };
269
270 assert!(error.contains("rejected authentication"), "got: {error}");
271 assert!(!error.contains("hunter2"), "the error must not leak the password");
272 }
273
274 #[tokio::test]
275 async fn a_reply_split_across_packets_is_reassembled() {
276 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
277 let port = listener.local_addr().unwrap().port();
278
279 tokio::spawn(async move {
280 let (mut socket, _) = listener.accept().await.unwrap();
281 let mut chunk = vec![0u8; 1024];
282 let _ = socket.read(&mut chunk).await;
283
284 for piece in [&b"$11\r\n"[..], &b"hello "[..], &b"world"[..], &b"\r\n"[..]] {
286 socket.write_all(piece).await.unwrap();
287 socket.flush().await.unwrap();
288 tokio::time::sleep(Duration::from_millis(5)).await;
289 }
290 tokio::time::sleep(Duration::from_millis(50)).await;
291 });
292
293 let config = RedisConfig { port, ..RedisConfig::default() };
294 let mut connection = Connection::connect(&config).await.unwrap();
295
296 assert_eq!(
297 connection.command(&[b"GET", b"greeting"]).await.unwrap(),
298 Value::Bulk(b"hello world".to_vec())
299 );
300 }
301
302 #[tokio::test]
303 async fn a_server_that_hangs_up_mid_reply_marks_the_connection_broken() {
304 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
305 let port = listener.local_addr().unwrap().port();
306
307 tokio::spawn(async move {
308 let (mut socket, _) = listener.accept().await.unwrap();
309 let mut chunk = vec![0u8; 1024];
310 let _ = socket.read(&mut chunk).await;
311 socket.write_all(b"$11\r\n").await.unwrap();
313 socket.shutdown().await.unwrap();
314 });
315
316 let config = RedisConfig { port, ..RedisConfig::default() };
317 let mut connection = Connection::connect(&config).await.unwrap();
318
319 let error = connection.command(&[b"GET", b"k"]).await.unwrap_err();
320 assert!(error.to_string().contains("closed the connection"), "got: {error}");
321 assert!(connection.is_broken(), "a truncated reply must never be reused");
322 }
323
324 #[tokio::test]
325 async fn something_that_is_not_redis_is_reported_as_a_protocol_error() {
326 let (config, _server) = fake_server(vec![b"HTTP/1.1 400 Bad Request\r\n".to_vec()]).await;
327 let mut connection = Connection::connect(&config).await.unwrap();
328
329 assert!(connection.command(&[b"PING"]).await.is_err());
330 assert!(connection.is_broken());
331 }
332
333 #[tokio::test]
334 async fn connecting_to_a_closed_port_explains_itself() {
335 let config = RedisConfig { port: 1, connect_timeout: Duration::from_secs(2), ..RedisConfig::default() };
336 let error = match Connection::connect(&config).await {
337 Ok(_) => panic!("connecting should have failed"),
338 Err(e) => e.to_string(),
339 };
340
341 assert!(error.contains("cannot connect to redis://"), "got: {error}");
342 }
343}