1use std::io::{self, Write};
6use std::time::{Duration, Instant};
7
8use kevy_resp::Reply;
9use kevy_resp_client::RespClient;
10
11pub struct RateLimiter {
16 rate: u64,
17 tokens: f64,
18 last: Instant,
19}
20
21impl RateLimiter {
22 pub fn new(rate: u64) -> Self {
24 Self { rate, tokens: 0.0, last: Instant::now() }
25 }
26
27 pub fn take(&mut self) {
29 if self.rate == 0 {
30 return;
31 }
32 loop {
33 let now = Instant::now();
34 self.tokens = (self.tokens
35 + now.duration_since(self.last).as_secs_f64() * self.rate as f64)
36 .min(self.rate as f64);
37 self.last = now;
38 if self.tokens >= 1.0 {
39 self.tokens -= 1.0;
40 return;
41 }
42 std::thread::sleep(Duration::from_millis(2));
43 }
44 }
45}
46
47fn scan_page(
48 client: &mut RespClient,
49 cursor: &[u8],
50 pattern: &[u8],
51) -> io::Result<(Vec<u8>, Vec<Vec<u8>>)> {
52 let reply = client.request_borrowed(&[b"SCAN", cursor, b"MATCH", pattern, b"COUNT", b"512"])?;
53 let Reply::Array(items) = reply else {
54 return Err(io::Error::new(io::ErrorKind::InvalidData, "SCAN reply shape"));
55 };
56 let (Some(Reply::Bulk(next)), Some(Reply::Array(keys))) = (items.first(), items.get(1)) else {
57 return Err(io::Error::new(io::ErrorKind::InvalidData, "SCAN reply shape"));
58 };
59 let keys = keys
60 .iter()
61 .filter_map(|k| if let Reply::Bulk(b) = k { Some(b.clone()) } else { None })
62 .collect();
63 Ok((next.clone(), keys))
64}
65
66pub fn run_delete_prefix(
68 client: &mut RespClient,
69 prefix: &[u8],
70 rate: u64,
71 dry_run: bool,
72) -> io::Result<u64> {
73 let mut pattern = prefix.to_vec();
74 pattern.push(b'*');
75 let mut cursor: Vec<u8> = b"0".to_vec();
76 let mut limiter = RateLimiter::new(rate);
77 let mut n = 0u64;
78 loop {
79 let (next, keys) = scan_page(client, &cursor, &pattern)?;
80 for key in &keys {
81 if dry_run {
82 n += 1;
83 continue;
84 }
85 limiter.take();
86 if let Reply::Int(d) = client.request_borrowed(&[b"UNLINK", key])? {
87 n += d as u64;
88 }
89 }
90 cursor = next;
91 if cursor == b"0" {
92 return Ok(n);
93 }
94 }
95}
96
97pub fn run_copy_prefix(
105 client: &mut RespClient,
106 src_prefix: &[u8],
107 dst_prefix: &[u8],
108 rate: u64,
109) -> io::Result<crate::migrate::Export> {
110 let mut pattern = src_prefix.to_vec();
111 pattern.push(b'*');
112 let mut cursor: Vec<u8> = b"0".to_vec();
113 let mut limiter = RateLimiter::new(rate);
114 let mut n = 0u64;
115 let mut skipped: std::collections::BTreeMap<Vec<u8>, u64> = Default::default();
118 loop {
119 let (next, keys) = scan_page(client, &cursor, &pattern)?;
120 for key in &keys {
121 limiter.take();
122 let mut dst = dst_prefix.to_vec();
123 dst.extend_from_slice(&key[src_prefix.len()..]);
124 let frames = match crate::migrate::rebuild_frames(client, key, &dst)? {
125 crate::migrate::Rebuilt::Frames(f) => f,
126 crate::migrate::Rebuilt::Vanished => continue,
127 crate::migrate::Rebuilt::UnsupportedType(ty) => {
128 *skipped.entry(ty).or_insert(0u64) += 1;
129 continue;
130 }
131 };
132 let n_cmds = count_commands(&frames);
133 for r in client.pipeline_raw(&frames, n_cmds)? {
134 if let Reply::Error(e) = r {
135 return Err(io::Error::new(
136 io::ErrorKind::InvalidData,
137 String::from_utf8_lossy(&e).into_owned(),
138 ));
139 }
140 }
141 n += 1;
142 }
143 cursor = next;
144 if cursor == b"0" {
145 return Ok(crate::migrate::Export { keys: n, skipped });
146 }
147 }
148}
149
150fn count_commands(mut b: &[u8]) -> usize {
152 let mut n = 0;
153 while let Some(len) = crate::migrate::command_len_pub(b) {
154 n += 1;
155 b = &b[len..];
156 }
157 n
158}
159
160pub fn run_digest(client: &mut RespClient, prefix: &[u8]) -> io::Result<(i64, String)> {
162 let r = client.request_borrowed(&[b"PREFIX.DIGEST", prefix])?;
163 let Reply::Array(items) = r else {
164 return Err(io::Error::new(io::ErrorKind::InvalidData, "PREFIX.DIGEST reply"));
165 };
166 let (Some(Reply::Int(n)), Some(Reply::Bulk(hex))) = (items.first(), items.get(1)) else {
167 return Err(io::Error::new(io::ErrorKind::InvalidData, "PREFIX.DIGEST reply"));
168 };
169 Ok((*n, String::from_utf8_lossy(hex).into_owned()))
170}
171
172pub fn run_diff(
175 a: &mut RespClient,
176 b: &mut RespClient,
177 prefixes: &[Vec<u8>],
178 out: &mut impl Write,
179) -> io::Result<Vec<Vec<u8>>> {
180 let mut bad = Vec::new();
181 for p in prefixes {
182 let (na, da) = run_digest(a, p)?;
183 let (nb, db) = run_digest(b, p)?;
184 let ok = na == nb && da == db;
185 writeln!(
186 out,
187 "{} A: {na} keys {da} B: {nb} keys {db} {}",
188 String::from_utf8_lossy(p),
189 if ok { "OK" } else { "MISMATCH" }
190 )?;
191 if !ok {
192 bad.push(p.clone());
193 }
194 }
195 Ok(bad)
196}
197
198pub fn run_inspect(client: &mut RespClient, prefix: &[u8], out: &mut impl Write) -> io::Result<()> {
200 let mut pattern = prefix.to_vec();
201 pattern.push(b'*');
202 let mut cursor: Vec<u8> = b"0".to_vec();
203 let mut total = 0u64;
204 let mut by_type: Vec<(String, u64)> = Vec::new();
205 let mut samples: Vec<String> = Vec::new();
206 loop {
207 let (next, keys) = scan_page(client, &cursor, &pattern)?;
208 for key in &keys {
209 total += 1;
210 if samples.len() < 8 {
211 samples.push(String::from_utf8_lossy(key).into_owned());
212 }
213 if let Reply::Simple(t) = client.request_borrowed(&[b"TYPE", key])? {
214 let t = String::from_utf8_lossy(&t).into_owned();
215 match by_type.iter_mut().find(|(n, _)| *n == t) {
216 Some((_, c)) => *c += 1,
217 None => by_type.push((t, 1)),
218 }
219 }
220 }
221 cursor = next;
222 if cursor == b"0" {
223 break;
224 }
225 }
226 writeln!(out, "prefix {}: {total} keys", String::from_utf8_lossy(prefix))?;
227 by_type.sort_by_key(|(_, c)| std::cmp::Reverse(*c));
228 for (t, c) in &by_type {
229 writeln!(out, " {t}: {c}")?;
230 }
231 if !samples.is_empty() {
232 writeln!(out, " samples: {}", samples.join(", "))?;
233 }
234 Ok(())
235}