Coverage Report

Created: 2026-07-21 15:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-redis-tester/src/read_only_redis.rs
Line
Count
Source
1
// Copyright 2026 The NativeLink Authors. 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 = "5148c724ce419ea27d1971dcb61c111dbbc6b63e";
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<()>) {
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
28
        let 
inner3
= move |buf: &[u8]| -> String {
56
28
            let mut output = String::new();
57
28
            let mut buf_index = 0;
58
            loop {
59
48
                let frame = match decode(&buf[buf_index..]).unwrap() {
60
48
                    Some((frame, amt)) => {
61
48
                        buf_index += amt;
62
48
                        frame
63
                    }
64
                    None => {
65
0
                        panic!("No frame!");
66
                    }
67
                };
68
48
                let (cmd, args) = {
69
48
                    if let OwnedFrame::Array(a) = frame {
70
48
                        if let OwnedFrame::BulkString(s) = a.first().unwrap() {
71
48
                            let args: Vec<_> = a[1..].to_vec();
72
48
                            (str::from_utf8(s).unwrap().to_string(), args)
73
                        } else {
74
0
                            panic!("Array not starting with cmd: {a:?}");
75
                        }
76
                    } else {
77
0
                        panic!("Non array cmd: {frame:?}");
78
                    }
79
                };
80
81
48
                let ret: Either<Value, String> = match cmd.as_str() {
82
48
                    "HELLO" => 
Either::Left(Value::Map(10
vec!10
[(
83
10
                        Value::SimpleString("server".into()),
84
10
                        Value::SimpleString("redis".into()),
85
10
                    )])),
86
38
                    "CLIENT" => {
87
                        // We can safely ignore these, as it's just setting the library name/version
88
20
                        Either::Left(Value::Int(0))
89
                    }
90
18
                    "SCRIPT" => {
91
5
                        assert_eq!(args[0], OwnedFrame::BulkString(b"LOAD".to_vec()));
92
93
5
                        let OwnedFrame::BulkString(ref _script) = args[1] else {
94
0
                            panic!("Script should be a bulkstring: {args:?}");
95
                        };
96
5
                        Either::Left(Value::SimpleString(FAKE_SCRIPT_SHA.to_string()))
97
                    }
98
13
                    "ROLE" => 
Either::Left(Value::Array(5
vec!5
[
99
5
                        Value::BulkString(b"master".to_vec()),
100
5
                        Value::Int(0),
101
5
                        Value::Array(vec![]),
102
5
                    ])),
103
8
                    "SETRANGE" => {
104
2
                        let value = self.readonly_triggered.load(Ordering::Relaxed);
105
2
                        if value {
106
1
                            Either::Left(Value::Int(5))
107
                        } else {
108
1
                            self.readonly_triggered.store(true, Ordering::Relaxed);
109
1
                            Either::Right(readonly_err.clone())
110
                        }
111
                    }
112
6
                    "STRLEN" => 
Either::Left(Value::Int(5))1
,
113
5
                    "RENAME" | 
"HMSET"4
=> {
114
3
                        let value = self.readonly_triggered.load(Ordering::Relaxed);
115
3
                        if value {
116
2
                            Either::Left(Value::Okay)
117
                        } else {
118
1
                            self.readonly_triggered.store(true, Ordering::Relaxed);
119
1
                            Either::Right(readonly_err.clone())
120
                        }
121
                    }
122
2
                    "EVALSHA" => 
Either::Left(Value::Array(1
vec!1
[Value::Int(1), Value::Int(0)])),
123
1
                    "EXPIRE" => {
124
1
                        assert_eq!(args[1], OwnedFrame::BulkString(b"60".to_vec()));
125
1
                        let value = self.readonly_triggered.load(Ordering::Relaxed);
126
1
                        if value {
127
1
                            Either::Left(Value::Int(1))
128
                        } else {
129
0
                            self.readonly_triggered.store(true, Ordering::Relaxed);
130
0
                            Either::Right(readonly_err.clone())
131
                        }
132
                    }
133
0
                    actual => {
134
0
                        panic!("Mock command not implemented! {actual:?}");
135
                    }
136
                };
137
138
48
                match ret {
139
46
                    Either::Left(v) => {
140
46
                        arg_as_string(&mut output, v);
141
46
                    }
142
2
                    Either::Right(s) => {
143
2
                        write!(&mut output, "{s}").unwrap();
144
2
                    }
145
                }
146
147
48
                if buf_index == buf.len() {
148
28
                    break;
149
20
                }
150
            }
151
28
            output
152
28
        };
153
3
        fake_redis_internal(listener, listener_ready_tx, vec![inner]).await;
154
0
    }
155
156
3
    
pub async fn run(self) -> u160
{
157
3
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
158
3
        let port = listener.local_addr().unwrap().port();
159
3
        info!("Using port {port}");
160
161
3
        let (listener_ready_tx, listener_ready_rx) = oneshot::channel::<()>();
162
163
3
        background_spawn!("listener", async move {
164
3
            self.dynamic_fake_redis(listener, listener_ready_tx).await;
165
0
        });
166
167
3
        listener_ready_rx
168
3
            .await
169
3
            .expect("Expected successful listener boot");
170
171
3
        port
172
3
    }
173
}