yo-kv 0.3.29

The Redis data structures, as plain Rust types with no protocol attached
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! Walking the keyspace: `SCAN`, `KEYS` and `RANDOMKEY`.
//!
//! Three commands that all want the same thing, which is to look at keys the
//! caller has not named, and that want it in three different shapes. `KEYS`
//! wants every key now and does not care what it costs. `SCAN` wants a bounded
//! bite and a number it can come back with. `RANDOMKEY` wants one key and does
//! not want to look at the others to find it.
//!
//! # A walk reaps what it walks past
//!
//! A key past its deadline is skipped, and then it is deleted once the walk has
//! finished. It cannot be deleted while the walk is running, because the walk
//! holds a shared borrow of the index for as long as it is handing out borrowed
//! keys, so the dead names go in a list and the deletes happen after. The list
//! is empty in the ordinary case and only ever holds keys that were already
//! dead, so a database with nothing expiring in it allocates nothing.
//!
//! This matters because there is no active expiry cycle yet, so a key nobody
//! reads again is a key nobody collects. Without the reap, `SET k v PX 50` and
//! a wait would leave `DBSIZE` answering one more than Redis answers, which is
//! the difference a side by side run against 8.10.1 actually turned up. Redis
//! collects that key from its own cycle within a hundred milliseconds, and a
//! walk finding it is the closest thing we have until the cycle lands in M5.
//!
//! The cost is that these three take `&mut self` rather than `&self`, which is
//! what every other read in this crate already takes for the same reason.
//!
//! # `COUNT` is a floor
//!
//! [`Keyspace::scan`] stops at the first bucket boundary past the budget, so a
//! `COUNT 10` can come back with fifteen keys, and a `MATCH` that rejects all
//! of them can come back with none and a cursor that is not zero. That is
//! Redis's behaviour exactly, and a client that treats an empty batch as the
//! end of the scan is broken against Redis too.
//!
//! The filtering happens in the caller's closure and the budget is counted
//! before it, so a `MATCH` that matches nothing still walks the whole keyspace
//! a bucket at a time rather than in one unbounded call.

use yo_index::Cursor as KeyCursor;

use crate::keyspace::Keyspace;
use crate::value::{self, Kind};

/// How many random buckets to try before giving up and walking the whole thing.
///
/// A bucket holds fourteen entries and the index keeps the table loaded, so the
/// first try finds a key in almost every database anyone has. The tries only
/// come into play when the keyspace is tiny or has just had most of it deleted,
/// and the walk behind them is what stops those cases from being wrong rather
/// than slow.
const TRIES: usize = 64;

impl Keyspace {
    /// A batch of keys, and where the next batch starts.
    ///
    /// This is `SCAN`. `budget` is `COUNT`, `ty` is `TYPE`, and `MATCH` belongs
    /// to the caller because a glob is a wire concern and this is not the wire.
    ///
    /// The cursor is opaque to the client and is not opaque here: it names a
    /// place in the keyspace rather than a place in memory, which is what lets
    /// it survive the index doubling between two calls. The reasoning is in
    /// [`yo_index::Cursor`].
    ///
    /// A key that is there for the whole scan comes back at least once. A key
    /// added or removed partway through may or may not, and any key may come
    /// back twice. That is Redis's contract and a client written against Redis
    /// already copes with all three.
    pub fn scan(
        &mut self,
        from: KeyCursor,
        budget: usize,
        ty: Option<Kind>,
        mut out: impl FnMut(&[u8]),
    ) -> KeyCursor {
        let now = self.clock.now_ms();
        let mut dead = Vec::new();
        let next = self.map.scan(from, budget, |key, rec| {
            if value::is_expired(rec, now) {
                dead.push(key.to_vec());
                return;
            }
            if ty.is_some_and(|want| value::kind(rec) != want) {
                return;
            }
            out(key);
        });
        self.reap_all(dead);
        next
    }

    /// Every key in the database, once each.
    ///
    /// This is `KEYS`, and it is the command whose reputation is deserved: it
    /// visits every bucket in the index before it answers anything, and a
    /// database of ten million keys is ten million calls to `out` with the
    /// shard doing nothing else. It is here because tooling needs it and
    /// because `SCAN` is the answer for everything else.
    ///
    /// One walk with an unbounded budget rather than a loop over [`Keyspace::scan`],
    /// which is the same walk without the chance of a duplicate, because nothing
    /// can split the index while this is running.
    pub fn keys(&mut self, out: impl FnMut(&[u8])) {
        self.scan(KeyCursor::START, usize::MAX, None, out);
    }

    /// Drop the dead keys a walk went past, now that the walk has let go.
    ///
    /// The count goes up by one per key, the same as a lazy reap on an ordinary
    /// read, because `INFO stats` reports one number for both and Redis counts
    /// its active cycle into it too.
    fn reap_all(&mut self, dead: Vec<Vec<u8>>) {
        for key in dead {
            self.reaped(&key);
        }
    }

