Skip to main content

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