Skip to main content

yo_kv/
scan.rs

1//! The scan cursor, and what it has to survive.
2//!
3//! `SCAN`, `SSCAN`, `HSCAN` and `ZSCAN` all hand the client an opaque number and
4//! promise something quite specific about what happens when it comes back. Every
5//! element that was there for the whole scan is returned at least once. An
6//! element that arrived or left during the scan may or may not be. The same
7//! element may be returned more than once, and the client is expected to cope.
8//!
9//! Redis buys that guarantee with reverse binary iteration over a bucket array,
10//! because its table rehashes under the scan and buckets split. Ours is a
11//! different structure and needs a different trick, which is K9's downward
12//! cursor, `((P << 52) | (part << 40) | (idx + 1))`.
13//!
14//! # Why downward
15//!
16//! A collection is a dense array of rows in insertion order. Two things move a
17//! row: an insert appends at the top, and a removal moves the top row down into
18//! the hole it made. Nothing else moves anything.
19//!
20//! Walk that array downward and both of those are harmless. An insert lands
21//! above the cursor, in the part already walked, so a member added during the
22//! scan is simply not returned, which is allowed. A removal moves the top row,
23//! which is also above the cursor and so already returned, down into the hole.
24//! If the hole is below the cursor that member is returned a second time, which
25//! is allowed. What cannot happen is a member below the cursor being lifted above
26//! it, because nothing ever moves a row upward, and that is exactly the case the
27//! guarantee forbids.
28//!
29//! Walking upward has none of that. A removal at the top would drop an unvisited
30//! member into a visited position and it would never be returned, which is the
31//! bug the guarantee exists to rule out.
32//!
33//! # Why the partition count is in the cursor
34//!
35//! Above 262,144 elements a collection is partitioned (`05` §4.3), and it can
36//! gain partitions while a client is halfway through scanning it. A member lives
37//! in the partition its hash's low bits name, so growing from `P` to `2P` splits
38//! each old partition in two and moves nothing else. Carrying `P` in the cursor
39//! is what lets the resume work out which of the new partitions the client has
40//! already been through: everything whose low `log2(P)` bits are above the
41//! partition it stopped in. See [`Cursor::rebase`], which is the whole of that
42//! arithmetic and is written and tested here even though the partitioned band
43//! itself lands later, because a cursor format that gets this wrong is a wire
44//! format that has already shipped.
45
46/// Where a scan stopped, as the client sees it.
47///
48/// Opaque to the client, and deliberately so, but not opaque in here: it is a
49/// partition count, a partition, and a row index, packed the way `08` §4 names
50/// them.
51///
52/// ```text
53///  63    52 51    40 39                                   0
54/// +--------+--------+--------------------------------------+
55/// |   P    |  part  |               idx + 1                |
56/// +--------+--------+--------------------------------------+
57/// ```
58///
59/// Zero is both the start and the end, which is Redis's convention and is
60/// unambiguous here because a real cursor always names a partition count and a
61/// partition count is never zero.
62///
63/// An `idx + 1` of zero is not a row, it means the top of that partition,
64/// whatever its length turns out to be. A resume needs to be able to say that
65/// without knowing how long the partition is, because [`Cursor::rebase`] moves a
66/// cursor into a partition it has never looked at.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
68pub struct Cursor(u64);
69
70/// Bits 40 to 51, the partition.
71const PART_SHIFT: u32 = 40;
72/// Bits 52 to 63, the partition count the cursor was issued under.
73const PARTS_SHIFT: u32 = 52;
74/// Twelve bits each for the count and the partition.
75const PART_MASK: u64 = 0xFFF;
76/// Forty bits for the row index, which is more than any one partition holds.
77const IDX_MASK: u64 = (1 << PART_SHIFT) - 1;
78
79/// The most partitions a collection may have.
80///
81/// Two thousand and forty eight, and the missing factor of two is worth a
82/// sentence because it is the sort of thing that looks like a typo. The field is
83/// twelve bits, so it holds 0 through 4,095, but zero is spoken for: a cursor
84/// that has not been anywhere yet is all zeroes and has to read as one partition
85/// rather than as none. That leaves 1 through 4,095, and a layout is always a
86/// power of two, so the largest one that fits is 2,048 rather than 4,096.
87///
88/// Storing `parts - 1` would win the bit back and it is not worth it. A cursor
89/// is a wire format, a client holds one across a reconnect, and changing what the
90/// bits mean to reach a layout that needs 134 million elements in one collection
91/// is a bad trade. Above that ceiling the partitions simply get larger.
92pub const MAX_PARTS: u32 = (PART_MASK as u32 + 1) >> 1;
93
94impl Cursor {
95    /// Start at the beginning, which for a downward walk is the top.
96    pub const START: Cursor = Cursor(0);
97
98    /// Nothing left. The same value as [`Cursor::START`], which is what the
99    /// protocol says and what every Redis client already loops on.
100    pub const END: Cursor = Cursor(0);
101
102    /// A cursor as the client sent it back.
103    #[inline]
104    #[must_use]
105    pub const fn from_raw(raw: u64) -> Cursor {
106        Cursor(raw)
107    }
108
109    /// The number to put on the wire.
110    #[inline]
111    #[must_use]
112    pub const fn raw(self) -> u64 {
113        self.0
114    }
115
116    /// Whether the scan is over.
117    #[inline]
118    #[must_use]
119    pub const fn is_end(self) -> bool {
120        self.0 == 0
121    }
122
123    /// Resume at a row.
124    ///
125    /// `parts` and `part` are clamped rather than rejected. A client can send
126    /// any number back and Redis answers all of them, so a cursor that names a
127    /// partition that does not exist has to mean something sane rather than be
128    /// an error.
129    #[must_use]
130    pub const fn at(parts: u32, part: u32, idx: u64) -> Cursor {
131        Cursor(pack(parts, part, (idx + 1) & IDX_MASK))
132    }
133
134    /// Resume at the top of a partition, without saying how long it is.
135    #[must_use]
136    pub const fn top(parts: u32, part: u32) -> Cursor {
137        Cursor(pack(parts, part, 0))
138    }
139
140    /// How many partitions the collection had when this was issued.
141    ///
142    /// One for a cursor that has not been anywhere yet, which is also the
143    /// truth for every collection below the partitioned band.
144    #[inline]
145    #[must_use]
146    pub const fn parts(self) -> u32 {
147        let p = ((self.0 >> PARTS_SHIFT) & PART_MASK) as u32;
148        if p == 0 { 1 } else { p }
149    }
150
151    /// Which partition it stopped in.
152    #[inline]
153    #[must_use]
154    pub const fn part(self) -> u32 {
155        ((self.0 >> PART_SHIFT) & PART_MASK) as u32
156    }
157
158    /// The next row to read, or `None` for the top of the partition.
159    #[inline]
160    #[must_use]
161    pub const fn idx(self) -> Option<u64> {
162        let i = self.0 & IDX_MASK;
163        if i == 0 { None } else { Some(i - 1) }
164    }
165
166    /// Move a cursor into the layout the collection has now.
167    ///
168    /// Growing from `P` to some larger power of two splits every partition and
169    /// moves nothing between the halves, because a member's partition is the low
170    /// bits of its hash and growing only reads more of them. So an old partition
171    /// `part` becomes the new partitions whose low `log2(P)` bits are `part`, and
172    /// every new partition whose low bits are above `part` has already been
173    /// walked in full.
174    ///
175    /// Resuming at the top of the highest new partition with those low bits
176    /// covers all of the work that is left, and walking down from there also
177    /// passes back over some partitions that were already done. That is
178    /// duplicate work and duplicates are allowed. What it never does is skip
179    /// one, and it never restarts the whole scan either, which is the other easy
180    /// answer and the one that turns a growth into a full second pass.
181    ///
182    /// The row index is dropped rather than carried across. A split redistributes
183    /// the rows, so an index into the old partition's array names a different
184    /// member in the new one, and resuming at the top of the partition it stopped
185    /// in is the only thing that can be said honestly.
186    ///
187    /// Shrinking is the other direction and is not something the size ladder
188    /// does under a live scan, so a cursor from a larger layout is answered by
189    /// starting the current one at the top. A repeat is allowed. A miss is not.
190    #[must_use]
191    pub const fn rebase(self, parts_now: u32) -> Cursor {
192        let was = self.parts();
193        if was == parts_now || self.is_end() {
194            return self;
195        }
196        if parts_now < was {
197            return Cursor::top(parts_now, parts_now.saturating_sub(1));
198        }
199        Cursor::top(parts_now, self.part() + (parts_now - was))
200    }
201}
202
203/// Pack the three fields, clamping the two that come from outside.
204///
205/// Both clamp to [`MAX_PARTS`] rather than to the width of the field. A count
206/// above the ceiling is a client making something up, and answering it with the
207/// largest layout that can actually exist is saner than answering it with a
208/// number no collection will ever have been laid out on.
209const fn pack(parts: u32, part: u32, idx_plus_one: u64) -> u64 {
210    let parts = if parts > MAX_PARTS {
211        MAX_PARTS as u64
212    } else {
213        parts as u64
214    };
215    let part = if part >= MAX_PARTS {
216        (MAX_PARTS - 1) as u64
217    } else {
218        part as u64
219    };
220    (parts << PARTS_SHIFT) | (part << PART_SHIFT) | idx_plus_one
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn zero_is_the_start_and_the_end() {
229        assert!(Cursor::START.is_end());
230        assert_eq!(Cursor::START, Cursor::END);
231        assert_eq!(Cursor::START.parts(), 1);
232        assert_eq!(Cursor::START.part(), 0);
233        assert_eq!(Cursor::START.idx(), None);
234    }
235
236    #[test]
237    fn the_three_fields_survive_the_wire() {
238        let c = Cursor::at(8, 5, 1234);
239        let back = Cursor::from_raw(c.raw());
240        assert_eq!(back.parts(), 8);
241        assert_eq!(back.part(), 5);
242        assert_eq!(back.idx(), Some(1234));
243        assert!(!back.is_end());
244    }
245
246    /// The layout is the one `08` section 4 writes down, and it is a wire format
247    /// once a client has a cursor in its hand, so it is pinned here.
248    #[test]
249    fn the_layout_is_the_one_the_spec_names() {
250        let c = Cursor::at(4, 3, 41);
251        assert_eq!(c.raw(), (4 << 52) | (3 << 40) | 42);
252    }
253
254    #[test]
255    fn the_top_of_a_partition_has_no_row_yet() {
256        let c = Cursor::top(16, 9);
257        assert_eq!(c.parts(), 16);
258        assert_eq!(c.part(), 9);
259        assert_eq!(c.idx(), None);
260        assert!(!c.is_end(), "the top of a partition is not the end");
261    }
262
263    /// The point of carrying the partition count. A scan that stopped in
264    /// partition 2 of 4 has already been through 3 of 4, so in a layout of 8 it
265    /// has been through everything whose low two bits are 3, and resuming at 6
266    /// covers the rest.
267    #[test]
268    fn growing_the_partitions_does_not_skip_any() {
269        let stopped = Cursor::at(4, 2, 500);
270        let now = stopped.rebase(8);
271        assert_eq!(now.parts(), 8);
272        assert_eq!(now.part(), 6);
273        assert_eq!(
274            now.idx(),
275            None,
276            "the split moved the rows, so start at the top"
277        );
278
279        // Every new partition the old cursor had not finished is at or below the
280        // resume point, so walking down from there reaches all of them. Some
281        // that were finished are below it too, and those are walked a second
282        // time, which is the trade and is allowed.
283        for n in 0..8u32 {
284            let done = (n & 3) > 2;
285            assert!(
286                done || n <= now.part(),
287                "new partition {n} would be skipped"
288            );
289        }
290    }
291
292    #[test]
293    fn a_cursor_from_the_same_layout_is_left_alone() {
294        let c = Cursor::at(8, 5, 77);
295        assert_eq!(c.rebase(8), c);
296        assert_eq!(Cursor::END.rebase(64), Cursor::END);
297    }
298
299    /// Not a case the ladder produces, but a client can send anything, and the
300    /// answer has to be a repeat rather than a miss.
301    #[test]
302    fn a_cursor_from_a_bigger_layout_starts_again_at_the_top() {
303        let c = Cursor::at(64, 40, 9).rebase(4);
304        assert_eq!(c.parts(), 4);
305        assert_eq!(c.part(), 3);
306        assert_eq!(c.idx(), None);
307    }
308
309    #[test]
310    fn a_partition_count_past_the_field_is_clamped_and_not_wrapped() {
311        let c = Cursor::at(MAX_PARTS * 4, MAX_PARTS * 4, 1);
312        assert!(c.parts() <= MAX_PARTS);
313        assert!(c.part() < MAX_PARTS);
314        assert_eq!(c.idx(), Some(1));
315    }
316}