    /// One key, chosen at random, or `None` if the database is empty.
    ///
    /// This is `RANDOMKEY`. It picks a random position in the index and takes a
    /// key from the bucket that lands in, which is a constant number of loads
    /// and does not depend on how many keys there are.
    ///
    /// Uniform within the bucket and only roughly uniform across the keyspace,
    /// since a bucket holding two keys and a bucket holding twelve are equally
    /// likely to be landed on. Redis's is biased the same way and for the same
    /// reason. What it is not is skewed towards any particular key, which is
    /// what matters for the thing `RANDOMKEY` is actually used for, which is
    /// sampling a live database to see what is in it.
    ///
    /// The answer borrows the database's scratch buffer, so it is good until the
    /// next call and the caller copies it if it wants to keep it. That is what
    /// takes the allocation off the command: sampling is a thing callers do in a
    /// loop, and a key name is a handful of bytes that used to cost a malloc and
    /// a free every time round.
    pub fn random_key(&mut self) -> Option<&[u8]> {
        if self.map.is_empty() {
            return None;
        }
        let mut found = false;
        for _ in 0..TRIES {
            let from = KeyCursor::from_raw(self.rng.next_u64());
            if self.sample(from, 0) {
                found = true;
                break;
            }
        }
        // Every bucket that was tried was empty or held nothing but dead keys.
        // A full walk is the only answer left that can tell an unlucky run of
        // tries from a database whose keys have all expired.
        if !found {
            found = self.sample(KeyCursor::START, usize::MAX);
        }
        // Written out rather than returned from inside the loop because the
        // answer borrows `self` and the next turn of the loop wants it back.
        found.then_some(self.scratch.as_slice())
    }

