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