Skip to main content

yo_kv/
walk.rs

1//! Walking the keyspace: `SCAN`, `KEYS` and `RANDOMKEY`.
2//!
3//! Three commands that all want the same thing, which is to look at keys the
4//! caller has not named, and that want it in three different shapes. `KEYS`
5//! wants every key now and does not care what it costs. `SCAN` wants a bounded
6//! bite and a number it can come back with. `RANDOMKEY` wants one key and does
7//! not want to look at the others to find it.
8//!
9//! # A walk reaps what it walks past
10//!
11//! A key past its deadline is skipped, and then it is deleted once the walk has
12//! finished. It cannot be deleted while the walk is running, because the walk
13//! holds a shared borrow of the index for as long as it is handing out borrowed
14//! keys, so the dead names go in a list and the deletes happen after. The list
15//! is empty in the ordinary case and only ever holds keys that were already
16//! dead, so a database with nothing expiring in it allocates nothing.
17//!
18//! This matters because there is no active expiry cycle yet, so a key nobody
19//! reads again is a key nobody collects. Without the reap, `SET k v PX 50` and
20//! a wait would leave `DBSIZE` answering one more than Redis answers, which is
21//! the difference a side by side run against 8.10.1 actually turned up. Redis
22//! collects that key from its own cycle within a hundred milliseconds, and a
23//! walk finding it is the closest thing we have until the cycle lands in M5.
24//!
25//! The cost is that these three take `&mut self` rather than `&self`, which is
26//! what every other read in this crate already takes for the same reason.
27//!
28//! # `COUNT` is a floor
29//!
30//! [`Keyspace::scan`] stops at the first bucket boundary past the budget, so a
31//! `COUNT 10` can come back with fifteen keys, and a `MATCH` that rejects all
32//! of them can come back with none and a cursor that is not zero. That is
33//! Redis's behaviour exactly, and a client that treats an empty batch as the
34//! end of the scan is broken against Redis too.
35//!
36//! The filtering happens in the caller's closure and the budget is counted
37//! before it, so a `MATCH` that matches nothing still walks the whole keyspace
38//! a bucket at a time rather than in one unbounded call.
39
40use yo_index::Cursor as KeyCursor;
41
42use crate::keyspace::Keyspace;
43use crate::value::{self, Kind};
44
45/// How many random buckets to try before giving up and walking the whole thing.
46///
47/// A bucket holds fourteen entries and the index keeps the table loaded, so the
48/// first try finds a key in almost every database anyone has. The tries only
49/// come into play when the keyspace is tiny or has just had most of it deleted,
50/// and the walk behind them is what stops those cases from being wrong rather
51/// than slow.
52const TRIES: usize = 64;
53
54impl Keyspace {
55    /// A batch of keys, and where the next batch starts.
56    ///
57    /// This is `SCAN`. `budget` is `COUNT`, `ty` is `TYPE`, and `MATCH` belongs
58    /// to the caller because a glob is a wire concern and this is not the wire.
59    ///
60    /// The cursor is opaque to the client and is not opaque here: it names a
61    /// place in the keyspace rather than a place in memory, which is what lets
62    /// it survive the index doubling between two calls. The reasoning is in
63    /// [`yo_index::Cursor`].
64    ///
65    /// A key that is there for the whole scan comes back at least once. A key
66    /// added or removed partway through may or may not, and any key may come
67    /// back twice. That is Redis's contract and a client written against Redis
68    /// already copes with all three.
69    pub fn scan(
70        &mut self,
71        from: KeyCursor,
72        budget: usize,
73        ty: Option<Kind>,
74        mut out: impl FnMut(&[u8]),
75    ) -> KeyCursor {
76        let now = self.clock.now_ms();
77        let mut dead = Vec::new();
78        let next = self.map.scan(from, budget, |key, rec| {
79            if value::is_expired(rec, now) {
80                dead.push(key.to_vec());
81                return;
82            }
83            if ty.is_some_and(|want| value::kind(rec) != want) {
84                return;
85            }
86            out(key);
87        });
88        self.reap_all(dead);
89        next
90    }
91
92    /// Every key in the database, once each.
93    ///
94    /// This is `KEYS`, and it is the command whose reputation is deserved: it
95    /// visits every bucket in the index before it answers anything, and a
96    /// database of ten million keys is ten million calls to `out` with the
97    /// shard doing nothing else. It is here because tooling needs it and
98    /// because `SCAN` is the answer for everything else.
99    ///
100    /// One walk with an unbounded budget rather than a loop over [`Keyspace::scan`],
101    /// which is the same walk without the chance of a duplicate, because nothing
102    /// can split the index while this is running.
103    pub fn keys(&mut self, out: impl FnMut(&[u8])) {
104        self.scan(KeyCursor::START, usize::MAX, None, out);
105    }
106
107    /// Drop the dead keys a walk went past, now that the walk has let go.
108    ///
109    /// The count goes up by one per key, the same as a lazy reap on an ordinary
110    /// read, because `INFO stats` reports one number for both and Redis counts
111    /// its active cycle into it too.
112    fn reap_all(&mut self, dead: Vec<Vec<u8>>) {
113        for key in dead {
114            self.reaped(&key);
115        }
116    }
117
118    /// One key, chosen at random, or `None` if the database is empty.
119    ///
120    /// This is `RANDOMKEY`. It picks a random position in the index and takes a
121    /// key from the bucket that lands in, which is a constant number of loads
122    /// and does not depend on how many keys there are.
123    ///
124    /// Uniform within the bucket and only roughly uniform across the keyspace,
125    /// since a bucket holding two keys and a bucket holding twelve are equally
126    /// likely to be landed on. Redis's is biased the same way and for the same
127    /// reason. What it is not is skewed towards any particular key, which is
128    /// what matters for the thing `RANDOMKEY` is actually used for, which is
129    /// sampling a live database to see what is in it.
130    ///
131    /// The answer borrows the database's scratch buffer, so it is good until the
132    /// next call and the caller copies it if it wants to keep it. That is what
133    /// takes the allocation off the command: sampling is a thing callers do in a
134    /// loop, and a key name is a handful of bytes that used to cost a malloc and
135    /// a free every time round.
136    pub fn random_key(&mut self) -> Option<&[u8]> {
137        if self.map.is_empty() {
138            return None;
139        }
140        let mut found = false;
141        for _ in 0..TRIES {
142            let from = KeyCursor::from_raw(self.rng.next_u64());
143            if self.sample(from, 0) {
144                found = true;
145                break;
146            }
147        }
148        // Every bucket that was tried was empty or held nothing but dead keys.
149        // A full walk is the only answer left that can tell an unlucky run of
150        // tries from a database whose keys have all expired.
151        if !found {
152            found = self.sample(KeyCursor::START, usize::MAX);
153        }
154        // Written out rather than returned from inside the loop because the
155        // answer borrows `self` and the next turn of the loop wants it back.
156        found.then_some(self.scratch.as_slice())
157    }
158
159    /// Walk from `from` and leave one key in the scratch buffer, `true` if there
160    /// was one.
161    ///
162    /// Reservoir sampling, which is the version that needs one pass and one
163    /// slot of memory. Taking the first key instead would answer the same key
164    /// every time for as long as the bucket held still.
165    ///
166    /// The slot is the scratch buffer, taken out for the walk and put back
167    /// after it, because the closure has to hold it while `self.map` is
168    /// borrowed by the scan.
169    fn sample(&mut self, from: KeyCursor, budget: usize) -> bool {
170        let now = self.clock.now_ms();
171        // Named separately so the closure borrows the counter and not the whole
172        // keyspace, which the walk is holding.
173        let rng = &mut self.rng;
174        let mut buf = std::mem::take(&mut self.scratch);
175        let mut seen = 0usize;
176        let mut found = false;
177        let mut dead = Vec::new();
178        self.map.scan(from, budget, |key, rec| {
179            if value::is_expired(rec, now) {
180                dead.push(key.to_vec());
181                return;
182            }
183            seen += 1;
184            if rng.below(seen) == 0 {
185                buf.clear();
186                buf.extend_from_slice(key);
187                found = true;
188            }
189        });
190        self.scratch = buf;
191        self.reap_all(dead);
192        found
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use std::collections::HashSet;
199
200    use super::*;
201    use crate::Clock;
202
203    fn db() -> Keyspace {
204        Keyspace::with_clock(Clock::fixed(1_000_000))
205    }
206
207    fn put(d: &mut Keyspace, key: &[u8]) {
208        d.set_plain(key, b"v").expect("room for a record");
209    }
210
211    fn keys_of(db: &mut Keyspace) -> HashSet<Vec<u8>> {
212        let mut out = HashSet::new();
213        db.keys(|k| {
214            out.insert(k.to_vec());
215        });
216        out
217    }
218
219    #[test]
220    fn an_empty_database_has_nothing_to_walk() {
221        let mut db = db();
222        assert!(keys_of(&mut db).is_empty());
223        assert_eq!(db.random_key(), None);
224        assert!(db.scan(KeyCursor::START, 10, None, |_| {}).is_end());
225    }
226
227    #[test]
228    fn a_scan_comes_back_with_every_key_once() {
229        let mut db = db();
230        for i in 0..2_000u32 {
231            put(&mut db, format!("k{i}").as_bytes());
232        }
233
234        let mut seen: Vec<Vec<u8>> = Vec::new();
235        let mut at = KeyCursor::START;
236        loop {
237            at = db.scan(at, 10, None, |k| seen.push(k.to_vec()));
238            if at.is_end() {
239                break;
240            }
241        }
242
243        let unique: HashSet<Vec<u8>> = seen.iter().cloned().collect();
244        assert_eq!(unique.len(), 2_000);
245        assert_eq!(seen.len(), 2_000, "a quiet scan returned a key twice");
246        assert_eq!(unique, keys_of(&mut db));
247    }
248
249    #[test]
250    fn a_scan_can_ask_for_one_type() {
251        let mut db = db();
252        put(&mut db, b"s");
253        db.sadd(b"members", [b"a".as_slice()].into_iter())
254            .expect("a fresh key");
255        db.hset(b"h", [(b"f".as_slice(), b"v".as_slice())].into_iter())
256            .expect("a fresh key");
257
258        for (want, name) in [
259            (Kind::String, "s"),
260            (Kind::Set, "members"),
261            (Kind::Hash, "h"),
262        ] {
263            let mut seen = Vec::new();
264            let mut at = KeyCursor::START;
265            loop {
266                at = db.scan(at, 100, Some(want), |k| seen.push(k.to_vec()));
267                if at.is_end() {
268                    break;
269                }
270            }
271            assert_eq!(seen, vec![name.as_bytes().to_vec()], "type {want:?}");
272        }
273    }
274
275    #[test]
276    fn a_key_past_its_deadline_is_collected_by_the_walk() {
277        let mut db = db();
278        put(&mut db, b"alive");
279        put(&mut db, b"dead");
280        assert!(db.set_expiry(b"dead", Some(1_000_500)));
281
282        db.clock().advance(1_000);
283        let before = db.expired_keys();
284        assert_eq!(keys_of(&mut db), HashSet::from([b"alive".to_vec()]));
285        // Gone from the map and counted, so `DBSIZE` after a walk answers what
286        // Redis answers after its active cycle has been round.
287        assert_eq!(db.len(), 1);
288        assert_eq!(db.expired_keys(), before + 1);
289
290        // And the walk is the only thing that touched it, so a second walk has
291        // nothing left to collect and does not count it twice.
292        assert_eq!(keys_of(&mut db), HashSet::from([b"alive".to_vec()]));
293        assert_eq!(db.expired_keys(), before + 1);
294    }
295
296    #[test]
297    fn a_random_key_is_a_key_that_is_there() {
298        let mut db = db();
299        for i in 0..500u32 {
300            put(&mut db, format!("k{i}").as_bytes());
301        }
302
303        let all = keys_of(&mut db);
304        let mut picked = HashSet::new();
305        for _ in 0..200 {
306            // Copied out, because the answer borrows the buffer the next draw
307            // writes into.
308            let k = db.random_key().expect("the database is not empty").to_vec();
309            assert!(
310                all.contains(&k),
311                "randomkey answered a key that is not there"
312            );
313            picked.insert(k);
314        }
315        // Not a distribution test, just a check that it is not answering the
316        // same key every time, which is what a walk that always takes the first
317        // hit would do.
318        assert!(
319            picked.len() > 10,
320            "only {} distinct keys in 200 draws",
321            picked.len()
322        );
323    }
324
325    /// `RANDOMKEY` used to hand back an owned key, which is a malloc and a free
326    /// on a command whose whole job is to be called in a loop.
327    #[test]
328    fn randomkey_does_not_allocate() {
329        let mut db = db();
330        for i in 0..500u32 {
331            put(&mut db, format!("k{i}").as_bytes());
332        }
333        // Nothing here has expired, so the walk never has a key to reap and the
334        // only thing left that could allocate is the answer itself.
335        let (_, allocs) = crate::tally::counted(|| {
336            for _ in 0..200 {
337                assert!(db.random_key().is_some(), "the database is not empty");
338            }
339        });
340        assert_eq!(
341            allocs, 0,
342            "randomkey allocated {allocs} times in two hundred"
343        );
344    }
345
346    #[test]
347    fn the_last_key_left_is_the_one_randomkey_finds() {
348        let mut db = db();
349        for i in 0..5_000u32 {
350            put(&mut db, format!("k{i}").as_bytes());
351        }
352        for i in 0..5_000u32 {
353            if i != 4_242 {
354                db.del(format!("k{i}").as_bytes());
355            }
356        }
357
358        // One key in a directory that grew to hold five thousand, so every
359        // random try misses and the fallback walk is what answers.
360        assert_eq!(db.random_key(), Some(&b"k4242"[..]));
361    }
362
363    #[test]
364    fn a_scan_survives_the_keyspace_growing_underneath_it() {
365        let mut db = db();
366        for i in 0..2_000u32 {
367            put(&mut db, format!("k{i}").as_bytes());
368        }
369
370        let mut seen: HashSet<Vec<u8>> = HashSet::new();
371        let mut at = KeyCursor::START;
372        let mut added = 2_000u32;
373        loop {
374            at = db.scan(at, 8, None, |k| {
375                seen.insert(k.to_vec());
376            });
377            if at.is_end() {
378                break;
379            }
380            for _ in 0..64 {
381                put(&mut db, format!("k{added}").as_bytes());
382                added += 1;
383            }
384        }
385
386        for i in 0..2_000u32 {
387            let k = format!("k{i}").into_bytes();
388            assert!(
389                seen.contains(&k),
390                "k{i} was there throughout and never came back"
391            );
392        }
393    }
394}