Skip to main content

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}