use core::fmt::Write as _;
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, digest, lookups};
use super::args::{self, Args, is};
use super::{Server, Session, persist};
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 if is(sub, b"RELOAD") {
return reload(server, args, out);
} else if is(sub, b"OBJECT") && args.len() == 3 {
return object(server, session, args.get(2), out);
} else if is(sub, b"SDSLEN") && args.len() == 3 {
return sdslen(server, session, args.get(2), out);
} else if is(sub, b"LISTPACK") && args.len() == 3 {
return packing(server, session, args.get(2), Packing::Listpack, out);
} else if is(sub, b"QUICKLIST") && (3..=4).contains(&args.len()) {
return packing(server, session, args.get(2), Packing::Quicklist, out);
} else if is(sub, b"MARK-INTERNAL-CLIENT")
&& (args.len() == 2 || (args.len() == 3 && is(args.get(2), b"UNMARK")))
{
session.serve_internal(args.len() == 2);
out.ok();
} else if is(sub, b"INTERNAL_SECRET") && args.len() == 2 {
let secret = server.cluster_secret();
if secret.is_empty() {
return Err(Error::new(Code::Invalid, "Internal secret is missing"));
}
out.int(i64::from(yo_common::crc::crc16(secret.as_bytes())));
} else if is(sub, b"CHANGE-REPL-ID") && args.len() == 2 {
server.change_id();
out.ok();
} else if is(sub, b"DIGEST") && args.len() == 2 {
whole_digest(server, out);
} else if is(sub, b"DIGEST-VALUE") {
value_digests(server, session, args, 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(())
}
const LOAD_FAILED: &str = "Error trying to load the RDB dump, check server logs.";
fn reload(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
let (mut save, mut flush) = (true, true);
for i in 2..args.len() {
let word = args.get(i);
if is(word, b"NOSAVE") {
save = false;
} else if is(word, b"NOFLUSH") {
flush = false;
} else if !is(word, b"MERGE") {
return Err(Error::new(
Code::Invalid,
"DEBUG RELOAD only supports the MERGE, NOFLUSH and NOSAVE options.",
));
}
}
if save {
if !persist::write_file(server) {
out.error(b"ERR");
return Ok(());
}
let lost = persist::skipped(server);
if flush && lost > 0 {
return Err(if lost == 1 {
Error::new(
Code::Invalid,
"DEBUG RELOAD would drop 1 key with no RDB form, use NOFLUSH to keep it",
)
} else {
Error::fmt(
Code::Invalid,
format_args!(
"DEBUG RELOAD would drop {lost} keys with no RDB form, use NOFLUSH to keep them"
),
)
});
}
}
if yo_alloc::allow(|| load_file(server, flush)) {
out.ok();
Ok(())
} else {
Err(Error::new(Code::Invalid, LOAD_FAILED))
}
}
fn load_file(server: &Server, flush: bool) -> bool {
let path = server.dir().join(persist::FILE);
let image = match std::fs::read(&path) {
Ok(image) => image,
Err(e) => {
eprintln!("yodb: DEBUG RELOAD: {}: {e}", path.display());
return false;
}
};
match server.load_image(&image, flush) {
Ok(_) => true,
Err(refused) => {
eprintln!("yodb: DEBUG RELOAD: {refused}");
false
}
}
}
const NO_SUCH_KEY: &str = "no such key";
const LRU_CLOCK_MAX: u64 = (1 << 24) - 1;
const DUMP_AROUND: usize = 11;
fn object(server: &Server, session: &Session, key: &[u8], out: &mut Out) -> Result<()> {
let mut held = server.dbs[session.db].hold(key);
let Some(encoding) = held.encoding_name(key) else {
return Err(Error::new(Code::Invalid, NO_SUCH_KEY));
};
let _quiet = lookups::quiet();
let at = held.value_address(key).unwrap_or(0);
let idle = held.idle_secs(key).unwrap_or(0);
let serialized = held
.dump(key)
.map_or(0, |payload| payload.len() - DUMP_AROUND);
let quicklist = (encoding == "quicklist")
.then(|| held.list_shape(key))
.flatten()
.map(|(nodes, bytes)| {
let len = held.llen(key).unwrap_or(0);
let fill = list_fill(&held.bands().list);
(nodes, len as f64 / nodes.max(1) as f64, fill, bytes)
});
drop(held);
let now = server.clock.now_ms() / 1_000;
let lru = now.saturating_sub(idle) & LRU_CLOCK_MAX;
let mut line = String::with_capacity(192);
yo_alloc::allow(|| {
let _ = write!(
line,
"Value at:{at:#x} refcount:1 encoding:{encoding} \
serializedlength:{serialized} lru:{lru} lru_seconds_idle:{idle}",
);
if let Some((nodes, avg, fill, bytes)) = quicklist {
let _ = write!(
line,
" ql_nodes:{nodes} ql_avg_node:{avg:.2} ql_listpack_max:{fill} \
ql_compressed:0 ql_uncompressed_size:{bytes}",
);
}
});
out.simple(line.as_bytes());
Ok(())
}
fn list_fill(limits: &yo_kv::list::Limits) -> i32 {
if let Some(count) = limits.max_packed_entries {
return i32::try_from(count).unwrap_or(i32::MAX);
}
match limits.max_packed_bytes {
4096 => -1,
16384 => -3,
32768 => -4,
65536 => -5,
_ => -2,
}
}
fn sdslen(server: &Server, session: &Session, key: &[u8], out: &mut Out) -> Result<()> {
let mut held = server.dbs[session.db].hold(key);
let Some(encoding) = held.encoding_name(key) else {
return Err(Error::new(Code::Invalid, NO_SUCH_KEY));
};
if !matches!(encoding, "raw" | "embstr") {
return Err(Error::new(Code::Invalid, "Not an sds encoded string."));
}
let _quiet = lookups::quiet();
let len = held.strlen(key).unwrap_or(0);
drop(held);
let mut line = String::with_capacity(128);
yo_alloc::allow(|| {
let _ = write!(
line,
"key_sds_len:{}, key_sds_avail:0, key_zmalloc: {}, \
val_sds_len:{len}, val_sds_avail:0, val_zmalloc: {len}",
key.len(),
key.len(),
);
});
out.simple(line.as_bytes());
Ok(())
}
#[derive(Clone, Copy)]
enum Packing {
Listpack,
Quicklist,
}
impl Packing {
const fn word(self) -> &'static str {
match self {
Packing::Listpack => "LISTPACK",
Packing::Quicklist => "QUICKLIST",
}
}
const fn encoding(self) -> &'static str {
match self {
Packing::Listpack => "listpack",
Packing::Quicklist => "quicklist",
}
}
const fn said(self) -> &'static [u8] {
match self {
Packing::Listpack => b"Listpack structure printed on stdout",
Packing::Quicklist => b"Quicklist structure printed on stdout",
}
}
const fn refusal(self) -> &'static str {
match self {
Packing::Listpack => "Not a listpack encoded object.",
Packing::Quicklist => "Not a quicklist encoded object.",
}
}
}
fn packing(
server: &Server,
session: &Session,
key: &[u8],
which: Packing,
out: &mut Out,
) -> Result<()> {
let mut held = server.dbs[session.db].hold(key);
let Some(encoding) = held.encoding_name(key) else {
return Err(Error::new(Code::Invalid, NO_SUCH_KEY));
};
if encoding != which.encoding() {
return Err(Error::new(Code::Invalid, which.refusal()));
}
let _quiet = lookups::quiet();
let kind = held.type_name(key).unwrap_or("none");
let shape = held.list_shape(key);
let serialized = held
.dump(key)
.map_or(0, |payload| payload.len() - DUMP_AROUND);
drop(held);
yo_alloc::allow(|| {
let name = String::from_utf8_lossy(key);
let mut line = format!(
"yodb: DEBUG {}: {name}: {kind}, {serialized} byte(s)",
which.word()
);
if let Some((nodes, bytes)) = shape {
let _ = write!(line, ", {nodes} node(s) holding {bytes}");
}
println!("{line}");
});
out.simple(which.said());
Ok(())
}
fn whole_digest(server: &Server, out: &mut Out) {
let _quiet = lookups::quiet();
let mut whole = digest::EMPTY;
for (i, db) in server.dbs.iter().enumerate() {
if db.is_empty() {
continue;
}
digest::number(&mut whole, i as u32);
db.digest(&mut whole);
}
out.simple(&digest::hex(&whole));
}
fn value_digests(server: &Server, session: &Session, args: Args<'_>, out: &mut Out) {
out.array(args.len() - 2);
for i in 2..args.len() {
let key = args.get(i);
let mut one = digest::EMPTY;
server.dbs[session.db].hold(key).digest_value(key, &mut one);
out.simple(&digest::hex(&one));
}
}
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:",
"CHANGE-REPL-ID",
" Change the replication IDs of the server. Useful for testing the",
" replication sub system.",
"DICT-RESIZING <0|1>",
" Enable or disable the background reclaim of room the store no longer",
" needs.",
"DIGEST",
" Output a hex signature representing the current DB content.",
"DIGEST-VALUE <key> [<key> ...]",
" Output a hex signature of the values of all the specified keys.",
"ERROR <string>",
" Return a Redis protocol error with <string> as message. Useful for",
" clients unit tests to simulate Redis errors.",
"INTERNAL_SECRET",
" Return the cluster internal secret (hashed with crc16) or error if not in cluster mode.",
"LISTPACK <key>",
" Show low level info about the listpack encoding of <key>.",
"LOG <message>",
" Write <message> to the server log.",
"MARK-INTERNAL-CLIENT [UNMARK]",
" Promote the current connection to an internal connection.",
"OBJECT <key>",
" Show low level info about `key` and associated value.",
"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 <key> [<0|1>]",
" Show low level info about the quicklist encoding of <key>.",
" The optional argument (0 by default) sets the level of detail",
"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.",
"RELOAD [MERGE] [NOFLUSH] [NOSAVE]",
" Save the dataset to the RDB file and load it back. NOSAVE reads the file",
" that is already there, NOFLUSH keeps what is in memory and lets the file",
" land on top of it, and MERGE is accepted and does nothing.",
"SDSLEN <key>",
" Show low level SDS string info representing `key` and value.",
"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)
);
}
}
}