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}