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}