/build/source/nativelink-redis-tester/src/read_only_redis.rs
Line | Count | Source |
1 | | // Copyright 2026 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::fmt::Write; |
16 | | use core::sync::atomic::{AtomicBool, Ordering}; |
17 | | use std::sync::Arc; |
18 | | |
19 | | use either::Either; |
20 | | use nativelink_util::background_spawn; |
21 | | use redis::Value; |
22 | | use redis_protocol::resp2::decode::decode; |
23 | | use redis_protocol::resp2::types::OwnedFrame; |
24 | | use tokio::net::TcpListener; |
25 | | use tokio::sync::oneshot::{self, Sender}; |
26 | | use tracing::info; |
27 | | |
28 | | use crate::fake_redis::{arg_as_string, fake_redis_internal}; |
29 | | |
30 | | const FAKE_SCRIPT_SHA: &str = "d89e3573a1f9689c22115e8a41bd332d0c7c2643"; |
31 | | |
32 | | #[derive(Clone, Debug)] |
33 | | pub struct ReadOnlyRedis { |
34 | | // The first time we hit SETRANGE/HMSET, we output a ReadOnly. Next time, we assume we're reconnected and do correct values |
35 | | readonly_triggered: Arc<AtomicBool>, |
36 | | } |
37 | | |
38 | | impl Default for ReadOnlyRedis { |
39 | 0 | fn default() -> Self { |
40 | 0 | Self::new() |
41 | 0 | } |
42 | | } |
43 | | |
44 | | impl ReadOnlyRedis { |
45 | 3 | pub fn new() -> Self { |
46 | 3 | Self { |
47 | 3 | readonly_triggered: Arc::new(AtomicBool::new(false)), |
48 | 3 | } |
49 | 3 | } |
50 | | |
51 | 3 | async fn dynamic_fake_redis(self, listener: TcpListener, listener_ready_tx: Sender<()>)0 { |
52 | 3 | let readonly_err_str = "READONLY You can't write against a read only replica."; |
53 | 3 | let readonly_err = format!("!{}\r\n{readonly_err_str}\r\n", readonly_err_str.len()); |
54 | | |
55 | 29 | let inner3 = move |buf: &[u8]| -> String { |
56 | 29 | let mut output = String::new(); |
57 | 29 | let mut buf_index = 0; |
58 | | // The replies queued since a MULTI, answered by its EXEC. |
59 | 29 | let mut queued: Option<Vec<Value>> = None; |
60 | | loop { |
61 | 55 | let frame = match decode(&buf[buf_index..]).unwrap() { |
62 | 55 | Some((frame, amt)) => { |
63 | 55 | buf_index += amt; |
64 | 55 | frame |
65 | | } |
66 | | None => { |
67 | 0 | panic!("No frame!"); |
68 | | } |
69 | | }; |
70 | 55 | let (cmd, args) = { |
71 | 55 | if let OwnedFrame::Array(a) = frame { |
72 | 55 | if let OwnedFrame::BulkString(s) = a.first().unwrap() { |
73 | 55 | let args: Vec<_> = a[1..].to_vec(); |
74 | 55 | (str::from_utf8(s).unwrap().to_string(), args) |
75 | | } else { |
76 | 0 | panic!("Array not starting with cmd: {a:?}"); |
77 | | } |
78 | | } else { |
79 | 0 | panic!("Non array cmd: {frame:?}"); |
80 | | } |
81 | | }; |
82 | | |
83 | 55 | let ret: Either<Value, String> = match cmd.as_str() { |
84 | 55 | "HELLO" => Either::Left(Value::Map(10 vec!10 [( |
85 | 10 | Value::SimpleString("server".into()), |
86 | 10 | Value::SimpleString("redis".into()), |
87 | 10 | )])), |
88 | 45 | "CLIENT" => { |
89 | | // We can safely ignore these, as it's just setting the library name/version |
90 | 20 | Either::Left(Value::Int(0)) |
91 | | } |
92 | 25 | "SCRIPT" => { |
93 | 5 | assert_eq!(args[0], OwnedFrame::BulkString(b"LOAD".to_vec())); |
94 | | |
95 | 5 | let OwnedFrame::BulkString(ref _script) = args[1] else { |
96 | 0 | panic!("Script should be a bulkstring: {args:?}"); |
97 | | }; |
98 | 5 | Either::Left(Value::SimpleString(FAKE_SCRIPT_SHA.to_string())) |
99 | | } |
100 | 20 | "ROLE" => Either::Left(Value::Array(5 vec!5 [ |
101 | 5 | Value::BulkString(b"master".to_vec()), |
102 | 5 | Value::Int(0), |
103 | 5 | Value::Array(vec![]), |
104 | 5 | ])), |
105 | 15 | "SETRANGE" => { |
106 | 1 | let value = self.readonly_triggered.load(Ordering::Relaxed); |
107 | 1 | if value { |
108 | 1 | Either::Left(Value::Int(5)) |
109 | | } else { |
110 | 0 | self.readonly_triggered.store(true, Ordering::Relaxed); |
111 | 0 | Either::Right(readonly_err.clone()) |
112 | | } |
113 | | } |
114 | 14 | "STRLEN" => Either::Left(Value::Int(5))1 , |
115 | 13 | "TTL" => Either::Left(Value::Int(60))2 , |
116 | 11 | "PERSIST" => Either::Left(Value::Int(1))1 , |
117 | 10 | "SET" | "RENAME"8 | "HMSET"7 => { |
118 | 5 | let value = self.readonly_triggered.load(Ordering::Relaxed); |
119 | 5 | if value { |
120 | 3 | Either::Left(Value::Okay) |
121 | | } else { |
122 | 2 | self.readonly_triggered.store(true, Ordering::Relaxed); |
123 | 2 | Either::Right(readonly_err.clone()) |
124 | | } |
125 | | } |
126 | 5 | "EVALSHA" => Either::Left(Value::Array(1 vec!1 [Value::Int(1), Value::Int(0)])), |
127 | 4 | "MULTI" => { |
128 | 1 | queued = Some(Vec::new()); |
129 | 1 | Either::Left(Value::Okay) |
130 | | } |
131 | 3 | "EXEC" => Either::Left(Value::Array(1 queued1 .take().unwrap_or_default())), |
132 | 2 | "EXPIRE" => { |
133 | 2 | let value = self.readonly_triggered.load(Ordering::Relaxed); |
134 | 2 | if value { |
135 | 2 | Either::Left(Value::Int(1)) |
136 | | } else { |
137 | 0 | self.readonly_triggered.store(true, Ordering::Relaxed); |
138 | 0 | Either::Right(readonly_err.clone()) |
139 | | } |
140 | | } |
141 | 0 | actual => { |
142 | 0 | panic!("Mock command not implemented! {actual:?}"); |
143 | | } |
144 | | }; |
145 | | |
146 | 55 | let ret = match (ret, queued.as_mut(), cmd.as_str()) { |
147 | 3 | (Either::Left(v2 ), Some(queued2 ), cmd2 ) if cmd != "MULTI" && cmd2 != "EXEC"2 => { |
148 | 2 | queued.push(v); |
149 | 2 | Either::Left(Value::SimpleString("QUEUED".into())) |
150 | | } |
151 | 53 | (ret, _, _) => ret, |
152 | | }; |
153 | 55 | match ret { |
154 | 53 | Either::Left(v) => { |
155 | 53 | arg_as_string(&mut output, v); |
156 | 53 | } |
157 | 2 | Either::Right(s) => { |
158 | 2 | write!(&mut output, "{s}").unwrap(); |
159 | 2 | } |
160 | | } |
161 | | |
162 | 55 | if buf_index == buf.len() { |
163 | 29 | break; |
164 | 26 | } |
165 | | } |
166 | 29 | output |
167 | 29 | }; |
168 | 3 | fake_redis_internal(listener, listener_ready_tx, vec![inner]).await; |
169 | 0 | } |
170 | | |
171 | 3 | pub async fn run(self) -> u16 { |
172 | 3 | let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); |
173 | 3 | let port = listener.local_addr().unwrap().port(); |
174 | 3 | info!("Using port {port}"); |
175 | | |
176 | 3 | let (listener_ready_tx, listener_ready_rx) = oneshot::channel::<()>(); |
177 | | |
178 | 3 | background_spawn!("listener", async move { |
179 | 3 | self.dynamic_fake_redis(listener, listener_ready_tx).await; |
180 | 0 | }); |
181 | | |
182 | 3 | listener_ready_rx |
183 | 3 | .await |
184 | 3 | .expect("Expected successful listener boot"); |
185 | | |
186 | 3 | port |
187 | 3 | } |
188 | | } |