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.drop_key(&key);
115            self.expired += 1;
116        }
117    }
118
119    /// One key, chosen at random, or `None` if the database is empty.
120    ///
121    /// This is `RANDOMKEY`. It picks a random position in the index and takes a
122    /// key from the bucket that lands in, which is a constant number of loads
123    /// and does not depend on how many keys there are.
124    ///
125    /// Uniform within the bucket and only roughly uniform across the keyspace,
126    /// since a bucket holding two keys and a bucket holding twelve are equally
127    /// likely to be landed on. Redis's is biased the same way and for the same
128    /// reason. What it is not is skewed towards any particular key, which is
129    /// what matters for the thing `RANDOMKEY` is actually used for, which is
130    /// sampling a live database to see what is in it.
131    ///
132    /// The answer borrows the database's scratch buffer, so it is good until the
133    /// next call and the caller copies it if it wants to keep it. That is what
134    /// takes the allocation off the command: sampling is a thing callers do in a
135    /// loop, and a key name is a handful of bytes that used to cost a malloc and
136    /// a free every time round.
137    pub fn random_key(&mut self) -> Option<&[u8]> {
138        if self.map.is_empty() {
139            return None;
140        }
141        let mut found = false;
142        for _ in 0..TRIES {
143            let from = KeyCursor::from_raw(self.rng.next_u64());
144            if self.sample(from, 0) {
145                found = true;
146                break;
147            }
148        }
149        // Every bucket that was tried was empty or held nothing but dead keys.
150        // A full walk is the only answer left that can tell an unlucky run of
151        // tries from a database whose keys have all expired.
152        if !found {
153            found = self.sample(KeyCursor::START, usize::MAX);
154        }
155        // Written out rather than returned from inside the loop because the
156        // answer borrows `self` and the next turn of the loop wants it back.
157        found.then_some(self.scratch.as_slice())
158    }
159
160    /// Walk from `from` and leave one key in the scratch buffer, `true` if there
161    /// was one.
162    ///
163    /// Reservoir sampling, which is the version that needs one pass and one
164    /// slot of memory. Taking the first key instead would answer the same key
165    /// every time for as long as the bucket held still.
166    ///
167    /// The slot is the scratch buffer, taken out for the walk and put back
168    /// after it, because the closure has to hold it while `self.map` is
169    /// borrowed by the scan.
170    fn sample(&mut self, from: KeyCursor, budget: usize) -> bool {
171        let now = self.clock.now_ms();
172        // Named separately so the closure borrows the counter and not the whole
173        // keyspace, which the walk is holding.
174        let rng = &mut self.rng;
175        let mut buf = std::mem::take(&mut self.scratch);
176        let mut seen = 0usize;
177        let mut found = false;
178        let mut dead = Vec::new();
179        self.map.scan(from, budget, |key, rec| {
180            if value::is_expired(rec, now) {
181                dead.push(key.to_vec());
182                return;
183            }
184            seen += 1;
185            if rng.below(seen) == 0 {
186                buf.clear();
187                buf.extend_from_slice(key);
188                found = true;
189            }
190        });
191        self.scratch = buf;
192        self.reap_all(dead);
193        found
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use std::collections::HashSet;
200
201    use super::*;
202    use crate::Clock;
203
204    fn db() -> Keyspace {
205        Keyspace::with_clock(Clock::fixed(1_000_000))
206    }
207
208    fn put(d: &mut Keyspace, key: &[u8]) {
209        d.set_plain(key, b"v").expect("room for a record");
210    }
211
212    fn keys_of(db: &mut Keyspace) -> HashSet<Vec<u8>> {
213        let mut out = HashSet::new();
214        db.keys(|k| {
215            out.insert(k.to_vec());
216        });
217        out
218    }
219
220    #[test]
221    fn an_empty_database_has_nothing_to_walk() {
222        let mut db = db();
223        assert!(keys_of(&mut db).is_empty());
224        assert_eq!(db.random_key(), None);
225        assert!(db.scan(KeyCursor::START, 10, None, |_| {}).is_end());
226    }
227
228    #[test]
229    fn a_scan_comes_back_with_every_key_once() {
230        let mut db = db();
231        for i in 0..2_000u32 {
232            put(&mut db, format!("k{i}").as_bytes());
233        }
234
235        let mut seen: Vec<Vec<u8>> = Vec::new();
236        let mut at = KeyCursor::START;
237        loop {
238            at = db.scan(at, 10, None, |k| seen.push(k.to_vec()));
239            if at.is_end() {
240                break;
241            }
242        }
243
244        let unique: HashSet<Vec<u8>> = seen.iter().cloned().collect();
245        assert_eq!(unique.len(), 2_000);
246        assert_eq!(seen.len(), 2_000, "a quiet scan returned a key twice");
247        assert_eq!(unique, keys_of(&mut db));
248    }
249
250    #[test]
251    fn a_scan_can_ask_for_one_type() {
252        let mut db = db();
253        put(&mut db, b"s");
254        db.sadd(b"members", [b"a".as_slice()].into_iter())
255            .expect("a fresh key");
256        db.hset(b"h", [(b"f".as_slice(), b"v".as_slice())].into_iter())
257            .expect("a fresh key");
258
259        for (want, name) in [
260            (Kind::String, "s"),
261            (Kind::Set, "members"),
262            (Kind::Hash, "h"),
263        ] {
264            let mut seen = Vec::new();
265            let mut at = KeyCursor::START;
266            loop {
267                at = db.scan(at, 100, Some(want), |k| seen.push(k.to_vec()));
268                if at.is_end() {
269                    break;
270                }
271            }
272            assert_eq!(seen, vec![name.as_bytes().to_vec()], "type {want:?}");
273        }
274    }
275
276    #[test]
277    fn a_key_past_its_deadline_is_collected_by_the_walk() {
278        let mut db = db();
279        put(&mut db, b"alive");
280        put(&mut db, b"dead");
281        assert!(db.set_expiry(b"dead", Some(1_000_500)));
282
283        db.clock().advance(1_000);
284        let before = db.expired_keys();
285        assert_eq!(keys_of(&mut db), HashSet::from([b"alive".to_vec()]));
286        // Gone from the map and counted, so `DBSIZE` after a walk answers what
287        // Redis answers after its active cycle has been round.
288        assert_eq!(db.len(), 1);
289        assert_eq!(db.expired_keys(), before + 1);
290
291        // And the walk is the only thing that touched it, so a second walk has
292        // nothing left to collect and does not count it twice.
293        assert_eq!(keys_of(&mut db), HashSet::from([b"alive".to_vec()]));
294        assert_eq!(db.expired_keys(), before + 1);
295    }
296
297    #[test]
298    fn a_random_key_is_a_key_that_is_there() {
299        let mut db = db();
300        for i in 0..500u32 {
301            put(&mut db, format!("k{i}").as_bytes());
302        }
303
304        let all = keys_of(&mut db);
305        let mut picked = HashSet::new();
306        for _ in 0..200 {
307            // Copied out, because the answer borrows the buffer the next draw
308            // writes into.
309            let k = db.random_key().expect("the database is not empty").to_vec();
310            assert!(
311                all.contains(&k),
312                "randomkey answered a key that is not there"
313            );
314            picked.insert(k);
315        }
316        // Not a distribution test, just a check that it is not answering the
317        // same key every time, which is what a walk that always takes the first
318        // hit would do.
319        assert!(
320            picked.len() > 10,
321            "only {} distinct keys in 200 draws",
322            picked.len()
323        );
324    }
325
326    /// `RANDOMKEY` used to hand back an owned key, which is a malloc and a free
327    /// on a command whose whole job is to be called in a loop.
328    #[test]
329    fn randomkey_does_not_allocate() {
330        let mut db = db();
331        for i in 0..500u32 {
332            put(&mut db, format!("k{i}").as_bytes());
333        }
334        // Nothing here has expired, so the walk never has a key to reap and the
335        // only thing left that could allocate is the answer itself.
336        let (_, allocs) = crate::tally::counted(|| {
337            for _ in 0..200 {
338                assert!(db.random_key().is_some(), "the database is not empty");
339            }
340        });
341        assert_eq!(
342            allocs, 0,
343            "randomkey allocated {allocs} times in two hundred"
344        );
345    }
346
347    #[test]
348    fn the_last_key_left_is_the_one_randomkey_finds() {
349        let mut db = db();
350        for i in 0..5_000u32 {
351            put(&mut db, format!("k{i}").as_bytes());
352        }
353        for i in 0..5_000u32 {
354            if i != 4_242 {
355                db.del(format!("k{i}").as_bytes());
356            }
357        }
358
359        // One key in a directory that grew to hold five thousand, so every
360        // random try misses and the fallback walk is what answers.
361        assert_eq!(db.random_key(), Some(&b"k4242"[..]));
362    }
363
364    #[test]
365    fn a_scan_survives_the_keyspace_growing_underneath_it() {
366        let mut db = db();
367        for i in 0..2_000u32 {
368            put(&mut db, format!("k{i}").as_bytes());
369        }
370
371        let mut seen: HashSet<Vec<u8>> = HashSet::new();
372        let mut at = KeyCursor::START;
373        let mut added = 2_000u32;
374        loop {
375            at = db.scan(at, 8, None, |k| {
376                seen.insert(k.to_vec());
377            });
378            if at.is_end() {
379                break;
380            }
381            for _ in 0..64 {
382                put(&mut db, format!("k{added}").as_bytes());
383                added += 1;
384            }
385        }
386
387        for i in 0..2_000u32 {
388            let k = format!("k{i}").into_bytes();
389            assert!(
390                seen.contains(&k),
391                "k{i} was there throughout and never came back"
392            );
393        }
394    }
395}