use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering::Relaxed;
use yo_common::num::parse_i64;
use yo_common::{Code, Error, Result};
use yo_kv::SetOptions;
use super::args::{self, Args, is};
use super::{Server, Session};
use crate::reply::Out;
#[derive(Debug)]
pub(crate) struct Knobs {
expiring: AtomicU64,
cron: AtomicU64,
resizing: AtomicU64,
packed: AtomicU64,
}
impl Default for Knobs {
fn default() -> Knobs {
Knobs {
expiring: AtomicU64::new(1),
cron: AtomicU64::new(1),
resizing: AtomicU64::new(1),
packed: AtomicU64::new(DEFAULT_PACKED),
}
}
}
const DEFAULT_PACKED: u64 = 1 << 30;
const MAX_PACKED: u64 = (1 << 32) - (1 << 20);
impl Server {
#[must_use]
pub(crate) fn expiring(&self) -> bool {
self.debug.expiring.load(Relaxed) != 0
}
#[must_use]
pub fn cron_running(&self) -> bool {
self.debug.cron.load(Relaxed) != 0
}
#[must_use]
pub(crate) fn resizing(&self) -> bool {
self.debug.resizing.load(Relaxed) != 0
}
}
pub(super) fn execute(
server: &Server,
session: &mut Session,
args: Args<'_>,
out: &mut Out,
) -> Result<()> {
let sub = args.get(1);
if is(sub, b"HELP") && args.len() == 2 {
super::server::help(out, HELP);
} else if is(sub, b"PROTOCOL") && args.len() == 3 {
return protocol(args.get(2), out);
} else if is(sub, b"ERROR") && args.len() == 3 {
out.error_line(b"", args.get(2));
} else if is(sub, b"LOG") && args.len() == 3 {
yo_alloc::allow(|| {
eprintln!("yodb: DEBUG LOG: {}", String::from_utf8_lossy(args.get(2)));
});
out.ok();
} else if is(sub, b"SLEEP") && args.len() == 3 {
sleep(args.get(2));
out.ok();
} else if is(sub, b"POPULATE") && (3..=5).contains(&args.len()) {
return populate(server, session, args, out);
} else if is(sub, b"SET-ACTIVE-EXPIRE") && args.len() == 3 {
server.debug.expiring.store(flag(args.get(2)), Relaxed);
out.ok();
} else if is(sub, b"PAUSE-CRON") && args.len() == 3 {
server.debug.cron.store(1 - flag(args.get(2)), Relaxed);
out.ok();
} else if is(sub, b"DICT-RESIZING") && args.len() == 3 {
server.debug.resizing.store(flag(args.get(2)), Relaxed);
out.ok();
} else if is(sub, b"SET-SKIP-CHECKSUM-VALIDATION") && args.len() == 3 {
yo_kv::rdb::skip_checksums(flag(args.get(2)) != 0);
out.ok();
} else if is(sub, b"QUICKLIST-PACKED-THRESHOLD") && args.len() == 3 {
return packed(server, args.get(2), out);
} else {
return Err(args::subcommand_syntax(sub, "DEBUG"));
}
Ok(())
}
fn flag(value: &[u8]) -> u64 {
let value = value.strip_prefix(b"-").unwrap_or(value);
let digits = value
.iter()
.take_while(|b| b.is_ascii_digit())
.fold(0u64, |n, b| {
n.saturating_mul(10).saturating_add(u64::from(b - b'0'))
});
u64::from(digits != 0)
}
fn sleep(value: &[u8]) {
let text = core::str::from_utf8(value).unwrap_or("");
let seconds = leading_double(text);
if seconds > 0.0 {
std::thread::sleep(std::time::Duration::from_secs_f64(seconds));
}
}
fn leading_double(text: &str) -> f64 {
let mut end = 0;
for (at, _) in text.char_indices() {
if text[..=at].parse::<f64>().is_ok() {
end = at + 1;
}
}
text[..end].parse().unwrap_or(0.0)
}
fn packed(server: &Server, value: &[u8], out: &mut Out) -> Result<()> {
let size = super::server::parse_memory(value).filter(|&n| n <= MAX_PACKED);
let Some(size) = size else {
return Err(Error::new(
Code::Invalid,
"argument must be a memory value bigger than 1 and smaller than 4gb",
));
};
let size = if size == 0 { DEFAULT_PACKED } else { size };
server.debug.packed.store(size, Relaxed);
out.ok();
Ok(())
}
fn populate(server: &Server, session: &mut Session, args: Args<'_>, out: &mut Out) -> Result<()> {
let count = positive(args.get(2))?;
let prefix = if args.len() >= 4 { args.get(3) } else { b"key" };
let size = if args.len() == 5 {
positive(args.get(4))? as usize
} else {
0
};
let mut key = Vec::with_capacity(prefix.len() + 24);
let mut value = Vec::with_capacity(size.max(32));
let db = &server.dbs[session.db];
for n in 0..count {
key.clear();
key.extend_from_slice(prefix);
key.push(b':');
push_int(&mut key, n);
value.clear();
value.extend_from_slice(b"value:");
push_int(&mut value, n);
if size != 0 {
value.resize(size, 0);
}
db.hold(&key)
.set(&key, &value, SetOptions::PLAIN.if_missing())?;
}
out.ok();
Ok(())
}
fn positive(value: &[u8]) -> Result<i64> {
parse_i64(value)
.filter(|&n| n >= 0)
.ok_or_else(|| Error::new(Code::Invalid, "value is out of range, must be positive"))
}
fn push_int(out: &mut Vec<u8>, mut n: i64) {
let start = out.len();
if n == 0 {
out.push(b'0');
return;
}
while n > 0 {
out.push(b'0' + (n % 10) as u8);
n /= 10;
}
out[start..].reverse();
}
#[allow(clippy::approx_constant)]
fn protocol(kind: &[u8], out: &mut Out) -> Result<()> {
if is(kind, b"string") {
out.bulk(b"Hello World");
} else if is(kind, b"integer") {
out.int(12345);
} else if is(kind, b"double") {
out.double(3.141);
} else if is(kind, b"bignum") {
out.big_number(b"1234567999999999999999999999999999999");
} else if is(kind, b"null") {
out.nil();
} else if is(kind, b"array") {
out.array(3);
for n in 0..3 {
out.int(n);
}
} else if is(kind, b"set") {
out.set(3);
for n in 0..3 {
out.int(n);
}
} else if is(kind, b"map") {
out.map(3);
for n in 0..3 {
out.int(n);
out.bool(n == 1);
}
} else if is(kind, b"attrib") {
if out.proto().is_resp3() {
out.attribute(1);
out.bulk(b"key-popularity");
out.array(2);
out.bulk(b"key:123");
out.int(90);
}
out.bulk(b"Some real reply following the attribute");
} else if is(kind, b"push") {
if !out.proto().is_resp3() {
return Err(Error::new(
Code::Invalid,
"RESP2 is not supported by this command",
));
}
out.bulk(b"Some real reply following the push reply");
out.push(2);
out.bulk(b"server-cpu-usage");
out.int(42);
} else if is(kind, b"verbatim") {
out.verbatim(b"txt", b"This is a verbatim\nstring");
} else if is(kind, b"true") {
out.bool(true);
} else if is(kind, b"false") {
out.bool(false);
} else {
return Err(Error::new(
Code::Invalid,
"Wrong protocol type name. Please use one of the following: string|integer|double|bignum|null|array|set|map|attrib|push|verbatim|true|false",
));
}
Ok(())
}
const HELP: &[&str] = &[
"DEBUG <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
"DICT-RESIZING <0|1>",
" Enable or disable the background reclaim of room the store no longer",
" needs.",
"ERROR <string>",
" Return a Redis protocol error with <string> as message. Useful for",
" clients unit tests to simulate Redis errors.",
"LOG <message>",
" Write <message> to the server log.",
"PAUSE-CRON <0|1>",
" Stop periodic cron job processing.",
"POPULATE <count> [<prefix>] [<size>]",
" Create <count> string keys named key:<num>. If <prefix> is specified",
" then it is used instead of the 'key' prefix. A key that already exists",
" is left alone.",
"PROTOCOL <type>",
" Reply with a test value of the specified type. <type> can be: string,",
" integer, double, bignum, null, array, set, map, attrib, push, verbatim,",
" true, false.",
"QUICKLIST-PACKED-THRESHOLD <size>",
" Sets the threshold for elements to be inserted as plain vs packed nodes",
" Default value is 1GB, allows values up to 4GB. Setting to 0 restores to default.",
"SET-ACTIVE-EXPIRE <0|1>",
" Setting it to 0 disables expiring keys in background when they are not",
" accessed (otherwise the Redis behavior). Setting it to 1 reenables back",
" the default.",
"SET-SKIP-CHECKSUM-VALIDATION <0|1>",
" Enables or disables checksum checks for RESTORE's payload.",
"SLEEP <seconds>",
" Stop the server for <seconds>. Decimals allowed.",
"HELP",
" Print this help.",
];
#[cfg(test)]
mod tests {
use super::{flag, leading_double};
#[test]
fn a_flag_is_atoi_and_anything_unreadable_is_off() {
for (text, want) in [
(&b"0"[..], 0),
(b"1", 1),
(b"00", 0),
(b"01", 1),
(b"2", 1),
(b"-1", 1),
(b"-0", 0),
(b"x", 0),
(b"", 0),
(b"1x", 1),
(b"true", 0),
(b"18446744073709551617", 1),
] {
assert_eq!(flag(text), want, "{}", String::from_utf8_lossy(text));
}
}
#[test]
fn a_sleep_reads_the_longest_number_at_the_front() {
for (text, want) in [
("0", 0.0),
("0.05", 0.05),
("-1", -1.0),
("abc", 0.0),
("", 0.0),
("1.5s", 1.5),
("2x3", 2.0),
] {
assert!(
(leading_double(text) - want).abs() < 1e-9,
"{text} read as {}",
leading_double(text)
);
}
}
}