yo_resp/dispatch/debug.rs
1//! `DEBUG`, the container a test suite talks to rather than a client.
2//!
3//! # What it is for
4//!
5//! Every other command here exists so that somebody can store something and get
6//! it back. This one exists so that somebody can make the server do a thing that
7//! would otherwise be impossible to arrange from the outside: send a reply of a
8//! type no ordinary command sends, stop sweeping expired keys, stop the clock
9//! work, fill a database with a hundred thousand keys without a hundred thousand
10//! round trips, or answer with an error whose text the caller chose.
11//!
12//! Redis's own test suite leans on it heavily, which is why it is here at all:
13//! most of the suite's `assert_encoding` and expiry tests do not run at all
14//! against a server that has no `DEBUG`.
15//!
16//! # Which subcommands are here
17//!
18//! A real server has around sixty and most of them are about parts that do not
19//! exist here: the AOF, cluster links, atomic slot migration, forking, crashing
20//! on purpose. What is here is the part that is about this server, and `DEBUG
21//! HELP` lists exactly that rather than listing what Redis has, for the same
22//! reason `CLIENT HELP` does: somebody reading it to find out what they can send
23//! should not be told about a subcommand that would come back unknown.
24//!
25//! The four knobs are the interesting ones, because a knob that is remembered
26//! and read by nothing is worse than no knob at all. Three of them really move
27//! something: `SET-ACTIVE-EXPIRE` gates the sweep that reclaims keys nobody asks
28//! for again, `PAUSE-CRON` gates the whole maintenance slice the shard loop runs
29//! between batches, and `SET-SKIP-CHECKSUM-VALIDATION` is read by the code that
30//! opens a `RESTORE` payload. `DICT-RESIZING` gates arena compaction, which is
31//! the nearest thing here to the dictionary resize it turns off on a real
32//! server: both are the background reclaim of room a table no longer needs. The
33//! one that is remembered and does nothing is
34//! `QUICKLIST-PACKED-THRESHOLD`, which is D-128.
35//!
36//! # How the errors work
37//!
38//! Every complaint in this file is the same sentence, `unknown subcommand or
39//! wrong number of arguments for '<what was sent>'. Try DEBUG HELP.`, and that
40//! is not a shortcut. A real server's `DEBUG` is a chain of `strcasecmp` tests
41//! each of which also checks `argc`, and anything that falls off the end of the
42//! chain gets that one line, so a subcommand that does not exist and a
43//! subcommand handed the wrong number of arguments are the same case. The name
44//! is echoed in the case it was sent in.
45//!
46//! The two exceptions are the two subcommands that read their argument and can
47//! fail on the value rather than on the count, which are
48//! `QUICKLIST-PACKED-THRESHOLD` and `POPULATE`, and each has its own sentence.
49
50use std::sync::atomic::AtomicU64;
51use std::sync::atomic::Ordering::Relaxed;
52
53use yo_common::num::parse_i64;
54use yo_common::{Code, Error, Result};
55use yo_kv::SetOptions;
56
57use super::args::{self, Args, is};
58use super::{Server, Session};
59use crate::reply::Out;
60
61/// The knobs `DEBUG` turns, all of them on a word each.
62///
63/// One word rather than a lock because the readers are the shard loop's
64/// maintenance slice and the payload reader, which is to say the hottest places
65/// that could possibly read a debugging flag, and the writer is a human at a
66/// test suite. The three gates are stored as their `true` meaning, so a default
67/// `Knobs` is a server with everything running.
68#[derive(Debug)]
69pub(crate) struct Knobs {
70 /// Whether the expiry sweep runs, which `SET-ACTIVE-EXPIRE 0` turns off.
71 expiring: AtomicU64,
72 /// Whether the maintenance slice runs at all, which `PAUSE-CRON 1` stops.
73 cron: AtomicU64,
74 /// Whether arena compaction runs, which `DICT-RESIZING 0` stops.
75 resizing: AtomicU64,
76 /// The packed node threshold, which nothing here reads. See D-128.
77 packed: AtomicU64,
78}
79
80impl Default for Knobs {
81 fn default() -> Knobs {
82 Knobs {
83 expiring: AtomicU64::new(1),
84 cron: AtomicU64::new(1),
85 resizing: AtomicU64::new(1),
86 packed: AtomicU64::new(DEFAULT_PACKED),
87 }
88 }
89}
90
91/// What the packed threshold goes back to when it is set to nought, which is a
92/// gigabyte and is Redis's default.
93const DEFAULT_PACKED: u64 = 1 << 30;
94
95/// The largest packed threshold that is taken, which is four gigabytes less a
96/// megabyte.
97///
98/// Redis's `quicklistSetPackedThreshold` refuses anything above this, with a
99/// comment saying it will not allow the threshold even slightly below four
100/// gigabytes. The error text says bigger than one and smaller than 4gb, and
101/// neither half of that sentence is quite what the code checks, since one is
102/// taken and `4294967295` is not.
103const MAX_PACKED: u64 = (1 << 32) - (1 << 20);
104
105impl Server {
106 /// Whether the expiry sweep should run.
107 #[must_use]
108 pub(crate) fn expiring(&self) -> bool {
109 self.debug.expiring.load(Relaxed) != 0
110 }
111
112 /// Whether the maintenance slice should run at all.
113 #[must_use]
114 pub fn cron_running(&self) -> bool {
115 self.debug.cron.load(Relaxed) != 0
116 }
117
118 /// Whether arena compaction should run.
119 #[must_use]
120 pub(crate) fn resizing(&self) -> bool {
121 self.debug.resizing.load(Relaxed) != 0
122 }
123}
124
125/// `DEBUG <subcommand> [...]`.
126pub(super) fn execute(
127 server: &Server,
128 session: &mut Session,
129 args: Args<'_>,
130 out: &mut Out,
131) -> Result<()> {
132 let sub = args.get(1);
133 if is(sub, b"HELP") && args.len() == 2 {
134 super::server::help(out, HELP);
135 } else if is(sub, b"PROTOCOL") && args.len() == 3 {
136 return protocol(args.get(2), out);
137 } else if is(sub, b"ERROR") && args.len() == 3 {
138 // Straight out, with no code in front of it and no checking of what is
139 // in it beyond the newlines, because the whole point is to hand a client
140 // library an error line it chose. The empty prefix is there because this
141 // is the one error line the server did not write any of, and the newline
142 // folding that comes with it is what a real server does too and is what
143 // stops this from being a way to write two replies with one command.
144 out.error_line(b"", args.get(2));
145 } else if is(sub, b"LOG") && args.len() == 3 {
146 // The server log is stderr here, which is what the service file or the
147 // shell redirection points wherever the operator wants it.
148 yo_alloc::allow(|| {
149 eprintln!("yodb: DEBUG LOG: {}", String::from_utf8_lossy(args.get(2)));
150 });
151 out.ok();
152 } else if is(sub, b"SLEEP") && args.len() == 3 {
153 sleep(args.get(2));
154 out.ok();
155 } else if is(sub, b"POPULATE") && (3..=5).contains(&args.len()) {
156 return populate(server, session, args, out);
157 } else if is(sub, b"SET-ACTIVE-EXPIRE") && args.len() == 3 {
158 server.debug.expiring.store(flag(args.get(2)), Relaxed);
159 out.ok();
160 } else if is(sub, b"PAUSE-CRON") && args.len() == 3 {
161 // The one gate that is stored the other way up from how it is written,
162 // because the subcommand names the stopping and the field names the
163 // running.
164 server.debug.cron.store(1 - flag(args.get(2)), Relaxed);
165 out.ok();
166 } else if is(sub, b"DICT-RESIZING") && args.len() == 3 {
167 server.debug.resizing.store(flag(args.get(2)), Relaxed);
168 out.ok();
169 } else if is(sub, b"SET-SKIP-CHECKSUM-VALIDATION") && args.len() == 3 {
170 yo_kv::rdb::skip_checksums(flag(args.get(2)) != 0);
171 out.ok();
172 } else if is(sub, b"QUICKLIST-PACKED-THRESHOLD") && args.len() == 3 {
173 return packed(server, args.get(2), out);
174 } else {
175 return Err(args::subcommand_syntax(sub, "DEBUG"));
176 }
177 Ok(())
178}
179
180/// A `0` or `1` argument, read the way C reads one.
181///
182/// Which is `atoi`, so anything that is not a number at all is nought and the
183/// gate goes off. That is worth reproducing rather than tidying up, because a
184/// test suite that sends `DEBUG SET-ACTIVE-EXPIRE no` gets a server with the
185/// sweep turned off on a real server and would get one with it left on here if
186/// this refused what it could not read.
187fn flag(value: &[u8]) -> u64 {
188 let value = value.strip_prefix(b"-").unwrap_or(value);
189 let digits = value
190 .iter()
191 .take_while(|b| b.is_ascii_digit())
192 .fold(0u64, |n, b| {
193 n.saturating_mul(10).saturating_add(u64::from(b - b'0'))
194 });
195 u64::from(digits != 0)
196}
197
198/// `DEBUG SLEEP <seconds>`, which stops this thread where it stands.
199///
200/// Decimals allowed and read with C's `strtod`, so a word is nought seconds and
201/// a negative number is nought seconds, and both answer `OK` at once. There is
202/// no upper bound, which is the point: a suite that wants a server that does not
203/// answer for ten seconds asks for ten seconds.
204///
205/// On a server with one shard thread, which is the default, this is the whole
206/// server, which is what it is on Redis. Above one thread it is the thread this
207/// connection landed on and the others keep answering, which is D-129.
208fn sleep(value: &[u8]) {
209 let text = core::str::from_utf8(value).unwrap_or("");
210 let seconds = leading_double(text);
211 if seconds > 0.0 {
212 std::thread::sleep(std::time::Duration::from_secs_f64(seconds));
213 }
214}
215
216/// As much of the front of `text` as reads as a double, or nought.
217///
218/// `strtod` takes the longest prefix that is a number and stops, so `1.5s` is a
219/// second and a half and `abc` is nothing. Rust's parser wants the whole string,
220/// so the prefix is found here.
221fn leading_double(text: &str) -> f64 {
222 let mut end = 0;
223 for (at, _) in text.char_indices() {
224 if text[..=at].parse::<f64>().is_ok() {
225 end = at + 1;
226 }
227 }
228 text[..end].parse().unwrap_or(0.0)
229}
230
231/// `DEBUG QUICKLIST-PACKED-THRESHOLD <size>`.
232fn packed(server: &Server, value: &[u8], out: &mut Out) -> Result<()> {
233 let size = super::server::parse_memory(value).filter(|&n| n <= MAX_PACKED);
234 let Some(size) = size else {
235 return Err(Error::new(
236 Code::Invalid,
237 "argument must be a memory value bigger than 1 and smaller than 4gb",
238 ));
239 };
240 // Nought is not a threshold of nothing, it is the word for putting the
241 // default back, which is the one part of this subcommand that is not
242 // guessable from its name.
243 let size = if size == 0 { DEFAULT_PACKED } else { size };
244 server.debug.packed.store(size, Relaxed);
245 out.ok();
246 Ok(())
247}
248
249/// `DEBUG POPULATE <count> [<prefix> [<size>]]`.
250///
251/// Keys are `<prefix>:<n>` counting from nought, with `key` as the prefix if
252/// none was given, and each value is `value:<n>`. A size pads that with zero
253/// bytes to exactly that many, or cuts it short, and a size of nought means the
254/// value is left as it is rather than made empty.
255///
256/// A key that is already there is left alone, value and deadline both, which is
257/// the surprising half and is what makes this safe to run twice. A real server
258/// checks the dictionary and skips, and it does that because the whole point of
259/// the subcommand is filling a database quickly, and quickly means not paying
260/// for a delete of something it is about to write over. Here that falls out of
261/// asking for the write the way `SET key value NX` asks for it.
262///
263/// Nothing is told about the keys this writes: no keyspace notification, no
264/// index update. The notifications are the reference's choice, since it adds the
265/// keys to the dictionary directly and never goes near the event code. The
266/// indexes are this build's, and they are safe to leave out rather than merely
267/// cheap: an index follows hashes or JSON documents and every key here is a
268/// string, and a key that was already a document is one of the keys this skips.
269fn populate(server: &Server, session: &mut Session, args: Args<'_>, out: &mut Out) -> Result<()> {
270 let count = positive(args.get(2))?;
271 let prefix = if args.len() >= 4 { args.get(3) } else { b"key" };
272 let size = if args.len() == 5 {
273 positive(args.get(4))? as usize
274 } else {
275 0
276 };
277 // Two buffers reused across the whole run rather than a pair of allocations
278 // per key, since the count a suite passes here is routinely a hundred
279 // thousand and every one of those is the same two shapes with a different
280 // number on the end.
281 let mut key = Vec::with_capacity(prefix.len() + 24);
282 let mut value = Vec::with_capacity(size.max(32));
283 let db = &server.dbs[session.db];
284 for n in 0..count {
285 key.clear();
286 key.extend_from_slice(prefix);
287 key.push(b':');
288 push_int(&mut key, n);
289 value.clear();
290 value.extend_from_slice(b"value:");
291 push_int(&mut value, n);
292 if size != 0 {
293 // Shorter than the name is a cut and longer is zero bytes on the
294 // end, which is what the reference's `sdsgrowzero` does and is why
295 // a size of five gives `value` and not `value:0` cut to five.
296 value.resize(size, 0);
297 }
298 // One stripe held per key rather than one for the run, because the keys
299 // are spread across every stripe by design and holding them all would
300 // be holding the whole database against every other thread for as long
301 // as the fill takes.
302 db.hold(&key)
303 .set(&key, &value, SetOptions::PLAIN.if_missing())?;
304 }
305 out.ok();
306 Ok(())
307}
308
309/// A count argument, which has to be a whole number that is not negative.
310///
311/// The reference reads both of `POPULATE`'s numbers with the same call and says
312/// the same thing about both, so a size that is not a number complains about a
313/// range rather than about not being a number.
314fn positive(value: &[u8]) -> Result<i64> {
315 parse_i64(value)
316 .filter(|&n| n >= 0)
317 .ok_or_else(|| Error::new(Code::Invalid, "value is out of range, must be positive"))
318}
319
320/// A whole number, appended.
321fn push_int(out: &mut Vec<u8>, mut n: i64) {
322 let start = out.len();
323 if n == 0 {
324 out.push(b'0');
325 return;
326 }
327 while n > 0 {
328 out.push(b'0' + (n % 10) as u8);
329 n /= 10;
330 }
331 out[start..].reverse();
332}
333
334/// `DEBUG PROTOCOL <type>`, which is one reply of each type RESP3 has.
335///
336/// This is the command a client library's own test suite points at itself to
337/// find out whether it decodes the protocol, so every one of these was read off
338/// the wire of an 8.10.1 rather than off the documentation, on both protocols.
339/// Two of them are worth spelling out.
340///
341/// `attrib` on RESP3 sends an attribute and then a real reply behind it, and on
342/// RESP2 sends only the reply, because RESP2 has no way to carry the attribute
343/// and dropping it is what the other side does. `push` is the other way round:
344/// on RESP3 the real reply goes out first and the push follows it, and on RESP2
345/// the whole subcommand is an error, because a push on RESP2 would be an
346/// ordinary array and a client would read it as the reply.
347// The double the reference sends is 3.141, which is close enough to pi for the
348// lint to think somebody meant pi and typed it badly. Nobody did: it is a test
349// value chosen to have three decimal places, and rounding it to the real
350// constant would change the bytes on the wire, which are the whole point.
351#[allow(clippy::approx_constant)]
352fn protocol(kind: &[u8], out: &mut Out) -> Result<()> {
353 if is(kind, b"string") {
354 out.bulk(b"Hello World");
355 } else if is(kind, b"integer") {
356 out.int(12345);
357 } else if is(kind, b"double") {
358 out.double(3.141);
359 } else if is(kind, b"bignum") {
360 out.big_number(b"1234567999999999999999999999999999999");
361 } else if is(kind, b"null") {
362 out.nil();
363 } else if is(kind, b"array") {
364 out.array(3);
365 for n in 0..3 {
366 out.int(n);
367 }
368 } else if is(kind, b"set") {
369 out.set(3);
370 for n in 0..3 {
371 out.int(n);
372 }
373 } else if is(kind, b"map") {
374 // The keys are numbers and the values are booleans, so a RESP2 client
375 // sees three pairs flattened with the booleans as `:0` and `:1`, which
376 // is the shape a RESP2 client already gets from every map here.
377 out.map(3);
378 for n in 0..3 {
379 out.int(n);
380 out.bool(n == 1);
381 }
382 } else if is(kind, b"attrib") {
383 if out.proto().is_resp3() {
384 out.attribute(1);
385 out.bulk(b"key-popularity");
386 out.array(2);
387 out.bulk(b"key:123");
388 out.int(90);
389 }
390 out.bulk(b"Some real reply following the attribute");
391 } else if is(kind, b"push") {
392 if !out.proto().is_resp3() {
393 return Err(Error::new(
394 Code::Invalid,
395 "RESP2 is not supported by this command",
396 ));
397 }
398 out.bulk(b"Some real reply following the push reply");
399 out.push(2);
400 out.bulk(b"server-cpu-usage");
401 out.int(42);
402 } else if is(kind, b"verbatim") {
403 out.verbatim(b"txt", b"This is a verbatim\nstring");
404 } else if is(kind, b"true") {
405 out.bool(true);
406 } else if is(kind, b"false") {
407 out.bool(false);
408 } else {
409 return Err(Error::new(
410 Code::Invalid,
411 "Wrong protocol type name. Please use one of the following: string|integer|double|bignum|null|array|set|map|attrib|push|verbatim|true|false",
412 ));
413 }
414 Ok(())
415}
416
417/// What `DEBUG HELP` says, which is what is here and not what Redis has.
418const HELP: &[&str] = &[
419 "DEBUG <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
420 "DICT-RESIZING <0|1>",
421 " Enable or disable the background reclaim of room the store no longer",
422 " needs.",
423 "ERROR <string>",
424 " Return a Redis protocol error with <string> as message. Useful for",
425 " clients unit tests to simulate Redis errors.",
426 "LOG <message>",
427 " Write <message> to the server log.",
428 "PAUSE-CRON <0|1>",
429 " Stop periodic cron job processing.",
430 "POPULATE <count> [<prefix>] [<size>]",
431 " Create <count> string keys named key:<num>. If <prefix> is specified",
432 " then it is used instead of the 'key' prefix. A key that already exists",
433 " is left alone.",
434 "PROTOCOL <type>",
435 " Reply with a test value of the specified type. <type> can be: string,",
436 " integer, double, bignum, null, array, set, map, attrib, push, verbatim,",
437 " true, false.",
438 "QUICKLIST-PACKED-THRESHOLD <size>",
439 " Sets the threshold for elements to be inserted as plain vs packed nodes",
440 " Default value is 1GB, allows values up to 4GB. Setting to 0 restores to default.",
441 "SET-ACTIVE-EXPIRE <0|1>",
442 " Setting it to 0 disables expiring keys in background when they are not",
443 " accessed (otherwise the Redis behavior). Setting it to 1 reenables back",
444 " the default.",
445 "SET-SKIP-CHECKSUM-VALIDATION <0|1>",
446 " Enables or disables checksum checks for RESTORE's payload.",
447 "SLEEP <seconds>",
448 " Stop the server for <seconds>. Decimals allowed.",
449 "HELP",
450 " Print this help.",
451];
452
453#[cfg(test)]
454mod tests {
455 use super::{flag, leading_double};
456
457 /// The flag reads what C reads out of the same bytes.
458 #[test]
459 fn a_flag_is_atoi_and_anything_unreadable_is_off() {
460 for (text, want) in [
461 (&b"0"[..], 0),
462 (b"1", 1),
463 (b"00", 0),
464 (b"01", 1),
465 (b"2", 1),
466 (b"-1", 1),
467 (b"-0", 0),
468 (b"x", 0),
469 (b"", 0),
470 (b"1x", 1),
471 (b"true", 0),
472 (b"18446744073709551617", 1),
473 ] {
474 assert_eq!(flag(text), want, "{}", String::from_utf8_lossy(text));
475 }
476 }
477
478 /// A sleep argument reads as much of itself as is a number.
479 #[test]
480 fn a_sleep_reads_the_longest_number_at_the_front() {
481 for (text, want) in [
482 ("0", 0.0),
483 ("0.05", 0.05),
484 ("-1", -1.0),
485 ("abc", 0.0),
486 ("", 0.0),
487 ("1.5s", 1.5),
488 ("2x3", 2.0),
489 ] {
490 assert!(
491 (leading_double(text) - want).abs() < 1e-9,
492 "{text} read as {}",
493 leading_double(text)
494 );
495 }
496 }
497}