Skip to main content

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}