Skip to main content

yo_index/
scan.rs

1//! Walking the whole index while it is being written to.
2//!
3//! `KEYS` can walk the index in one go and be done with it. `SCAN` cannot: it
4//! hands the client a number, the client goes away and does something else, and
5//! then it comes back and expects the walk to carry on. Between those two calls
6//! the index may have doubled its directory and split any number of segments,
7//! and the promise `SCAN` makes has to survive all of it.
8//!
9//! The promise is one sided and worth stating exactly, because half of what
10//! makes it implementable is what it does not say. A key that is there for the
11//! whole walk is returned at least once. A key added or removed partway through
12//! may or may not appear. A key may appear twice. That is Redis's contract and
13//! it is the contract here.
14//!
15//! # The prefix does not move
16//!
17//! Redis walks a power of two table and doubles it by adding a bit at the top of
18//! the bucket index, so a bucket that was `i` becomes `i` and `i + n`. That is
19//! why its cursor counts in reverse binary: it is the only order in which the
20//! two halves of a split bucket stay next to each other.
21//!
22//! This index doubles the other way round. [`Index::dir_index`] takes the top
23//! `global_depth` bits below the tag, and doubling the directory copies each
24//! entry to two neighbouring slots, so a directory index `d` becomes `2d` and
25//! `2d + 1`. A bit is added at the bottom, not the top.
26//!
27//! That makes the cursor simple, because there is a number that does not move at
28//! all. Take the full 48 bits the directory could ever use, left aligned, and
29//! call it the prefix. It is a function of the key's hash and nothing else, so
30//! it is the same number before a doubling and after one, and the directory
31//! index at any depth is just the top `global_depth` bits of it. Walk in
32//! increasing prefix order and the boundary between what has been seen and what
33//! has not is a number that means the same thing in every version of the index
34//! this walk will ever see.
35//!
36//! # What a split does
37//!
38//! A segment covers a contiguous run of prefixes. Splitting it cuts that run in
39//! half and gives the top half to a new segment, which is a change to where the
40//! keys live and not to any key's prefix.
41//!
42//! If the cursor is partway through a segment when it splits, the walk resumes
43//! in the half that still holds the cursor's prefix, finishes it, and then
44//! starts the other half from its first bucket. Keys in the top half that had
45//! already been returned are returned again. That is the duplicate the contract
46//! allows, and it is the price of never having to stop the world.
47//!
48//! # The shape of the number
49//!
50//! ```text
51//!  63                              16 15      6 5        0
52//! +----------------------------------+---------+----------+
53//! |         directory prefix         |    0    |  bucket  |
54//! +----------------------------------+---------+----------+
55//! ```
56//!
57//! The bucket within a segment comes off the bottom of the hash and the prefix
58//! comes off the top, so the two never overlap and a split cannot move a key
59//! from one bucket to another. The ten bits in the middle are spare. They are
60//! not padding for its own sake: a segment is 64 buckets today and the day it is
61//! not, the field grows into them without the cursors clients are holding
62//! meaning something different.
63//!
64//! Zero is both the start and the end, which is Redis's convention and is not an
65//! ambiguity in practice: a walk that has finished says zero, and a client that
66//! says zero is starting a new one.
67
68/// How far a scan has got, and the number the client holds between calls.
69///
70/// It is a position in the keyspace and not a position in memory. Two calls a
71/// week apart with the same cursor resume at the same place, even if every
72/// segment in the index has split in between.
73#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
74pub struct Cursor(u64);
75
76/// Bits the directory can ever use, which is the index's `MAX_DEPTH`.
77pub(crate) const PREFIX_BITS: u32 = 48;
78
79/// Where the prefix sits in the cursor, leaving room below it for the bucket.
80pub(crate) const PREFIX_SHIFT: u32 = 16;
81
82/// Bits of bucket index, which is `log2` of [`SEGMENT_BUCKETS`](super::SEGMENT_BUCKETS).
83pub(crate) const BUCKET_BITS: u32 = 6;
84
85impl Cursor {
86    /// The start of a walk, and the same value as the end of one.
87    pub const START: Cursor = Cursor(0);
88
89    /// The cursor a client sent, whatever it sent.
90    ///
91    /// Any number is a valid cursor. A made up one resumes somewhere arbitrary
92    /// and answers keys from there, which is what Redis does and is the only
93    /// behaviour that does not require the server to remember every cursor it
94    /// has ever handed out.
95    #[must_use]
96    pub const fn from_raw(raw: u64) -> Cursor {
97        Cursor(raw)
98    }
99
100    /// The number to hand the client.
101    #[must_use]
102    pub const fn raw(self) -> u64 {
103        self.0
104    }
105
106    /// Whether the walk is over.
107    #[must_use]
108    pub const fn is_end(self) -> bool {
109        self.0 == 0
110    }
111
112    /// The prefix half, which says which segment.
113    #[must_use]
114    pub(crate) const fn prefix(self) -> u64 {
115        (self.0 >> PREFIX_SHIFT) & ((1 << PREFIX_BITS) - 1)
116    }
117
118    /// The bucket half, which says where in the segment.
119    #[must_use]
120    pub(crate) const fn bucket(self) -> usize {
121        (self.0 & ((1 << BUCKET_BITS) - 1)) as usize
122    }
123
124    /// Put the two halves back together.
125    ///
126    /// A prefix that has run off the top of its 48 bits means the last segment
127    /// is done, which is the end of the walk and therefore zero.
128    #[must_use]
129    pub(crate) const fn at(prefix: u64, bucket: usize) -> Cursor {
130        if prefix >= (1 << PREFIX_BITS) {
131            return Cursor::START;
132        }
133        Cursor((prefix << PREFIX_SHIFT) | (bucket as u64 & ((1 << BUCKET_BITS) - 1)))
134    }
135
136    /// The prefix of a key, which is the part of its hash the directory reads.
137    ///
138    /// Left aligned into the full 48 bits rather than into `global_depth` of
139    /// them, which is the whole trick: this number is the same before a doubling
140    /// and after one.
141    ///
142    /// Only the tests need this. The walk itself never goes from a key to a
143    /// cursor, it only ever goes forward from the cursor it was handed, so this
144    /// is the statement of the invariant rather than a step in the code.
145    #[cfg(test)]
146    #[must_use]
147    pub(crate) const fn prefix_of(hash: u64) -> u64 {
148        (hash >> (super::index::DIR_BITS - PREFIX_BITS)) & ((1 << PREFIX_BITS) - 1)
149    }
150}
151
152impl From<u64> for Cursor {
153    fn from(raw: u64) -> Cursor {
154        Cursor::from_raw(raw)
155    }
156}
157
158impl From<Cursor> for u64 {
159    fn from(c: Cursor) -> u64 {
160        c.raw()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn the_two_halves_survive_the_round_trip() {
170        for prefix in [0u64, 1, 255, (1 << PREFIX_BITS) - 1] {
171            for bucket in [0usize, 1, 63] {
172                let c = Cursor::at(prefix, bucket);
173                assert_eq!(c.prefix(), prefix, "prefix {prefix} bucket {bucket}");
174                assert_eq!(c.bucket(), bucket, "prefix {prefix} bucket {bucket}");
175            }
176        }
177    }
178
179    #[test]
180    fn a_prefix_past_the_end_is_the_end() {
181        assert_eq!(Cursor::at(1 << PREFIX_BITS, 0), Cursor::START);
182        assert!(Cursor::at(1 << PREFIX_BITS, 7).is_end());
183        // And zero with a bucket in it is not the end, because a walk that has
184        // done one bucket of the first segment has not finished.
185        assert!(!Cursor::at(0, 1).is_end());
186    }
187
188    #[test]
189    fn a_prefix_is_the_directory_index_at_every_depth() {
190        // What the index does at depth g, spelled out here rather than reached
191        // through a private method, so the two are checked against each other.
192        let hash = 0x1234_5678_9abc_def0u64;
193        let prefix = Cursor::prefix_of(hash);
194        for g in 1..=16u32 {
195            let dir_bits = super::super::index::DIR_BITS;
196            let want = (hash >> (dir_bits - g)) & ((1 << g) - 1);
197            assert_eq!(prefix >> (PREFIX_BITS - g), want, "depth {g}");
198        }
199    }
200
201    #[test]
202    fn the_cursor_a_client_holds_is_just_a_number() {
203        let c = Cursor::from_raw(0x0001_0000_0000_002a);
204        assert_eq!(u64::from(c), 0x0001_0000_0000_002a);
205        assert_eq!(Cursor::from(7u64).bucket(), 7);
206    }
207}