Skip to main content

kevy_rt/
exec_slowlog.rs

1//! `SLOWLOG` — per-shard slow-command ring buffer and the GET/LEN/RESET/HELP
2//! fan-out. Each shard owns its own [`SlowlogState`]; `SLOWLOG GET` and
3//! `SLOWLOG LEN` aggregate across shards, `SLOWLOG RESET` clears them all.
4//!
5//! Timing position: the inline fast-path and the forwarded `Shard::run_dispatch` path
6//! both measure `Instant::now()` around the dispatch call only (no AOF /
7//! WATCH / notify overhead is charged to the recorded micros). Records only
8//! when `state.slower_than_micros >= 0` AND elapsed micros strictly exceed
9//! the threshold (Redis semantics). When OFF (`-1`), `Instant::now()` is
10//! never called.
11
12use std::collections::VecDeque;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use crate::Commands;
16use crate::message::{Agg, Op, Part};
17use crate::shard::Shard;
18use kevy_resp::{ArgvView, encode_array_len, encode_bulk, encode_integer};
19
20/// One slow-command entry (Redis `SLOWLOG GET` field shape).
21#[derive(Debug, Clone)]
22pub struct SlowlogEntry {
23    /// Globally unique id (`(shard_id << 48) | local_seq`). Monotonic
24    /// per-shard; not globally monotonic (cross-shard SLOWLOG GET sorts
25    /// by timestamp DESC anyway).
26    pub id: u64,
27    /// Unix epoch seconds at the time the command finished.
28    pub timestamp_secs: i64,
29    /// Wall-clock execution time in microseconds.
30    pub micros: u64,
31    /// The command argv (up to [`MAX_ARGV_RECORDED`] elements). Each
32    /// element is owned bytes — the source `ArgvView` may not outlive
33    /// the ring.
34    pub argv: Vec<Vec<u8>>,
35    /// "ip:port" of the client, or empty when unknown. v1 always empty;
36    /// hooking sock.peer_addr() is left for a follow-up since the conn
37    /// doesn't currently track its own addr.
38    pub client_addr: Vec<u8>,
39    /// `CLIENT SETNAME` value, or empty. v1 always empty.
40    pub client_name: Vec<u8>,
41}
42
43/// Per-shard slowlog state — bundled into one field on [`Shard`] so the
44/// 4-field add doesn't worsen `shard.rs`'s already-over-cap LOC count.
45pub(crate) struct SlowlogState {
46    pub(crate) buf: VecDeque<SlowlogEntry>,
47    /// Record any command whose elapsed micros strictly exceed this
48    /// value. `-1` disables (hot-path checks this first → zero clock
49    /// reads); `0` records all (every commands' `Instant::now()`
50    /// difference is > 0).
51    pub(crate) slower_than_micros: i64,
52    /// Maximum entries kept; oldest evicted on insert overflow.
53    pub(crate) max_len: u32,
54    /// Local sequence counter. Packed with `shard_id` into the public
55    /// `id` so cross-shard merges retain uniqueness.
56    pub(crate) next_local_seq: u64,
57}
58
59impl SlowlogState {
60    pub(crate) fn new(slower_than_micros: i64, max_len: u32) -> Self {
61        Self {
62            buf: VecDeque::with_capacity(max_len.min(1024) as usize),
63            slower_than_micros,
64            max_len,
65            next_local_seq: 0,
66        }
67    }
68}
69
70/// How many argv elements to keep in a recorded entry. Mirrors Redis's
71/// `SLOWLOG_ENTRY_MAX_ARGC = 32` cap so a flood of huge MSET-like
72/// commands doesn't bloat the ring.
73const MAX_ARGV_RECORDED: usize = 32;
74
75/// Cap on per-argument byte length recorded. Mirrors Redis's
76/// `SLOWLOG_ENTRY_MAX_STRING = 128`.
77const MAX_ARG_BYTES_RECORDED: usize = 128;
78
79impl<C: Commands> Shard<C> {
80    /// Record a slow-command entry if `elapsed_micros` exceeds the
81    /// current threshold. Hot-path callers must early-out on the
82    /// `slower_than_micros < 0` check BEFORE taking the `Instant::now()`
83    /// pair; this function repeats the check defensively but does not
84    /// remove the clock read from the caller.
85    #[inline]
86    pub(crate) fn slowlog_record<A: ArgvView + ?Sized>(&mut self, args: &A, elapsed_micros: u64) {
87        let threshold = self.slowlog.slower_than_micros;
88        if threshold < 0 {
89            return;
90        }
91        // Skip strictly below threshold — `elapsed == threshold` records,
92        // matching Redis's `if (duration < slowlog_log_slower_than) return;`
93        // and making `slowlog-log-slower-than 0` record every command
94        // (including the sub-microsecond `as_micros() → 0` ones that hit
95        // in release-profile measurement).
96        if (elapsed_micros as i64) < threshold {
97            return;
98        }
99        let local_seq = self.slowlog.next_local_seq;
100        self.slowlog.next_local_seq = self.slowlog.next_local_seq.wrapping_add(1);
101        // Pack `(shard_id, local_seq)` so cross-shard ids stay unique.
102        // 16 bits for shard_id is plenty (kevy targets ≤ 256 cores).
103        let id = ((self.id as u64) << 48) | (local_seq & 0x0000_FFFF_FFFF_FFFF);
104        let timestamp_secs =
105            SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64);
106        let mut argv: Vec<Vec<u8>> = Vec::with_capacity(args.len().min(MAX_ARGV_RECORDED));
107        for i in 0..args.len().min(MAX_ARGV_RECORDED) {
108            let a = &args[i];
109            if a.len() > MAX_ARG_BYTES_RECORDED {
110                argv.push(a[..MAX_ARG_BYTES_RECORDED].to_vec());
111            } else {
112                argv.push(a.to_vec());
113            }
114        }
115        self.slowlog.buf.push_back(SlowlogEntry {
116            id,
117            timestamp_secs,
118            micros: elapsed_micros,
119            argv,
120            client_addr: Vec::new(),
121            client_name: Vec::new(),
122        });
123        let cap = self.slowlog.max_len as usize;
124        while self.slowlog.buf.len() > cap {
125            self.slowlog.buf.pop_front();
126        }
127    }
128
129    /// Dispatch a `SLOWLOG GET/LEN/RESET/HELP` request. Help short-circuits
130    /// to an immediate static reply; the other three fan out to every shard
131    /// using the standard `Agg`/`Part` pipeline.
132    pub(crate) fn start_slowlog(&mut self, conn_id: u64, seq: u64, sub: SlowlogSub) {
133        match sub {
134            SlowlogSub::Help => self.slowlog_immediate(conn_id, seq, slowlog_help_bytes()),
135            SlowlogSub::Err(b) => self.slowlog_immediate(conn_id, seq, b),
136            SlowlogSub::Reset => {
137                self.slowlog_fanout(conn_id, seq, Agg::AllOk, || Op::SlowlogReset);
138            }
139            SlowlogSub::Len => {
140                self.slowlog_fanout(conn_id, seq, Agg::SumInt(0), || Op::SlowlogLen);
141            }
142            SlowlogSub::Get(count) => self.slowlog_fanout(
143                conn_id,
144                seq,
145                Agg::SlowlogGet { count, entries: Vec::new() },
146                || Op::SlowlogGet,
147            ),
148        }
149    }
150
151    fn slowlog_immediate(&mut self, conn_id: u64, seq: u64, bytes: Vec<u8>) {
152        self.push_pending_slot(conn_id, 1, Agg::First(None), false);
153        self.fold(conn_id, seq, Part::Reply(crate::message::SmallReply::from_vec(bytes)));
154    }
155
156    fn slowlog_fanout(&mut self, conn_id: u64, seq: u64, agg: Agg, mk_op: impl Fn() -> Op) {
157        let targets: Vec<(usize, Op)> = (0..self.nshards).map(|s| (s, mk_op())).collect();
158        self.push_pending_slot(conn_id, targets.len() as u32, agg, false);
159        self.dispatch_targets(conn_id, seq, targets);
160    }
161}
162
163/// Parsed `SLOWLOG <sub> [args]` decision — picked at routing time so
164/// the runtime knows whether to fan out or short-circuit.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum SlowlogSub {
167    /// `SLOWLOG GET [count]`. `None` = use Redis default of 10. `Some(n)`
168    /// where `n < 0` means "all entries".
169    Get(Option<i64>),
170    /// `SLOWLOG LEN`.
171    Len,
172    /// `SLOWLOG RESET`.
173    Reset,
174    /// `SLOWLOG HELP`.
175    Help,
176    /// Routing-time error: malformed or unknown subcommand. The byte
177    /// slice carries the full RESP error reply (e.g. `-ERR ...\r\n`)
178    /// so dispatch is a one-step `Part::Reply`.
179    Err(Vec<u8>),
180}
181
182/// RESP encoding of a completed [`Agg::SlowlogGet`]. Each entry is a
183/// 6-element nested array per the Redis SLOWLOG GET wire spec:
184/// `[id, ts_secs, micros, argv-array, client_addr, client_name]`.
185/// Sorting is timestamp-DESC then id-DESC for ties; truncation to
186/// `count` (or default 10) happens last.
187pub(crate) fn encode_slowlog_get(count: Option<i64>, mut entries: Vec<SlowlogEntry>) -> Vec<u8> {
188    entries.sort_by(|a, b| b.timestamp_secs.cmp(&a.timestamp_secs).then_with(|| b.id.cmp(&a.id)));
189    let limit = match count {
190        None => 10,
191        Some(n) if n < 0 => entries.len(),
192        Some(n) => n as usize,
193    };
194    let n = entries.len().min(limit);
195    let mut out = Vec::with_capacity(64 + n * 64);
196    encode_array_len(&mut out, n as i64);
197    for e in entries.iter().take(n) {
198        encode_array_len(&mut out, 6);
199        encode_integer(&mut out, e.id as i64);
200        encode_integer(&mut out, e.timestamp_secs);
201        encode_integer(&mut out, e.micros as i64);
202        encode_array_len(&mut out, e.argv.len() as i64);
203        for a in &e.argv {
204            encode_bulk(&mut out, a);
205        }
206        encode_bulk(&mut out, &e.client_addr);
207        encode_bulk(&mut out, &e.client_name);
208    }
209    out
210}
211
212/// Static `SLOWLOG HELP` reply body (Redis text, lightly adapted).
213pub(crate) fn slowlog_help_bytes() -> Vec<u8> {
214    const LINES: &[&str] = &[
215        "SLOWLOG <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
216        "GET [<count>]",
217        "    Return top <count> entries from the slowlog (default: 10, -1 mean all).",
218        "    Entries are made of:",
219        "    id, timestamp, time in microseconds, arguments array, client IP and port,",
220        "    client name",
221        "LEN",
222        "    Return the length of the slowlog.",
223        "RESET",
224        "    Reset the slowlog.",
225        "HELP",
226        "    Print this help.",
227    ];
228    let mut out = Vec::with_capacity(512);
229    encode_array_len(&mut out, LINES.len() as i64);
230    for l in LINES {
231        encode_bulk(&mut out, l.as_bytes());
232    }
233    out
234}
235
236/// Parse `args` ( `[verb, sub, ...]` ) into a [`SlowlogSub`]. Verb name
237/// is assumed to already be SLOWLOG (the caller's route table dispatched
238/// to here). Embedders call this from their `Commands::resolve` /
239/// `Commands::route` impl.
240pub fn parse_slowlog_sub<A: ArgvView + ?Sized>(args: &A) -> SlowlogSub {
241    let Some(sub) = args.get(1) else {
242        return SlowlogSub::Err(slowlog_err_bytes("wrong number of arguments for 'slowlog'"));
243    };
244    let mut buf = [0u8; 16];
245    let upper = ascii_upper_into(sub, &mut buf);
246    match upper {
247        b"GET" => parse_slowlog_get(args),
248        b"LEN" if args.len() == 2 => SlowlogSub::Len,
249        b"RESET" if args.len() == 2 => SlowlogSub::Reset,
250        b"HELP" => SlowlogSub::Help,
251        b"LEN" | b"RESET" => SlowlogSub::Err(slowlog_arg_count_err(upper)),
252        _ => SlowlogSub::Err(slowlog_unknown_sub_err(sub)),
253    }
254}
255
256fn parse_slowlog_get<A: ArgvView + ?Sized>(args: &A) -> SlowlogSub {
257    if args.len() == 2 {
258        return SlowlogSub::Get(None);
259    }
260    if args.len() != 3 {
261        return SlowlogSub::Err(slowlog_err_bytes("wrong number of arguments for 'slowlog|get'"));
262    }
263    match std::str::from_utf8(&args[2]).ok().and_then(|s| s.parse::<i64>().ok()) {
264        Some(n) => SlowlogSub::Get(Some(n)),
265        None => SlowlogSub::Err(slowlog_err_bytes("value is not an integer or out of range")),
266    }
267}
268
269fn slowlog_arg_count_err(sub_upper: &[u8]) -> Vec<u8> {
270    let lower: String = sub_upper.iter().map(|b| b.to_ascii_lowercase() as char).collect();
271    slowlog_err_bytes(&format!("wrong number of arguments for 'slowlog|{lower}'"))
272}
273
274fn slowlog_unknown_sub_err(sub: &[u8]) -> Vec<u8> {
275    let msg = format!(
276        "ERR Unknown SLOWLOG subcommand or wrong number of arguments for '{}'",
277        String::from_utf8_lossy(sub),
278    );
279    let mut out = Vec::with_capacity(msg.len() + 3);
280    out.push(b'-');
281    out.extend_from_slice(msg.as_bytes());
282    out.extend_from_slice(b"\r\n");
283    out
284}
285
286fn slowlog_err_bytes(msg: &str) -> Vec<u8> {
287    let mut out = Vec::with_capacity(msg.len() + 7);
288    out.extend_from_slice(b"-ERR ");
289    out.extend_from_slice(msg.as_bytes());
290    out.extend_from_slice(b"\r\n");
291    out
292}
293
294fn ascii_upper_into<'a>(src: &[u8], buf: &'a mut [u8; 16]) -> &'a [u8] {
295    let n = src.len().min(buf.len());
296    for i in 0..n {
297        buf[i] = src[i].to_ascii_uppercase();
298    }
299    &buf[..n]
300}