yo_kv/hash.rs
1//! A hash, in whichever of the two representations currently fits it.
2//!
3//! A hash is a listpack of alternating fields and values, or an element table
4//! that keeps each value behind its field name. Which one is not a free choice:
5//! `OBJECT
6//! ENCODING` has to answer `listpack` or `hashtable` at exactly the sizes a real
7//! server answers them, so the rule here is `hash_max_listpack_entries` and
8//! `hash_max_listpack_value` read off `t_hash.c` in the 8.10.1 tarball.
9//!
10//! ```text
11//! small, any bytes everything else
12//! +---------------------------+ +---------------------------------+
13//! | f | v | f | v | f | v ... |-->| element table, value behind the |
14//! | ~2 B a side, walked | | name; one probe, no cap |
15//! +---------------------------+ +---------------------------------+
16//! to 512 fields, 64 B a side
17//! ```
18//!
19//! Promotion is one-way and upward, which is Y4. The set has three bands because
20//! an all integer set has an intset to be; a hash has no equivalent, because
21//! there is no representation that is cheaper for a hash whose fields happen to
22//! be numbers.
23//!
24//! # Where the values live
25//!
26//! In the listpack they are simply the odd elements, which is why
27//! [`Listpack::find`] takes a step: a field is at an even index and its value is
28//! the next one along, and searching with a step of two never matches a value by
29//! accident. `HSET h a b` followed by `HGET h b` finds nothing, which is right,
30//! and a search with a step of one would have found the `b` that is a value.
31//!
32//! In the table band the value goes into the element table's own blob directly
33//! behind the field name, with its length in front of the bytes. That is `05`
34//! section 4.2's element per row: a value is bytes in a shared stretch, not an
35//! allocation of its own, and rewriting one appends and abandons rather than
36//! moving everything after it. The abandoned bytes are counted and come back
37//! when they outnumber the live ones.
38//!
39//! Behind the name and not in a blob of its own, because a second blob needs a
40//! four byte offset beside every row saying where in it to look, and that offset
41//! was the largest single piece of overhead a field carried, bigger than the row
42//! and bigger than the slot. The row already says where the name starts and the
43//! name says how long it is, so the value that follows it needs nothing but its
44//! own length, which the separate blob was writing anyway.
45//!
46//! What it costs is that a rewrite copies the field name again, since the new
47//! value need not be the length of the old one. That is the one thing the split
48//! blobs did better, it measured at nineteen percent on a write to a field that
49//! was already there, and a write to a field that is new got twenty seven
50//! percent quicker for the same reason.
51//!
52//! Field names are still interned, so a hash holding a thousand copies of the
53//! same field name across a thousand rewrites holds one of them.
54//!
55//! What a field costs, then, is eight bytes of row, one of value length, about
56//! eight of slot array, and the field name and the value themselves. The two
57//! arrays are the rest and the only way past them is to stop interning field
58//! names, which would put the name back in front of every value and pay for it
59//! again on every rewrite.
60//!
61//! # Field TTL
62//!
63//! A field can be given its own deadline, which is the `HEXPIRE` family, and the
64//! two bands pay for it differently.
65//!
66//! The packed band grows a third element per field the first time any field of
67//! that hash is given one, holding the deadline in unix milliseconds or a zero
68//! for no deadline. That is what Redis does and it is why `OBJECT ENCODING`
69//! grows a third answer, `listpackex`. Everything below stays single path
70//! because the walk takes a step of two or of three rather than there being two
71//! copies of the code, and a hash that never sees `HEXPIRE` never widens.
72//!
73//! The table band hands the job to [`Deadlines`], a side array indexed by row
74//! position that allocates nothing until the first deadline. [`crate::ttl`] is
75//! where the reasoning for that lives, including why it is indexed by the row
76//! and not by a number in the row.
77//!
78//! Widening is one way in both bands, the same as promotion: a hash whose last
79//! deadline has been taken off keeps the shape, because going back would mean
80//! rewriting the whole thing to save a byte a field on a hash that has already
81//! shown it uses deadlines.
82//!
83//! # Expiry is lazy here too
84//!
85//! A field past its deadline is still sitting in the structure until something
86//! looks at it. [`Hash::reap`] is that look, it is called by the keyspace before
87//! any hash command runs, and it is guarded by one comparison against the
88//! earliest deadline in the hash, so a hash with no field TTL pays a load and a
89//! branch and nothing else. Every read path below can therefore treat what it
90//! finds as live, which is what keeps `HGET` the shape it was before any of this
91//! landed.
92//!
93//! A write clears a field's deadline. `HSET` on a field that had one leaves it
94//! with no deadline, which is Redis's rule since 7.4 and is the reason `HGETEX`
95//! exists to read a field without disturbing it.
96//!
97//! # The blob goes both ways
98//!
99//! The listpack this band holds is byte for byte what Redis's `HASH_LISTPACK` is,
100//! which is why `Hash::packed_bytes` hands it to `DUMP` uncopied. Read
101//! backwards, that says a `RESTORE` should move the blob in whole rather than
102//! set a field at a time, and it is worth much more coming in than going out:
103//! setting a field scans everything already there to see whether it is a repeat,
104//! so a hundred fields is five thousand comparisons to build something that
105//! arrived ready to use.
106//!
107//! `Hash::from_packed` is that, and the one thing it has to prove is that no
108//! field is in the blob twice, because `Packed::find` answers with the first
109//! row that matches and stops. It proves it by hashing every field into a stack
110//! array and sorting that, one pass and a sort, no allocation, and a collision
111//! costs a fallback to the walk rather than a wrong answer. Anything it will not
112//! take is handed back so the caller can walk it without parsing it again.
113//!
114//! Only the band without deadlines. `packed_bytes` will not copy the wider one
115//! out and this will not take one in, and it is the same reason both times.
116
117use yo_common::num::{self, parse_i64};
118
119use crate::elem::Elements;
120use crate::frozen::{self, Broken};
121use crate::listpack::{self, Listpack};
122use crate::scan::Cursor;
123use crate::ttl::{Applied, Ask, Cond, Deadlines, decide};
124
125/// No deadline, the same sentinel [`crate::ttl`] uses and for the same reason.
126const NONE: u64 = u64::MAX;
127
128/// The most fields `Hash::from_packed` will check for a repeat in one go.
129///
130/// It is the size of a stack array, so it has to be a constant, and it is
131/// [`Limits::DEFAULT`]'s field count because that is the largest hash a stock
132/// server will hand over on this band. A blob with more fields than this is
133/// walked instead of adopted, which is only slower and never wrong, and it can
134/// only come from a server with `hash-max-listpack-entries` raised above the
135/// default. Four kilobytes of stack for the length of one `RESTORE` is a fair
136/// price for taking the square out of the common case.
137const CHECK_MAX: usize = Limits::DEFAULT.max_listpack_entries;
138
139/// The packed band with two elements a field, which is Redis's `HASH_LISTPACK`.
140const FORM_PACKED: u8 = 1;
141/// The packed band with three, a deadline behind every value.
142const FORM_PACKED_EX: u8 = 2;
143/// The element table, written out as its fields.
144const FORM_FIELDS: u8 = 3;
145/// On a table, that a deadline follows every pair.
146///
147/// The top bit of the form byte rather than a form of its own, so that a hash
148/// with no field TTL, which is nearly all of them, does not pay a byte a field
149/// for a column of zeroes.
150const HAS_TTL: u8 = 0x80;
151
152/// A field name or a value, as it is stored.
153///
154/// Both sides of a pair are the same thing to a listpack, which stores something
155/// that looks like an integer as an integer. `HSET h f 42` and `HSET h f 042`
156/// hold different bytes and both answer with what went in, and the formatting
157/// happens once, into the reply buffer, the way Y18 asks.
158pub type Text<'a> = listpack::Entry<'a>;
159
160/// Where the encoding changes over.
161///
162/// These are `hash-max-listpack-entries` and `hash-max-listpack-value`, runtime
163/// configuration in Redis, so they are passed in rather than being constants.
164/// The value limit applies to a field name and to a value alike, which is what
165/// `hashTypeTryConversion` does.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct Limits {
168 /// At this many fields a hash stops being a listpack.
169 pub max_listpack_entries: usize,
170 /// A field or a value longer than this cannot go in a listpack.
171 pub max_listpack_value: usize,
172}
173
174impl Limits {
175 /// Redis's defaults: 512 and 64.
176 ///
177 /// The count is 512 and not the 128 everyone remembers, and everyone
178 /// remembers 128 because that is what it was for years. Read off a running
179 /// 8.10.1 with nothing in its config file rather than off the documentation,
180 /// which is the only way to be sure of a number like this. It matters
181 /// because a hash of two hundred fields answers `listpack` there, so it has
182 /// to answer `listpack` here too.
183 pub const DEFAULT: Limits = Limits {
184 max_listpack_entries: 512,
185 max_listpack_value: 64,
186 };
187}
188
189impl Default for Limits {
190 fn default() -> Limits {
191 Limits::DEFAULT
192 }
193}
194
195/// Which representation a hash is in, which is what `OBJECT ENCODING` reports.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum Encoding {
198 /// One packed blob of alternating fields and values, walked linearly.
199 Listpack,
200 /// The same blob widened to three elements a field, the third a deadline.
201 ///
202 /// Not a third band, which is the point: it is the packed band with a wider
203 /// step, and a hash arrives here by being given a field deadline rather than
204 /// by growing.
205 ListpackEx,
206 /// The element table, each value behind its field name.
207 Hashtable,
208}
209
210impl Encoding {
211 /// The word `OBJECT ENCODING` replies with.
212 #[inline]
213 #[must_use]
214 pub const fn name(self) -> &'static str {
215 match self {
216 Encoding::Listpack => "listpack",
217 Encoding::ListpackEx => "listpackex",
218 Encoding::Hashtable => "hashtable",
219 }
220 }
221}
222
223/// The packed band, at two elements a field or at three.
224#[derive(Debug, Clone)]
225struct Packed {
226 lp: Listpack,
227 /// Whether there is a deadline element after every value.
228 ///
229 /// One bool rather than two variants of a band, so that everything reached
230 /// through [`Packed::step`] is written once and a hash without field TTL
231 /// runs the same code a hash with it does.
232 ex: bool,
233 /// A lower bound on the earliest deadline here, or [`NONE`].
234 ///
235 /// Leans early for the reason [`Deadlines::soonest`] gives: it goes down
236 /// when a deadline is set and does not go back up when one is taken off, so
237 /// [`Hash::reap`] can walk for nothing but cannot sleep through an expiry.
238 soonest: u64,
239}
240
241impl Packed {
242 fn new() -> Packed {
243 Packed {
244 lp: Listpack::new(),
245 ex: false,
246 soonest: NONE,
247 }
248 }
249
250 /// Two elements a field, or three once any field has a deadline.
251 #[inline]
252 const fn step(&self) -> usize {
253 if self.ex { 3 } else { 2 }
254 }
255
256 #[inline]
257 fn len(&self) -> usize {
258 self.lp.len() / self.step()
259 }
260
261 /// Where `field`'s name is, which is also where its row starts.
262 #[inline]
263 fn find(&self, field: &[u8]) -> Option<usize> {
264 self.lp.find(field, self.step())
265 }
266
267 /// The deadline on the row starting at `at`, if it has one.
268 fn deadline(&self, at: usize) -> Option<u64> {
269 if !self.ex {
270 return None;
271 }
272 match self.lp.get(at + 2) {
273 // A field with no deadline holds a zero rather than the slot being
274 // left out, so the rows stay three wide and the step stays a
275 // constant. Redis writes the same zero.
276 Some(Text::Int(n)) => u64::try_from(n).ok().filter(|&at| at != 0),
277 // A deadline goes in as digits and a listpack holds digits as a
278 // number, so nothing else is a shape this band can be in.
279 _ => None,
280 }
281 }
282
283 /// Write a deadline, or a zero for none, onto the row starting at `at`.
284 fn write_deadline(&mut self, at: usize, deadline: u64) {
285 debug_assert!(self.ex, "widen before writing a deadline");
286 let mut buf = [0u8; num::DIGITS_MAX];
287 self.lp.replace(at + 2, num::u64_digits(&mut buf, deadline));
288 }
289
290 /// Store `value` against `field` and say whether the field is new.
291 fn set(&mut self, field: &[u8], value: &[u8]) -> bool {
292 match self.find(field) {
293 Some(at) => {
294 self.lp.replace(at + 1, value);
295 if self.ex {
296 // A write clears the deadline. Redis's rule, and the reason
297 // HGETEX is a command rather than a flag on HGET.
298 self.write_deadline(at, 0);
299 }
300 false
301 }
302 None => {
303 self.lp.push(field);
304 self.lp.push(value);
305 if self.ex {
306 self.lp.push(b"0");
307 }
308 true
309 }
310 }
311 }
312
313 /// Take the whole row starting at `at` out.
314 #[inline]
315 fn remove_at(&mut self, at: usize) -> bool {
316 self.lp.delete(at, self.step())
317 }
318
319 /// Grow the third element, which is where `listpackex` starts.
320 fn widen(&mut self) {
321 if self.ex {
322 return;
323 }
324 let mut fresh = Listpack::new();
325 let mut pair = self.lp.iter();
326 while let (Some(field), Some(value)) = (pair.next(), pair.next()) {
327 push_text(&mut fresh, field);
328 push_text(&mut fresh, value);
329 fresh.push(b"0");
330 }
331 self.lp = fresh;
332 self.ex = true;
333 }
334
335 /// The earliest deadline actually here, or [`NONE`].
336 ///
337 /// A walk, so only [`Hash::reap`] calls it, and only once it has walked the
338 /// whole thing anyway and knows the bound it was carrying is stale.
339 fn earliest(&self) -> u64 {
340 let mut soonest = NONE;
341 let mut at = 0;
342 while at < self.lp.len() {
343 if let Some(deadline) = self.deadline(at) {
344 soonest = soonest.min(deadline);
345 }
346 at += self.step();
347 }
348 soonest
349 }
350
351 /// Drop every field whose deadline has passed, and say how many went.
352 fn reap(&mut self, now: u64) -> usize {
353 let mut gone = 0;
354 let mut at = 0;
355 while at < self.lp.len() {
356 match self.deadline(at) {
357 Some(deadline) if deadline <= now => {
358 self.remove_at(at);
359 gone += 1;
360 }
361 // Only step past a row that survived, because taking one out
362 // moves the next row into this position.
363 _ => at += self.step(),
364 }
365 }
366 gone
367 }
368}
369
370/// Put an entry back into a listpack, writing the digits of a number once.
371///
372/// The only caller is [`Packed::widen`], which is copying a listpack it already
373/// holds, so a number that went in as a number comes back out as one and is
374/// stored as one again.
375/// The bytes an entry stands for, writing the digits of a number into `digits`.
376///
377/// A listpack holds something that looks like an integer as an integer, so the
378/// field `10` comes back out as a number and has to be turned back into the two
379/// bytes it was written as before anything compares or hashes it.
380fn bytes_of<'a>(t: Text<'a>, digits: &'a mut [u8; num::DIGITS_MAX]) -> &'a [u8] {
381 match t {
382 Text::Str(s) => s,
383 Text::Int(n) => num::i64_digits(digits, n),
384 }
385}
386
387/// How many blob bytes a hash of `n` fields promoted from `p` wants.
388///
389/// The blob holds a field name and its value back to back, so this counts both.
390///
391/// The old answer was sixteen a field whatever the values were, and at a
392/// thousand eight byte values that guess stayed visible in the measurement: the
393/// blob opened at nearly twice what it needed and doubled from there, so it held
394/// 16.42 bytes a field to store nine. A blob never shrinks on its own, so an
395/// overshoot at the start is still an overshoot four doublings later.
396///
397/// There is no reason to guess here, because the listpack in hand has real
398/// fields and values in it and the ones coming after them are almost always the
399/// same shape. The average includes the length byte written in front of each
400/// value, so it is the blob cost and not the payload length. It costs one walk
401/// of at most `max_listpack_entries` entries on a promotion that is about to
402/// copy every one of them anyway.
403fn blob_bytes_for(p: &Packed, n: usize) -> usize {
404 if p.len() == 0 {
405 return 0;
406 }
407 let mut seen = 0usize;
408 let mut digits = [0u8; num::DIGITS_MAX];
409 for i in 0..p.len() {
410 let at = i * p.step();
411 let (Some(f), Some(v)) = (p.lp.get(at), p.lp.get(at + 1)) else {
412 break;
413 };
414 seen += text_len(&mut digits, f) + text_len(&mut digits, v) + 1;
415 }
416 seen.saturating_mul(n) / p.len()
417}
418
419/// How many bytes one listpack entry is once it is written out as bytes.
420fn text_len(digits: &mut [u8; num::DIGITS_MAX], t: Text<'_>) -> usize {
421 match t {
422 Text::Str(s) => s.len(),
423 Text::Int(x) => num::i64_digits(digits, x).len(),
424 }
425}
426
427fn push_text(lp: &mut Listpack, t: Text<'_>) {
428 match t {
429 Text::Str(s) => lp.push(s),
430 Text::Int(n) => {
431 let mut buf = [0u8; num::DIGITS_MAX];
432 lp.push(num::i64_digits(&mut buf, n));
433 }
434 }
435}
436
437/// The native band: interned field names, each with its value behind it.
438///
439/// The value used to live in a blob of its own with a four byte offset beside
440/// every row saying where in it to look. Behind the name instead, the offset is
441/// not needed, because the row already says where the name starts and the name
442/// says how long it is. That was the largest single piece of overhead a field
443/// carried, bigger than the row and bigger than the slot, and `Elements::tailed`
444/// is where the rest of the argument lives.
445#[derive(Debug, Clone)]
446struct Table {
447 fields: Elements<()>,
448 /// One slot per row once any field has a deadline, and nothing before then.
449 ///
450 /// It has to be told about every row this table gains or loses, in the same
451 /// order, or the deadlines after a hole belong to the wrong fields. That is
452 /// what the `inserted` and `removed` calls below are, and there is a test in
453 /// [`crate::ttl`] that fails when one goes missing.
454 ttl: Deadlines,
455}
456
457impl Table {
458 /// A table with room for `hint` fields and `value_bytes` of values.
459 ///
460 /// Both are hints and being wrong about either costs a realloc, which is
461 /// what a hint is allowed to cost. The value one is worth passing properly
462 /// where the caller knows it, because a blob doubles, so an overshoot at
463 /// the start is still an overshoot several doublings later and every field
464 /// in the hash is charged for it.
465 fn new(hint: usize, value_bytes: usize) -> Table {
466 Table {
467 fields: Elements::tailed(hint, value_bytes),
468 ttl: Deadlines::new(),
469 }
470 }
471
472 #[inline]
473 fn get(&self, field: &[u8]) -> Option<&[u8]> {
474 self.fields.tail(field)
475 }
476
477 /// Store `value` against `field` and say whether the field is new.
478 fn set(&mut self, field: &[u8], value: &[u8]) -> bool {
479 match self.fields.set_tailed(field, value, ()) {
480 Ok((_, true)) => {
481 self.ttl.inserted();
482 true
483 }
484 Ok((row, false)) => {
485 // A write clears the deadline, the same as in the packed band.
486 self.ttl.clear(row);
487 false
488 }
489 // A field name over NAME_MAX or a table at MAX_ROWS. Nothing was
490 // written, so there is nothing to give back.
491 Err(_) => false,
492 }
493 }
494
495 fn remove(&mut self, field: &[u8]) -> bool {
496 match self.fields.index_of(field) {
497 Some(row) => {
498 self.remove_at(row);
499 true
500 }
501 None => false,
502 }
503 }
504
505 /// Take the row at `row` out, keeping the deadlines lined up with it.
506 ///
507 /// The one place a row leaves this table, so that the swap remove and the
508 /// deadline that has to follow it cannot drift apart in a later edit.
509 fn remove_at(&mut self, row: usize) {
510 self.fields
511 .remove_at(row)
512 .expect("the caller found the row");
513 self.ttl.removed(row);
514 }
515}
516
517/// The two representations.
518#[derive(Debug, Clone)]
519enum Body {
520 Packed(Packed),
521 Table(Table),
522}
523
524/// A hash of fields to values.
525#[derive(Debug, Clone)]
526pub struct Hash {
527 body: Body,
528}
529
530impl Default for Hash {
531 fn default() -> Hash {
532 Hash::new()
533 }
534}
535
536impl Hash {
537 /// An empty hash, which starts as a listpack.
538 #[must_use]
539 pub fn new() -> Hash {
540 Hash {
541 body: Body::Packed(Packed::new()),
542 }
543 }
544
545 /// An empty hash sized for what is about to go in it.
546 ///
547 /// `HSET k f1 v1 f2 v2 ...` with a thousand pairs builds a table once rather
548 /// than converting on the way there. The hint is only a hint and being wrong
549 /// costs a conversion and no correctness.
550 #[must_use]
551 pub fn with_hint(hint: usize, limits: &Limits) -> Hash {
552 if hint <= limits.max_listpack_entries {
553 Hash::new()
554 } else {
555 Hash {
556 // Sixteen bytes a field for the names and values together,
557 // because a caller who names a field count and nothing else has
558 // told us everything it knows. Undershooting costs a realloc and
559 // overshooting is charged to every field, so this leans low.
560 body: Body::Table(Table::new(hint, hint.saturating_mul(16))),
561 }
562 }
563 }
564
565 /// Take a listpack that is already in this band's layout, if it really is.
566 ///
567 /// The blob a `RESTORE` carries for a `HASH_LISTPACK` is byte for byte what
568 /// this band holds, so the fast answer is to move it in whole rather than to
569 /// set a field at a time. Setting costs a scan of everything already there to
570 /// see whether the field is a repeat, so a hundred fields is five thousand
571 /// comparisons to build a thing that arrived ready to use.
572 ///
573 /// The blob comes back on refusal so that a caller who has to walk it after
574 /// all does not have to parse it a second time.
575 ///
576 /// What has to be ruled out is a repeated field, because `Packed::find`
577 /// answers with the first row that matches and stops. A blob holding the same
578 /// field twice would give a hash whose `HLEN` counts both and whose `HGET`
579 /// and `HDEL` only ever reach one, so the length would disagree with
580 /// `HGETALL` and a delete would leave the field behind. The sorted set got
581 /// this for nothing in #192 because its blob is ordered and strictly
582 /// increasing rules out a repeat on the way past, and a hash blob is in
583 /// insertion order, so it has to be looked for on purpose.
584 ///
585 /// It is looked for by hashing each field into a stack array and sorting
586 /// that, which is one pass and a sort rather than the square of the count,
587 /// and allocates nothing. A hash collision costs a fallback to the walk and
588 /// not a wrong answer, and over at most [`CHECK_MAX`] fields a 64 bit
589 /// collision is not going to happen. The array is why there is a cap: a blob
590 /// with more fields than that is walked, which is what every blob did before
591 /// this, and the cap is Redis's own default for this band so a hash from a
592 /// stock server is always under it.
593 pub(crate) fn from_packed(lp: Listpack, limits: &Limits) -> Result<Hash, Listpack> {
594 let n = lp.len();
595 if n == 0 || !n.is_multiple_of(2) {
596 return Err(lp);
597 }
598 let fields = n / 2;
599 if fields > limits.max_listpack_entries || fields > CHECK_MAX {
600 return Err(lp);
601 }
602 let mut marks = [0u64; CHECK_MAX];
603 let ok = {
604 let mut field_digits = [0u8; num::DIGITS_MAX];
605 let mut value_digits = [0u8; num::DIGITS_MAX];
606 let mut walk = lp.iter();
607 let mut i = 0;
608 loop {
609 let Some(field) = walk.next() else { break true };
610 // The count is even, checked above, so there is always a value
611 // behind a field.
612 let Some(value) = walk.next() else {
613 break false;
614 };
615 let name = bytes_of(field, &mut field_digits);
616 if name.len() > limits.max_listpack_value {
617 break false;
618 }
619 marks[i] = Elements::<u32>::hash_of(name);
620 i += 1;
621 if bytes_of(value, &mut value_digits).len() > limits.max_listpack_value {
622 break false;
623 }
624 }
625 };
626 if !ok {
627 return Err(lp);
628 }
629 let marks = &mut marks[..fields];
630 marks.sort_unstable();
631 if marks.windows(2).any(|pair| pair[0] == pair[1]) {
632 return Err(lp);
633 }
634 Ok(Hash {
635 body: Body::Packed(Packed {
636 lp,
637 ex: false,
638 soonest: NONE,
639 }),
640 })
641 }
642
643 /// Which representation this is in.
644 #[inline]
645 #[must_use]
646 pub const fn encoding(&self) -> Encoding {
647 match &self.body {
648 Body::Packed(p) if p.ex => Encoding::ListpackEx,
649 Body::Packed(_) => Encoding::Listpack,
650 Body::Table(_) => Encoding::Hashtable,
651 }
652 }
653
654 /// The bytes behind a hash on the packed band, for `DUMP` to copy.
655 ///
656 /// Field and value alternate in here exactly as `HASH_LISTPACK` wants them.
657 /// `None` on the table, and `None` on the wider band as well: the deadline
658 /// column has its own type byte and its own header, and a hash that has been
659 /// widened once keeps the third element per field even after every deadline
660 /// has been taken off again, so the blob is only ever safe to copy when
661 /// there is no deadline column at all.
662 #[inline]
663 pub(crate) fn packed_bytes(&self) -> Option<&[u8]> {
664 match &self.body {
665 Body::Packed(p) if !p.ex => Some(p.lp.as_bytes()),
666 _ => None,
667 }
668 }
669
670 /// Write this hash out as the bytes it comes back from.
671 ///
672 /// What a demotion turns the body into so the record can hold an address
673 /// instead of a slab slot. The form byte says which band left, because the
674 /// band is visible through `OBJECT ENCODING` and a hash that came back on a
675 /// different one would be a hash whose answer depends on memory pressure.
676 ///
677 /// Both packed bands go out as the listpack bytes they already are, so they
678 /// cost a byte of overhead and no walk. `listpackex` carries the earliest
679 /// deadline in front of them, because that bound is what stops the active
680 /// cycle from having to walk a hash to find out it has nothing to do, and
681 /// recomputing it on the way back in would be a walk on the fault path.
682 ///
683 /// The table goes out as its fields, with the deadline column written only
684 /// if a field actually has one.
685 pub fn freeze(&self, out: &mut Vec<u8>) {
686 match &self.body {
687 Body::Packed(p) if !p.ex => {
688 out.push(FORM_PACKED);
689 out.extend_from_slice(p.lp.as_bytes());
690 }
691 Body::Packed(p) => {
692 out.push(FORM_PACKED_EX);
693 frozen::put_uint(out, p.soonest);
694 out.extend_from_slice(p.lp.as_bytes());
695 }
696 Body::Table(t) => {
697 let with_ttl = !t.ttl.is_empty();
698 out.push(if with_ttl {
699 FORM_FIELDS | HAS_TTL
700 } else {
701 FORM_FIELDS
702 });
703 let n = t.fields.len();
704 frozen::put_uint(out, n as u64);
705 // The blob the table opens with on the way back. A blob never
706 // shrinks on its own, so a guess here is charged to every field
707 // for as long as the hash lives, and the exact number is one
708 // walk of a thing that is about to be walked anyway.
709 let mut tail = 0usize;
710 for i in 0..n {
711 let (f, v) = t.fields.pair_at(i).expect("index is under the length");
712 tail += f.len() + v.len() + 1;
713 }
714 frozen::put_uint(out, tail as u64);
715 for i in 0..n {
716 let (f, v) = t.fields.pair_at(i).expect("index is under the length");
717 frozen::put_bytes(out, f);
718 frozen::put_bytes(out, v);
719 if with_ttl {
720 frozen::put_uint(out, t.ttl.get(i).unwrap_or(0));
721 }
722 }
723 }
724 }
725 }
726
727 /// Read a hash back out of what [`Hash::freeze`] wrote.
728 ///
729 /// Answers an error rather than panicking on anything that is not the shape
730 /// it wrote, because the bytes have been to a device and back and a torn
731 /// chunk has to reach the caller as a failed read.
732 pub fn thaw(bytes: &[u8]) -> Result<Hash, Broken> {
733 let mut cut = frozen::Cut::new(bytes);
734 let tag = cut.byte()?;
735 match tag {
736 FORM_PACKED => Ok(Hash {
737 body: Body::Packed(Packed {
738 lp: Listpack::from_bytes(cut.rest()).map_err(|_| Broken::Body)?,
739 ex: false,
740 soonest: NONE,
741 }),
742 }),
743 FORM_PACKED_EX => {
744 let soonest = cut.uint()?;
745 Ok(Hash {
746 body: Body::Packed(Packed {
747 lp: Listpack::from_bytes(cut.rest()).map_err(|_| Broken::Body)?,
748 ex: true,
749 soonest,
750 }),
751 })
752 }
753 _ if tag & !HAS_TTL == FORM_FIELDS => {
754 let with_ttl = tag & HAS_TTL != 0;
755 let n = usize::try_from(cut.uint()?).map_err(|_| Broken::Short)?;
756 let tail = usize::try_from(cut.uint()?).map_err(|_| Broken::Short)?;
757 // A pair is at least two bytes, so a count larger than what is
758 // left cannot be honest and is not worth an allocation.
759 if n > cut.rest().len() || tail > cut.rest().len() {
760 return Err(Broken::Body);
761 }
762 let mut t = Table::new(n, tail);
763 for _ in 0..n {
764 let field = cut.bytes()?;
765 let value = cut.bytes()?;
766 let deadline = if with_ttl { cut.uint()? } else { 0 };
767 if !t.set(field, value) {
768 // A repeat field, a name over the limit or a table at
769 // its row cap. None of those is something freeze wrote.
770 return Err(Broken::Body);
771 }
772 if deadline != 0 {
773 // Zero is not a deadline anything stores, so `now` of
774 // zero rejects nothing that was really there. The row
775 // is the one just appended.
776 let row = t.fields.len() - 1;
777 let applied = t.ttl.set(row, deadline, Cond::Always, 0);
778 debug_assert_eq!(applied, Applied::Ok, "a deadline that was stored");
779 }
780 }
781 Ok(Hash {
782 body: Body::Table(t),
783 })
784 }
785 _ => Err(Broken::Form),
786 }
787 }
788
789 /// How many fields. This is `HLEN`.
790 #[inline]
791 #[must_use]
792 pub fn len(&self) -> usize {
793 match &self.body {
794 Body::Packed(p) => p.len(),
795 Body::Table(t) => t.fields.len(),
796 }
797 }
798
799 /// Whether there are none.
800 ///
801 /// An empty hash does not exist in Redis, so the caller deletes the key when
802 /// this turns true rather than storing an empty one.
803 #[inline]
804 #[must_use]
805 pub fn is_empty(&self) -> bool {
806 self.len() == 0
807 }
808
809 /// What is stored against `field`. This is `HGET`.
810 ///
811 /// A field past its deadline is still here until [`Hash::reap`] runs, and
812 /// the keyspace runs it before any command, so what this finds is live.
813 #[must_use]
814 pub fn get(&self, field: &[u8]) -> Option<Text<'_>> {
815 match &self.body {
816 Body::Packed(p) => {
817 let at = p.find(field)?;
818 p.lp.get(at + 1)
819 }
820 Body::Table(t) => t.get(field).map(Text::Str),
821 }
822 }
823
824 /// Whether `field` is here at all. This is `HEXISTS`.
825 #[must_use]
826 pub fn contains(&self, field: &[u8]) -> bool {
827 match &self.body {
828 Body::Packed(p) => p.find(field).is_some(),
829 Body::Table(t) => t.fields.contains(field),
830 }
831 }
832
833 /// How long the value against `field` is. This is `HSTRLEN`.
834 ///
835 /// A missing field is zero to Redis and `None` here, because the layer that
836 /// knows it is answering `HSTRLEN` is the one that should decide that a
837 /// missing field and an empty value give the same number.
838 #[must_use]
839 pub fn value_len(&self, field: &[u8]) -> Option<usize> {
840 match &self.body {
841 Body::Packed(_) => self.get(field).map(|v| v.byte_len()),
842 Body::Table(t) => t.fields.tail_len(field),
843 }
844 }
845
846 /// The pair at `index`, in whatever order the representation holds them.
847 ///
848 /// Insertion order in both bands, and neither is a promise. `HRANDFIELD`
849 /// needs positions and this is what gives it them, the same way `SPOP` uses
850 /// the set's.
851 #[must_use]
852 pub fn at(&self, index: usize) -> Option<(Text<'_>, Text<'_>)> {
853 match &self.body {
854 Body::Packed(p) => {
855 let at = index * p.step();
856 let field = p.lp.get(at)?;
857 let value = p.lp.get(at + 1)?;
858 Some((field, value))
859 }
860 Body::Table(t) => {
861 let (name, value) = t.fields.pair_at(index)?;
862 Some((Text::Str(name), Text::Str(value)))
863 }
864 }
865 }
866
867 /// The deadline on the field at `index`, if it has one.
868 ///
869 /// The positional twin of [`Hash::deadline`], which takes a field name.
870 /// `DUMP` is what wants this: it is already walking by index and looking the
871 /// name back up to ask about its deadline would mean formatting every
872 /// integer field into digits just to hand them straight back.
873 #[must_use]
874 pub fn deadline_at(&self, index: usize) -> Option<u64> {
875 match &self.body {
876 Body::Packed(p) => p.deadline(index * p.step()),
877 Body::Table(t) => t.ttl.get(index),
878 }
879 }
880
881 /// Every field and its value, in insertion order.
882 pub fn iter(&self) -> impl Iterator<Item = (Text<'_>, Text<'_>)> {
883 (0..self.len()).map(|i| self.at(i).expect("index is under the length"))
884 }
885
886 /// Walk part of the hash and say where to resume. This is `HSCAN`.
887 ///
888 /// Only the table band walks in windows, for the reason [`crate::set::Set`]
889 /// gives: a hundred and twenty eight fields is smaller than the arithmetic
890 /// to split them up, and a hash that small cannot hold the loop long enough
891 /// for splitting to buy anything. A listpack hands back everything and
892 /// [`Cursor::END`], ignoring the cursor it was given, which is safe because
893 /// promotion is one way.
894 pub fn scan<F>(&self, cursor: Cursor, count: usize, mut f: F) -> Cursor
895 where
896 F: FnMut(Text<'_>, Text<'_>),
897 {
898 match &self.body {
899 Body::Table(t) => t.fields.scan_pairs(cursor, count, |name, value| {
900 f(Text::Str(name), Text::Str(value));
901 }),
902 Body::Packed(_) => {
903 for (field, value) in self.iter() {
904 f(field, value);
905 }
906 Cursor::END
907 }
908 }
909 }
910
911 /// Store `value` against `field`, promoting if it no longer fits.
912 ///
913 /// Answers whether the field is new, which is the number `HSET` reports.
914 pub fn set(&mut self, field: &[u8], value: &[u8], limits: &Limits) -> bool {
915 if let Body::Packed(p) = &mut self.body {
916 // Redis checks both sides against the value limit before it writes,
917 // in hashTypeTryConversion, so a pair too long for the band converts
918 // the hash and is never briefly stored in a listpack that should not
919 // hold it.
920 if field.len() > limits.max_listpack_value || value.len() > limits.max_listpack_value {
921 self.become_table(1);
922 } else {
923 let fresh = p.set(field, value);
924 // Strictly greater, so the 128th field is still a listpack and
925 // the 129th is not.
926 if fresh && p.len() > limits.max_listpack_entries {
927 self.become_table(0);
928 }
929 return fresh;
930 }
931 }
932 match &mut self.body {
933 Body::Table(t) => t.set(field, value),
934 Body::Packed(_) => unreachable!("the conversion above left a table"),
935 }
936 }
937
938 /// Take `field` out. Answers whether it was there. This is `HDEL`.
939 ///
940 /// Never demotes, which is Y4's one-way rule and Redis's behaviour.
941 pub fn remove(&mut self, field: &[u8]) -> bool {
942 match &mut self.body {
943 Body::Packed(p) => match p.find(field) {
944 // The field, its value and its deadline go together and they are
945 // adjacent, which is the whole reason a row is stored this way
946 // round.
947 Some(at) => p.remove_at(at),
948 None => false,
949 },
950 Body::Table(t) => t.remove(field),
951 }
952 }
953
954 /// The earliest deadline any field here has, or `None`.
955 ///
956 /// A bound and not the answer, which [`crate::ttl`] explains: it can be
957 /// earlier than the truth and never later, so acting on it wastes a walk at
958 /// worst and cannot miss an expiry. M5's active cycle is the other caller.
959 #[inline]
960 #[must_use]
961 pub fn soonest_deadline(&self) -> Option<u64> {
962 match &self.body {
963 Body::Packed(p) if p.soonest == NONE => None,
964 Body::Packed(p) => Some(p.soonest),
965 Body::Table(t) => t.ttl.soonest(),
966 }
967 }
968
969 /// Drop every field whose deadline has passed, and say how many went.
970 ///
971 /// The keyspace calls this before every hash command, so it has to be cheap
972 /// on a hash that has no deadlines at all, and it is: one load and one
973 /// comparison. Only a hash that has actually been given a deadline that has
974 /// actually passed pays for the walk.
975 ///
976 /// The caller deletes the key when this empties the hash, the same way it
977 /// does after an `HDEL` that takes the last field, because an empty hash is
978 /// not a thing Redis stores.
979 pub fn reap(&mut self, now: u64) -> usize {
980 match self.soonest_deadline() {
981 Some(soonest) if soonest <= now => {}
982 _ => return 0,
983 }
984 match &mut self.body {
985 Body::Packed(p) => {
986 let gone = p.reap(now);
987 // The bound has been leaning early and this walk is the one that
988 // knows the truth, so it is the one that pays to fix it.
989 p.soonest = p.earliest();
990 gone
991 }
992 Body::Table(t) => {
993 let mut gone = 0;
994 let mut row = 0;
995 while row < t.fields.len() {
996 if t.ttl.is_expired(row, now) {
997 // The last row moves into this one, so stay put and look
998 // at whatever landed here.
999 t.remove_at(row);
1000 gone += 1;
1001 } else {
1002 row += 1;
1003 }
1004 }
1005 t.ttl.refresh_soonest();
1006 gone
1007 }
1008 }
1009 }
1010
1011 /// Put a deadline on `field`, in absolute unix milliseconds.
1012 ///
1013 /// This is the whole `HEXPIRE` family, which all turn their argument into an
1014 /// absolute millisecond before they get here. [`Applied::Deleted`] means the
1015 /// deadline had already passed and the field has been taken out, which is
1016 /// what makes `HEXPIRE key 0 FIELDS 1 f` a roundabout `HDEL`.
1017 ///
1018 /// The caller has already checked `at` against [`crate::ttl::MAX_AT`],
1019 /// because Redis rejects the whole command rather than failing field by
1020 /// field.
1021 pub fn expire(&mut self, field: &[u8], at: u64, cond: Cond, now: u64) -> Applied {
1022 match &mut self.body {
1023 Body::Packed(p) => {
1024 let Some(row) = p.find(field) else {
1025 return Applied::Missing;
1026 };
1027 let applied = decide(p.deadline(row), at, cond, now);
1028 match applied {
1029 Applied::Ok => {
1030 // Widening moves every row, so the position has to be
1031 // found again. It happens once in the life of a hash.
1032 if !p.ex {
1033 p.widen();
1034 }
1035 let row = p.find(field).expect("widening kept every field");
1036 p.write_deadline(row, at);
1037 p.soonest = p.soonest.min(at);
1038 }
1039 Applied::Deleted => {
1040 p.remove_at(row);
1041 }
1042 Applied::Missing | Applied::NotMet => {}
1043 }
1044 applied
1045 }
1046 Body::Table(t) => {
1047 let Some(row) = t.fields.index_of(field) else {
1048 return Applied::Missing;
1049 };
1050 let applied = t.ttl.set(row, at, cond, now);
1051 if applied == Applied::Deleted {
1052 t.remove_at(row);
1053 }
1054 applied
1055 }
1056 }
1057 }
1058
1059 /// What deadline `field` has. This is `HTTL` and its relatives.
1060 #[must_use]
1061 pub fn deadline(&self, field: &[u8]) -> Ask {
1062 match &self.body {
1063 Body::Packed(p) => match p.find(field) {
1064 None => Ask::Missing,
1065 Some(at) => match p.deadline(at) {
1066 Some(at) => Ask::At(at),
1067 None => Ask::NoDeadline,
1068 },
1069 },
1070 Body::Table(t) => match t.fields.index_of(field) {
1071 None => Ask::Missing,
1072 Some(row) => t.ttl.ask(row),
1073 },
1074 }
1075 }
1076
1077 /// Take `field`'s deadline off. This is `HPERSIST`.
1078 ///
1079 /// [`Ask::NoDeadline`] means there was nothing to take off, which is the -1
1080 /// Redis replies, and [`Ask::At`] hands back what was there.
1081 pub fn persist(&mut self, field: &[u8]) -> Ask {
1082 match &mut self.body {
1083 Body::Packed(p) => {
1084 let Some(at) = p.find(field) else {
1085 return Ask::Missing;
1086 };
1087 match p.deadline(at) {
1088 Some(was) => {
1089 p.write_deadline(at, 0);
1090 Ask::At(was)
1091 }
1092 None => Ask::NoDeadline,
1093 }
1094 }
1095 Body::Table(t) => match t.fields.index_of(field) {
1096 None => Ask::Missing,
1097 Some(row) => t.ttl.clear(row),
1098 },
1099 }
1100 }
1101
1102 /// How many fields carry a deadline.
1103 #[must_use]
1104 pub fn deadline_count(&self) -> usize {
1105 match &self.body {
1106 Body::Packed(p) if !p.ex => 0,
1107 Body::Packed(p) => (0..p.len())
1108 .filter(|i| p.deadline(i * p.step()).is_some())
1109 .count(),
1110 Body::Table(t) => t.ttl.len(),
1111 }
1112 }
1113
1114 /// Bytes held by whichever representation this is.
1115 #[must_use]
1116 pub fn memory_bytes(&self) -> usize {
1117 match &self.body {
1118 Body::Packed(p) => p.lp.byte_len(),
1119 Body::Table(t) => t.fields.memory_bytes() + t.ttl.memory_bytes(),
1120 }
1121 }
1122
1123 /// Value bytes no field points at any more.
1124 ///
1125 /// Reported rather than hidden, the same as the element table's dead name
1126 /// bytes, because a hash that has been rewritten holds them and `INFO
1127 /// memory` should be able to say so.
1128 #[must_use]
1129 pub fn dead_value_bytes(&self) -> usize {
1130 match &self.body {
1131 Body::Packed(_) => 0,
1132 Body::Table(t) => t.fields.dead_name_bytes(),
1133 }
1134 }
1135
1136 /// Move to the table band, with room for `extra` more fields than are here.
1137 fn become_table(&mut self, extra: usize) {
1138 let Body::Packed(p) = &self.body else {
1139 return;
1140 };
1141 let n = p.len() + extra;
1142 let mut t = Table::new(n, blob_bytes_for(p, n));
1143 for i in 0..p.len() {
1144 let at = i * p.step();
1145 let (Some(field), Some(value)) = (p.lp.get(at), p.lp.get(at + 1)) else {
1146 break;
1147 };
1148 // A listpack holds a field that looks like a number as a number, and
1149 // the table holds names as bytes, so this is where the digits get
1150 // written. Once, on promotion, and never again.
1151 let f = field.to_vec();
1152 let v = value.to_vec();
1153 t.set(&f, &v);
1154 // The deadline comes over with the field. Set through Deadlines
1155 // rather than written straight in, so the array gets allocated and
1156 // the bound gets moved exactly the way an HEXPIRE would do it.
1157 if let Some(deadline) = p.deadline(at) {
1158 let row = t.fields.index_of(&f).expect("just inserted");
1159 t.ttl.set(row, deadline, Cond::Always, 0);
1160 }
1161 }
1162 self.body = Body::Table(t);
1163 }
1164}
1165
1166/// Whether these bytes would be stored as an integer, for a caller deciding
1167/// what `OBJECT ENCODING` or an RDB writer should say about them.
1168#[must_use]
1169#[inline]
1170pub fn stores_as_int(bytes: &[u8]) -> bool {
1171 parse_i64(bytes).is_some()
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176 use super::*;
1177
1178 /// What a hash actually costs per field, which is the other half of M3's
1179 /// memory gate row and was an argument rather than a number until this was
1180 /// written.
1181 ///
1182 /// Run it with `cargo test -p yo-kv --release measure_bytes_per_field --
1183 /// --ignored --nocapture`. Ignored and printing for the same reasons the
1184 /// set and sorted set measurements next to it are.
1185 ///
1186 /// The gate is sixteen bytes a field, and the payload has to be named for
1187 /// that to mean anything, so this uses an eight byte field and an eight
1188 /// byte value. Sixteen bytes a field is then a hash that holds what it
1189 /// stores and nothing else, which nothing can reach, so the number to read
1190 /// is the overhead column and the gate is really thirty two total.
1191 ///
1192 /// The `gate` row at the bottom is the shape spec `14` section 5 actually
1193 /// names, a million fields over a thousand hashes rather than a million in
1194 /// one. It has come down twice. It printed 29.11 of overhead when the value
1195 /// blob opened at sixteen bytes a value whatever the values were, because a
1196 /// blob doubles rather than shrinks and that overshoot was still there four
1197 /// doublings later, holding 16.42 bytes a field to store nine. Sizing a
1198 /// promoted hash's blob from the values it can already see took it to 21.93.
1199 ///
1200 /// The columns are where the rest went. At 21.93 they read slots 8.19, rows
1201 /// 12.31, names 8.19 and values 9.23, and two of those four are nearly all
1202 /// payload: names is the eight byte field name plus blob slack and values is
1203 /// the eight byte value plus a length byte and slack. The overhead was a
1204 /// four byte slot at 2.1 slots a field, an eight byte row, and a four byte
1205 /// offset into the value blob beside every row.
1206 ///
1207 /// Slots is 8.19 rather than 5.33 only because the slot array rounds up to a
1208 /// power of two, and a thousand fields wants 1334 slots and gets 2048, so
1209 /// the table sits at under half load. Sizing it exactly would save about
1210 /// three bytes a field, and #178's control run already priced that at
1211 /// roughly nothing on a hit and eighteen to twenty percent on a miss.
1212 ///
1213 /// That said the gate could not be reached by tuning. Even with the slot
1214 /// array sized exactly and no blob slack at all the three arrays came to
1215 /// 5.33 plus 8 plus 4, which is 17.33, and the bar is 16. One of the three
1216 /// had to go rather than shrink, and the one that went is the value offset:
1217 /// a field's name and its value sit back to back in one blob now, so the
1218 /// row's `at` finds both and the four byte column is gone. What it costs is
1219 /// a length byte for the value in the blob, which the separate blob was
1220 /// writing anyway, so it is four bytes a field back.
1221 #[test]
1222 #[ignore = "a measurement, run it by name"]
1223 fn measure_bytes_per_field() {
1224 let limits = Limits::DEFAULT;
1225 for n in [512usize, 1_000, 100_000, 1_000_000] {
1226 let mut h = Hash::new();
1227 let mut payload = 0usize;
1228 for i in 0..n {
1229 let f = format!("f{i:07}");
1230 let v = format!("v{i:07}");
1231 payload += f.len() + v.len();
1232 h.set(f.as_bytes(), v.as_bytes(), &limits);
1233 }
1234 let total = h.memory_bytes();
1235 let per = |b: usize| b as f64 / n as f64;
1236 match &h.body {
1237 Body::Table(t) => println!(
1238 "table n={n:<9} total={total:<10} payload={payload:<9} per_field={:.2} over_per_field={:.2} slots={:.2} rows={:.2} blob={:.2}",
1239 per(total),
1240 per(total - payload),
1241 per(t.fields.slot_bytes()),
1242 per(t.fields.row_bytes()),
1243 per(t.fields.name_bytes()),
1244 ),
1245 Body::Packed(_) => println!(
1246 "listpack n={n:<9} total={total:<10} payload={payload:<9} per_field={:.2} over_per_field={:.2}",
1247 per(total),
1248 per(total - payload),
1249 ),
1250 }
1251 }
1252 // The shape the gate actually names, which is a million fields spread
1253 // over a thousand hashes rather than a million in one. It matters
1254 // because every fixed cost in a table is charged a thousand times here,
1255 // and because a thousand field table rounds its slot array up
1256 // differently from a million field one.
1257 let hashes = 1_000;
1258 let each = 1_000;
1259 let mut all = Vec::with_capacity(hashes);
1260 let mut payload = 0usize;
1261 for h in 0..hashes {
1262 let mut one = Hash::new();
1263 for i in 0..each {
1264 let f = format!("f{i:07}");
1265 let v = format!("v{h:03}{i:04}");
1266 payload += f.len() + v.len();
1267 one.set(f.as_bytes(), v.as_bytes(), &limits);
1268 }
1269 all.push(one);
1270 }
1271 let n = hashes * each;
1272 let per = |b: usize| b as f64 / n as f64;
1273 let sum = |f: fn(&Table) -> usize| -> usize {
1274 all.iter()
1275 .map(|h| match &h.body {
1276 Body::Table(t) => f(t),
1277 Body::Packed(_) => 0,
1278 })
1279 .sum()
1280 };
1281 let total: usize = all.iter().map(Hash::memory_bytes).sum();
1282 println!(
1283 "gate n={n:<9} total={total:<10} payload={payload:<9} per_field={:.2} over_per_field={:.2} slots={:.2} rows={:.2} blob={:.2}",
1284 per(total),
1285 per(total - payload),
1286 per(sum(|t| t.fields.slot_bytes())),
1287 per(sum(|t| t.fields.row_bytes())),
1288 per(sum(|t| t.fields.name_bytes())),
1289 );
1290 }
1291
1292 /// What a field read and a field write cost, in nanoseconds.
1293 ///
1294 /// Run it with `cargo test -p yo-kv --release measure_field_access --
1295 /// --ignored --nocapture`. The memory measurement next to it is the one that
1296 /// matters for the gate, and this is here so that a change made for memory
1297 /// has to say what it did to the time as well.
1298 ///
1299 /// Four cases, because the value living behind the name rather than in a
1300 /// blob of its own moves them in different directions. What it did, on a
1301 /// hundred thousand field hash, against the same test run with a separate
1302 /// value blob:
1303 ///
1304 /// ```text
1305 /// blob behind change
1306 /// get hit 10.2 ns 10.1 ns flat
1307 /// get miss 13.2 ns 13.0 ns flat
1308 /// set old 20.2 ns 24.1 ns +19%
1309 /// set new 27.3 ns 20.0 ns -27%
1310 /// ```
1311 ///
1312 /// The reads were expected to get quicker, since they follow one chain
1313 /// instead of two, and they did not: the value blob was being read straight
1314 /// after the name blob and the prefetcher was already covering it.
1315 ///
1316 /// The writes are the real trade and they go both ways. A write to a field
1317 /// that is already there copies the name again, because the new value need
1318 /// not be the length of the old one, and that is the 19 percent. A write to
1319 /// a field that is not there pushes one span instead of two and touches one
1320 /// array fewer, and that is the 27. A fill is all new fields and a counter
1321 /// being bumped is all old ones, so which way this lands depends on the
1322 /// workload, and the memory it buys does not.
1323 #[test]
1324 #[ignore = "a measurement, run it by name"]
1325 fn measure_field_access() {
1326 use std::time::Instant;
1327 let limits = Limits::DEFAULT;
1328 let n = 100_000usize;
1329 let fields: Vec<String> = (0..n).map(|i| format!("f{i:07}")).collect();
1330 let mut h = Hash::with_hint(n, &limits);
1331 for f in &fields {
1332 h.set(f.as_bytes(), b"v0000000", &limits);
1333 }
1334 let time = |label: &str, reps: usize, f: &mut dyn FnMut(usize)| {
1335 let start = Instant::now();
1336 for i in 0..reps {
1337 f(i);
1338 }
1339 let ns = start.elapsed().as_nanos() as f64 / reps as f64;
1340 println!("{label:<12} {ns:.2} ns");
1341 };
1342 let mut sink = 0usize;
1343 time("get hit", n, &mut |i| {
1344 sink += h.get(fields[i % n].as_bytes()).map_or(0, |v| v.byte_len());
1345 });
1346 let absent: Vec<String> = (0..n).map(|i| format!("g{i:07}")).collect();
1347 time("get miss", n, &mut |i| {
1348 sink += usize::from(h.get(absent[i].as_bytes()).is_none());
1349 });
1350 assert!(sink > 0, "the reads are not optimised away");
1351 let mut w = h.clone();
1352 time("set old", n, &mut |i| {
1353 w.set(fields[i % n].as_bytes(), b"v1111111", &limits);
1354 });
1355 let mut fresh = Hash::with_hint(n, &limits);
1356 time("set new", n, &mut |i| {
1357 fresh.set(fields[i].as_bytes(), b"v0000000", &limits);
1358 });
1359 }
1360
1361 /// The blob of a promoted hash is sized from the values, not a guess.
1362 ///
1363 /// A thousand eight byte names and values are seventeen thousand bytes of
1364 /// blob, and the blob used to open at sixteen a value and double from there.
1365 /// A quarter of slack is the most a doubling blob can be carrying when it
1366 /// opened at the right size.
1367 #[test]
1368 fn a_promoted_hash_does_not_size_its_values_by_guesswork() {
1369 let mut h = Hash::new();
1370 for i in 0..1000 {
1371 h.set(
1372 format!("f{i:07}").as_bytes(),
1373 format!("v{i:07}").as_bytes(),
1374 &Limits::DEFAULT,
1375 );
1376 }
1377 let Body::Table(t) = &h.body else {
1378 panic!("a thousand fields is the table band");
1379 };
1380 let held = 1000 * 17;
1381 assert!(
1382 t.fields.name_bytes() < held + held / 4,
1383 "the blob is {} bytes to hold {held}",
1384 t.fields.name_bytes()
1385 );
1386 }
1387
1388 /// A hash that never leaves the listpack band.
1389 const SMALL: Limits = Limits::DEFAULT;
1390 /// A hash that promotes on the 129th field.
1391 ///
1392 /// The default used to be this and the promotion tests used to lean on it.
1393 /// They say the number themselves now, because a test of where the line is
1394 /// should not move when the default does.
1395 const AT_128: Limits = Limits {
1396 max_listpack_entries: 128,
1397 max_listpack_value: 64,
1398 };
1399 /// A hash that is a table from its second field.
1400 const AS_TABLE: Limits = Limits {
1401 max_listpack_entries: 1,
1402 max_listpack_value: 64,
1403 };
1404
1405 fn text(t: Text<'_>) -> Vec<u8> {
1406 t.to_vec()
1407 }
1408
1409 /// A listpack in the layout `HASH_LISTPACK` arrives in.
1410 fn packed(rows: &[(&[u8], &[u8])]) -> Listpack {
1411 let mut lp = Listpack::new();
1412 for (f, v) in rows {
1413 lp.push(f);
1414 lp.push(v);
1415 }
1416 lp
1417 }
1418
1419 #[test]
1420 fn a_payload_in_this_layout_is_taken_whole() {
1421 // `10` and `9` go in as numbers, because that is what a listpack does
1422 // with anything that looks like one, and they have to come back out as
1423 // the bytes they were written as.
1424 let rows: &[(&[u8], &[u8])] = &[
1425 (b"a", b"1"),
1426 (b"b", b"two"),
1427 (b"10", b"ten"),
1428 (b"9", b""),
1429 (b"", b"empty field name"),
1430 ];
1431 let h = Hash::from_packed(packed(rows), &SMALL).expect("this band can hold it");
1432 assert_eq!(h.encoding(), Encoding::Listpack);
1433 assert_eq!(h.len(), rows.len());
1434 for (f, v) in rows {
1435 assert_eq!(h.get(f).map(text).as_deref(), Some(*v), "field {f:?}");
1436 }
1437 assert_eq!(h.soonest_deadline(), None);
1438 // And it behaves like one built a field at a time after it lands.
1439 let mut h = h;
1440 assert!(h.remove(b"10"));
1441 assert_eq!(h.len(), rows.len() - 1);
1442 assert_eq!(h.get(b"10"), None);
1443 assert!(!h.set(b"a", b"other", &SMALL));
1444 assert_eq!(h.get(b"a").map(text).as_deref(), Some(&b"other"[..]));
1445 }
1446
1447 #[test]
1448 fn a_blob_this_band_cannot_hold_is_handed_back() {
1449 let long = vec![b'x'; SMALL.max_listpack_value + 1];
1450 let cases: Vec<(&str, Listpack)> = vec![
1451 (
1452 "the same field twice",
1453 packed(&[(b"a", b"1"), (b"a", b"2")]),
1454 ),
1455 (
1456 "the same field twice as a number",
1457 packed(&[(b"7", b"1"), (b"7", b"2")]),
1458 ),
1459 ("a field past the value limit", packed(&[(&long, b"1")])),
1460 ("a value past the value limit", packed(&[(b"a", &long)])),
1461 ("empty", Listpack::new()),
1462 ("an odd count", {
1463 let mut lp = packed(&[(b"a", b"1")]);
1464 lp.push(b"b");
1465 lp
1466 }),
1467 ];
1468 for (why, lp) in cases {
1469 assert!(
1470 Hash::from_packed(lp, &SMALL).is_err(),
1471 "{why} should have been handed back"
1472 );
1473 }
1474
1475 // And more fields than the band takes, which is the limit talking and
1476 // not the stack array.
1477 let rows: Vec<(Vec<u8>, Vec<u8>)> = (0..3)
1478 .map(|i| (format!("f{i}").into_bytes(), b"v".to_vec()))
1479 .collect();
1480 let borrowed: Vec<(&[u8], &[u8])> = rows
1481 .iter()
1482 .map(|(f, v)| (f.as_slice(), v.as_slice()))
1483 .collect();
1484 assert!(Hash::from_packed(packed(&borrowed), &AS_TABLE).is_err());
1485 assert!(Hash::from_packed(packed(&borrowed), &SMALL).is_ok());
1486 }
1487
1488 #[test]
1489 fn a_blob_with_more_fields_than_the_check_array_is_handed_back() {
1490 // The cap is a stack array and not a limit anybody configured, so a
1491 // server with `hash-max-listpack-entries` raised past it still has to be
1492 // correct, which here means walking rather than adopting.
1493 let wide = Limits {
1494 max_listpack_entries: CHECK_MAX * 2,
1495 max_listpack_value: 64,
1496 };
1497 let rows: Vec<(Vec<u8>, Vec<u8>)> = (0..CHECK_MAX + 1)
1498 .map(|i| (format!("f{i:05}").into_bytes(), b"v".to_vec()))
1499 .collect();
1500 let borrowed: Vec<(&[u8], &[u8])> = rows
1501 .iter()
1502 .map(|(f, v)| (f.as_slice(), v.as_slice()))
1503 .collect();
1504 assert!(Hash::from_packed(packed(&borrowed), &wide).is_err());
1505 }
1506
1507 fn pairs(h: &Hash) -> Vec<(String, String)> {
1508 let mut out: Vec<(String, String)> = h
1509 .iter()
1510 .map(|(f, v)| {
1511 (
1512 String::from_utf8(text(f)).expect("utf8"),
1513 String::from_utf8(text(v)).expect("utf8"),
1514 )
1515 })
1516 .collect();
1517 out.sort();
1518 out
1519 }
1520
1521 #[test]
1522 fn a_field_written_comes_back() {
1523 for limits in [&SMALL, &AS_TABLE] {
1524 let mut h = Hash::new();
1525 assert!(h.set(b"a", b"1", limits), "the field is new");
1526 assert!(h.set(b"b", b"2", limits));
1527 assert!(!h.set(b"a", b"3", limits), "and now it is not");
1528
1529 assert_eq!(h.len(), 2);
1530 assert_eq!(h.get(b"a").map(text), Some(b"3".to_vec()));
1531 assert_eq!(h.get(b"b").map(text), Some(b"2".to_vec()));
1532 assert_eq!(h.get(b"c"), None);
1533 assert!(h.contains(b"a") && !h.contains(b"c"));
1534 }
1535 }
1536
1537 #[test]
1538 fn a_value_is_never_mistaken_for_a_field() {
1539 // The listpack band searches with a step of two, and this is the shape
1540 // that catches a step of one: b is a value and never a field.
1541 let mut h = Hash::new();
1542 h.set(b"a", b"b", &SMALL);
1543 assert_eq!(h.get(b"b"), None, "b is a value, not a field");
1544 assert!(!h.contains(b"b"));
1545 assert!(!h.remove(b"b"), "and it cannot be deleted as one");
1546 assert_eq!(h.len(), 1);
1547
1548 assert!(h.set(b"b", b"c", &SMALL), "so writing b is a new field");
1549 assert_eq!(h.get(b"a").map(text), Some(b"b".to_vec()));
1550 assert_eq!(h.get(b"b").map(text), Some(b"c".to_vec()));
1551 }
1552
1553 #[test]
1554 fn deleting_takes_the_value_with_the_field() {
1555 for limits in [&SMALL, &AS_TABLE] {
1556 let mut h = Hash::new();
1557 for (f, v) in [("a", "1"), ("b", "2"), ("c", "3")] {
1558 h.set(f.as_bytes(), v.as_bytes(), limits);
1559 }
1560 assert!(h.remove(b"b"));
1561 assert!(!h.remove(b"b"), "twice is once");
1562
1563 assert_eq!(h.len(), 2);
1564 assert_eq!(
1565 pairs(&h),
1566 [
1567 ("a".to_owned(), "1".to_owned()),
1568 ("c".to_owned(), "3".to_owned())
1569 ],
1570 "and nothing shifted into the wrong pairing"
1571 );
1572 }
1573 }
1574
1575 #[test]
1576 fn it_promotes_on_the_count_and_on_the_length() {
1577 let mut h = Hash::new();
1578 for i in 0..128u32 {
1579 h.set(format!("f{i}").as_bytes(), b"v", &AT_128);
1580 }
1581 assert_eq!(h.encoding(), Encoding::Listpack, "128 is still a listpack");
1582 h.set(b"one more", b"v", &AT_128);
1583 assert_eq!(h.encoding(), Encoding::Hashtable, "and 129 is not");
1584 assert_eq!(h.len(), 129);
1585
1586 // Either side being too long converts on its own, at any count.
1587 let long = vec![b'x'; 65];
1588 let mut by_value = Hash::new();
1589 by_value.set(b"f", &long, &AT_128);
1590 assert_eq!(by_value.encoding(), Encoding::Hashtable);
1591 assert_eq!(by_value.get(b"f").map(text), Some(long.clone()));
1592
1593 let mut by_field = Hash::new();
1594 by_field.set(&long, b"v", &AT_128);
1595 assert_eq!(by_field.encoding(), Encoding::Hashtable);
1596 assert_eq!(by_field.get(&long).map(text), Some(b"v".to_vec()));
1597 }
1598
1599 #[test]
1600 fn promotion_carries_every_pair_over_intact() {
1601 let mut h = Hash::new();
1602 // Numbers, so the listpack holds them as integers and the promotion has
1603 // to write the digits out on the way to the table.
1604 for i in 0..128u32 {
1605 h.set(
1606 format!("{i}").as_bytes(),
1607 format!("{}", i * 2).as_bytes(),
1608 &AT_128,
1609 );
1610 }
1611 assert_eq!(h.encoding(), Encoding::Listpack);
1612 let before = pairs(&h);
1613
1614 h.set(b"last", b"one", &AT_128);
1615 assert_eq!(h.encoding(), Encoding::Hashtable);
1616
1617 let mut after = pairs(&h);
1618 after.retain(|(f, _)| f != "last");
1619 assert_eq!(after, before, "the pairs survived the conversion");
1620 for i in 0..128u32 {
1621 assert_eq!(
1622 h.get(format!("{i}").as_bytes()).map(text),
1623 Some(format!("{}", i * 2).into_bytes()),
1624 "field {i} is findable by its digits"
1625 );
1626 }
1627 }
1628
1629 #[test]
1630 fn it_never_demotes() {
1631 let mut h = Hash::new();
1632 for i in 0..200u32 {
1633 h.set(format!("f{i}").as_bytes(), b"v", &AT_128);
1634 }
1635 assert_eq!(h.encoding(), Encoding::Hashtable);
1636 for i in 0..199u32 {
1637 h.remove(format!("f{i}").as_bytes());
1638 }
1639 assert_eq!(h.len(), 1);
1640 assert_eq!(
1641 h.encoding(),
1642 Encoding::Hashtable,
1643 "one field left and still a table"
1644 );
1645 }
1646
1647 #[test]
1648 fn a_length_is_answered_without_writing_the_digits() {
1649 for limits in [&SMALL, &AS_TABLE] {
1650 let mut h = Hash::new();
1651 h.set(b"n", b"1234567", limits);
1652 h.set(b"s", b"hello", limits);
1653 h.set(b"e", b"", limits);
1654
1655 assert_eq!(h.value_len(b"n"), Some(7));
1656 assert_eq!(h.value_len(b"s"), Some(5));
1657 assert_eq!(h.value_len(b"e"), Some(0));
1658 assert_eq!(h.value_len(b"missing"), None);
1659 }
1660 }
1661
1662 #[test]
1663 fn a_rewritten_value_gives_its_bytes_back_eventually() {
1664 let mut h = Hash::with_hint(1000, &SMALL);
1665 assert_eq!(h.encoding(), Encoding::Hashtable);
1666 let big = vec![b'z'; 200];
1667 for _ in 0..200 {
1668 h.set(b"one", &big, &SMALL);
1669 }
1670 assert_eq!(h.len(), 1);
1671 assert_eq!(h.get(b"one").map(text), Some(big.clone()));
1672 assert!(
1673 h.dead_value_bytes() < 4096,
1674 "{} bytes left dead",
1675 h.dead_value_bytes()
1676 );
1677 }
1678
1679 #[test]
1680 fn compacting_the_values_moves_every_field_to_the_right_bytes() {
1681 let mut h = Hash::with_hint(1000, &SMALL);
1682 // Each field's value is its own name repeated, so a reference that moved
1683 // to the wrong place is visible rather than merely wrong.
1684 let want: Vec<(Vec<u8>, Vec<u8>)> = (0..300u32)
1685 .map(|i| {
1686 let f = format!("field{i}").into_bytes();
1687 let v = f.repeat(20);
1688 (f, v)
1689 })
1690 .collect();
1691 for (f, v) in &want {
1692 h.set(f, v, &SMALL);
1693 }
1694 // Rewrite every one of them, which abandons the whole first copy and is
1695 // far over both the floor and the ratio.
1696 for (f, v) in &want {
1697 h.set(f, v, &SMALL);
1698 }
1699 for (f, v) in &want {
1700 assert_eq!(
1701 h.get(f).map(text).as_deref(),
1702 Some(&v[..]),
1703 "field moved wrongly"
1704 );
1705 }
1706 assert_eq!(h.len(), 300);
1707 }
1708
1709 #[test]
1710 fn a_scan_walks_a_hash_of_any_size_exactly_once() {
1711 for hint in [0usize, 2000] {
1712 let mut h = Hash::with_hint(hint, &SMALL);
1713 for i in 0..100u32 {
1714 h.set(
1715 format!("f{i}").as_bytes(),
1716 format!("v{i}").as_bytes(),
1717 &SMALL,
1718 );
1719 }
1720 let mut seen: Vec<(String, String)> = Vec::new();
1721 let mut cursor = Cursor::START;
1722 loop {
1723 cursor = h.scan(cursor, 7, |f, v| {
1724 seen.push((
1725 String::from_utf8(text(f)).expect("utf8"),
1726 String::from_utf8(text(v)).expect("utf8"),
1727 ));
1728 });
1729 if cursor.is_end() {
1730 break;
1731 }
1732 }
1733 seen.sort();
1734 assert_eq!(seen.len(), 100, "at hint {hint}");
1735 assert_eq!(seen, pairs(&h), "at hint {hint}");
1736 }
1737 }
1738
1739 #[test]
1740 fn a_draw_reaches_every_pair_and_pairs_them_right() {
1741 for limits in [&SMALL, &AS_TABLE] {
1742 let mut h = Hash::new();
1743 for i in 0..50u32 {
1744 h.set(
1745 format!("f{i}").as_bytes(),
1746 format!("v{i}").as_bytes(),
1747 limits,
1748 );
1749 }
1750 for i in 0..h.len() {
1751 let (f, v) = h.at(i).expect("under the length");
1752 let f = String::from_utf8(text(f)).expect("utf8");
1753 let v = String::from_utf8(text(v)).expect("utf8");
1754 assert_eq!(v, f.replace('f', "v"), "row {i} paired wrongly");
1755 }
1756 assert_eq!(h.at(h.len()), None, "and there is nothing past the end");
1757 }
1758 }
1759
1760 #[test]
1761 fn a_hint_that_is_wrong_costs_a_conversion_and_no_answers() {
1762 // Sized for a table and given three fields, which is a waste and not a
1763 // bug, and sized for a listpack and given two hundred, which converts.
1764 let mut big = Hash::with_hint(5000, &SMALL);
1765 big.set(b"a", b"1", &SMALL);
1766 assert_eq!(big.encoding(), Encoding::Hashtable);
1767 assert_eq!(big.get(b"a").map(text), Some(b"1".to_vec()));
1768
1769 let mut small = Hash::with_hint(2, &AT_128);
1770 for i in 0..200u32 {
1771 small.set(format!("f{i}").as_bytes(), b"v", &AT_128);
1772 }
1773 assert_eq!(small.encoding(), Encoding::Hashtable);
1774 assert_eq!(small.len(), 200);
1775 }
1776
1777 /// Filled with `n` fields under `limits`, `f0` through `f{n-1}`.
1778 fn filled(n: u32, limits: &Limits) -> Hash {
1779 let mut h = Hash::new();
1780 for i in 0..n {
1781 h.set(
1782 format!("f{i}").as_bytes(),
1783 format!("v{i}").as_bytes(),
1784 limits,
1785 );
1786 }
1787 h
1788 }
1789
1790 #[test]
1791 fn the_packed_band_widens_the_first_time_a_field_is_given_a_deadline() {
1792 let mut h = filled(3, &SMALL);
1793 assert_eq!(h.encoding(), Encoding::Listpack);
1794 assert_eq!(h.deadline(b"f1"), Ask::NoDeadline);
1795
1796 assert_eq!(h.expire(b"f1", 5000, Cond::Always, 0), Applied::Ok);
1797 assert_eq!(h.encoding(), Encoding::ListpackEx, "three wide now");
1798
1799 // And everything that was there is still there, still paired up.
1800 assert_eq!(h.len(), 3);
1801 assert_eq!(
1802 pairs(&h),
1803 [
1804 ("f0".to_owned(), "v0".to_owned()),
1805 ("f1".to_owned(), "v1".to_owned()),
1806 ("f2".to_owned(), "v2".to_owned()),
1807 ]
1808 );
1809 assert_eq!(h.deadline(b"f1"), Ask::At(5000));
1810 assert_eq!(h.deadline(b"f0"), Ask::NoDeadline, "and only that one");
1811 assert_eq!(h.deadline(b"nope"), Ask::Missing);
1812 assert_eq!(h.deadline_count(), 1);
1813 assert_eq!(h.soonest_deadline(), Some(5000));
1814 }
1815
1816 #[test]
1817 fn the_table_band_keeps_deadlines_beside_the_rows() {
1818 let mut h = filled(3, &AS_TABLE);
1819 assert_eq!(h.encoding(), Encoding::Hashtable);
1820 assert_eq!(h.expire(b"f1", 5000, Cond::Always, 0), Applied::Ok);
1821 assert_eq!(
1822 h.encoding(),
1823 Encoding::Hashtable,
1824 "the table has nothing to widen"
1825 );
1826 assert_eq!(h.deadline(b"f1"), Ask::At(5000));
1827 assert_eq!(h.deadline(b"f0"), Ask::NoDeadline);
1828 assert_eq!(h.deadline(b"nope"), Ask::Missing);
1829 assert_eq!(h.deadline_count(), 1);
1830 assert_eq!(h.soonest_deadline(), Some(5000));
1831 }
1832
1833 #[test]
1834 fn a_field_is_reaped_only_once_its_moment_has_passed() {
1835 for limits in [&SMALL, &AS_TABLE] {
1836 let mut h = filled(3, limits);
1837 h.expire(b"f1", 1000, Cond::Always, 0);
1838
1839 assert_eq!(h.reap(999), 0, "not yet");
1840 assert_eq!(h.len(), 3);
1841 assert!(h.contains(b"f1"), "and it is still readable until then");
1842
1843 assert_eq!(h.reap(1000), 1, "the deadline itself has passed");
1844 assert_eq!(h.len(), 2);
1845 assert!(!h.contains(b"f1"));
1846 assert!(h.contains(b"f0") && h.contains(b"f2"), "and only that one");
1847 assert_eq!(h.reap(1000), 0, "twice takes nothing");
1848 assert_eq!(h.soonest_deadline(), None, "the bound is exact again");
1849 }
1850 }
1851
1852 #[test]
1853 fn a_hash_with_no_deadlines_is_reaped_without_a_walk() {
1854 for limits in [&SMALL, &AS_TABLE] {
1855 let mut h = filled(50, limits);
1856 assert_eq!(h.soonest_deadline(), None);
1857 assert_eq!(h.reap(u64::MAX), 0);
1858 assert_eq!(h.len(), 50);
1859 }
1860 }
1861
1862 #[test]
1863 fn a_write_clears_the_deadline_it_wrote_over() {
1864 for limits in [&SMALL, &AS_TABLE] {
1865 let mut h = filled(3, limits);
1866 h.expire(b"f1", 1000, Cond::Always, 0);
1867 assert_eq!(h.deadline(b"f1"), Ask::At(1000));
1868
1869 assert!(!h.set(b"f1", b"fresh", limits), "not a new field");
1870 assert_eq!(
1871 h.deadline(b"f1"),
1872 Ask::NoDeadline,
1873 "and HSET took the deadline off"
1874 );
1875 assert_eq!(h.reap(u64::MAX), 0, "so nothing expires it");
1876 assert_eq!(h.get(b"f1").map(text), Some(b"fresh".to_vec()));
1877 }
1878 }
1879
1880 #[test]
1881 fn a_deadline_already_past_deletes_the_field_instead_of_being_stored() {
1882 for limits in [&SMALL, &AS_TABLE] {
1883 let mut h = filled(3, limits);
1884 assert_eq!(h.expire(b"f1", 500, Cond::Always, 500), Applied::Deleted);
1885 assert!(!h.contains(b"f1"));
1886 assert_eq!(h.len(), 2);
1887 assert_eq!(h.deadline_count(), 0);
1888 assert_eq!(h.expire(b"gone", 9000, Cond::Always, 0), Applied::Missing);
1889 }
1890 }
1891
1892 #[test]
1893 fn the_conditions_reach_both_bands_the_same_way() {
1894 for limits in [&SMALL, &AS_TABLE] {
1895 let mut h = filled(2, limits);
1896 assert_eq!(h.expire(b"f0", 1000, Cond::AlreadySet, 0), Applied::NotMet);
1897 assert_eq!(h.deadline(b"f0"), Ask::NoDeadline);
1898 assert_eq!(h.expire(b"f0", 1000, Cond::NotSet, 0), Applied::Ok);
1899 assert_eq!(h.expire(b"f0", 2000, Cond::NotSet, 0), Applied::NotMet);
1900 assert_eq!(h.expire(b"f0", 500, Cond::Greater, 0), Applied::NotMet);
1901 assert_eq!(h.expire(b"f0", 2000, Cond::Greater, 0), Applied::Ok);
1902 assert_eq!(h.deadline(b"f0"), Ask::At(2000));
1903 // The condition is checked before the past deadline is, so this is
1904 // a 0 and the field survives rather than being deleted.
1905 assert_eq!(h.expire(b"f0", 0, Cond::NotSet, 5), Applied::NotMet);
1906 assert!(h.contains(b"f0"));
1907 }
1908 }
1909
1910 #[test]
1911 fn persisting_takes_the_deadline_off_and_says_what_was_there() {
1912 for limits in [&SMALL, &AS_TABLE] {
1913 let mut h = filled(3, limits);
1914 h.expire(b"f1", 1000, Cond::Always, 0);
1915
1916 assert_eq!(h.persist(b"f1"), Ask::At(1000));
1917 assert_eq!(
1918 h.persist(b"f1"),
1919 Ask::NoDeadline,
1920 "twice is -1, not an error"
1921 );
1922 assert_eq!(h.persist(b"gone"), Ask::Missing);
1923 assert_eq!(h.deadline_count(), 0);
1924 assert_eq!(h.reap(u64::MAX), 0, "and it does not expire any more");
1925 assert_eq!(h.len(), 3);
1926 }
1927 }
1928
1929 /// The one that would silently give a deadline to the wrong field.
1930 #[test]
1931 fn deadlines_follow_their_fields_through_a_removal() {
1932 for limits in [&SMALL, &AS_TABLE] {
1933 let mut h = filled(5, limits);
1934 // Each field's deadline is derived from its own name, so a deadline
1935 // that has drifted is visible rather than merely plausible.
1936 for i in 0..5u32 {
1937 assert_eq!(
1938 h.expire(
1939 format!("f{i}").as_bytes(),
1940 1000 + u64::from(i),
1941 Cond::Always,
1942 0
1943 ),
1944 Applied::Ok
1945 );
1946 }
1947 // The table swap removes, so taking a middle field out moves the
1948 // last row into the hole.
1949 assert!(h.remove(b"f1"));
1950
1951 assert_eq!(h.len(), 4);
1952 for i in [0u32, 2, 3, 4] {
1953 assert_eq!(
1954 h.deadline(format!("f{i}").as_bytes()),
1955 Ask::At(1000 + u64::from(i)),
1956 "f{i} kept someone else's deadline"
1957 );
1958 }
1959 assert_eq!(h.deadline_count(), 4);
1960 }
1961 }
1962
1963 #[test]
1964 fn a_deadline_comes_over_with_its_field_on_promotion() {
1965 let mut h = Hash::new();
1966 for i in 0..128u32 {
1967 h.set(format!("f{i}").as_bytes(), b"v", &AT_128);
1968 }
1969 h.expire(b"f7", 4000, Cond::Always, 0);
1970 h.expire(b"f9", 2000, Cond::Always, 0);
1971 assert_eq!(h.encoding(), Encoding::ListpackEx);
1972
1973 h.set(b"one more", b"v", &AT_128);
1974 assert_eq!(h.encoding(), Encoding::Hashtable, "and now it is a table");
1975
1976 assert_eq!(h.len(), 129);
1977 assert_eq!(h.deadline(b"f7"), Ask::At(4000));
1978 assert_eq!(h.deadline(b"f9"), Ask::At(2000));
1979 assert_eq!(h.deadline(b"f8"), Ask::NoDeadline);
1980 assert_eq!(h.deadline_count(), 2);
1981 assert_eq!(h.soonest_deadline(), Some(2000));
1982
1983 assert_eq!(h.reap(3000), 1, "f9 and not f7");
1984 assert!(!h.contains(b"f9") && h.contains(b"f7"));
1985 }
1986
1987 #[test]
1988 fn a_widened_hash_still_scans_and_draws_every_pair_once() {
1989 for hint in [0usize, 2000] {
1990 let mut h = Hash::with_hint(hint, &SMALL);
1991 for i in 0..100u32 {
1992 h.set(
1993 format!("f{i}").as_bytes(),
1994 format!("v{i}").as_bytes(),
1995 &SMALL,
1996 );
1997 }
1998 h.expire(b"f42", 9000, Cond::Always, 0);
1999
2000 let mut seen: Vec<(String, String)> = Vec::new();
2001 let mut cursor = Cursor::START;
2002 loop {
2003 cursor = h.scan(cursor, 7, |f, v| {
2004 seen.push((
2005 String::from_utf8(text(f)).expect("utf8"),
2006 String::from_utf8(text(v)).expect("utf8"),
2007 ));
2008 });
2009 if cursor.is_end() {
2010 break;
2011 }
2012 }
2013 seen.sort();
2014 assert_eq!(seen.len(), 100, "at hint {hint}");
2015 assert_eq!(seen, pairs(&h), "at hint {hint}");
2016
2017 // And the draw positions still pair a field with its own value.
2018 for i in 0..h.len() {
2019 let (f, v) = h.at(i).expect("under the length");
2020 let f = String::from_utf8(text(f)).expect("utf8");
2021 let v = String::from_utf8(text(v)).expect("utf8");
2022 assert_eq!(
2023 v,
2024 f.replace('f', "v"),
2025 "row {i} paired wrongly at hint {hint}"
2026 );
2027 }
2028 }
2029 }
2030
2031 /// Reaping takes every field that is due, including two in a row, which is
2032 /// where a walk that stepped past the row it just removed would go wrong.
2033 #[test]
2034 fn a_run_of_expired_fields_all_go_together() {
2035 for limits in [&SMALL, &AS_TABLE] {
2036 let mut h = filled(6, limits);
2037 for i in [1u32, 2, 3] {
2038 h.expire(format!("f{i}").as_bytes(), 100, Cond::Always, 0);
2039 }
2040 assert_eq!(h.reap(200), 3);
2041 assert_eq!(h.len(), 3);
2042 for i in [0u32, 4, 5] {
2043 assert!(h.contains(format!("f{i}").as_bytes()), "f{i} went too");
2044 }
2045 }
2046 }
2047
2048 #[test]
2049 fn an_empty_hash_has_allocated_almost_nothing() {
2050 let h = Hash::new();
2051 assert!(h.is_empty());
2052 assert_eq!(h.len(), 0);
2053 assert_eq!(h.get(b"a"), None);
2054 assert!(h.memory_bytes() < 64, "{} bytes", h.memory_bytes());
2055 }
2056
2057 /// Freeze a hash, read it back, and check it is the same hash.
2058 ///
2059 /// The band and the length are checked as well as the pairs, because coming
2060 /// back on a different band would change what `OBJECT ENCODING` says about a
2061 /// value that nobody wrote to.
2062 fn round_trip(h: &Hash) -> Hash {
2063 let mut out = Vec::new();
2064 h.freeze(&mut out);
2065 let back = Hash::thaw(&out).expect("it came back");
2066 assert_eq!(back.len(), h.len(), "the field count");
2067 assert_eq!(back.encoding(), h.encoding(), "the band");
2068 assert_eq!(pairs(&back), pairs(h), "the fields");
2069 back
2070 }
2071
2072 #[test]
2073 fn a_frozen_hash_comes_back_in_the_band_it_left() {
2074 round_trip(&Hash::new());
2075 round_trip(&filled(3, &SMALL));
2076 round_trip(&filled(300, &AT_128));
2077 round_trip(&filled(3, &AS_TABLE));
2078
2079 // And a value too long for the packed band, which is the other way a
2080 // hash becomes a table.
2081 let mut h = Hash::new();
2082 h.set(b"f", &[b'x'; 200], &SMALL);
2083 assert_eq!(h.encoding(), Encoding::Hashtable);
2084 let back = round_trip(&h);
2085 assert_eq!(back.get(b"f").map(text), Some(vec![b'x'; 200]));
2086 }
2087
2088 #[test]
2089 fn every_field_deadline_survives_the_trip() {
2090 for limits in [&SMALL, &AS_TABLE] {
2091 let mut h = filled(4, limits);
2092 h.expire(b"f1", 5000, Cond::Always, 0);
2093 h.expire(b"f3", 9000, Cond::Always, 0);
2094 let back = round_trip(&h);
2095 assert_eq!(back.deadline(b"f1"), Ask::At(5000));
2096 assert_eq!(back.deadline(b"f3"), Ask::At(9000));
2097 assert_eq!(back.deadline(b"f0"), Ask::NoDeadline);
2098 assert_eq!(back.deadline(b"f2"), Ask::NoDeadline);
2099 assert_eq!(back.deadline_count(), 2);
2100 // The bound the active cycle reads. It can be early and it cannot be
2101 // late, and a hash that came back with no bound at all would be a
2102 // hash the cycle sleeps through.
2103 assert_eq!(back.soonest_deadline(), Some(5000));
2104 }
2105 }
2106
2107 /// A hash that has widened and then had every deadline taken off again.
2108 ///
2109 /// It stays `listpackex`, because widening is one way, and the third element
2110 /// per field is still there holding a zero. Both facts have to survive or the
2111 /// encoding changes under a client that only ever called `HPERSIST`.
2112 #[test]
2113 fn a_widened_hash_with_no_deadlines_left_still_comes_back_widened() {
2114 let mut h = filled(3, &SMALL);
2115 h.expire(b"f1", 5000, Cond::Always, 0);
2116 assert_eq!(h.persist(b"f1"), Ask::At(5000));
2117 assert_eq!(h.encoding(), Encoding::ListpackEx);
2118 assert_eq!(h.deadline_count(), 0);
2119 let back = round_trip(&h);
2120 assert_eq!(back.deadline_count(), 0);
2121 assert_eq!(back.soonest_deadline(), Some(5000), "the bound only falls");
2122 }
2123
2124 #[test]
2125 fn a_frozen_hash_that_arrives_damaged_is_an_error_and_not_a_panic() {
2126 for h in [filled(3, &SMALL), filled(3, &AS_TABLE)] {
2127 let mut out = Vec::new();
2128 h.freeze(&mut out);
2129 for cut in 0..out.len() {
2130 // Every prefix. Some of them are a hash of fewer fields and that
2131 // is fine, what matters is that none of them panics or hangs.
2132 let _ = Hash::thaw(&out[..cut]);
2133 }
2134 }
2135 assert_eq!(Hash::thaw(&[]).err(), Some(Broken::Short));
2136 assert_eq!(Hash::thaw(&[9]).err(), Some(Broken::Form));
2137 assert_eq!(Hash::thaw(&[FORM_PACKED, 1, 2]).err(), Some(Broken::Body));
2138 // A field count that no amount of what is left could fill, which is the
2139 // one an allocation would be sized from.
2140 assert_eq!(
2141 Hash::thaw(&[FORM_FIELDS, 0xff, 0xff, 0x7f, 0]).err(),
2142 Some(Broken::Body)
2143 );
2144 }
2145}