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}