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}