yo_kv/set.rs
1//! A set, in whichever representation currently fits it.
2//!
3//! A set is one of an [`Intset`], a [`Listpack`], an [`Elements`] table or a
4//! [`Parts`] band, and which one is not a choice this file gets to make freely.
5//! `OBJECT ENCODING` has to answer `intset`, `listpack` or `hashtable` at exactly
6//! the sizes a real server answers them, because clients and test suites read it
7//! (`08` ยง1), so the promotion rules here are Redis's rules and they were read
8//! off `t_set.c` in the 8.10.1 tarball rather than reasoned out from what each
9//! structure is good at.
10//!
11//! ```text
12//! all integers small, any bytes everything else large
13//! +-------------+ +------------------+ +---------------+ +-----------+
14//! | intset |-->| listpack |-->| element table |-->| partition |
15//! | 2 B member | | ~2 B + payload | | one probe | | band |
16//! +-------------+ +------------------+ +---------------+ +-----------+
17//! to 512 members to 128 members past 262144
18//! <------- both say `hashtable` ---->
19//! ```
20//!
21//! The fourth step is the one Redis does not have, and it is invisible on
22//! purpose. A set past 262,144 members becomes several element tables rather than
23//! one, which is what makes the merges and the growth pauses bounded, and it
24//! still answers `hashtable` because a client that gets a fourth word back from
25//! `OBJECT ENCODING` is a client whose assertions break. What partitioning
26//! changes is how a large set is stored, not what it is. See [`crate::parts`].
27//!
28//! Promotion is one-way and upward, which is Y4. A set that has been a hash
29//! table does not go back to an intset when it shrinks, and neither does Redis's:
30//! a set that demoted on the way down would rewrite itself on every second
31//! operation for a workload that adds and removes across a threshold. The band
32//! follows the same rule for the same reason, so a set hovering at the partition
33//! threshold rehashes once rather than on every other `SREM`.
34//!
35//! # The rules, and the two that are not obvious
36//!
37//! Adding an integer to an intset keeps it an intset until it holds more than
38//! `set-max-intset-entries`, and then it becomes a **hash table** and not a
39//! listpack, because the intset ceiling is 512 and the listpack ceiling is 128
40//! and something over the first is well over the second.
41//!
42//! Adding a non integer to an intset is the asymmetric one. It becomes a
43//! listpack only if the intset is currently under the *listpack* ceiling of 128,
44//! so an intset of 200 integers that receives one string goes straight to a hash
45//! table and is never a listpack at all. Reading that off the source was worth
46//! more than reasoning about it, because the natural implementation converts to
47//! a listpack whenever the members would fit and gets a different encoding name
48//! than a real server for a shape a test suite actually builds.
49//!
50//! # Members
51//!
52//! A member comes back as a [`Member`], which is [`listpack::Entry`] under
53//! another name: either the bytes as they lie or an integer that has not been
54//! formatted yet. All three representations can produce one without copying, and
55//! the formatting happens once, into the reply buffer, at the moment the reply is
56//! built. That is Y18, and it is the same reason [`crate::value::Str`] has two
57//! arms.
58
59use crate::elem::Elements;
60use crate::intset::Intset;
61use crate::listpack::{self, Listpack};
62use crate::parts::{PARTITION_AT, Parts, parts_for};
63use crate::scan::Cursor;
64use yo_common::num::{DIGITS_MAX, i64_digits, i64_len, parse_i64};
65
66/// A set member: bytes as they lie, or an integer not yet formatted.
67pub type Member<'a> = listpack::Entry<'a>;
68
69/// A member on its way to being asked about, in every form the three
70/// representations want it in.
71///
72/// Set algebra walks one set and asks every other set the same question about
73/// each member, and the three representations do not want the question in the
74/// same shape. An intset wants a number, a listpack wants bytes and a number
75/// because it holds both kinds, and an element table wants bytes and their
76/// hash. Asking through [`Set::contains`] would redo all of that per question:
77/// a parse for the intset, another parse inside the listpack, and a hash per
78/// table. This does each once per member and then asks `k - 1` times.
79///
80/// The hash is computed whether or not any operand is a table, which is waste
81/// when none is. It is waste worth taking, because the sets where it is wasted
82/// are an intset or a listpack, which are capped at a few hundred members, and
83/// an operation over sets that small is finished before the saving could have
84/// been measured. The sets where the hash pays are the large ones, and those
85/// are tables by definition.
86#[derive(Debug, Clone, Copy)]
87pub struct Needle<'a> {
88 /// The member as bytes, which for an integer member is a caller's buffer.
89 bytes: &'a [u8],
90 /// The number it is, if it is one, under the same rule that decides whether
91 /// a set stores it as one.
92 int: Option<i64>,
93 /// What an element table would key it under.
94 hash: u64,
95}
96
97impl<'a> Needle<'a> {
98 /// A needle from bytes, which is what a command line argument is.
99 #[must_use]
100 pub fn new(bytes: &'a [u8]) -> Needle<'a> {
101 Needle {
102 bytes,
103 int: parse_i64(bytes),
104 hash: Elements::<()>::hash_of(bytes),
105 }
106 }
107
108 /// A needle from a member walked out of a set.
109 ///
110 /// `digits` is where an integer member's text goes, because an intset holds
111 /// the number and the digits do not exist anywhere until somebody writes
112 /// them. It is the caller's buffer rather than a field so that the needle
113 /// stays a borrow and the buffer is written once per member rather than
114 /// allocated once per member.
115 ///
116 /// A member that came out as bytes is still parsed, because the set being
117 /// asked may be an intset and `SINTER ints strings` has to find the members
118 /// they share. A member that came out as a number is not, which is the
119 /// whole saving.
120 #[must_use]
121 pub fn of(member: Member<'a>, digits: &'a mut [u8; DIGITS_MAX]) -> Needle<'a> {
122 match member {
123 Member::Str(s) => Needle::new(s),
124 Member::Int(n) => {
125 let bytes = i64_digits(digits, n);
126 Needle {
127 bytes,
128 int: Some(n),
129 hash: Elements::<()>::hash_of(bytes),
130 }
131 }
132 }
133 }
134
135 /// The member as bytes, which is what a caller collecting an answer wants.
136 #[must_use]
137 pub const fn bytes(&self) -> &'a [u8] {
138 self.bytes
139 }
140}
141
142/// Where the encodings change over.
143///
144/// These are `set-max-intset-entries`, `set-max-listpack-entries` and
145/// `set-max-listpack-value`, and they are runtime configuration in Redis, so
146/// they are a value passed in here rather than three constants. The defaults are
147/// Redis's defaults and a client that never touches `CONFIG SET` sees exactly
148/// the encodings a real server would give it.
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct Limits {
151 /// Past this many members an all integer set stops being an intset.
152 pub max_intset_entries: usize,
153 /// At this many members a set stops being a listpack.
154 pub max_listpack_entries: usize,
155 /// A member longer than this cannot go in a listpack.
156 pub max_listpack_value: usize,
157}
158
159impl Limits {
160 /// Redis's defaults: 512, 128 and 64.
161 pub const DEFAULT: Limits = Limits {
162 max_intset_entries: 512,
163 max_listpack_entries: 128,
164 max_listpack_value: 64,
165 };
166}
167
168impl Default for Limits {
169 fn default() -> Limits {
170 Limits::DEFAULT
171 }
172}
173
174/// Which of the three a set is in, which is what `OBJECT ENCODING` reports.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum Encoding {
177 /// All members are integers and there are few enough of them.
178 Intset,
179 /// One packed blob, walked linearly.
180 Listpack,
181 /// The element table.
182 Hashtable,
183}
184
185impl Encoding {
186 /// The word `OBJECT ENCODING` replies with.
187 #[inline]
188 pub const fn name(self) -> &'static str {
189 match self {
190 Encoding::Intset => "intset",
191 Encoding::Listpack => "listpack",
192 Encoding::Hashtable => "hashtable",
193 }
194 }
195}
196
197/// The four representations, of which `OBJECT ENCODING` can see three.
198///
199/// [`Body::Split`] is the partitioned band and it is deliberately invisible from
200/// outside. Redis has three set encodings and a client that gets a fourth word
201/// back from `OBJECT ENCODING` is a client whose assertions break, so a split set
202/// answers `hashtable` like the table it was. What partitioning changes is how a
203/// large set is stored and merged, not what it is.
204#[derive(Debug, Clone)]
205enum Body {
206 Ints(Intset),
207 Packed(Listpack),
208 Table(Elements<()>),
209 Split(Parts<()>),
210}
211
212/// A set of members.
213#[derive(Debug, Clone)]
214pub struct Set {
215 body: Body,
216 /// Whether an all integer set has passed `set-max-intset-entries`.
217 ///
218 /// This is where Redis rehashes the members into a dictionary and starts
219 /// answering `hashtable` to `OBJECT ENCODING`. Nothing is rehashed here,
220 /// because [`Intset`] holds a large set in runs and stays at two to eight
221 /// bytes a member where a table would cost thirty, so the only thing the
222 /// ceiling still decides is the word.
223 ///
224 /// It is a flag and not a comparison against the length because the ceiling
225 /// is configurable and [`Set::encoding`] is not handed the configuration. It
226 /// is also one way, like every promotion here: a set that has been called a
227 /// hashtable once does not go back to being called an intset when members
228 /// are removed, which is Y4 and is what Redis does.
229 ///
230 /// Only ever true while [`Body::Ints`] is the body. Every other body already
231 /// knows what to answer.
232 ints_past_limit: bool,
233}
234
235impl Set {
236 /// An empty set, which starts as an intset.
237 ///
238 /// This is what `SADD` on a missing key creates when it has no size hint,
239 /// and the first member decides nothing: an intset that receives a string
240 /// converts on the spot, and it costs a conversion of nothing.
241 #[must_use]
242 pub fn new() -> Set {
243 Set {
244 body: Body::Ints(Intset::new()),
245 ints_past_limit: false,
246 }
247 }
248
249 /// An empty set sized for what is about to go in it.
250 ///
251 /// Redis's `setTypeCreate`, which picks the representation from the first
252 /// member and the count the caller expects, so that `SADD k a b c ...` with
253 /// a thousand arguments builds a table once rather than converting twice on
254 /// the way there. `hint` is only a hint and being wrong about it costs a
255 /// conversion and no correctness.
256 ///
257 /// An integer first member sends it to the intset even when the count is
258 /// past `set-max-intset-entries`, where Redis would go straight to a
259 /// dictionary. A large set of integers is the case the runs exist for, and
260 /// building a table and never leaving it would give up the whole saving on
261 /// the one call that said in advance it was going to matter. The listpack
262 /// band in between is still honoured, because a set that small has nothing
263 /// to save and a server configured that way expects a listpack.
264 #[must_use]
265 pub fn with_hint(first: &[u8], hint: usize, limits: &Limits) -> Set {
266 let ints = parse_i64(first).is_some()
267 && (hint <= limits.max_intset_entries || hint > limits.max_listpack_entries);
268 if ints {
269 Set {
270 body: Body::Ints(Intset::with_capacity(hint)),
271 ints_past_limit: hint > limits.max_intset_entries,
272 }
273 } else if hint <= limits.max_listpack_entries {
274 Set {
275 body: Body::Packed(Listpack::new()),
276 ints_past_limit: false,
277 }
278 } else if hint > PARTITION_AT {
279 // A caller that says up front it is about to load a million members
280 // should not build one table, fill it past the threshold and then
281 // rehash the lot into partitions. The hint is only a hint, and being
282 // wrong about it here costs a set with more partitions than it needs
283 // rather than anything incorrect.
284 Set {
285 body: Body::Split(Parts::with_parts(parts_for(hint))),
286 ints_past_limit: false,
287 }
288 } else {
289 Set {
290 body: Body::Table(Elements::with_capacity(hint)),
291 ints_past_limit: false,
292 }
293 }
294 }
295
296 /// Which representation this is in.
297 ///
298 /// Four bodies and three words, and the intset accounts for two of the
299 /// missing ones. The partitioned body answers `hashtable` because it is one,
300 /// and an intset past `set-max-intset-entries` answers `hashtable` because
301 /// that is what a real server would have turned into by then, even though
302 /// nothing here was rehashed.
303 #[inline]
304 #[must_use]
305 pub const fn encoding(&self) -> Encoding {
306 match self.body {
307 Body::Ints(_) if self.ints_past_limit => Encoding::Hashtable,
308 Body::Ints(_) => Encoding::Intset,
309 Body::Packed(_) => Encoding::Listpack,
310 Body::Table(_) | Body::Split(_) => Encoding::Hashtable,
311 }
312 }
313
314 /// How many members. This is `SCARD`.
315 #[inline]
316 pub fn len(&self) -> usize {
317 match &self.body {
318 Body::Ints(s) => s.len(),
319 Body::Packed(lp) => lp.len(),
320 Body::Table(t) => t.len(),
321 Body::Split(p) => p.len(),
322 }
323 }
324
325 /// Whether there are none.
326 ///
327 /// An empty set does not exist in Redis, so the caller deletes the key when
328 /// this turns true rather than storing an empty one.
329 #[inline]
330 pub fn is_empty(&self) -> bool {
331 self.len() == 0
332 }
333
334 /// Whether `member` is in the set. This is `SISMEMBER`.
335 #[must_use]
336 pub fn contains(&self, member: &[u8]) -> bool {
337 match &self.body {
338 // A member that is not an integer cannot be in a set of integers,
339 // and answering that costs a parse rather than a search.
340 Body::Ints(s) => parse_i64(member).is_some_and(|v| s.contains(v)),
341 Body::Packed(lp) => lp.find(member, 1).is_some(),
342 Body::Table(t) => t.contains(member),
343 Body::Split(p) => p.contains(member),
344 }
345 }
346
347 /// The same question asked with the work already done. See [`Needle`].
348 ///
349 /// This is what set algebra probes with. Every arm is the arm
350 /// [`Set::contains`] would have taken, with the parse and the hash lifted
351 /// out of it, so the two cannot disagree about what a member is.
352 #[must_use]
353 #[inline]
354 pub fn has(&self, needle: &Needle<'_>) -> bool {
355 match &self.body {
356 Body::Ints(s) => needle.int.is_some_and(|v| s.contains(v)),
357 Body::Packed(lp) => lp.find_parsed(needle.bytes, needle.int, 1).is_some(),
358 Body::Table(t) => t.contains_hashed(needle.hash, needle.bytes),
359 Body::Split(p) => p.contains_hashed(needle.hash, needle.bytes),
360 }
361 }
362
363 /// The member at `index`, in whatever order the representation holds them.
364 ///
365 /// Ascending for an intset, insertion order for the other two. Redis makes
366 /// no promise about set order and neither does this, but a uniform draw
367 /// needs positions and this is what gives it them (K9).
368 #[must_use]
369 pub fn at(&self, index: usize) -> Option<Member<'_>> {
370 match &self.body {
371 Body::Ints(s) => s.get(index).map(Member::Int),
372 Body::Packed(lp) => lp.get(index),
373 Body::Table(t) => t.at(index).map(|(name, _)| Member::Str(name)),
374 Body::Split(p) => p.at(index).map(|(name, _)| Member::Str(name)),
375 }
376 }
377
378 /// Every member.
379 pub fn iter(&self) -> impl Iterator<Item = Member<'_>> {
380 (0..self.len()).map(|i| self.at(i).expect("index is under the length"))
381 }
382
383 /// The members as a sorted array of integers, when that is what this is.
384 ///
385 /// The one place the representation is not an implementation detail, and it
386 /// is here for [`crate::setops`]: two sorted arrays intersect by stepping
387 /// through both of them with no hash anywhere, which is a different order of
388 /// cost from asking a table a question per member. That was worth nothing
389 /// while an all integer set turned into a table at five hundred and twelve
390 /// members, and it is worth a great deal now that it does not.
391 #[inline]
392 #[must_use]
393 pub const fn ints(&self) -> Option<&Intset> {
394 match &self.body {
395 Body::Ints(s) => Some(s),
396 _ => None,
397 }
398 }
399
400 /// Walk part of the set and say where to resume. This is `SSCAN`.
401 ///
402 /// Only the table and the partitioned band walk in windows. An intset or a
403 /// listpack hands back
404 /// every member in one call and a cursor of [`Cursor::END`], ignoring the
405 /// cursor it was given, which is what Redis does for the same two encodings
406 /// and for the same reason: a hundred and twenty eight members is smaller
407 /// than the reply header arithmetic to split them up, and a set that small
408 /// cannot block the loop long enough for the split to be worth anything.
409 ///
410 /// Ignoring the cursor is safe rather than merely convenient, because
411 /// promotion is one way. A set that gave a client a listpack cursor is not
412 /// going to be a listpack again, so the only way to arrive at those two arms
413 /// with a cursor from somewhere else is for the key to have been deleted and
414 /// remade underneath the scan, and returning everything to that client
415 /// returns a member twice at worst, which the guarantee allows.
416 ///
417 /// A table cursor arriving at the band is the one crossing that does happen,
418 /// because a set can split part way through a client's scan. That is handled
419 /// rather than ignored: a table cursor names one partition, and
420 /// [`Cursor::rebase`] reads the widening and restarts the walk at the top of
421 /// the new layout, so the client sees some members a second time and misses
422 /// none. Repeats are what the `SCAN` guarantee gives up in exchange for
423 /// surviving a resize, and a set only splits once.
424 pub fn scan<F>(&self, cursor: Cursor, count: usize, mut f: F) -> Cursor
425 where
426 F: FnMut(Member<'_>),
427 {
428 match &self.body {
429 Body::Table(t) => t.scan(cursor, count, |name, ()| f(Member::Str(name))),
430 Body::Split(p) => p.scan(cursor, count, |name, ()| f(Member::Str(name))),
431 // An intset past the ceiling is the one thing outside the table
432 // band that a single reply cannot hold. Redis has a dictionary by
433 // this point and walks it in windows, and a set of a million
434 // integers answering `SSCAN` with a million members in one go would
435 // be a several megabyte reply and a loop iteration nobody could
436 // measure. So it walks in windows too, and by index, which the runs
437 // answer in a walk down their tree rather than a walk along them.
438 //
439 // Downward, which is the direction the element table walks and for
440 // the same reason. Positions in a sorted array shift when a member
441 // below them goes, so an upward walk would miss a member for every
442 // one removed behind it, and `SSCAN` followed by `SREM` on what it
443 // found is the commonest thing anyone does with this command.
444 // Walking down means those removals are all above the cursor, where
445 // they cost nothing.
446 Body::Ints(s) if self.ints_past_limit && !s.is_empty() => {
447 let top = s.len() - 1;
448 let mut at = match cursor.rebase(1).idx() {
449 Some(idx) => (idx as usize).min(top),
450 None => top,
451 };
452 for _ in 0..count.max(1) {
453 f(Member::Int(s.at(at)));
454 if at == 0 {
455 return Cursor::END;
456 }
457 at -= 1;
458 }
459 Cursor::at(1, 0, at as u64)
460 }
461 _ => {
462 for m in self.iter() {
463 f(m);
464 }
465 Cursor::END
466 }
467 }
468 }
469
470 /// Bytes held by whichever representation this is.
471 #[must_use]
472 pub fn memory_bytes(&self) -> usize {
473 match &self.body {
474 Body::Ints(s) => s.memory_bytes(),
475 Body::Packed(lp) => lp.byte_len(),
476 Body::Table(t) => t.memory_bytes(),
477 Body::Split(p) => p.memory_bytes(),
478 }
479 }
480
481 /// Add `member`, promoting if it no longer fits. Answers whether it was new.
482 ///
483 /// This is `setTypeAdd`, arm for arm.
484 pub fn add(&mut self, member: &[u8], limits: &Limits) -> bool {
485 match &mut self.body {
486 Body::Table(t) => {
487 let new = t.insert(member, ()).is_ok_and(|old| old.is_none());
488 // Checked after the insert rather than before, so the set that
489 // splits is the one that has actually outgrown a table and not
490 // the one that is about to.
491 if t.len() > PARTITION_AT {
492 self.become_split();
493 }
494 return new;
495 }
496 Body::Split(p) => {
497 let new = p.insert(member, ()).is_ok_and(|old| old.is_none());
498 // Asked rather than decided, because growing is a rehash of the
499 // whole set and the band leaves the timing to whoever knows
500 // whether this is one write or the middle of a bulk load.
501 if let Some(want) = p.wants_parts() {
502 p.grow_to(want);
503 }
504 return new;
505 }
506 Body::Packed(lp) => {
507 if lp.find(member, 1).is_some() {
508 return false;
509 }
510 if lp.len() < limits.max_listpack_entries
511 && member.len() <= limits.max_listpack_value
512 {
513 lp.push(member);
514 return true;
515 }
516 // Too many members, or one too long. It falls out to a table.
517 }
518 Body::Ints(s) => {
519 if let Some(v) = parse_i64(member) {
520 if !s.add(v) {
521 return false;
522 }
523 // Strictly greater, so the 512th member is still an intset
524 // and the 513th is what a real server would call a
525 // hashtable. Nothing is rewritten, only the word changes.
526 //
527 // Unless the ceilings have been configured the wrong way
528 // round, where a set past the intset ceiling is still under
529 // the listpack one and a real server puts it in a listpack.
530 // That set is a handful of members and there is no memory
531 // argument for keeping it here, so it goes where it would
532 // have gone.
533 if s.len() > limits.max_intset_entries {
534 if self.ints_fit_a_listpack_alone(limits) {
535 self.become_listpack();
536 } else {
537 self.ints_past_limit = true;
538 }
539 }
540 return true;
541 }
542 // Not an integer, so it is certainly not in a set of integers
543 // already. If the set is still small enough it becomes a
544 // listpack, and otherwise it falls out to a table.
545 if self.ints_fit_a_listpack(member, limits) {
546 self.become_listpack();
547 self.push_new(member);
548 return true;
549 }
550 }
551 }
552 self.become_table(1);
553 self.push_new(member);
554 true
555 }
556
557 /// Put in a member already known to be new and known to fit where it is.
558 ///
559 /// Only ever called on the far side of a promotion, where both of those are
560 /// facts the promotion established and not things worth establishing twice.
561 fn push_new(&mut self, member: &[u8]) {
562 match &mut self.body {
563 Body::Packed(lp) => lp.push(member),
564 Body::Table(t) => {
565 t.insert(member, ())
566 .expect("the table was sized for this one");
567 }
568 Body::Split(p) => {
569 p.insert(member, ())
570 .expect("the band was sized for this one");
571 }
572 Body::Ints(_) => unreachable!("no promotion ever lands on an intset"),
573 }
574 }
575
576 /// Remove `member`. Answers whether it was there.
577 ///
578 /// Never demotes, which is Y4's one-way rule and Redis's behaviour.
579 pub fn remove(&mut self, member: &[u8]) -> bool {
580 match &mut self.body {
581 Body::Ints(s) => parse_i64(member).is_some_and(|v| s.remove(v)),
582 Body::Packed(lp) => match lp.find(member, 1) {
583 Some(at) => lp.delete(at, 1),
584 None => false,
585 },
586 Body::Table(t) => t.remove(member).is_some(),
587 Body::Split(p) => p.remove(member).is_some(),
588 }
589 }
590
591 /// Take out the member at `index` and hand it back.
592 ///
593 /// This is what `SPOP` runs on top of. The table moves its last row into the
594 /// hole rather than shifting, so the position of every other member is
595 /// stable except for one; the other two shift. Neither is a promise a caller
596 /// can lean on, and `SPOP` does not need one because it draws again from the
597 /// new length each time.
598 pub fn remove_at(&mut self, index: usize) -> Option<Vec<u8>> {
599 match &mut self.body {
600 Body::Ints(s) => {
601 let v = s.get(index)?;
602 s.remove(v);
603 let mut out = Vec::with_capacity(i64_len(v));
604 Member::Int(v).write_to(&mut out);
605 Some(out)
606 }
607 Body::Packed(lp) => {
608 let out = lp.get(index)?.to_vec();
609 lp.delete(index, 1);
610 Some(out)
611 }
612 Body::Table(t) => t.take_at(index).map(|(name, ())| name),
613 Body::Split(p) => p.take_at(index).map(|(name, ())| name),
614 }
615 }
616
617 /// Take out the member at `index` without building it into a `Vec` first.
618 ///
619 /// The same removal as [`Set::remove_at`] for a caller that has already read
620 /// the member and does not need it handed back. That caller is `SPOP` on the
621 /// wire, which reads with [`Set::at`], writes the bytes straight into the
622 /// reply buffer, and only then calls this. It is an allocation a member
623 /// saved on the one command in the set whose whole cost is the allocating.
624 ///
625 /// [`Set::remove_at`] stays for the embedded API, where the caller wants the
626 /// bytes and has nowhere to put them.
627 pub fn drop_at(&mut self, index: usize) -> bool {
628 match &mut self.body {
629 Body::Ints(s) => match s.get(index) {
630 Some(v) => {
631 s.remove(v);
632 true
633 }
634 None => false,
635 },
636 Body::Packed(lp) => {
637 if index >= lp.len() {
638 return false;
639 }
640 lp.delete(index, 1);
641 true
642 }
643 Body::Table(t) => t.remove_at(index).is_some(),
644 Body::Split(p) => p.remove_at(index).is_some(),
645 }
646 }
647
648 /// Whether an intset plus one non integer member would still be a listpack.
649 ///
650 /// Three tests, and the first is the asymmetric one: the count is compared
651 /// against the *listpack* ceiling and not the intset one, so an intset with
652 /// two hundred members is already too big to become a listpack even though
653 /// it is a perfectly legal intset. The other two are the new member's length
654 /// and the longest existing member's length once it is written as digits,
655 /// which only bites when `set-max-listpack-value` has been turned down,
656 /// because no integer is more than twenty characters.
657 fn ints_fit_a_listpack(&self, member: &[u8], limits: &Limits) -> bool {
658 let Body::Ints(s) = &self.body else {
659 return false;
660 };
661 s.len() < limits.max_listpack_entries
662 && member.len() <= limits.max_listpack_value
663 && self.ints_are_short_enough(limits)
664 }
665
666 /// Whether this intset on its own would fit a listpack.
667 ///
668 /// The same question with no new member in it, which is what an intset that
669 /// has just passed `set-max-intset-entries` asks. It only ever answers yes
670 /// when the two ceilings have been configured the wrong way round, because
671 /// 512 is not under 128, and a server run that way expects a listpack there.
672 fn ints_fit_a_listpack_alone(&self, limits: &Limits) -> bool {
673 let Body::Ints(s) = &self.body else {
674 return false;
675 };
676 s.len() <= limits.max_listpack_entries && self.ints_are_short_enough(limits)
677 }
678
679 /// Whether every member, written as digits, is under the listpack ceiling.
680 fn ints_are_short_enough(&self, limits: &Limits) -> bool {
681 let Body::Ints(s) = &self.body else {
682 return false;
683 };
684 // The two ends bound the digits of everything between them, so there is
685 // nothing to walk.
686 let widest = s
687 .min()
688 .map(i64_len)
689 .unwrap_or(0)
690 .max(s.max().map(i64_len).unwrap_or(0));
691 widest <= limits.max_listpack_value
692 }
693
694 /// Rewrite as a listpack, which only an intset ever does.
695 fn become_listpack(&mut self) {
696 let Body::Ints(s) = &self.body else {
697 return;
698 };
699 let mut lp = Listpack::new();
700 let mut buf = Vec::with_capacity(20);
701 for v in s.iter() {
702 buf.clear();
703 Member::Int(v).write_to(&mut buf);
704 lp.push(&buf);
705 }
706 self.body = Body::Packed(lp);
707 }
708
709 /// Rewrite as an element table, with room for `extra` more members.
710 fn become_table(&mut self, extra: usize) {
711 let mut t = Elements::with_capacity(self.len() + extra);
712 let mut buf = Vec::with_capacity(20);
713 for m in self.iter() {
714 match m {
715 Member::Str(b) => {
716 t.insert(b, ()).expect("room, and every member was unique");
717 }
718 Member::Int(v) => {
719 buf.clear();
720 Member::Int(v).write_to(&mut buf);
721 t.insert(&buf, ())
722 .expect("room, and every member was unique");
723 }
724 }
725 }
726 self.body = Body::Table(t);
727 }
728
729 /// Spread an element table over partitions.
730 ///
731 /// One way, like every other promotion here. A set that drops back under the
732 /// threshold keeps its partitions, which is Y4's rule and Redis's behaviour
733 /// for the encodings it does expose: the cost of a representation is paid
734 /// when it is entered, and paying it again on the way back out turns one
735 /// `SREM` at the boundary into a rehash of the whole set.
736 fn become_split(&mut self) {
737 if let Body::Table(t) = &self.body {
738 let p = Parts::from_table(t, parts_for(t.len()));
739 self.body = Body::Split(p);
740 }
741 }
742}
743
744impl Default for Set {
745 fn default() -> Set {
746 Set::new()
747 }
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753
754 fn of(members: &[&str]) -> Set {
755 let mut s = Set::new();
756 for m in members {
757 assert!(s.add(m.as_bytes(), &Limits::DEFAULT), "{m} was new");
758 }
759 s
760 }
761
762 /// What a set actually costs per member, which is half of M3's memory gate
763 /// row and was an argument rather than a number until this was written.
764 ///
765 /// Run it with `cargo test -p yo-kv --release measure_bytes_per_member --
766 /// --ignored --nocapture`. Ignored because a million members is not
767 /// something every `cargo test` should pay for, and it prints rather than
768 /// asserts because the number it prints is the thing being reported.
769 ///
770 /// Two shapes, because the gate names one of them and the other is what
771 /// most sets actually hold. Integers first, at every band an all integer
772 /// set passes through, and then strings, which never see the intset at all.
773 #[test]
774 #[ignore = "a measurement, run it by name"]
775 fn measure_bytes_per_member() {
776 let limits = Limits::DEFAULT;
777 for n in [512usize, 1_000, 100_000, 1_000_000] {
778 let mut s = Set::new();
779 for i in 0..n {
780 s.add(i.to_string().as_bytes(), &limits);
781 }
782 println!(
783 "int n={n:<9} band={:<10} total={:<10} per_member={:.2}",
784 band(&s),
785 s.memory_bytes(),
786 s.memory_bytes() as f64 / n as f64
787 );
788 }
789 // Sixteen byte members, so the payload is a round number and the
790 // overhead is whatever is above it.
791 for n in [128usize, 1_000, 100_000, 1_000_000] {
792 let mut s = Set::new();
793 let mut payload = 0usize;
794 for i in 0..n {
795 let m = format!("member:{i:09}");
796 payload += m.len();
797 s.add(m.as_bytes(), &limits);
798 }
799 let total = s.memory_bytes();
800 println!(
801 "bytes n={n:<9} band={:<10} total={total:<10} payload={payload:<9} per_member={:.2} over_per_member={:.2}",
802 band(&s),
803 total as f64 / n as f64,
804 (total as f64 - payload as f64) / n as f64
805 );
806 }
807 }
808
809 /// Which of the four a set is in, spelled out rather than through
810 /// [`Set::encoding`], which folds the two table bands into one word because
811 /// that is what `OBJECT ENCODING` has to say.
812 fn band(s: &Set) -> &'static str {
813 match &s.body {
814 Body::Ints(_) => "intset",
815 Body::Packed(_) => "listpack",
816 Body::Table(_) => "table",
817 Body::Split(_) => "split",
818 }
819 }
820
821 /// Everything about the partitioned band that needs a real set past the real
822 /// threshold, in one test, because building 262,145 members is the expensive
823 /// part and there is no reason to pay for it four times.
824 #[test]
825 fn a_set_past_the_threshold_splits_without_the_client_being_able_to_tell() {
826 let limits = Limits::DEFAULT;
827 let mut s = Set::new();
828 // One short of the threshold. Still one table: the check is strictly
829 // greater, so the set that splits is the one that has outgrown a table
830 // and not the one that is about to.
831 for i in 0..PARTITION_AT {
832 assert!(s.add(format!("m{i}").as_bytes(), &limits));
833 }
834 assert!(matches!(s.body, Body::Table(_)));
835 assert_eq!(s.len(), PARTITION_AT);
836
837 // The member that tips it over.
838 assert!(s.add(b"tipping", &limits));
839 assert!(matches!(s.body, Body::Split(_)), "it should have split");
840 assert_eq!(s.len(), PARTITION_AT + 1);
841
842 // And the client cannot tell. This is the whole point: Redis has three
843 // set encodings and a fourth word here breaks every suite that reads it.
844 assert_eq!(s.encoding(), Encoding::Hashtable);
845 assert_eq!(s.encoding().name(), "hashtable");
846
847 // Every member survived the rehash, asked both ways.
848 assert!(s.contains(b"tipping"));
849 assert!(s.contains(b"m0"));
850 assert!(s.contains(b"m262143"));
851 assert!(!s.contains(b"m262144"));
852 assert!(s.has(&Needle::new(b"m1000")));
853 assert!(!s.has(&Needle::new(b"nothing")));
854
855 // A rewrite is still not an add.
856 assert!(!s.add(b"m0", &limits));
857 assert_eq!(s.len(), PARTITION_AT + 1);
858
859 // Removing goes back through the same partition it went into, and the
860 // band never demotes however far the set shrinks.
861 assert!(s.remove(b"tipping"));
862 assert!(!s.remove(b"tipping"));
863 assert_eq!(s.len(), PARTITION_AT);
864 assert!(matches!(s.body, Body::Split(_)), "promotion is one way");
865 assert_eq!(s.encoding(), Encoding::Hashtable);
866
867 // The draw `SPOP` and `SRANDMEMBER` run on reaches the last position,
868 // which is the one that has to land in the highest non empty partition.
869 let last = s.at(s.len() - 1).expect("inside the set").to_vec();
870 assert!(s.contains(&last));
871 assert!(s.at(s.len()).is_none());
872 assert_eq!(s.remove_at(s.len() - 1), Some(last.clone()));
873 assert!(!s.contains(&last));
874 assert!(s.drop_at(0));
875 assert_eq!(s.len(), PARTITION_AT - 2);
876
877 // And a full scan sees every member exactly once.
878 let mut seen = 0usize;
879 let mut cursor = Cursor::START;
880 let mut rounds = 0;
881 loop {
882 cursor = s.scan(cursor, 1_000, |_| seen += 1);
883 rounds += 1;
884 assert!(rounds < 100_000, "the scan is not finishing");
885 if cursor.is_end() {
886 break;
887 }
888 }
889 assert_eq!(seen, s.len());
890 }
891
892 /// The crossing that actually happens in production: a client holding a
893 /// cursor from before the split. It has to see every member that stayed, and
894 /// repeats are what the `SCAN` guarantee gives up in exchange.
895 #[test]
896 fn a_scan_survives_the_set_splitting_underneath_it() {
897 let limits = Limits::DEFAULT;
898 let mut s = Set::new();
899 for i in 0..PARTITION_AT {
900 s.add(format!("m{i}").as_bytes(), &limits);
901 }
902 assert!(matches!(s.body, Body::Table(_)));
903
904 let mut seen = Vec::new();
905 let cursor = s.scan(Cursor::START, 5_000, |m| seen.push(m.to_vec()));
906 assert!(!cursor.is_end(), "the scan should have stopped part way");
907
908 s.add(b"tipping", &limits);
909 assert!(matches!(s.body, Body::Split(_)));
910
911 let mut cursor = cursor;
912 let mut rounds = 0;
913 loop {
914 cursor = s.scan(cursor, 5_000, |m| seen.push(m.to_vec()));
915 rounds += 1;
916 assert!(rounds < 100_000, "the scan is not finishing");
917 if cursor.is_end() {
918 break;
919 }
920 }
921 seen.sort_unstable();
922 seen.dedup();
923 assert_eq!(
924 seen.len(),
925 PARTITION_AT + 1,
926 "the split lost a member the client was entitled to"
927 );
928 }
929
930 #[test]
931 fn a_hint_past_the_threshold_builds_the_band_up_front() {
932 let limits = Limits::DEFAULT;
933 // A caller loading a million members should not fill one table, cross the
934 // threshold and then rehash the lot.
935 let s = Set::with_hint(b"first", 1_000_000, &limits);
936 assert!(matches!(s.body, Body::Split(_)));
937 assert_eq!(s.encoding(), Encoding::Hashtable);
938 assert!(s.is_empty());
939
940 // A hint at the threshold is still one table, matching the add path.
941 let s = Set::with_hint(b"first", PARTITION_AT, &limits);
942 assert!(matches!(s.body, Body::Table(_)));
943
944 // And a hint is only a hint: the band takes members like anything else.
945 let mut s = Set::with_hint(b"a", 1_000_000, &limits);
946 assert!(s.add(b"a", &limits));
947 assert!(!s.add(b"a", &limits));
948 assert!(s.contains(b"a"));
949 assert_eq!(s.len(), 1);
950 assert_eq!(s.at(0).map(|m| m.to_vec()), Some(b"a".to_vec()));
951 }
952
953 fn members(s: &Set) -> Vec<String> {
954 let mut v: Vec<String> = s
955 .iter()
956 .map(|m| String::from_utf8(m.to_vec()).expect("utf8 in these tests"))
957 .collect();
958 // Order is not part of the contract and the three representations do not
959 // agree on it, so every assertion here is against a sorted list.
960 v.sort();
961 v
962 }
963
964 #[test]
965 fn a_new_set_is_an_empty_intset() {
966 let s = Set::new();
967 assert_eq!(s.encoding(), Encoding::Intset);
968 assert_eq!(s.len(), 0);
969 assert!(s.is_empty());
970 assert!(!s.contains(b"1"));
971 assert_eq!(s.at(0), None);
972 }
973
974 #[test]
975 fn integers_stay_an_intset_and_come_back_as_members() {
976 let s = of(&["1", "2", "3"]);
977 assert_eq!(s.encoding(), Encoding::Intset);
978 assert_eq!(members(&s), ["1", "2", "3"]);
979 assert!(s.contains(b"2"));
980 assert!(!s.contains(b"4"));
981 assert!(!s.contains(b"two"));
982 }
983
984 #[test]
985 fn adding_the_same_member_twice_says_so_in_all_three() {
986 let mut ints = of(&["1", "2"]);
987 assert!(!ints.add(b"2", &Limits::DEFAULT));
988 assert_eq!(ints.len(), 2);
989
990 let mut packed = of(&["a", "b"]);
991 assert_eq!(packed.encoding(), Encoding::Listpack);
992 assert!(!packed.add(b"b", &Limits::DEFAULT));
993 assert_eq!(packed.len(), 2);
994
995 let mut table = of(&["a", "b"]);
996 table.become_table(0);
997 assert!(!table.add(b"b", &Limits::DEFAULT));
998 assert_eq!(table.len(), 2);
999 }
1000
1001 #[test]
1002 fn a_string_turns_a_small_intset_into_a_listpack() {
1003 let mut s = of(&["1", "2", "3"]);
1004 assert!(s.add(b"hello", &Limits::DEFAULT));
1005 assert_eq!(s.encoding(), Encoding::Listpack);
1006 assert_eq!(members(&s), ["1", "2", "3", "hello"]);
1007 assert!(s.contains(b"1"), "the integers survived the rewrite");
1008 assert!(s.contains(b"hello"));
1009 }
1010
1011 #[test]
1012 fn a_string_turns_a_big_intset_straight_into_a_table() {
1013 // The asymmetric rule, and the one a natural implementation gets wrong.
1014 // Two hundred integers is a legal intset and is already past the
1015 // listpack ceiling, so this never passes through the listpack at all.
1016 let mut s = Set::new();
1017 for i in 0..200 {
1018 s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1019 }
1020 assert_eq!(s.encoding(), Encoding::Intset);
1021 assert_eq!(s.len(), 200);
1022
1023 assert!(s.add(b"hello", &Limits::DEFAULT));
1024 assert_eq!(s.encoding(), Encoding::Hashtable);
1025 assert_eq!(s.len(), 201);
1026 assert!(s.contains(b"199"));
1027 assert!(s.contains(b"hello"));
1028 }
1029
1030 #[test]
1031 fn an_intset_holds_five_hundred_and_twelve_and_converts_at_the_next_one() {
1032 let mut s = Set::new();
1033 for i in 0..512 {
1034 s.add(i.to_string().as_bytes(), &Limits::DEFAULT);
1035 }
1036 assert_eq!(s.encoding(), Encoding::Intset, "512 is still an intset");
1037 assert_eq!(s.len(), 512);
1038
1039 s.add(b"512", &Limits::DEFAULT);
1040 assert_eq!(s.encoding(), Encoding::Hashtable, "513 is not");
1041 assert_eq!(s.len(), 513);
1042 // And not a listpack on the way, because 513 is well past 128.
1043 for i in 0..513 {
1044 assert!(s.contains(i.to_string().as_bytes()), "{i} survived");
1045 }
1046 }
1047
1048 #[test]
1049 fn a_listpack_converts_at_a_hundred_and_twenty_eight_members() {
1050 let mut s = of(&["x"]);
1051 assert_eq!(s.encoding(), Encoding::Listpack);
1052 for i in 0..127 {
1053 s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
1054 }
1055 assert_eq!(s.len(), 128);
1056 assert_eq!(s.encoding(), Encoding::Listpack, "128 is still a listpack");
1057
1058 s.add(b"one more", &Limits::DEFAULT);
1059 assert_eq!(s.len(), 129);
1060 assert_eq!(s.encoding(), Encoding::Hashtable);
1061 assert!(s.contains(b"x"));
1062 assert!(s.contains(b"m126"));
1063 assert!(s.contains(b"one more"));
1064 }
1065
1066 #[test]
1067 fn a_long_member_converts_a_listpack_whatever_the_count() {
1068 let mut s = of(&["a"]);
1069 let long = vec![b'z'; 65];
1070 assert!(s.add(&long, &Limits::DEFAULT));
1071 assert_eq!(s.encoding(), Encoding::Hashtable, "65 is past 64");
1072 assert_eq!(s.len(), 2);
1073 assert!(s.contains(&long));
1074
1075 // And exactly at the boundary it does not.
1076 let mut ok = of(&["a"]);
1077 ok.add(&[b'z'; 64], &Limits::DEFAULT);
1078 assert_eq!(ok.encoding(), Encoding::Listpack, "64 fits");
1079 }
1080
1081 #[test]
1082 fn a_long_member_sends_an_intset_to_a_table_and_not_a_listpack() {
1083 let mut s = of(&["1", "2"]);
1084 assert!(s.add(&[b'z'; 65], &Limits::DEFAULT));
1085 assert_eq!(s.encoding(), Encoding::Hashtable);
1086 assert_eq!(s.len(), 3);
1087 }
1088
1089 #[test]
1090 fn the_limits_are_configuration_and_moving_them_moves_the_encodings() {
1091 let tight = Limits {
1092 max_intset_entries: 2,
1093 max_listpack_entries: 2,
1094 max_listpack_value: 3,
1095 };
1096 let mut s = Set::new();
1097 s.add(b"1", &tight);
1098 s.add(b"2", &tight);
1099 assert_eq!(s.encoding(), Encoding::Intset);
1100 s.add(b"3", &tight);
1101 assert_eq!(s.encoding(), Encoding::Hashtable, "three is past two");
1102
1103 // And a member longer than three characters cannot go in a listpack.
1104 let mut t = Set::new();
1105 t.add(b"abc", &tight);
1106 assert_eq!(t.encoding(), Encoding::Listpack, "three characters fit");
1107 t.add(b"defg", &tight);
1108 assert_eq!(t.encoding(), Encoding::Hashtable, "four do not");
1109 assert!(t.contains(b"abc"));
1110 assert!(t.contains(b"defg"));
1111
1112 // Including as the first member, which is a table from the off rather
1113 // than a listpack that converts on the next thing to arrive. Redis gets
1114 // to the same place by the other road: it creates the listpack, tries
1115 // the add, and converts before the reply.
1116 let mut u = Set::new();
1117 u.add(b"abcd", &tight);
1118 assert_eq!(u.encoding(), Encoding::Hashtable);
1119 assert!(u.contains(b"abcd"));
1120 }
1121
1122 #[test]
1123 fn with_hint_picks_the_representation_up_front() {
1124 let d = &Limits::DEFAULT;
1125 assert_eq!(
1126 Set::with_hint(b"1", 10, d).encoding(),
1127 Encoding::Intset,
1128 "an integer and few enough of them"
1129 );
1130 assert_eq!(
1131 Set::with_hint(b"1", 1000, d).encoding(),
1132 Encoding::Hashtable,
1133 "an integer and too many"
1134 );
1135 assert_eq!(
1136 Set::with_hint(b"x", 10, d).encoding(),
1137 Encoding::Listpack,
1138 "not an integer and few enough"
1139 );
1140 assert_eq!(
1141 Set::with_hint(b"x", 1000, d).encoding(),
1142 Encoding::Hashtable,
1143 "not an integer and too many"
1144 );
1145 }
1146
1147 #[test]
1148 fn removing_works_in_all_three_and_never_demotes() {
1149 let mut ints = of(&["1", "2", "3"]);
1150 assert!(ints.remove(b"2"));
1151 assert!(!ints.remove(b"2"));
1152 assert!(!ints.remove(b"nope"), "not an integer, so not a member");
1153 assert_eq!(members(&ints), ["1", "3"]);
1154 assert_eq!(ints.encoding(), Encoding::Intset);
1155
1156 let mut packed = of(&["a", "b", "c"]);
1157 assert!(packed.remove(b"b"));
1158 assert!(!packed.remove(b"b"));
1159 assert_eq!(members(&packed), ["a", "c"]);
1160 assert_eq!(packed.encoding(), Encoding::Listpack);
1161
1162 let mut table = of(&["a", "b", "c"]);
1163 table.become_table(0);
1164 assert!(table.remove(b"b"));
1165 assert!(!table.remove(b"b"));
1166 assert_eq!(members(&table), ["a", "c"]);
1167 assert_eq!(
1168 table.encoding(),
1169 Encoding::Hashtable,
1170 "down to two members and still a table"
1171 );
1172 }
1173
1174 #[test]
1175 fn a_set_can_be_emptied_a_member_at_a_time() {
1176 for mut s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
1177 let all: Vec<Vec<u8>> = s.iter().map(|m| m.to_vec()).collect();
1178 for m in &all {
1179 assert!(s.remove(m));
1180 }
1181 assert!(s.is_empty());
1182 assert_eq!(s.at(0), None);
1183 }
1184 }
1185
1186 #[test]
1187 fn removing_by_position_hands_the_member_back() {
1188 // What `SPOP` runs on. Drawing position zero every time has to empty the
1189 // set rather than run off the end or repeat a member, in all three.
1190 let mut table = of(&["a", "b", "c", "d"]);
1191 table.become_table(0);
1192 for mut s in [
1193 of(&["10", "20", "30", "40"]),
1194 of(&["a", "b", "c", "d"]),
1195 table,
1196 ] {
1197 let mut got = Vec::new();
1198 while !s.is_empty() {
1199 got.push(String::from_utf8(s.remove_at(0).expect("not empty")).expect("utf8"));
1200 }
1201 got.sort();
1202 assert_eq!(got.len(), 4, "four members and no repeats");
1203 assert_eq!(s.len(), 0);
1204 assert_eq!(s.remove_at(0), None);
1205 }
1206 }
1207
1208 #[test]
1209 fn an_integer_member_is_the_same_member_however_it_is_written() {
1210 // An intset holds 42 as a number, so `SADD s 42` twice is one member.
1211 // `042` does not parse as an integer, so it is a different member and it
1212 // converts the set, which is what a real server does too.
1213 let mut s = of(&["42"]);
1214 assert!(!s.add(b"42", &Limits::DEFAULT));
1215 assert_eq!(s.len(), 1);
1216 assert!(s.add(b"042", &Limits::DEFAULT));
1217 assert_eq!(s.encoding(), Encoding::Listpack);
1218 assert_eq!(members(&s), ["042", "42"]);
1219 assert!(s.contains(b"42"));
1220 assert!(s.contains(b"042"));
1221 }
1222
1223 #[test]
1224 fn the_small_bands_answer_a_scan_in_one_go() {
1225 for s in [of(&["1", "2", "3"]), of(&["a", "b", "c"])] {
1226 let mut seen = Vec::new();
1227 // A count of one, which the table band would honour and these two
1228 // do not, and a cursor from nowhere, which these two ignore.
1229 let next = s.scan(Cursor::at(1, 0, 99), 1, |m| seen.push(m.to_vec()));
1230 assert!(next.is_end(), "{:?} split a scan up", s.encoding());
1231 assert_eq!(seen.len(), 3);
1232 }
1233 }
1234
1235 #[test]
1236 fn the_table_band_walks_a_scan_in_windows_and_misses_nothing() {
1237 let mut s = Set::new();
1238 for i in 0..300 {
1239 s.add(format!("m{i}").as_bytes(), &Limits::DEFAULT);
1240 }
1241 assert_eq!(s.encoding(), Encoding::Hashtable);
1242
1243 let mut seen = Vec::new();
1244 let mut c = Cursor::START;
1245 let mut turns = 0;
1246 loop {
1247 c = s.scan(c, 7, |m| seen.push(m.to_vec()));
1248 turns += 1;
1249 assert!(turns < 100, "the scan did not finish");
1250 if c.is_end() {
1251 break;
1252 }
1253 }
1254 assert!(
1255 turns > 1,
1256 "a window of seven over three hundred took one turn"
1257 );
1258 seen.sort();
1259 seen.dedup();
1260 assert_eq!(seen.len(), 300, "every member came back at least once");
1261 }
1262
1263 #[test]
1264 fn a_conversion_loses_no_member_at_any_of_the_three_boundaries() {
1265 // One walk over each path out of an intset and out of a listpack, each
1266 // checking every member is still findable after the rewrite rather than
1267 // only checking the count.
1268 let mut wide = Set::new();
1269 for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1270 wide.add(v.to_string().as_bytes(), &Limits::DEFAULT);
1271 }
1272 wide.add(b"str", &Limits::DEFAULT);
1273 assert_eq!(wide.encoding(), Encoding::Listpack);
1274 for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1275 assert!(wide.contains(v.to_string().as_bytes()), "{v} survived");
1276 }
1277
1278 wide.add(&[b'q'; 100], &Limits::DEFAULT);
1279 assert_eq!(wide.encoding(), Encoding::Hashtable);
1280 for v in [i64::MIN, -1, 0, 1, i64::MAX] {
1281 assert!(
1282 wide.contains(v.to_string().as_bytes()),
1283 "{v} survived twice"
1284 );
1285 }
1286 assert!(wide.contains(b"str"));
1287 assert_eq!(wide.len(), 7);
1288 }
1289}