    /// Walk from `from` and leave one key in the scratch buffer, `true` if there
    /// was one.
    ///
    /// Reservoir sampling, which is the version that needs one pass and one
    /// slot of memory. Taking the first key instead would answer the same key
    /// every time for as long as the bucket held still.
    ///
    /// The slot is the scratch buffer, taken out for the walk and put back
    /// after it, because the closure has to hold it while `self.map` is
    /// borrowed by the scan.
    fn sample(&mut self, from: KeyCursor, budget: usize) -> bool {
        let now = self.clock.now_ms();
        // Named separately so the closure borrows the counter and not the whole
        // keyspace, which the walk is holding.
        let rng = &mut self.rng;
        let mut buf = std::mem::take(&mut self.scratch);
        let mut seen = 0usize;
        let mut found = false;
        let mut dead = Vec::new();
        self.map.scan(from, budget, |key, rec| {
            if value::is_expired(rec, now) {
                dead.push(key.to_vec());
                return;
            }
            seen += 1;
            if rng.below(seen) == 0 {
                buf.clear();
                buf.extend_from_slice(key);
                found = true;
            }
        });
        self.scratch = buf;
        self.reap_all(dead);
        found
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashSet;

    use super::*;
    use crate::Clock;

    fn db() -> Keyspace {
        Keyspace::with_clock(Clock::fixed(1_000_000))
    }

    fn put(d: &mut Keyspace, key: &[u8]) {
        d.set_plain(key, b"v").expect("room for a record");
    }

    fn keys_of(db: &mut Keyspace) -> HashSet<Vec<u8>> {
        let mut out = HashSet::new();
        db.keys(|k| {
            out.insert(k.to_vec());
        });
        out
    }

    #[test]
    fn an_empty_database_has_nothing_to_walk() {
        let mut db = db();
        assert!(keys_of(&mut db).is_empty());
        assert_eq!(db.random_key(), None);
        assert!(db.scan(KeyCursor::START, 10, None, |_| {}).is_end());
    }

    #[test]
    fn a_scan_comes_back_with_every_key_once() {
        let mut db = db();
        for i in 0..2_000u32 {
            put(&mut db, format!("k{i}").as_bytes());
        }

        let mut seen: Vec<Vec<u8>> = Vec::new();
        let mut at = KeyCursor::START;
        loop {
            at = db.scan(at, 10, None, |k| seen.push(k.to_vec()));
            if at.is_end() {
                break;
            }
        }

        let unique: HashSet<Vec<u8>> = seen.iter().cloned().collect();
        assert_eq!(unique.len(), 2_000);
        assert_eq!(seen.len(), 2_000, "a quiet scan returned a key twice");
        assert_eq!(unique, keys_of(&mut db));
    }

    #[test]
    fn a_scan_can_ask_for_one_type() {
        let mut db = db();
        put(&mut db, b"s");
        db.sadd(b"members", [b"a".as_slice()].into_iter())
            .expect("a fresh key");
        db.hset(b"h", [(b"f".as_slice(), b"v".as_slice())].into_iter())
            .expect("a fresh key");

        for (want, name) in [
            (Kind::String, "s"),
            (Kind::Set, "members"),
            (Kind::Hash, "h"),
        ] {
            let mut seen = Vec::new();
            let mut at = KeyCursor::START;
            loop {
                at = db.scan(at, 100, Some(want), |k| seen.push(k.to_vec()));
                if at.is_end() {
                    break;
                }
            }
            assert_eq!(seen, vec![name.as_bytes().to_vec()], "type {want:?}");
        }
    }

    #[test]
    fn a_key_past_its_deadline_is_collected_by_the_walk() {
        let mut db = db();
        put(&mut db, b"alive");
        put(&mut db, b"dead");
        assert!(db.set_expiry(b"dead", Some(1_000_500)));

        db.clock().advance(1_000);
        let before = db.expired_keys();
        assert_eq!(keys_of(&mut db), HashSet::from([b"alive".to_vec()]));
        // Gone from the map and counted, so `DBSIZE` after a walk answers what
        // Redis answers after its active cycle has been round.
        assert_eq!(db.len(), 1);
        assert_eq!(db.expired_keys(), before + 1);

        // And the walk is the only thing that touched it, so a second walk has
        // nothing left to collect and does not count it twice.
        assert_eq!(keys_of(&mut db), HashSet::from([b"alive".to_vec()]));
        assert_eq!(db.expired_keys(), before + 1);
    }

    #[test]
    fn a_random_key_is_a_key_that_is_there() {
        let mut db = db();
        for i in 0..500u32 {
            put(&mut db, format!("k{i}").as_bytes());
        }

        let all = keys_of(&mut db);
        let mut picked = HashSet::new();
        for _ in 0..200 {
            // Copied out, because the answer borrows the buffer the next draw
            // writes into.
            let k = db.random_key().expect("the database is not empty").to_vec();
            assert!(
                all.contains(&k),
                "randomkey answered a key that is not there"
            );
            picked.insert(k);
        }
        // Not a distribution test, just a check that it is not answering the
        // same key every time, which is what a walk that always takes the first
        // hit would do.
        assert!(
            picked.len() > 10,
            "only {} distinct keys in 200 draws",
            picked.len()
        );
    }

    /// `RANDOMKEY` used to hand back an owned key, which is a malloc and a free
    /// on a command whose whole job is to be called in a loop.
    #[test]
    fn randomkey_does_not_allocate() {
        let mut db = db();
        for i in 0..500u32 {
            put(&mut db, format!("k{i}").as_bytes());
        }
        // Nothing here has expired, so the walk never has a key to reap and the
        // only thing left that could allocate is the answer itself.
        let (_, allocs) = crate::tally::counted(|| {
            for _ in 0..200 {
                assert!(db.random_key().is_some(), "the database is not empty");
            }
        });
        assert_eq!(
            allocs, 0,
            "randomkey allocated {allocs} times in two hundred"
        );
    }

    #[test]
    fn the_last_key_left_is_the_one_randomkey_finds() {
        // The keys are how the directory comes to be far bigger than what is
        // left in it, and how many of them there were is not the claim, so
        // under Miri it comes down. Five hundred buckets holding one key is
        // the same fallback walk as five thousand.
        let n = crate::many(5_000u32);
        let keep = n / 2 + 42;
        let mut db = db();
        for i in 0..n {
            put(&mut db, format!("k{i}").as_bytes());
        }
        for i in 0..n {
            if i != keep {
                db.del(format!("k{i}").as_bytes());
            }
        }

        // One key in a directory that grew to hold thousands, so every random
        // try misses and the fallback walk is what answers.
        let left = format!("k{keep}");
        assert_eq!(db.random_key(), Some(left.as_bytes()));
    }

    #[test]
    fn a_scan_survives_the_keyspace_growing_underneath_it() {
        // Both counts are how the directory comes to rehash while a cursor is
        // out in it, and neither of them is the claim, so under Miri they come
        // down together. What has to stay true is that keys keep arriving for
        // as long as the cursor is out, because a directory that grew before
        // the scan started is not the case this is about.
        let start = crate::many(2_000u32);
        let per_round = crate::many(64);
        let mut db = db();
        for i in 0..start {
            put(&mut db, format!("k{i}").as_bytes());
        }

        let mut seen: HashSet<Vec<u8>> = HashSet::new();
        let mut at = KeyCursor::START;
        let mut added = start;
        loop {
            at = db.scan(at, 8, None, |k| {
                seen.insert(k.to_vec());
            });
            if at.is_end() {
                break;
            }
            for _ in 0..per_round {
                put(&mut db, format!("k{added}").as_bytes());
                added += 1;
            }
        }

        for i in 0..start {
            let k = format!("k{i}").into_bytes();
            assert!(
                seen.contains(&k),
                "k{i} was there throughout and never came back"
            );
        }
    }
}