yo_resp/dispatch/server.rs
1//! The connection and server commands.
2//!
3//! None of these touch a key. They are here because a client library sends most
4//! of them before it sends anything else: a driver opens a socket, says `HELLO
5//! 3`, maybe `SELECT 4`, asks `COMMAND DOCS` or `COMMAND COUNT` to build its
6//! own routing table, and only then does any work. A server that answers `GET`
7//! perfectly and `HELLO` badly is a server no client library can talk to, which
8//! is why these land in the same milestone as the string commands rather than
9//! after them.
10//!
11//! The replies were read off a running Redis 8.8 in both protocols. The shapes
12//! are not obvious from the documentation: `HELLO` is a map on RESP3 and the
13//! same pairs flattened on RESP2, `CONFIG GET` is the same, `INFO` is a
14//! verbatim string on RESP3 and a bulk string on RESP2, and the flags in
15//! `COMMAND INFO` are simple strings inside an array rather than bulk strings.
16
17use super::args::{self, Args, is};
18use super::table::{self, Spec};
19use super::{DATABASES, Flow, Server, Session, backup, cpu, multi, notify};
20use crate::proto::Proto;
21use crate::reply::Out;
22use core::fmt::Write;
23use std::time::{SystemTime, UNIX_EPOCH};
24use yo_common::num::parse_i64;
25use yo_common::{Code, Error, Result, glob};
26use yo_kv::Keyspace;
27use yo_kv::access::Policy;
28
29/// What we tell a client we are.
30///
31/// It is a lie and it is a deliberate one. Every client library in the world
32/// branches on this pair to decide which commands exist, and a driver that
33/// reads `yo` here falls back to its oldest code path or refuses to connect.
34/// Divergence D-12 in `divergences.toml` says so, and the honest answer is in
35/// the `yo_version` field of `INFO` next to this one.
36const REPORTED_SERVER: &str = "redis";
37/// The Redis version we answer 100 percent of, which is what `HELLO` reports.
38///
39/// [`super::backup`] writes it into the `redis-ver` aux field of the base file
40/// it produces, so a server told to load one reads the same version out of the
41/// file that a client reads off the connection.
42pub(super) const REPORTED_VERSION: &str = "8.8.0";
43
44/// The settings that are fixed for the life of the process.
45///
46/// `CONFIG SET` accepts a write to one of these that changes nothing and
47/// refuses everything else rather than pretending to have taken it. A client
48/// that sets `appendonly no` on a server that already has no append only file
49/// gets an `OK` and is telling the truth; one that sets `appendonly yes` gets
50/// told it cannot, which is better than an `OK` and no file.
51const SETTINGS: &[(&str, &str)] = &[
52 ("appendonly", "no"),
53 ("appendfsync", "everysec"),
54 // Where `BACKUP` writes, under `dir`. Fixed here where a real server takes
55 // it at startup, because nothing in this build reads it from a file.
56 ("backupdirname", backup::DIR_NAME),
57 ("databases", "16"),
58 ("io-threads", "1"),
59 ("proto-max-bulk-len", "536870912"),
60 ("save", ""),
61 ("timeout", "0"),
62];
63
64/// Which number on the size ladder a settings name refers to.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66enum Knob {
67 SetIntsetEntries,
68 SetListpackEntries,
69 SetListpackValue,
70 HashListpackEntries,
71 HashListpackValue,
72 MaxmemorySamples,
73 LfuLogFactor,
74 LfuDecayTime,
75}
76
77/// The settings that move the size ladder, which are the ones that really move.
78///
79/// These decide where a collection stops being a packed blob and becomes an
80/// element table, so they decide what `OBJECT ENCODING` answers, and a client
81/// that reads `OBJECT ENCODING` after setting one of these expects the two to
82/// agree. That is the whole reason they are writable when nothing else here is.
83///
84/// The `ziplist` spellings are the names these had before Redis renamed them
85/// and it still answers to both, so this does too. Two names, one number: a
86/// `CONFIG SET hash-max-ziplist-entries 4` shows up under the listpack name
87/// too, which was checked against 8.10.1 rather than assumed.
88///
89/// Moving one of these leaves every collection that already exists exactly as
90/// it is, and only decides what the next write builds. Redis does the same, and
91/// it is the reason `CONFIG SET set-max-listpack-entries 0` does not rewrite
92/// the keyspace.
93///
94/// The three eviction numbers are in here too, which stretches the name a
95/// little. They belong with these rather than with the immutable settings for
96/// the same reason: a client that sets one and then reads `OBJECT FREQ` or
97/// watches `evicted_keys` expects the two to agree. `maxmemory-samples` says how
98/// many keys a round of sampling looks at, and the two `lfu` numbers set what
99/// the counter under an LFU policy actually measures.
100const LADDER: &[(&str, Knob)] = &[
101 ("hash-max-listpack-entries", Knob::HashListpackEntries),
102 ("hash-max-listpack-value", Knob::HashListpackValue),
103 ("hash-max-ziplist-entries", Knob::HashListpackEntries),
104 ("hash-max-ziplist-value", Knob::HashListpackValue),
105 ("lfu-decay-time", Knob::LfuDecayTime),
106 ("lfu-log-factor", Knob::LfuLogFactor),
107 ("maxmemory-samples", Knob::MaxmemorySamples),
108 ("set-max-intset-entries", Knob::SetIntsetEntries),
109 ("set-max-listpack-entries", Knob::SetListpackEntries),
110 ("set-max-listpack-value", Knob::SetListpackValue),
111];
112
113/// The setting that decides which way the access field on every record is read.
114///
115/// It is on its own rather than in [`SETTINGS`] or [`LADDER`] because it is the
116/// only writable setting that is not a number, and rather than immutable because
117/// it really moves: a client that sets it and then reads `OBJECT FREQ` expects
118/// the two to agree, which is the same argument the size ladder makes.
119///
120/// Setting it changes nothing about the keys already stored. Whatever is in
121/// their access field stays there and means something different from the moment
122/// the policy changes, which is what the `OBJECT FREQ` error text warns about.
123const MAXMEMORY_POLICY: &str = "maxmemory-policy";
124
125/// How much the server is allowed to hold before it starts evicting.
126///
127/// Also on its own, and for the third different reason. It is not immutable,
128/// it is not on the size ladder and it is the only setting whose value is not a
129/// plain integer: a client writes `maxmemory 100mb` and means a hundred and
130/// four million bytes, so it needs a parser of its own.
131///
132/// Zero means no limit, which is the default and is what makes the check in
133/// front of every write one comparison. Setting it to a number smaller than
134/// what the server is already holding is allowed and is a real thing to do: the
135/// next write that would allocate evicts until it fits or is refused, which is
136/// what the `maxmemory-policy` decides between.
137const MAXMEMORY: &str = "maxmemory";
138
139/// How much the server is allowed to keep on the file before it starts evicting.
140///
141/// The other half of the eviction inversion `14` section 4.1 describes, and the
142/// only setting here that has no counterpart in Redis. `maxmemory` is a limit on
143/// memory, and the right answer to a memory limit on a system with a file under
144/// it is to move data to the file. Throwing data away is the right answer to a
145/// limit on the file, and this is that limit.
146///
147/// Minus one is no limit and is the default, so a server that never sets this
148/// grows until the disk is full and then refuses writes, which is what a
149/// database does. Zero is a real setting and it means the file may hold nothing,
150/// so migration cannot make room and eviction is all that is left, which is
151/// Redis exactly and is the documented setting for a drop in cache.
152const MAXSTORE: &str = "maxstore";
153
154/// Where the server writes, which `BACKUP LIST` answers paths under.
155///
156/// On its own for a fourth reason: it is readable and not writable, and it is
157/// not writable in a way of its own. Redis calls it a protected config, which
158/// means `CONFIG SET dir` is refused with a sentence about protection rather
159/// than about immutability unless the server was started with protected configs
160/// enabled. That distinction is copied, because the two messages are what an
161/// operator reads when a `CONFIG SET` does not take.
162const DIR: &str = "dir";
163
164/// How long a sealed backup is kept before it cleans itself up.
165///
166/// Seconds, and zero is the default and means it is kept until somebody says
167/// `BACKUP CLEANUP`. Writable, since a backup taken by a script that then died
168/// is exactly the thing this is for and setting it afterwards has to work.
169const SEALED_TTL: &str = "backup-sealed-ttl";
170
171/// Which classes of keyspace change are published, and on which two channels.
172///
173/// On its own for a fifth reason: it is the only setting whose value is neither
174/// a number nor one of a fixed list of words, but a set of characters that reads
175/// back in a different spelling from the one it was written in. `CONFIG SET
176/// notify-keyspace-events KEA` reads back as `AKE`. See the `notify` module for
177/// what each character means and why the order is what it is.
178const NOTIFY: &str = "notify-keyspace-events";
179
180/// Read a byte count the way `CONFIG SET maxmemory` reads one.
181///
182/// This is Redis's `memtoull`. Digits, then an optional unit that is not case
183/// sensitive: nothing or `b` is bytes, `k` is a thousand and `kb` is a kibibyte,
184/// and the same pairing again for `m` and `g`. The two spellings meaning
185/// different numbers is a trap and it is Redis's trap, so it is repeated here
186/// rather than tidied up.
187///
188/// A unit that overflows clamps rather than failing, which is upstream's
189/// `ULLONG_MAX` arm. There is no sign: a leading minus is refused before the
190/// digits are read, so `maxmemory -1` is not a very large number.
191///
192/// Public because `yodb serve` takes the same limits on the command line that
193/// `CONFIG SET` takes at runtime, and a server that accepts `100mb` from one and
194/// not the other, or reads it as a different number, is a server that gets
195/// misconfigured. One parser, one answer.
196#[must_use]
197pub fn parse_memory(value: &[u8]) -> Option<u64> {
198 let split = value
199 .iter()
200 .position(|b| !b.is_ascii_digit())
201 .unwrap_or(value.len());
202 let (digits, unit) = value.split_at(split);
203 if digits.is_empty() {
204 return None;
205 }
206 let mul: u64 = match unit {
207 [] => 1,
208 u if u.eq_ignore_ascii_case(b"b") => 1,
209 u if u.eq_ignore_ascii_case(b"k") => 1000,
210 u if u.eq_ignore_ascii_case(b"kb") => 1024,
211 u if u.eq_ignore_ascii_case(b"m") => 1000 * 1000,
212 u if u.eq_ignore_ascii_case(b"mb") => 1024 * 1024,
213 u if u.eq_ignore_ascii_case(b"g") => 1000 * 1000 * 1000,
214 u if u.eq_ignore_ascii_case(b"gb") => 1024 * 1024 * 1024,
215 _ => return None,
216 };
217 let mut n: u64 = 0;
218 for d in digits {
219 n = n.saturating_mul(10).saturating_add(u64::from(d - b'0'));
220 }
221 Some(n.saturating_mul(mul))
222}
223
224/// Every policy name, joined the way `CONFIG SET` lists them when it refuses one.
225///
226/// This is a formatter and not a string because the error path should not touch
227/// the allocator, and it walks [`Policy::ALL`] rather than spelling the ten names
228/// out again so the two cannot drift apart. The order is the order in Redis's own
229/// enum table, which is the whole reason `Policy::ALL` is written down.
230struct PolicyNames;
231
232impl core::fmt::Display for PolicyNames {
233 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
234 for (at, policy) in Policy::ALL.iter().enumerate() {
235 if at > 0 {
236 f.write_str(", ")?;
237 }
238 f.write_str(policy.name())?;
239 }
240 Ok(())
241 }
242}
243
244/// Run one connection or server command.
245pub(super) fn execute(
246 server: &Server,
247 session: &mut Session,
248 spec: &Spec,
249 args: Args<'_>,
250 out: &mut Out,
251) -> Result<Flow> {
252 match spec.name {
253 // The arity in the table is a minimum of one, and a real server then
254 // refuses a second argument as a wrong number of them.
255 "ping" => {
256 if args.len() > 2 {
257 return Err(args::wrong_arity("ping"));
258 }
259 // A RESP2 connection in subscribe mode is answered a two element
260 // array with `pong` in front, so that everything reaching a
261 // subscribed client on RESP2 has the same shape. The one place a
262 // command in this file cares what the connection has subscribed to.
263 if super::pubsub::ping(session, args, out) {
264 return Ok(Flow::Continue);
265 }
266 if args.len() == 2 {
267 out.bulk(args.get(1));
268 } else {
269 out.simple(b"PONG");
270 }
271 }
272 "echo" => out.bulk(args.get(1)),
273 "hello" => hello(session, args, out)?,
274 "select" => {
275 let n = args.int(1)?;
276 let ok = usize::try_from(n).is_ok_and(|n| n < DATABASES);
277 if !ok {
278 return Err(Error::new(Code::Invalid, "DB index is out of range"));
279 }
280 session.db = n as usize;
281 out.ok();
282 }
283 "reset" => {
284 // Everything a connection carries goes back to what it was when it
285 // was opened, and that includes the protocol: a connection that
286 // said `HELLO 3` is speaking RESP2 again after this.
287 //
288 // The transaction and the watches go first because letting go of a
289 // watch is a change to the server and not to the connection, so
290 // clearing the list here without saying so would leave rows on the
291 // server that nobody is watching. `RESET` inside `MULTI` answers
292 // `+RESET` and leaves no transaction, which is why it is one of the
293 // six commands a transaction does not queue.
294 // The subscriptions go with them, and for the same reason: a
295 // subscription is a row on the server naming this connection, so
296 // clearing the connection's list alone would leave the server
297 // delivering into a slot that is not listening any more.
298 multi::release(server, session);
299 super::pubsub::release(server, session);
300 session.reset();
301 out.set_proto(Proto::Resp2);
302 out.simple(b"RESET");
303 }
304 // The reply goes out before the socket closes, which is why this is a
305 // flow answer and not something the body does to the connection.
306 "quit" => {
307 out.ok();
308 return Ok(Flow::Close);
309 }
310 "client" => return super::client::execute(server, session, spec, args, out),
311 "command" => command(args, out)?,
312 "config" => config(server, args, out)?,
313 "info" => info(server, args, out),
314 // A key that is past its deadline and has not been read since is still
315 // counted, which is what Redis does too: `DBSIZE` is the size of the
316 // dictionary and not a walk over it. Redis has an active expiry cycle
317 // that takes those keys out within a tick or so and we do not yet, so
318 // the two servers disagree for as long as a dead key sits unread. That
319 // gap closes with the maintenance slice rather than with a count here,
320 // because a count here would be O(N) on a command that is O(1)
321 // everywhere else.
322 "dbsize" => out.int(server.dbs[session.db].len() as i64),
323 "flushall" => {
324 flush_mode(args)?;
325 for db in &server.dbs {
326 db.clear();
327 }
328 server.search.lock().clear();
329 server.cursors.lock().wipe();
330 out.ok();
331 }
332 // The search indexes go too, and they go whichever database this is.
333 // An index that only ever followed keys on database zero is dropped by
334 // a `FLUSHDB` on database nine, which is measured against a real server
335 // rather than reasoned about: the module hangs its callback on the
336 // flush event without looking at which database flushed.
337 "flushdb" => {
338 flush_mode(args)?;
339 server.dbs[session.db].clear();
340 server.search.lock().clear();
341 server.cursors.lock().wipe();
342 out.ok();
343 }
344 // Two databases change places and no key moves. What is in the stripes
345 // is exchanged and the databases stay where they are, so this costs two
346 // pointer sized writes per stripe whatever is in either of them, which
347 // is what makes `SWAPDB` fast and dangerous at the same time.
348 //
349 // No connection is told. A client on database zero is still on database
350 // zero and is now looking at what used to be database one, which is the
351 // whole point of the command and is why Redis calls it dangerous. A
352 // client parked in `BLPOP` remembers the database index it blocked on
353 // and not the database, so it wakes up against the swapped in one, which
354 // is Redis's behaviour and falls out of the index being what is stored.
355 "swapdb" => {
356 let first = db_index(args.get(1), "invalid first DB index")?;
357 let second = db_index(args.get(2), "invalid second DB index")?;
358 server.striped(first).swap_with(server.striped(second));
359 out.ok();
360 }
361 "time" => time(out),
362 "backup" => backup::execute(server, args, out)?,
363 "shutdown" => return shutdown(server, args),
364 _ => return Err(args::unknown_command(args)),
365 }
366 Ok(Flow::Continue)
367}
368
369/// `TIME`, which is two bulk strings and not one integer.
370///
371/// Seconds first and then microseconds within that second, both written out as
372/// decimal text, which is a shape nobody would choose today and is the shape
373/// every client library parses.
374///
375/// It reads the wall clock rather than the coarse clock the keyspace uses. The
376/// coarse one is a cached millisecond that a background tick refreshes, which is
377/// the right trade for deciding whether a key has expired and the wrong one for
378/// a command whose entire job is to say what time it is. A client that calls
379/// `TIME` twice in a row and gets the same microsecond has been lied to.
380fn time(out: &mut Out) {
381 let now = SystemTime::now()
382 .duration_since(UNIX_EPOCH)
383 .unwrap_or_default();
384 out.array(2);
385 out.bulk(now.as_secs().to_string().as_bytes());
386 out.bulk(now.subsec_micros().to_string().as_bytes());
387}
388
389// ---------------------------------------------------------------- SHUTDOWN
390
391/// `SHUTDOWN [NOSAVE | SAVE] [NOW] [FORCE] [ABORT]`.
392///
393/// On success this writes nothing at all and the connection closes under the
394/// client, which is what a server that has stopped looks like from the outside
395/// and is what every client library already expects. There is no `OK`, because
396/// an `OK` would be a promise made by a process that is about to not exist.
397///
398/// The flags are taken and none of them changes what happens, which is the same
399/// answer `SAVE` gets from `CONFIG GET`: this server has no save points and no
400/// snapshot to write, so saving and not saving are the same act. What durability
401/// there is belongs to the file underneath and is already on disk by the time a
402/// command returns, so there is nothing for `SAVE` to do and nothing for
403/// `NOSAVE` to skip. `NOW` and `FORCE` are about not waiting for replicas and
404/// about going anyway when a save failed, and neither has anything to wait for
405/// or to fail here.
406///
407/// # Errors
408///
409/// [`Code::Invalid`] for a word that is not one of the five, for `SAVE` and
410/// `NOSAVE` in the same call, and for `ABORT` alongside any other flag, all of
411/// which is what 8.10.1 says. `ABORT` on its own gets Redis's message for a
412/// cancel with nothing to cancel, and here that is not a state that can be
413/// reached rather than one that happens to be empty: a shutdown is decided and
414/// done inside one turn of the loop, so there is never a window in which one is
415/// in progress and a second client could call it off.
416fn shutdown(server: &Server, args: Args<'_>) -> Result<Flow> {
417 let (mut save, mut nosave, mut abort, mut other) = (false, false, false, false);
418 for at in 1..args.len() {
419 let arg = args.get(at);
420 match () {
421 () if is(arg, b"save") => save = true,
422 () if is(arg, b"nosave") => nosave = true,
423 () if is(arg, b"abort") => abort = true,
424 () if is(arg, b"now") || is(arg, b"force") => other = true,
425 () => return Err(args::syntax()),
426 }
427 }
428 // Repeating one is fine and contradicting yourself is not, and `ABORT` says
429 // to do nothing so it cannot be combined with a word about how to do it.
430 if (save && nosave) || (abort && (save || nosave || other)) {
431 return Err(args::syntax());
432 }
433 if abort {
434 return Err(Error::new(Code::Invalid, "No shutdown in progress."));
435 }
436 server.stop();
437 // Closing is what stops anything the client pipelined behind this from
438 // being answered by a server that is on its way out.
439 Ok(Flow::Close)
440}
441
442// ------------------------------------------------------------------- FLUSH
443
444/// Check the optional `ASYNC` or `SYNC` on `FLUSHALL` and `FLUSHDB`.
445///
446/// Both are accepted and neither changes anything. On a real server the choice
447/// is whether the freeing happens on the connection's thread or on the lazy
448/// free thread, and either way the keyspace is empty before the `OK` goes out.
449/// That is the whole of what a client can observe, and it is the same here,
450/// so taking the word and ignoring it is answering the question rather than
451/// pretending to.
452///
453/// # Errors
454///
455/// [`Code::Invalid`] for a third argument, or for a second that is neither
456/// word, which is what Redis says about both.
457fn flush_mode(args: Args<'_>) -> Result<()> {
458 if args.len() == 1 {
459 return Ok(());
460 }
461 if args.len() > 2 || !(is(args.get(1), b"async") || is(args.get(1), b"sync")) {
462 return Err(args::syntax());
463 }
464 Ok(())
465}
466
467/// One of `SWAPDB`'s two database indexes, with Redis's two different
468/// complaints about it.
469///
470/// A word that is not a number, or a number too big to be a database index on a
471/// server that stores the index in a C `int`, gets the caller's message, which
472/// says which of the two arguments was wrong. A number that is a plausible index
473/// and is not one of ours gets the same out of range message `SELECT` gives. The
474/// split looks arbitrary and it is Redis's, and the reason for it is that the
475/// first check happens while reading the argument and the second happens inside
476/// the swap, so only the first one knows which argument it was looking at.
477fn db_index(arg: &[u8], bad: &'static str) -> Result<usize> {
478 let n = parse_i64(arg)
479 .filter(|n| i32::try_from(*n).is_ok())
480 .ok_or_else(|| Error::new(Code::Invalid, bad))?;
481 usize::try_from(n)
482 .ok()
483 .filter(|n| *n < DATABASES)
484 .ok_or_else(|| Error::new(Code::Invalid, "DB index is out of range"))
485}
486
487// ------------------------------------------------------------------- HELLO
488
489/// `HELLO [protover [AUTH username password] [SETNAME name]]`.
490fn hello(session: &mut Session, args: Args<'_>, out: &mut Out) -> Result<()> {
491 if args.len() > 1 {
492 let v = parse_i64(args.get(1)).ok_or_else(|| {
493 Error::new(
494 Code::Invalid,
495 "Protocol version is not an integer or out of range",
496 )
497 })?;
498 let Some(proto) = Proto::from_version(v) else {
499 // `NOPROTO` rather than `ERR`, and it is the one error in this file
500 // written straight into the buffer: the prefix is part of what the
501 // client branches on, and it is the only place in the engine that
502 // needs this one.
503 out.error(b"NOPROTO unsupported protocol version");
504 return Ok(());
505 };
506 let mut i = 2;
507 while i < args.len() {
508 let o = args.get(i);
509 if is(o, b"AUTH") && i + 2 < args.len() {
510 // No password is configured, so the default user is `nopass`
511 // and any password for it is the right one, which is how a
512 // real server with no `requirepass` behaves. Any other user
513 // does not exist.
514 if !is(args.get(i + 1), b"default") {
515 out.error(b"WRONGPASS invalid username-password pair or user is disabled.");
516 return Ok(());
517 }
518 i += 3;
519 } else if is(o, b"SETNAME") && i + 1 < args.len() {
520 session.set_name(args.get(i + 1));
521 i += 2;
522 } else {
523 return Err(yo_alloc::allow(|| {
524 Error::fmt(
525 Code::Invalid,
526 format_args!(
527 "Syntax error in HELLO option '{}'",
528 String::from_utf8_lossy(o)
529 ),
530 )
531 }));
532 }
533 }
534 // The reply is written in the protocol that was just agreed, not the
535 // one the request arrived in.
536 out.set_proto(proto);
537 }
538
539 let proto = out.proto().version();
540 out.map(7);
541 out.bulk(b"server");
542 out.bulk(REPORTED_SERVER.as_bytes());
543 out.bulk(b"version");
544 out.bulk(REPORTED_VERSION.as_bytes());
545 out.bulk(b"proto");
546 out.int(proto);
547 out.bulk(b"id");
548 out.int(session.id as i64);
549 out.bulk(b"mode");
550 out.bulk(b"standalone");
551 out.bulk(b"role");
552 out.bulk(b"master");
553 out.bulk(b"modules");
554 out.array(0);
555 Ok(())
556}
557
558// ----------------------------------------------------------------- COMMAND
559
560/// `COMMAND [COUNT|LIST|INFO|DOCS|GETKEYS|HELP]`.
561fn command(args: Args<'_>, out: &mut Out) -> Result<()> {
562 if args.len() == 1 {
563 out.array(table::COMMANDS.len());
564 for spec in table::COMMANDS {
565 write_spec(out, spec);
566 }
567 return Ok(());
568 }
569 let sub = args.get(1);
570 if is(sub, b"COUNT") {
571 out.int(table::COMMANDS.len() as i64);
572 } else if is(sub, b"INFO") {
573 if args.len() == 2 {
574 out.array(table::COMMANDS.len());
575 for spec in table::COMMANDS {
576 write_spec(out, spec);
577 }
578 } else {
579 out.array(args.len() - 2);
580 for i in 2..args.len() {
581 match table::lookup(args.get(i)) {
582 Some(spec) => write_spec(out, spec),
583 // A name nobody has heard of is a null in the list rather
584 // than an error, so one bad name in a batch does not cost
585 // the client the other answers. It is the plain null and
586 // not the array one, which on RESP2 is the difference
587 // between `$-1` and `*-1` and is what a real server sends.
588 None => out.nil(),
589 }
590 }
591 }
592 } else if is(sub, b"LIST") {
593 list(args, out)?;
594 } else if is(sub, b"DOCS") {
595 docs(args, out);
596 } else if is(sub, b"GETKEYS") {
597 getkeys(args, out)?;
598 } else if is(sub, b"HELP") {
599 help(out, COMMAND_HELP);
600 } else {
601 return Err(args::unknown_subcommand(sub, "COMMAND"));
602 }
603 Ok(())
604}
605
606/// `COMMAND LIST [FILTERBY MODULE m|ACLCAT c|PATTERN p]`.
607fn list(args: Args<'_>, out: &mut Out) -> Result<()> {
608 if args.len() == 2 {
609 out.array(table::COMMANDS.len());
610 for spec in table::COMMANDS {
611 out.bulk(spec.name.as_bytes());
612 }
613 return Ok(());
614 }
615 if args.len() != 5 || !is(args.get(2), b"FILTERBY") {
616 return Err(args::syntax());
617 }
618 let (how, what) = (args.get(3), args.get(4));
619 let keep = |spec: &Spec| {
620 if is(how, b"MODULE") {
621 // Nothing here came from a module, so every filter by one is empty.
622 false
623 } else if is(how, b"ACLCAT") {
624 spec.acl
625 .iter()
626 .any(|c| c.len() == what.len() + 1 && c.as_bytes()[1..].eq_ignore_ascii_case(what))
627 } else {
628 glob::matches(what, spec.name.as_bytes())
629 }
630 };
631 if !is(how, b"MODULE") && !is(how, b"ACLCAT") && !is(how, b"PATTERN") {
632 return Err(args::syntax());
633 }
634 out.array(table::COMMANDS.iter().filter(|s| keep(s)).count());
635 for spec in table::COMMANDS.iter().filter(|s| keep(s)) {
636 out.bulk(spec.name.as_bytes());
637 }
638 Ok(())
639}
640
641/// `COMMAND DOCS [name ...]`.
642///
643/// The arguments field a real server sends is left out. It describes the shape
644/// of every option of every command in a form nothing but `redis-cli`'s hinting
645/// reads, and getting it wrong would be worse than not sending it, since a
646/// client that finds the field trusts it.
647fn docs(args: Args<'_>, out: &mut Out) {
648 if args.len() == 2 {
649 out.map(table::COMMANDS.len());
650 for spec in table::COMMANDS {
651 write_docs(out, spec);
652 }
653 return;
654 }
655 let found = (2..args.len())
656 .filter(|&i| table::lookup(args.get(i)).is_some())
657 .count();
658 out.map(found);
659 for i in 2..args.len() {
660 if let Some(spec) = table::lookup(args.get(i)) {
661 write_docs(out, spec);
662 }
663 }
664}
665
666/// One command's documentation, as the name and then the map about it.
667fn write_docs(out: &mut Out, spec: &Spec) {
668 out.bulk(spec.name.as_bytes());
669 out.map(4);
670 out.bulk(b"summary");
671 out.bulk(spec.summary.as_bytes());
672 out.bulk(b"since");
673 out.bulk(spec.since.as_bytes());
674 out.bulk(b"group");
675 out.bulk(spec.group.as_bytes());
676 out.bulk(b"complexity");
677 out.bulk(spec.complexity.as_bytes());
678}
679
680/// `COMMAND GETKEYS <full command>`.
681///
682/// This is how a cluster aware client routes a command it does not have a rule
683/// for, so a wrong answer here is a client that sends a write to the wrong
684/// node. The generic path is the first, last and step triple from the table.
685fn getkeys(args: Args<'_>, out: &mut Out) -> Result<()> {
686 if args.len() < 3 {
687 return Err(args::wrong_arity_sub("command", "getkeys"));
688 }
689 let inner = args.get(2);
690 let spec = table::lookup(inner)
691 .ok_or_else(|| Error::new(Code::Unsupported, "Invalid command specified"))?;
692 let argc = args.len() - 2;
693 if !table::arity_ok(spec, argc) {
694 return Err(Error::new(
695 Code::Invalid,
696 "Invalid number of arguments specified for command",
697 ));
698 }
699 // Where the keys are is the same question `WATCH` asks about a command that
700 // has just run, so the answer is worked out in one place and the two
701 // callers differ only in how many arguments sit in front of the command.
702 let span = table::key_span(spec, args, 2).map_err(|why| match why {
703 table::NoKeys::Never => Error::new(Code::Invalid, "The command has no key arguments"),
704 table::NoKeys::BadCount => {
705 Error::new(Code::Invalid, "Invalid arguments specified for command")
706 }
707 })?;
708 out.array(span.count);
709 for i in 0..span.count {
710 out.bulk(args.get(span.first + i * span.step));
711 }
712 Ok(())
713}
714
715/// One command, in the ten field shape `COMMAND INFO` has had since 7.0.
716///
717/// The tips, the key specs and the subcommands are all empty. The triple above
718/// them says where the keys are for everything in this table except `MSETEX`,
719/// `TS.NRANGE` and `TS.NREVRANGE`, which is what `COMMAND GETKEYS` is for, and
720/// divergence D-13 says so.
721fn write_spec(out: &mut Out, spec: &Spec) {
722 out.array(10);
723 out.bulk(spec.name.as_bytes());
724 out.int(i64::from(spec.arity));
725 out.array(spec.flags.len());
726 for f in spec.flags {
727 out.simple(f.as_bytes());
728 }
729 out.int(i64::from(spec.first_key));
730 out.int(i64::from(spec.last_key));
731 out.int(i64::from(spec.step));
732 out.array(spec.acl.len());
733 for a in spec.acl {
734 out.simple(a.as_bytes());
735 }
736 out.array(0);
737 out.array(0);
738 out.array(0);
739}
740
741// ------------------------------------------------------------------ CONFIG
742
743/// What a ladder setting is set to now.
744fn read_knob(db: &Keyspace, knob: Knob) -> usize {
745 match knob {
746 Knob::SetIntsetEntries => db.limits().max_intset_entries,
747 Knob::SetListpackEntries => db.limits().max_listpack_entries,
748 Knob::SetListpackValue => db.limits().max_listpack_value,
749 Knob::HashListpackEntries => db.hash_limits().max_listpack_entries,
750 Knob::HashListpackValue => db.hash_limits().max_listpack_value,
751 Knob::MaxmemorySamples => db.samples(),
752 Knob::LfuLogFactor => db.lfu().log_factor as usize,
753 Knob::LfuDecayTime => db.lfu().decay_minutes as usize,
754 }
755}
756
757/// Move one ladder setting on one database.
758fn write_knob(db: &mut Keyspace, knob: Knob, n: usize) {
759 let mut set = *db.limits();
760 let mut hash = *db.hash_limits();
761 let mut lfu = db.lfu();
762 match knob {
763 Knob::SetIntsetEntries => set.max_intset_entries = n,
764 Knob::SetListpackEntries => set.max_listpack_entries = n,
765 Knob::SetListpackValue => set.max_listpack_value = n,
766 Knob::HashListpackEntries => hash.max_listpack_entries = n,
767 Knob::HashListpackValue => hash.max_listpack_value = n,
768 Knob::MaxmemorySamples => db.set_samples(n),
769 // Saturating rather than wrapping, because these two are read as `u32`
770 // and a client is free to send a number that does not fit. Redis clamps
771 // `lfu-log-factor` and `lfu-decay-time` to the same width.
772 Knob::LfuLogFactor => lfu.log_factor = u32::try_from(n).unwrap_or(u32::MAX),
773 Knob::LfuDecayTime => lfu.decay_minutes = u32::try_from(n).unwrap_or(u32::MAX),
774 }
775 db.set_limits(set);
776 db.set_hash_limits(hash);
777 db.set_lfu(lfu);
778}
779
780/// The two things a real server says about a number it will not take.
781///
782/// Both name the setting the client typed and not the one it is an alias for,
783/// so `hash-max-ziplist-entries` comes back saying `hash-max-ziplist-entries`.
784/// A value past the range of an `i64` is the parse complaint and not the range
785/// one, which is upstream reading it before it checks it.
786fn bad_setting(name: &str, parsed: bool) -> Error {
787 if parsed {
788 Error::fmt(
789 Code::Invalid,
790 format_args!(
791 "CONFIG SET failed (possibly related to argument '{name}') - argument must be between 0 and 9223372036854775807 inclusive"
792 ),
793 )
794 } else {
795 Error::fmt(
796 Code::Invalid,
797 format_args!(
798 "CONFIG SET failed (possibly related to argument '{name}') - argument couldn't be parsed into an integer"
799 ),
800 )
801 }
802}
803
804/// `CONFIG GET|SET|RESETSTAT|REWRITE|HELP`.
805fn config(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
806 let sub = args.get(1);
807 if is(sub, b"GET") {
808 if args.len() < 3 {
809 return Err(args::wrong_arity_sub("config", "get"));
810 }
811 let wanted =
812 |name: &str| (2..args.len()).any(|i| glob::matches(args.get(i), name.as_bytes()));
813 // A setting that two patterns both ask for is sent once, which is what
814 // makes this a count of settings rather than a count of matches. The
815 // two spellings of a ladder setting are two settings by that rule, so
816 // `CONFIG GET hash-max-*` sends the listpack name and the ziplist name
817 // and the same number under both, which is what a real server does.
818 let fixed = SETTINGS.iter().filter(|(k, _)| wanted(k));
819 let ladder = LADDER.iter().filter(|(k, _)| wanted(k));
820 let policy = wanted(MAXMEMORY_POLICY);
821 let limit = wanted(MAXMEMORY);
822 let store = wanted(MAXSTORE);
823 let where_ = wanted(DIR);
824 let ttl = wanted(SEALED_TTL);
825 let events = wanted(NOTIFY);
826 out.map(
827 fixed.clone().count()
828 + ladder.clone().count()
829 + usize::from(policy)
830 + usize::from(limit)
831 + usize::from(store)
832 + usize::from(where_)
833 + usize::from(ttl)
834 + usize::from(events),
835 );
836 for (k, v) in fixed {
837 out.bulk(k.as_bytes());
838 out.bulk(v.as_bytes());
839 }
840 for (k, knob) in ladder {
841 out.bulk(k.as_bytes());
842 out.bulk_int(read_knob(&server.settings(), *knob) as i64);
843 }
844 if policy {
845 out.bulk(MAXMEMORY_POLICY.as_bytes());
846 out.bulk(server.settings().policy().name().as_bytes());
847 }
848 if limit {
849 // Back as a plain number of bytes whatever the client typed to set
850 // it, which is what a real server does: `CONFIG SET maxmemory 1gb`
851 // reads back as 1073741824.
852 out.bulk(MAXMEMORY.as_bytes());
853 out.bulk_int(server.maxmemory() as i64);
854 }
855 if store {
856 // Minus one for no limit, and a plain number of bytes otherwise.
857 // Zero cannot mean no limit here the way it does for `maxmemory`,
858 // because zero is the setting that says the file holds nothing.
859 out.bulk(MAXSTORE.as_bytes());
860 out.bulk_int(server.maxstore().map_or(-1, |n| n as i64));
861 }
862 if where_ {
863 // Absolute, which is what a real server answers too: it resolves the
864 // directory at startup and reports the resolved one, so a client can
865 // tell where the files are without knowing where the process was
866 // launched from.
867 out.bulk(DIR.as_bytes());
868 yo_alloc::allow(|| out.bulk(server.dir().to_string_lossy().as_bytes()));
869 }
870 if ttl {
871 out.bulk(SEALED_TTL.as_bytes());
872 out.bulk_int(server.backup().ttl() as i64);
873 }
874 if events {
875 // The flags and not the string that set them, which is what a real
876 // server answers too and is why the parser has a formatter next to
877 // it rather than the text being kept.
878 out.bulk(NOTIFY.as_bytes());
879 let (buf, len) = notify::format(server.notify_flags());
880 out.bulk(&buf[..len]);
881 }
882 } else if is(sub, b"SET") {
883 // Too few is a wrong number of arguments and an odd number is a syntax
884 // error, which is not the same sentence and is not the same rule. A
885 // real server counts the pairs after it has decided there is at least
886 // one, so `CONFIG SET appendonly` is an arity error and `CONFIG SET
887 // appendonly no maxmemory` is a syntax one.
888 if args.len() < 4 {
889 return Err(args::wrong_arity_sub("config", "set"));
890 }
891 if !args.len().is_multiple_of(2) {
892 return Err(args::syntax());
893 }
894 // Every pair is checked before any of them is applied, because a real
895 // server takes the whole `CONFIG SET` or none of it. `CONFIG SET
896 // hash-max-listpack-entries 7 set-max-listpack-entries abc` leaves the
897 // hash setting where it was, which was checked rather than assumed.
898 let mut writes = [None; 16];
899 let mut count = 0;
900 let mut policy = None;
901 let mut limit = None;
902 let mut store = None;
903 let mut ttl = None;
904 let mut events = None;
905 let mut i = 2;
906 while i < args.len() {
907 let (name, value) = (args.get(i), args.get(i + 1));
908 i += 2;
909 if is(name, MAXMEMORY.as_bytes()) {
910 let Some(bytes) = parse_memory(value) else {
911 return Err(Error::fmt(
912 Code::Invalid,
913 format_args!(
914 "CONFIG SET failed (possibly related to argument '{MAXMEMORY}') - argument must be a memory value"
915 ),
916 ));
917 };
918 limit = Some(bytes);
919 continue;
920 }
921 if is(name, MAXSTORE.as_bytes()) {
922 // `-1` before the memory parser sees it, because that parser
923 // refuses a sign and should keep refusing one: `maxmemory -1`
924 // is not a very large number and never was.
925 let parsed = if value == b"-1" {
926 Some(None)
927 } else {
928 parse_memory(value).map(Some)
929 };
930 let Some(bytes) = parsed else {
931 return Err(Error::fmt(
932 Code::Invalid,
933 format_args!(
934 "CONFIG SET failed (possibly related to argument '{MAXSTORE}') - argument must be a memory value or -1"
935 ),
936 ));
937 };
938 store = Some(bytes);
939 continue;
940 }
941 if is(name, MAXMEMORY_POLICY.as_bytes()) {
942 // Named twice in one command, the last one wins, which is the
943 // same rule the ladder settings follow and is what a real server
944 // does with any setting repeated in a single `CONFIG SET`.
945 let Some(p) = Policy::parse(value) else {
946 return Err(Error::fmt(
947 Code::Invalid,
948 format_args!(
949 "CONFIG SET failed (possibly related to argument '{MAXMEMORY_POLICY}') - argument(s) must be one of the following: {PolicyNames}"
950 ),
951 ));
952 };
953 policy = Some(p);
954 continue;
955 }
956 if is(name, DIR.as_bytes()) {
957 // Refused whatever the value is, including the one it is already
958 // set to, which is the one place a setting here does not take
959 // the write that changes nothing. That is the reference's
960 // answer: a protected config is refused before anybody looks at
961 // what was asked for.
962 return Err(Error::fmt(
963 Code::Unsupported,
964 format_args!(
965 "CONFIG SET failed (possibly related to argument '{DIR}') - can't set protected config"
966 ),
967 ));
968 }
969 if is(name, NOTIFY.as_bytes()) {
970 // The only setting here whose error names what was wrong with
971 // the value rather than what the value should have been, and it
972 // quotes the accepted characters in the reference's order.
973 let Some(flags) = notify::parse(value) else {
974 return Err(Error::fmt(
975 Code::Invalid,
976 format_args!(
977 "CONFIG SET failed (possibly related to argument '{NOTIFY}') - Invalid event class character. Use '{}'.",
978 notify::ACCEPTED
979 ),
980 ));
981 };
982 events = Some(flags);
983 continue;
984 }
985 if is(name, SEALED_TTL.as_bytes()) {
986 let Some(n) = parse_i64(value).filter(|&n| n >= 0) else {
987 return Err(bad_setting(SEALED_TTL, parse_i64(value).is_some()));
988 };
989 ttl = Some(n as u64);
990 continue;
991 }
992 if let Some((k, knob)) = LADDER.iter().find(|(k, _)| is(name, k.as_bytes())) {
993 let Some(n) = parse_i64(value).filter(|&n| n >= 0) else {
994 return Err(bad_setting(k, parse_i64(value).is_some()));
995 };
996 if count == writes.len() {
997 // Sixteen pairs is more than the ten names there are, so
998 // getting here means a name was given twice enough times to
999 // fill it, and the last one would have won anyway.
1000 return Err(args::syntax());
1001 }
1002 writes[count] = Some((*knob, n as usize));
1003 count += 1;
1004 continue;
1005 }
1006 let Some((k, v)) = SETTINGS.iter().find(|(k, _)| is(name, k.as_bytes())) else {
1007 return Err(yo_alloc::allow(|| {
1008 Error::fmt(
1009 Code::Invalid,
1010 format_args!(
1011 "Unknown option or number of arguments for CONFIG SET - '{}'",
1012 String::from_utf8_lossy(name)
1013 ),
1014 )
1015 }));
1016 };
1017 if value != v.as_bytes() {
1018 return Err(Error::fmt(
1019 Code::Unsupported,
1020 format_args!(
1021 "CONFIG SET failed (possibly related to argument '{k}') - can't set immutable config"
1022 ),
1023 ));
1024 }
1025 }
1026 // Every stripe of every database, because these are one server wide
1027 // number in Redis and the fact that a `Keyspace` carries its own copy is
1028 // ours and not the client's problem. A stripe that missed one would put
1029 // a key in a different shape from the same key on the stripe next to it,
1030 // which `OBJECT ENCODING` would then answer differently for depending on
1031 // where the key happened to land.
1032 // The whole database is held while its stripes are set rather than one
1033 // stripe at a time, for the same reason they all get the same number: a
1034 // client that read `OBJECT ENCODING` in the middle of a half done change
1035 // would be told two different things about two keys depending on nothing
1036 // it can see.
1037 for (knob, n) in writes.iter().flatten() {
1038 for at in 0..DATABASES {
1039 let db = server.striped(at);
1040 let mut held = db.hold_many(0..db.width());
1041 for i in 0..db.width() {
1042 write_knob(held.stripe_mut(i), *knob, *n);
1043 }
1044 }
1045 }
1046 if let Some(p) = policy {
1047 for at in 0..DATABASES {
1048 let db = server.striped(at);
1049 let mut held = db.hold_many(0..db.width());
1050 for i in 0..db.width() {
1051 held.stripe_mut(i).set_policy(p);
1052 }
1053 }
1054 }
1055 if let Some(seconds) = ttl {
1056 server.backup().set_ttl(seconds);
1057 }
1058 if let Some(flags) = events {
1059 server.set_notify_flags(flags);
1060 }
1061 // Last, so that a `CONFIG SET maxmemory 1mb maxmemory-policy allkeys-lru`
1062 // has the policy in place before the limit that will act on it. The two
1063 // in the other order would run the first eviction under whatever the
1064 // policy used to be, which for a fresh server is `noeviction` and would
1065 // refuse the next write instead of making room for it.
1066 if let Some(bytes) = store {
1067 server.set_maxstore(bytes);
1068 }
1069 if let Some(bytes) = limit {
1070 server.set_maxmemory(bytes);
1071 }
1072 out.ok();
1073 } else if is(sub, b"RESETSTAT") {
1074 server.reset_stats();
1075 out.ok();
1076 } else if is(sub, b"REWRITE") {
1077 return Err(Error::new(
1078 Code::Unsupported,
1079 "The server is running without a config file",
1080 ));
1081 } else if is(sub, b"HELP") {
1082 help(out, CONFIG_HELP);
1083 } else {
1084 return Err(args::unknown_subcommand(sub, "CONFIG"));
1085 }
1086 Ok(())
1087}
1088
1089// -------------------------------------------------------------------- INFO
1090
1091/// `INFO [section ...]`.
1092///
1093/// Every number in here is one this layer can actually answer. There is no
1094/// `rdb_last_save_time` because there is no save, and a field that is not there
1095/// is a client falling back rather than a client believing a zero.
1096///
1097/// The `CPU` section used to be missing for the same reason and is here now,
1098/// because nothing measured it and then something did. It is one `getrusage`
1099/// call in [`super::cpu`], and the reason it went in is that Redis's own
1100/// `unit/info-command` tests fail without it: a monitoring tool graphs
1101/// processor time against wall clock to decide whether a server is busy or
1102/// waiting, so an absent field there is a real hole and not a tidy omission.
1103fn info(server: &Server, args: Args<'_>, out: &mut Out) {
1104 // Redis keeps two lists: the sections a bare `INFO` hands back, and the ones
1105 // that have to be asked for by name or by `all`. `commandstats` is in the
1106 // second, along with `latencystats` and `errorstats`, because they grow with
1107 // the number of distinct commands a server has seen and a monitoring tool
1108 // polling `INFO` every second does not want them.
1109 //
1110 // `unit/info-command` is exactly this distinction written down: it asks for
1111 // `INFO default` and insists `rejected_calls` is not in the answer, then
1112 // asks for `INFO all` and insists that it is.
1113 let named = |section: &str| (1..args.len()).any(|i| is(args.get(i), section.as_bytes()));
1114 let everything = (1..args.len()).any(|i| {
1115 let a = args.get(i);
1116 is(a, b"all") || is(a, b"everything")
1117 });
1118 let by_default = args.len() == 1 || (1..args.len()).any(|i| is(args.get(i), b"default"));
1119 let want = |section: &str| by_default || everything || named(section);
1120 let extra = |section: &str| everything || named(section);
1121 // One string, built once and written once. It allocates, which is allowed
1122 // here and nowhere near the commands that count: `INFO` is a monitoring
1123 // call and it is not on the path M2 is measured on.
1124 let text = yo_alloc::allow(|| {
1125 let mut s = String::with_capacity(1024);
1126 if want("server") {
1127 let _ = write!(
1128 s,
1129 "# Server\r\nredis_version:{REPORTED_VERSION}\r\nyo_version:{}\r\n\
1130 redis_mode:standalone\r\narch_bits:{}\r\nprocess_id:0\r\n\
1131 run_id:0000000000000000000000000000000000000000\r\ntcp_port:0\r\n\
1132 uptime_in_seconds:{}\r\nio_threads_active:0\r\n\r\n",
1133 env!("CARGO_PKG_VERSION"),
1134 usize::BITS,
1135 server.uptime_secs(),
1136 );
1137 }
1138 if want("clients") {
1139 let _ = write!(
1140 s,
1141 "# Clients\r\nconnected_clients:{}\r\nblocked_clients:{}\r\n\
1142 pubsub_clients:{}\r\ncluster_connections:0\r\n\r\n",
1143 server.totals().clients,
1144 server.parked(),
1145 server.pubsub_counts().clients,
1146 );
1147 }
1148 if want("memory") {
1149 // Both the cap and the quarter of it, because the quarter is an
1150 // empirical number and somebody surprised by it should be able to
1151 // see what it was a quarter of without reading the source. The
1152 // reasoning is written out in `cap`.
1153 let cap = crate::cap::cap();
1154 let compact = server.compaction();
1155 // Read out of its stripe before the write, because an argument list
1156 // keeps every temporary in it alive until the whole call is over
1157 // and one of the other arguments walks that same stripe.
1158 let policy = server.settings().policy().name();
1159 let _ = write!(
1160 s,
1161 "# Memory\r\nused_memory:{}\r\nused_memory_dataset:{}\r\n\
1162 used_memory_overhead:{}\r\nmem_arena_bytes:{}\r\n\
1163 mem_arena_segments:{}\r\nmem_compact_walked:{}\r\n\
1164 mem_compact_moved:{}\r\nmem_compact_bytes:{}\r\n\
1165 mem_index_bytes:{}\r\n\
1166 mem_client_buffers:{}\r\ntotal_system_memory:{}\r\n\
1167 mem_cgroup_limit:{}\r\nmem_limit:{}\r\nmem_budget:{}\r\n\
1168 maxmemory:{}\r\nmaxmemory_policy:{}\r\n\
1169 maxstore:{}\r\nyo_store_bytes:{}\r\nyo_memory_regime:{}\r\n\r\n",
1170 server.memory_bytes(),
1171 server.dataset_bytes(),
1172 server.memory_bytes() - server.dataset_bytes(),
1173 server.arena_bytes(),
1174 server.segment_count(),
1175 compact.walked,
1176 compact.moved,
1177 compact.bytes,
1178 server.index_bytes(),
1179 server.conn_bytes(),
1180 cap.host.unwrap_or(0),
1181 cap.cgroup.unwrap_or(0),
1182 cap.limit().unwrap_or(0),
1183 cap.budget(),
1184 server.maxmemory(),
1185 policy,
1186 server.maxstore().map_or(-1, |n| n as i64),
1187 server.store_bytes(),
1188 server.regime(),
1189 );
1190 }
1191 if want("stats") {
1192 // The cold counters live here and not in the memory section,
1193 // because they are totals since the server started and everything
1194 // in that section is a level right now. `yo_cold_faults` over the
1195 // point reads a run issued is the ratio G9 is a gate on, and it
1196 // cannot be worked out from outside the server.
1197 let cold = server.cold_stats();
1198 let totals = server.totals();
1199 let subs = server.pubsub_counts();
1200 let _ = write!(
1201 s,
1202 "# Stats\r\ntotal_connections_received:{}\r\n\
1203 total_commands_processed:{}\r\nexpired_subkeys:{}\r\n\
1204 expired_subkeys_active:{}\r\nexpired_keys:{}\r\n\
1205 evicted_keys:{}\r\nkeyspace_hits:{}\r\nkeyspace_misses:{}\r\n\
1206 yo_cold_demoted:{}\r\nyo_cold_promoted:{}\r\n\
1207 yo_cold_faults:{}\r\nyo_cold_served:{}\r\nyo_cold_bytes_out:{}\r\n\
1208 yo_cold_bytes_in:{}\r\npubsub_channels:{}\r\n\
1209 pubsub_patterns:{}\r\npubsubshard_channels:{}\r\n\r\n",
1210 totals.connections,
1211 totals.commands,
1212 server.expired_fields(),
1213 server.expired_fields_active(),
1214 server.expired_keys(),
1215 server.evicted_keys(),
1216 server.keyspace_hits(),
1217 server.keyspace_misses(),
1218 cold.demoted,
1219 cold.promoted,
1220 cold.faults,
1221 cold.served,
1222 cold.bytes_out,
1223 cold.bytes_in,
1224 subs.channels,
1225 subs.patterns,
1226 subs.shard,
1227 );
1228 }
1229 if want("cpu") {
1230 // Two of Redis's six are not here. `used_cpu_sys_main_thread` and
1231 // `used_cpu_user_main_thread` need `RUSAGE_THREAD`, which is Linux
1232 // only, and reporting the process totals under a name that says
1233 // main thread would be right on a single threaded server and wrong
1234 // on the one this becomes.
1235 if let Some(u) = cpu::usage() {
1236 let _ = write!(
1237 s,
1238 "# CPU\r\nused_cpu_sys:{:.6}\r\nused_cpu_user:{:.6}\r\n\
1239 used_cpu_sys_children:{:.6}\r\nused_cpu_user_children:{:.6}\r\n\r\n",
1240 u.sys, u.user, u.sys_children, u.user_children,
1241 );
1242 }
1243 }
1244 if want("replication") {
1245 // Four fields out of Redis's dozen, and the eight that are missing
1246 // all describe the replication backlog, which is a thing that does
1247 // not exist here rather than a thing that is empty. The four that
1248 // are here are true of a server with no replica attached: it is the
1249 // master, nobody is following it, no failover is in progress and
1250 // nothing has been written to a stream that does not exist, which is
1251 // an offset of zero.
1252 s.push_str(
1253 "# Replication\r\nrole:master\r\nconnected_slaves:0\r\n\
1254 master_failover_state:no-failover\r\nmaster_repl_offset:0\r\n\r\n",
1255 );
1256 }
1257 if extra("commandstats") {
1258 s.push_str("# Commandstats\r\n");
1259 for (name, row) in server.command_stats() {
1260 let _ = write!(
1261 s,
1262 "cmdstat_{name}:calls={},rejected_calls={},failed_calls={}\r\n",
1263 row.calls, row.rejected, row.failed,
1264 );
1265 }
1266 s.push_str("\r\n");
1267 }
1268 if want("keyspace") {
1269 s.push_str("# Keyspace\r\n");
1270 for i in 0..DATABASES {
1271 let keys = server.dbs[i].len();
1272 if keys > 0 {
1273 // `avg_ttl` is still a zero, and Redis reports a zero there
1274 // too on a server that has never run its active expiry
1275 // cycle, because the number is a running estimate that cycle
1276 // produces rather than something anybody measures on demand.
1277 let expires = server.dbs[i].expires();
1278 let _ = write!(s, "db{i}:keys={keys},expires={expires},avg_ttl=0\r\n");
1279 }
1280 }
1281 s.push_str("\r\n");
1282 }
1283 s
1284 });
1285 out.verbatim(b"txt", text.as_bytes());
1286}
1287
1288// -------------------------------------------------------------------- help
1289
1290/// The `HELP` reply, which is an array of simple strings on both protocols.
1291pub(super) fn help(out: &mut Out, lines: &[&str]) {
1292 out.array(lines.len());
1293 for line in lines {
1294 out.simple(line.as_bytes());
1295 }
1296}
1297
1298/// What `COMMAND HELP` says.
1299const COMMAND_HELP: &[&str] = &[
1300 "COMMAND <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
1301 "(no subcommand)",
1302 " Return details about all commands.",
1303 "COUNT",
1304 " Return the total number of commands in this server.",
1305 "LIST [FILTERBY <MODULE <module-name>|ACLCAT <category>|PATTERN <pattern>>]",
1306 " Return a list of all commands in this server.",
1307 "INFO [<command-name> ...]",
1308 " Return details about multiple commands.",
1309 "DOCS [<command-name> ...]",
1310 " Return documentation details about multiple commands.",
1311 "GETKEYS <full-command>",
1312 " Return the keys from a full command.",
1313 "HELP",
1314 " Print this help.",
1315];
1316
1317/// What `CONFIG HELP` says.
1318const CONFIG_HELP: &[&str] = &[
1319 "CONFIG <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
1320 "GET <pattern>",
1321 " Return parameters matching the glob-like <pattern> and their values.",
1322 "SET <directive> <value>",
1323 " Set the configuration <directive> to <value>.",
1324 "RESETSTAT",
1325 " Reset statistics reported by the INFO command.",
1326 "REWRITE",
1327 " Rewrite the configuration file.",
1328 "HELP",
1329 " Print this help.",
1330];