yo_kv/value.rs
1//! How a string value sits in a record, and what comes back out of one.
2//!
3//! The map underneath stores opaque bytes against a key. Everything a string
4//! needs beyond those bytes, which is its encoding and its expiry deadline, is
5//! carried in a one byte header in front of them:
6//!
7//! ```text
8//! +--------+-------------------------+-------------------+-----------+
9//! | meta | expire at, u64 LE | access, 24 bits | payload |
10//! | u8 | 8 bytes, only if tagged | 3 bytes, tagged | see below |
11//! +--------+-------------------------+-------------------+-----------+
12//! ```
13//!
14//! One byte, and eight more only for a key that has a deadline, which most keys
15//! do not. The alternative is a second lookup into a side table for the TTL, and
16//! a second lookup is a second cache miss on a path whose whole budget is one.
17//!
18//! # The access field
19//!
20//! Three bytes saying when the key was last read, or how often, depending on
21//! which eviction policy is in force. It is [`crate::access::Access`] and the
22//! reasoning about what goes in it lives there.
23//!
24//! It is behind a tag bit the same way the deadline is, but unlike the deadline
25//! every record written now has one. The bit is there so that a record written
26//! before the field existed still reads back correctly rather than to make the
27//! field optional, and it sits after the deadline for the same reason: the
28//! deadline stays at offset one and everything that reads one goes on working.
29//!
30//! Three bytes on every key is a real cost and it is worth being straight about
31//! why it is paid unconditionally rather than only under a policy that reads it.
32//! A key written under `noeviction` and then read under `allkeys-lru` has to be
33//! rankable, and it cannot become rankable later without the record growing,
34//! which means moving it, on what is usually a read. Paying three bytes always
35//! is the version where switching policy at runtime does the obvious thing.
36//!
37//! The payload depends on the encoding. An `int` holds the eight bytes of the
38//! integer and not its digits, which is what makes `INCR` a probe, an add and a
39//! store with no arena traffic at all (`08` section 2). An `embstr` and a `raw`
40//! hold the bytes as given. The difference between those two is the name
41//! `OBJECT ENCODING` reports and nothing else, which is also true in Redis:
42//! `embstr` there means the value was allocated next to its object header, a
43//! distinction `yo` does not have because every value is already next to its
44//! key.
45//!
46//! # The type tag
47//!
48//! The meta byte also says which type the key holds, in three bits that were
49//! spare. That is what `TYPE` reads and it is what a command will read before it
50//! decides whether it is looking at its own type or at somebody else's, and both
51//! of those want the answer to come out of the byte the lookup already fetched
52//! rather than out of a second structure. A string is zero, so nothing written
53//! before the tag existed reads back as anything else.
54//!
55//! # The tier tag
56//!
57//! The last bit says whether the payload is the value or a place on the file
58//! where the value is. That is `14` section 4.1's tier tag, and the reason it is
59//! a bit here rather than a lookup somewhere else is the number it exists to
60//! make possible: at most 1.05 device reads for every point read. A point read
61//! that has to consult a second structure to find out whether the value is
62//! resident has already spent the miss it was trying to avoid.
63//!
64//! A demoted record keeps its deadline, its access field, its type and its
65//! encoding, and adds a length. So `TTL`, `TYPE`, `OBJECT ENCODING`, `STRLEN`,
66//! `EXISTS` and the whole of eviction go on answering at memory speed on a key
67//! whose value is on the device, and only the commands that actually want the
68//! bytes pay for them. [`write_cold_record`] writes one and [`cold`] is the
69//! branch that reads it.
70
71use crate::access::Access;
72use yo_common::Addr;
73use yo_common::num::{parse_i64, push_i64};
74
75/// The longest value Redis calls `embstr` rather than `raw`.
76///
77/// Clients and test suites read `OBJECT ENCODING` and assert on the boundary,
78/// so it is 44 here because it is 44 there (`12` section 2).
79pub const EMBSTR_MAX: usize = 44;
80
81/// What `OBJECT ENCODING` calls a string.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Encoding {
84 /// The value is an integer, held as an integer.
85 Int,
86 /// A short string, at or under [`EMBSTR_MAX`] bytes.
87 Embstr,
88 /// Everything else.
89 Raw,
90}
91
92impl Encoding {
93 /// The string `OBJECT ENCODING` returns.
94 #[inline]
95 pub const fn name(self) -> &'static str {
96 match self {
97 Encoding::Int => "int",
98 Encoding::Embstr => "embstr",
99 Encoding::Raw => "raw",
100 }
101 }
102
103 /// The encoding Redis would choose for these bytes.
104 ///
105 /// Integer first, because `SET k 42` is int encoded in Redis whatever the
106 /// length, then the `embstr` boundary. The integer test is Redis's own
107 /// `string2ll`, which refuses a leading zero, a leading plus and `-0`, so
108 /// `SET k 007` stays a three byte string and gives back `007`.
109 #[inline]
110 pub fn of(bytes: &[u8]) -> Encoding {
111 if parse_i64(bytes).is_some() {
112 Encoding::Int
113 } else if bytes.len() <= EMBSTR_MAX {
114 Encoding::Embstr
115 } else {
116 Encoding::Raw
117 }
118 }
119}
120
121/// What `TYPE` calls a key, and what the meta byte's tag holds.
122///
123/// The numbers are the same numbers `yo_format::ValueType` uses on disk, so that
124/// saving a key is a copy of the tag rather than a translation of it. There is a
125/// test at the bottom of this file holding the two in step, and it takes a dev
126/// dependency on `yo-format` for no other reason.
127///
128/// The list is shorter than the on disk one because some of those are the same
129/// thing in memory. A bitmap is a string and a HyperLogLog is a string, in Redis
130/// as much as here, and `TYPE` on either answers `string`. The catalog draws
131/// finer lines because a reader wants to know what a blob meant.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum Kind {
134 /// A string, and everything Redis stores as one.
135 String = 0,
136 /// A hash.
137 Hash = 1,
138 /// A set.
139 Set = 2,
140 /// A sorted set.
141 Zset = 3,
142 /// A list.
143 List = 4,
144 /// A stream.
145 Stream = 5,
146 /// A sparse array.
147 Array = 6,
148 /// A body this crate does not know the type of.
149 ///
150 /// The last of the eight patterns the tag can hold, and it is spent on an
151 /// escape rather than on one more type. Everything below this crate is a
152 /// primitive that the document, vector and graph engines are built out of,
153 /// so those engines sit above it and cannot be named here without a cycle,
154 /// and there are three of them and one pattern. An escape holds all three,
155 /// and the body says what it is when asked.
156 ///
157 /// It is the one kind with no `yo_format::catalog::ValueType` beside it,
158 /// because this version's writer does not save a foreign body and there is
159 /// nothing on disk for the catalog to name. When one of them becomes
160 /// saveable it gets its own number there, which does not have to be this
161 /// one, since nothing in a file has to agree with a tag that only ever
162 /// exists in memory.
163 Foreign = 7,
164}
165
166impl Kind {
167 /// The word `TYPE` replies with.
168 ///
169 /// A foreign body knows its own word and this does not, so the answer here
170 /// is the one a caller sees when it has a kind and no body. Go through
171 /// [`crate::Keyspace::type_name`] to get what the client should be told.
172 #[inline]
173 pub const fn name(self) -> &'static str {
174 match self {
175 Kind::String => "string",
176 Kind::Hash => "hash",
177 Kind::Set => "set",
178 Kind::Zset => "zset",
179 Kind::List => "list",
180 Kind::Stream => "stream",
181 Kind::Array => "array",
182 Kind::Foreign => "foreign",
183 }
184 }
185
186 /// Whether the body lives in a slab rather than in the record.
187 ///
188 /// Everything but a string, which is its own record. The arms that walk a
189 /// slot ask this rather than listing six kinds, so a new body kind is one
190 /// arm here and not six lists to find.
191 #[inline]
192 pub const fn is_body(self) -> bool {
193 !matches!(self, Kind::String)
194 }
195
196 /// The kind for a three bit tag.
197 ///
198 /// All eight patterns are spoken for now, and zero is `String`, so this is
199 /// total and no longer has a fallback to explain.
200 #[inline]
201 const fn from_bits(bits: u8) -> Kind {
202 match bits {
203 KIND_HASH => Kind::Hash,
204 KIND_SET => Kind::Set,
205 KIND_ZSET => Kind::Zset,
206 KIND_LIST => Kind::List,
207 KIND_STREAM => Kind::Stream,
208 KIND_ARRAY => Kind::Array,
209 KIND_FOREIGN => Kind::Foreign,
210 _ => Kind::String,
211 }
212 }
213}
214
215/// Bits 0 and 1 of the meta byte: which encoding.
216const ENC_MASK: u8 = 0b0000_0011;
217const ENC_INT: u8 = 0;
218const ENC_EMBSTR: u8 = 1;
219const ENC_RAW: u8 = 2;
220/// Bit 2: whether eight bytes of deadline follow the meta byte.
221const HAS_EXPIRY: u8 = 0b0000_0100;
222/// Bit 6: whether three bytes of access data follow the deadline.
223///
224/// Everything this crate writes now sets it, and the bit exists so that a file
225/// written before it did still opens. A record with the bit clear has no access
226/// field and its payload starts where it always did, which is what makes reading
227/// an older file a matter of asking rather than of knowing which version wrote
228/// it.
229///
230/// It does not work in the other direction. A binary from before this bit reads
231/// a record that sets it, ignores the bit it does not know about, and takes the
232/// three access bytes for the front of the payload. There is no format version
233/// in the file to refuse on, which is worth fixing and is not this change.
234const HAS_ACCESS: u8 = 0b0100_0000;
235/// Bit 7: whether the payload is a place on the file rather than a value.
236///
237/// This is the tier tag `14` section 4.1 needs, and the reason it is a bit in
238/// the meta byte rather than a side table is the whole point of it. A point read
239/// has already paid for the cache line the record starts in by the time it looks
240/// at anything, so asking whether the value is resident is free, and asking any
241/// other way is a second miss on a path whose budget is one. That is what makes
242/// `1.05` device reads per point read a reachable number rather than `2.05`.
243///
244/// A cold record keeps its deadline and its access field in memory. Expiry has
245/// to work on a key whose value is on the device without reading the device, and
246/// so does eviction, or the policy would have to fault in the very keys it is
247/// deciding to get rid of.
248const COLD: u8 = 0b1000_0000;
249/// Bits 3, 4 and 5: which type the key holds.
250///
251/// String is zero, so every record written before the tag existed reads back as
252/// a string, which is what it was.
253const KIND_MASK: u8 = 0b0011_1000;
254const KIND_SHIFT: u32 = 3;
255const KIND_HASH: u8 = 1;
256const KIND_SET: u8 = 2;
257const KIND_ZSET: u8 = 3;
258const KIND_LIST: u8 = 4;
259const KIND_STREAM: u8 = 5;
260const KIND_ARRAY: u8 = 6;
261const KIND_FOREIGN: u8 = 7;
262
263/// Bytes of integer payload, which is a whole `i64` and never its digits.
264const INT_LEN: usize = 8;
265
266/// Bytes of access data, which is [`Access`] and is twenty four bits.
267const ACCESS_LEN: usize = 3;
268
269/// The meta byte in front of every stored string.
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub struct Meta(u8);
272
273impl Meta {
274 /// Build the byte for a type, an encoding and the presence of a deadline.
275 #[inline]
276 pub const fn new(kind: Kind, enc: Encoding, has_expiry: bool) -> Meta {
277 let bits = match enc {
278 Encoding::Int => ENC_INT,
279 Encoding::Embstr => ENC_EMBSTR,
280 Encoding::Raw => ENC_RAW,
281 };
282 Meta(bits | ((kind as u8) << KIND_SHIFT) | if has_expiry { HAS_EXPIRY } else { 0 })
283 }
284
285 /// The byte for a string, which is what everything in `strings.rs` writes.
286 #[inline]
287 pub const fn string(enc: Encoding, has_expiry: bool) -> Meta {
288 Meta::new(Kind::String, enc, has_expiry)
289 }
290
291 /// The byte for a value that lives in a slab, with the record holding a
292 /// number that says where.
293 ///
294 /// The encoding bits are written as zero and mean nothing here. A set's
295 /// encoding is which of the three representations it is in, and that is a
296 /// property of the body and not of the record, so `OBJECT ENCODING` follows
297 /// the number and asks. Keeping a copy of it in these two bits would want
298 /// the record rewritten every time a set was promoted, for a command nobody
299 /// calls in a loop, and two places to disagree about the same fact.
300 #[inline]
301 pub const fn slot(kind: Kind, has_expiry: bool) -> Meta {
302 Meta::new(kind, Encoding::Int, has_expiry)
303 }
304
305 /// Read the byte back.
306 ///
307 /// An unknown encoding is impossible from our own writer, so the two spare
308 /// bit patterns fall to `raw`, which is the reading that returns the bytes
309 /// unchanged rather than reinterpreting them as something else.
310 #[inline]
311 pub const fn from_byte(b: u8) -> Meta {
312 Meta(b)
313 }
314
315 /// The raw byte, as stored.
316 #[inline]
317 pub const fn byte(self) -> u8 {
318 self.0
319 }
320
321 /// Which encoding this value carries.
322 #[inline]
323 pub const fn encoding(self) -> Encoding {
324 match self.0 & ENC_MASK {
325 ENC_INT => Encoding::Int,
326 ENC_EMBSTR => Encoding::Embstr,
327 _ => Encoding::Raw,
328 }
329 }
330
331 /// Which type this key holds.
332 #[inline]
333 pub const fn kind(self) -> Kind {
334 Kind::from_bits((self.0 & KIND_MASK) >> KIND_SHIFT)
335 }
336
337 /// Whether a deadline follows.
338 #[inline]
339 pub const fn has_expiry(self) -> bool {
340 self.0 & HAS_EXPIRY != 0
341 }
342
343 /// Whether an access field follows the deadline.
344 ///
345 /// Everything this crate writes sets it, so in a running server it is always
346 /// true and the reader that checks it is checking something that cannot
347 /// happen. It is here anyway because a bit in the byte costs nothing and the
348 /// alternative was a flag day: without it, the day the field arrived, every
349 /// reader had to agree with every writer at exactly the same moment.
350 ///
351 /// It is not a file format concern. These records live in the arena and
352 /// never reach a file, and the on disk record in `yo_format` has its own
353 /// layout and its own versioning.
354 #[inline]
355 pub const fn has_access(self) -> bool {
356 self.0 & HAS_ACCESS != 0
357 }
358
359 /// The same byte with the access field declared.
360 #[inline]
361 const fn with_access(self) -> Meta {
362 Meta(self.0 | HAS_ACCESS)
363 }
364
365 /// Whether the payload is a place on the file rather than the value.
366 ///
367 /// The one question a point read asks before it decides whether it is going
368 /// to touch a device, and the answer comes out of the byte the lookup has
369 /// already fetched.
370 #[inline]
371 pub const fn is_cold(self) -> bool {
372 self.0 & COLD != 0
373 }
374
375 /// The same byte with the value declared to be on the file.
376 #[inline]
377 pub const fn with_cold(self) -> Meta {
378 Meta(self.0 | COLD)
379 }
380
381 /// Where the access field starts, counting from the meta byte.
382 ///
383 /// After the deadline rather than before it, which is what keeps a record
384 /// written before this field existed readable: the deadline is still at
385 /// offset one and everything that reads one can go on doing so.
386 #[inline]
387 const fn access_at(self) -> usize {
388 if self.has_expiry() { 1 + 8 } else { 1 }
389 }
390
391 /// Where the payload starts, counting from the meta byte.
392 #[inline]
393 pub const fn payload_at(self) -> usize {
394 self.access_at() + if self.has_access() { ACCESS_LEN } else { 0 }
395 }
396}
397
398/// A stored value, read back out of a record.
399///
400/// The point of the two arms is that neither of them copies. An integer comes
401/// back as an integer and is written into the reply buffer as digits at the
402/// moment the reply is built, and a string comes back as a slice of the record
403/// it lives in. Y18 asks for the reply to be constructed once in wire form, and
404/// a `Vec<u8>` in the middle of that is the thing it is asking to remove.
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum Str<'a> {
407 /// An int encoded value.
408 Int(i64),
409 /// Everything else, as it lies in the record.
410 Bytes(&'a [u8]),
411}
412
413impl Str<'_> {
414 /// How many bytes this value is as a string, which is what `STRLEN` returns.
415 #[inline]
416 pub fn len(&self) -> usize {
417 match self {
418 Str::Int(n) => yo_common::num::i64_len(*n),
419 Str::Bytes(b) => b.len(),
420 }
421 }
422
423 /// Whether the value is the empty string.
424 #[inline]
425 pub fn is_empty(&self) -> bool {
426 match self {
427 // No integer writes as no digits.
428 Str::Int(_) => false,
429 Str::Bytes(b) => b.is_empty(),
430 }
431 }
432
433 /// Append the string form to a buffer, which for an integer is its digits.
434 #[inline]
435 pub fn write_to(&self, out: &mut Vec<u8>) {
436 match self {
437 Str::Int(n) => push_i64(out, *n),
438 Str::Bytes(b) => out.extend_from_slice(b),
439 }
440 }
441
442 /// The string form, copied. For the reply path prefer [`Str::write_to`].
443 pub fn to_vec(&self) -> Vec<u8> {
444 let mut v = Vec::with_capacity(self.len());
445 self.write_to(&mut v);
446 v
447 }
448
449 /// The integer this value is, if it is one.
450 ///
451 /// A value can be an integer without being int encoded: `APPEND` and
452 /// `SETRANGE` leave a `raw` string behind, and `INCR` on `"10"` built that
453 /// way is 11 in Redis. So the bytes are parsed rather than the encoding
454 /// being trusted.
455 #[inline]
456 pub fn as_int(&self) -> Option<i64> {
457 match self {
458 Str::Int(n) => Some(*n),
459 Str::Bytes(b) => parse_i64(b),
460 }
461 }
462
463 /// The XXH3 of the value's string form, which is Redis's `DIGEST`.
464 ///
465 /// An int encoded value hashes its digits and not the eight bytes the
466 /// record holds, because the digest a client compares against is the digest
467 /// of what a client would have read.
468 #[must_use]
469 pub fn digest(&self) -> u64 {
470 match self {
471 Str::Bytes(b) => yo_common::xxh3::hash64(b),
472 // Twenty bytes at the most, and the alternative is a second
473 // formatter that writes into a stack buffer for a path nobody calls
474 // in a loop.
475 Str::Int(_) => yo_common::xxh3::hash64(&self.to_vec()),
476 }
477 }
478
479 /// Whether this value's string form is exactly `want`.
480 ///
481 /// `IFEQ` compares against what the client would have read, so an int
482 /// encoded 42 is equal to `"42"` and not to `"042"`. Doing that without
483 /// materialising the digits is why the integer arm exists.
484 #[inline]
485 pub(crate) fn eq_bytes(&self, want: &[u8]) -> bool {
486 match self {
487 Str::Bytes(b) => *b == want,
488 Str::Int(n) => parse_i64(want) == Some(*n),
489 }
490 }
491}
492
493/// How many bytes a record holding this value will occupy.
494///
495/// The access field is counted unconditionally, because every record this crate
496/// writes now carries one. It is not a parameter for that reason: making it one
497/// would put a flag through twenty six call sites to describe something none of
498/// them gets to decide.
499#[inline]
500pub fn record_len(enc: Encoding, payload: usize, has_expiry: bool) -> usize {
501 let head = (if has_expiry { 1 + 8 } else { 1 }) + ACCESS_LEN;
502 head + if enc == Encoding::Int {
503 INT_LEN
504 } else {
505 payload
506 }
507}
508
509/// Write a whole record into `out`, which must be exactly [`record_len`] long.
510///
511/// `bytes` is the string as the caller gave it. When `enc` is [`Encoding::Int`]
512/// the digits are not stored, the integer they parse to is, and the caller has
513/// already established that they parse by choosing that encoding.
514#[inline]
515pub fn write_record(out: &mut [u8], enc: Encoding, bytes: &[u8], expire_at: Option<u64>) {
516 out[0] = Meta::string(enc, expire_at.is_some()).with_access().byte();
517 let mut at = 1;
518 if let Some(ms) = expire_at {
519 out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
520 at += 8;
521 }
522 at += write_blank_access(&mut out[at..]);
523 match enc {
524 Encoding::Int => {
525 let n =
526 parse_i64(bytes).expect("int encoding was chosen for bytes that are not an int");
527 out[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
528 }
529 _ => out[at..].copy_from_slice(bytes),
530 }
531}
532
533/// Write a record whose value is an integer the caller already has.
534#[inline]
535pub fn write_int_record(out: &mut [u8], n: i64, expire_at: Option<u64>) {
536 out[0] = Meta::string(Encoding::Int, expire_at.is_some())
537 .with_access()
538 .byte();
539 let mut at = 1;
540 if let Some(ms) = expire_at {
541 out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
542 at += 8;
543 }
544 at += write_blank_access(&mut out[at..]);
545 out[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
546}
547
548/// Leave room for the access field and put nothing in it.
549///
550/// The writers here do not know the clock or which policy is in force, and a
551/// record layout is the wrong place to learn either. The keyspace stamps the
552/// field through [`set_access`] once the record is in, which is also where the
553/// decision about whether to stamp at all belongs.
554///
555/// Zero is the most evictable value a key can hold under either reading, which
556/// is the right way round for a default: a key that somehow never got stamped
557/// goes first rather than never.
558#[inline]
559fn write_blank_access(out: &mut [u8]) -> usize {
560 out[..ACCESS_LEN].fill(0);
561 ACCESS_LEN
562}
563
564/// Bytes of slab number, which is how a record points at a body.
565const SLOT_LEN: usize = 4;
566
567/// How many bytes a record pointing at a slab slot occupies.
568#[inline]
569pub fn slot_record_len(has_expiry: bool) -> usize {
570 (if has_expiry { 1 + 8 } else { 1 }) + ACCESS_LEN + SLOT_LEN
571}
572
573/// Write a record that points at `slot` in the slab for `kind`.
574///
575/// `out` must be exactly [`slot_record_len`] long.
576#[inline]
577pub fn write_slot_record(out: &mut [u8], kind: Kind, slot: u32, expire_at: Option<u64>) {
578 out[0] = Meta::slot(kind, expire_at.is_some()).with_access().byte();
579 let mut at = 1;
580 if let Some(ms) = expire_at {
581 out[at..at + 8].copy_from_slice(&ms.to_le_bytes());
582 at += 8;
583 }
584 at += write_blank_access(&mut out[at..]);
585 out[at..at + SLOT_LEN].copy_from_slice(&slot.to_le_bytes());
586}
587
588/// Bytes of file address and value length in a demoted record.
589///
590/// The length is here rather than only on the device because `STRLEN`, `TYPE`,
591/// `OBJECT ENCODING`, `TTL` and `MEMORY USAGE` all have answers that do not need
592/// the bytes, and a tiering layer that faults a value in to answer `STRLEN` is a
593/// tiering layer that has given the game away. Four bytes caps a demoted value
594/// at four gigabytes, which is above the proto-max-bulk-len a server will accept
595/// in the first place.
596const COLD_LEN: usize = 8 + 4;
597
598/// How many bytes a record pointing at a demoted value occupies.
599///
600/// Twelve bytes of payload against however many the value was, which is the
601/// whole reason demotion buys anything. A key with an eight byte value is not
602/// worth demoting and a caller that demotes one will find its memory going up.
603#[inline]
604pub fn cold_record_len(has_expiry: bool) -> usize {
605 (if has_expiry { 1 + 8 } else { 1 }) + ACCESS_LEN + COLD_LEN
606}
607
608/// Write a record saying the value is at `at` on the file and is `len` bytes.
609///
610/// `out` must be exactly [`cold_record_len`] long. `kind` and `enc` are carried
611/// across from the record being replaced so that the questions which can be
612/// answered without the device still can be.
613#[inline]
614pub fn write_cold_record(
615 out: &mut [u8],
616 kind: Kind,
617 enc: Encoding,
618 at: Addr,
619 len: u32,
620 expire_at: Option<u64>,
621) {
622 out[0] = Meta::new(kind, enc, expire_at.is_some())
623 .with_access()
624 .with_cold()
625 .byte();
626 let mut i = 1;
627 if let Some(ms) = expire_at {
628 out[i..i + 8].copy_from_slice(&ms.to_le_bytes());
629 i += 8;
630 }
631 i += write_blank_access(&mut out[i..]);
632 out[i..i + 8].copy_from_slice(&at.to_bits().to_le_bytes());
633 out[i + 8..i + 12].copy_from_slice(&len.to_le_bytes());
634}
635
636/// Where a demoted value is and how big it is.
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
638pub struct Cold {
639 /// Where on the file the value lives.
640 pub at: Addr,
641 /// How many bytes it is, so the reader can size its buffer in one go and
642 /// the commands that only want the length never go and look.
643 pub len: u32,
644}
645
646/// Where the value of a demoted record is, or `None` if the record is resident.
647///
648/// This is the branch every point read makes. `None` is the common answer and
649/// it costs one test of a bit in a byte that has already been fetched.
650#[inline]
651#[must_use]
652pub fn cold(rec: &[u8]) -> Option<Cold> {
653 let m = Meta::from_byte(rec[0]);
654 if !m.is_cold() {
655 return None;
656 }
657 let i = m.payload_at();
658 let mut a = [0u8; 8];
659 a.copy_from_slice(&rec[i..i + 8]);
660 let mut l = [0u8; 4];
661 l.copy_from_slice(&rec[i + 8..i + 12]);
662 Some(Cold {
663 at: Addr::from_bits(u64::from_le_bytes(a)),
664 len: u32::from_le_bytes(l),
665 })
666}
667
668/// How long the value in a record is, without reading it.
669///
670/// The point of the tier tag seen from the command side: `STRLEN` on a demoted
671/// key is the same cost as `STRLEN` on a resident one. `None` means the record
672/// holds something whose length is not a string length, which is every
673/// collection, and those keep their length in their body.
674#[inline]
675#[must_use]
676pub fn str_len(rec: &[u8]) -> Option<usize> {
677 let m = Meta::from_byte(rec[0]);
678 if m.kind() != Kind::String {
679 return None;
680 }
681 match cold(rec) {
682 Some(c) => Some(c.len as usize),
683 None => Some(read(rec).len()),
684 }
685}
686
687/// What the access field in a record says, or `None` if it has no room for one.
688///
689/// `None` means the record predates the field, which is a key that was written
690/// by an older build and read back out of a file. It is not an error and the
691/// caller should treat it as a key it knows nothing about rather than as a key
692/// that has never been touched.
693#[inline]
694#[must_use]
695pub fn access(rec: &[u8]) -> Option<Access> {
696 let m = Meta::from_byte(rec[0]);
697 if !m.has_access() {
698 return None;
699 }
700 let at = m.access_at();
701 Some(Access::from_bits(u32::from_le_bytes([
702 rec[at],
703 rec[at + 1],
704 rec[at + 2],
705 0,
706 ])))
707}
708
709/// Stamp the access field, in place, over whatever was there.
710///
711/// Returns false for a record with no room, which is the same older record
712/// [`access`] answers `None` for. It is not worth growing one to make room: the
713/// record would have to move, on a path that is usually a read, and the next
714/// write to that key rewrites it with a field anyway.
715#[inline]
716pub fn set_access(rec: &mut [u8], a: Access) -> bool {
717 let m = Meta::from_byte(rec[0]);
718 if !m.has_access() {
719 return false;
720 }
721 let at = m.access_at();
722 rec[at..at + ACCESS_LEN].copy_from_slice(&a.bits().to_le_bytes()[..ACCESS_LEN]);
723 true
724}
725
726/// The slab number in a record that has one.
727///
728/// # Panics
729///
730/// If the record is not one [`write_slot_record`] wrote, which is a caller that
731/// did not read the kind first.
732#[inline]
733pub fn slot(rec: &[u8]) -> u32 {
734 let at = Meta::from_byte(rec[0]).payload_at();
735 let mut b = [0u8; SLOT_LEN];
736 b.copy_from_slice(&rec[at..at + SLOT_LEN]);
737 u32::from_le_bytes(b)
738}
739
740/// The type a record holds.
741#[inline]
742pub fn kind(rec: &[u8]) -> Kind {
743 Meta::from_byte(rec[0]).kind()
744}
745
746/// The deadline in a record, if it has one.
747#[inline]
748pub fn expire_at(rec: &[u8]) -> Option<u64> {
749 let m = Meta::from_byte(rec[0]);
750 if !m.has_expiry() {
751 return None;
752 }
753 let mut b = [0u8; 8];
754 b.copy_from_slice(&rec[1..9]);
755 Some(u64::from_le_bytes(b))
756}
757
758/// Whether a record carries a deadline at all, without reading it.
759///
760/// One byte where [`expire_at`] reads nine, which is the difference between a
761/// question worth asking on every write and one that is not. The count of keys
762/// with deadlines is kept up to date on every record written and every record
763/// deleted, and all it ever needs is this bit.
764#[inline]
765pub fn has_expiry(rec: &[u8]) -> bool {
766 Meta::from_byte(rec[0]).has_expiry()
767}
768
769/// Whether a record's deadline has passed at `now_ms`.
770///
771/// A deadline exactly equal to now has passed, which is Redis's reading: a key
772/// set to expire at time T is gone at time T.
773#[inline]
774pub fn is_expired(rec: &[u8], now_ms: u64) -> bool {
775 match expire_at(rec) {
776 Some(at) => at <= now_ms,
777 None => false,
778 }
779}
780
781/// The value in a record.
782///
783/// # Panics
784///
785/// In a debug build, if the record is demoted. A demoted record's payload is a
786/// file address and reading it as a value would hand back twelve bytes of
787/// address as though they were the string, which is the kind of wrong that
788/// looks like data corruption three layers away. Callers check [`cold`] first,
789/// and that check is the branch they were going to make anyway.
790#[inline]
791pub fn read(rec: &[u8]) -> Str<'_> {
792 let m = Meta::from_byte(rec[0]);
793 debug_assert!(!m.is_cold(), "read on a record whose value is on the file");
794 let at = m.payload_at();
795 match m.encoding() {
796 Encoding::Int => {
797 let mut b = [0u8; INT_LEN];
798 b.copy_from_slice(&rec[at..at + INT_LEN]);
799 Str::Int(i64::from_le_bytes(b))
800 }
801 _ => Str::Bytes(&rec[at..]),
802 }
803}
804
805/// The integer in an int encoded record, and where its bytes start.
806///
807/// Returns `None` for any other encoding. This is the read half of `INCR`'s
808/// fast path, and the offset it hands back is what the write half stores into.
809#[inline]
810pub fn read_int_in_place(rec: &[u8]) -> Option<(i64, usize)> {
811 let m = Meta::from_byte(rec[0]);
812 if m.is_cold() || m.encoding() != Encoding::Int {
813 return None;
814 }
815 let at = m.payload_at();
816 let mut b = [0u8; INT_LEN];
817 b.copy_from_slice(&rec[at..at + INT_LEN]);
818 Some((i64::from_le_bytes(b), at))
819}
820
821/// The bytes of a raw record, to be written over where they lie.
822///
823/// `None` for the other two encodings, and that is not a missing case: a bitmap
824/// write turns an int or an embstr into a raw string, which means moving it, so
825/// there is nothing here for those two to write into. `OBJECT ENCODING` says
826/// `raw` after a `SETBIT` on a value that was `int` a moment before, which is
827/// the same rule seen from the outside.
828#[inline]
829pub fn raw_in_place(rec: &mut [u8]) -> Option<&mut [u8]> {
830 let m = Meta::from_byte(rec[0]);
831 if m.is_cold() || m.encoding() != Encoding::Raw {
832 return None;
833 }
834 let at = m.payload_at();
835 Some(&mut rec[at..])
836}
837
838/// Store `n` back over an int payload that starts at `at`.
839#[inline]
840pub fn write_int_in_place(rec: &mut [u8], at: usize, n: i64) {
841 rec[at..at + INT_LEN].copy_from_slice(&n.to_le_bytes());
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
849 fn encoding_follows_redis_boundaries() {
850 assert_eq!(Encoding::of(b"0"), Encoding::Int);
851 assert_eq!(Encoding::of(b"-1"), Encoding::Int);
852 assert_eq!(Encoding::of(b"9223372036854775807"), Encoding::Int);
853 // Past an i64, so it is text and not a number.
854 assert_eq!(Encoding::of(b"9223372036854775808"), Encoding::Embstr);
855 // The three shapes string2ll refuses, all of which must survive as text.
856 assert_eq!(Encoding::of(b"007"), Encoding::Embstr);
857 assert_eq!(Encoding::of(b"+1"), Encoding::Embstr);
858 assert_eq!(Encoding::of(b"-0"), Encoding::Embstr);
859 assert_eq!(Encoding::of(b""), Encoding::Embstr);
860 assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX]), Encoding::Embstr);
861 assert_eq!(Encoding::of(&[b'x'; EMBSTR_MAX + 1]), Encoding::Raw);
862 }
863
864 const KINDS: [Kind; 8] = [
865 Kind::String,
866 Kind::Hash,
867 Kind::Set,
868 Kind::Zset,
869 Kind::List,
870 Kind::Stream,
871 Kind::Array,
872 Kind::Foreign,
873 ];
874
875 /// The seven that a saved file has a number for, which is every kind but
876 /// the escape.
877 const SAVED: [Kind; 7] = [
878 Kind::String,
879 Kind::Hash,
880 Kind::Set,
881 Kind::Zset,
882 Kind::List,
883 Kind::Stream,
884 Kind::Array,
885 ];
886
887 #[test]
888 fn the_meta_byte_survives_a_round_trip() {
889 for kind in KINDS {
890 for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
891 for expiry in [false, true] {
892 let m = Meta::new(kind, enc, expiry);
893 let back = Meta::from_byte(m.byte());
894 assert_eq!(back.kind(), kind);
895 assert_eq!(back.encoding(), enc);
896 assert_eq!(back.has_expiry(), expiry);
897 assert_eq!(back.payload_at(), if expiry { 9 } else { 1 });
898 }
899 }
900 }
901 }
902
903 #[test]
904 fn the_three_fields_of_the_meta_byte_do_not_reach_into_each_other() {
905 // Forty two combinations, all of which have to come out of one byte
906 // with nothing borrowed from a neighbour. A tag that overlapped the
907 // expiry bit would read the payload at the wrong offset, which is a
908 // corrupt value rather than a wrong answer.
909 let mut seen = std::collections::HashSet::new();
910 for kind in KINDS {
911 for enc in [Encoding::Int, Encoding::Embstr, Encoding::Raw] {
912 for expiry in [false, true] {
913 assert!(
914 seen.insert(Meta::new(kind, enc, expiry).byte()),
915 "{kind:?} {enc:?} {expiry} collides with something else"
916 );
917 }
918 }
919 }
920 assert_eq!(seen.len(), KINDS.len() * 6);
921 }
922
923 #[test]
924 fn a_record_written_before_the_tag_existed_is_a_string() {
925 // Bits 3 to 7 were zero in every record M2 wrote, and zero is String.
926 // This is the whole reason String is zero, so it is worth a test that
927 // fails if somebody renumbers the enum alphabetically one day.
928 assert_eq!(Kind::String as u8, 0);
929 assert_eq!(Meta::from_byte(0b0000_0101).kind(), Kind::String);
930 assert_eq!(Meta::from_byte(0b0000_0101).encoding(), Encoding::Embstr);
931 assert!(Meta::from_byte(0b0000_0101).has_expiry());
932 }
933
934 #[test]
935 fn the_tag_is_the_number_the_file_format_uses() {
936 use yo_format::catalog::ValueType;
937 // Not a translation table, an assertion that no translation is needed.
938 // If these ever diverge, saving a key has to map between them, and the
939 // mapping is the kind of thing that gets one arm wrong.
940 assert_eq!(Kind::String as u8, ValueType::String as u8);
941 assert_eq!(Kind::Hash as u8, ValueType::Hash as u8);
942 assert_eq!(Kind::Set as u8, ValueType::Set as u8);
943 assert_eq!(Kind::Zset as u8, ValueType::Zset as u8);
944 assert_eq!(Kind::List as u8, ValueType::List as u8);
945 assert_eq!(Kind::Stream as u8, ValueType::Stream as u8);
946 assert_eq!(Kind::Array as u8, ValueType::Array as u8);
947 // `Kind::Foreign` is deliberately not in this list. It is the escape
948 // for a body that lives above this crate, nothing saves one yet, and so
949 // there is no number on disk for it to have to match. Its tag is 7 and
950 // the catalog's 7 is a bitmap, which is a string in memory and never
951 // becomes a `Kind`, so the two cannot meet.
952 // And the words agree, because both of them end up on a wire.
953 for k in SAVED {
954 let v = ValueType::from_u8(k as u8).expect("the catalog knows this one");
955 assert_eq!(k.name(), v.redis_name(), "{k:?}");
956 }
957 }
958
959 #[test]
960 fn a_string_record_is_tagged_as_one() {
961 for text in [&b"42"[..], b"hello", &[b'z'; 100]] {
962 for expire in [None, Some(9_000u64)] {
963 assert_eq!(kind(&record(text, expire)), Kind::String);
964 }
965 }
966 let mut v = vec![0u8; record_len(Encoding::Int, 0, false)];
967 write_int_record(&mut v, 7, None);
968 assert_eq!(kind(&v), Kind::String);
969 }
970
971 fn record(bytes: &[u8], expire: Option<u64>) -> Vec<u8> {
972 let enc = Encoding::of(bytes);
973 let mut v = vec![0u8; record_len(enc, bytes.len(), expire.is_some())];
974 write_record(&mut v, enc, bytes, expire);
975 v
976 }
977
978 #[test]
979 fn a_record_gives_back_what_went_into_it() {
980 for text in [
981 &b""[..],
982 b"x",
983 b"0",
984 b"-1",
985 b"42",
986 b"007",
987 b"-0",
988 b"hello world",
989 &[b'z'; 100],
990 ] {
991 for expire in [None, Some(1_234_567_890_123u64)] {
992 let r = record(text, expire);
993 assert_eq!(read(&r).to_vec(), text, "{text:?} at {expire:?}");
994 assert_eq!(read(&r).len(), text.len(), "{text:?} length");
995 assert_eq!(expire_at(&r), expire, "{text:?} deadline");
996 }
997 }
998 }
999
1000 #[test]
1001 fn an_integer_costs_the_same_however_many_digits_it_has() {
1002 let small = record(b"1", None);
1003 let large = record(b"-9223372036854775808", None);
1004 assert_eq!(small.len(), large.len());
1005 assert_eq!(read(&large), Str::Int(i64::MIN));
1006 assert_eq!(read(&large).to_vec(), b"-9223372036854775808");
1007 }
1008
1009 #[test]
1010 fn an_integer_is_incremented_where_it_lies() {
1011 let mut r = record(b"41", Some(99));
1012 let (n, at) = read_int_in_place(&r).expect("int encoded");
1013 assert_eq!(n, 41);
1014 write_int_in_place(&mut r, at, n + 1);
1015 assert_eq!(read(&r), Str::Int(42));
1016 // The deadline was in front of the payload and is still there.
1017 assert_eq!(expire_at(&r), Some(99));
1018 }
1019
1020 #[test]
1021 fn a_string_is_not_read_as_an_integer_in_place() {
1022 let r = record(b"hello", None);
1023 assert!(read_int_in_place(&r).is_none());
1024 }
1025
1026 #[test]
1027 fn a_deadline_that_is_now_has_passed() {
1028 let r = record(b"v", Some(100));
1029 assert!(!is_expired(&r, 99));
1030 assert!(is_expired(&r, 100));
1031 assert!(is_expired(&r, 101));
1032 let forever = record(b"v", None);
1033 assert!(!is_expired(&forever, u64::MAX));
1034 }
1035
1036 #[test]
1037 fn a_value_that_is_text_can_still_be_a_number() {
1038 // What `APPEND` leaves behind, and what `INCR` has to accept.
1039 assert_eq!(Str::Bytes(b"10").as_int(), Some(10));
1040 assert_eq!(Str::Bytes(b"10x").as_int(), None);
1041 assert_eq!(Str::Int(-5).as_int(), Some(-5));
1042 }
1043
1044 #[test]
1045 fn an_unknown_encoding_reads_as_raw_bytes() {
1046 // Nothing we write produces bit pattern three, but a record that has
1047 // been through a future writer might, and guessing `int` on it would
1048 // reinterpret eight bytes of somebody's string as a number.
1049 let m = Meta::from_byte(0b11);
1050 assert_eq!(m.encoding(), Encoding::Raw);
1051 }
1052
1053 #[test]
1054 fn every_tag_pattern_is_spoken_for() {
1055 // All eight of them now, the last one being the escape a body this
1056 // crate cannot name is held under. There is no pattern left over to
1057 // read as a string, so the day a ninth kind is wanted the tag has to
1058 // grow a bit rather than borrow one.
1059 assert_eq!(Meta::from_byte(6 << 3).kind(), Kind::Array);
1060 assert_eq!(Meta::from_byte(7 << 3).kind(), Kind::Foreign);
1061 }
1062
1063 #[test]
1064 fn the_bits_above_the_tag_do_not_disturb_it() {
1065 // Bit 6 is now the access flag and bit 7 is still free, and neither of
1066 // them may move the tag, so this is the check that the tag is three
1067 // bits and not five.
1068 assert_eq!(Meta::from_byte(0b1100_0000).kind(), Kind::String);
1069 assert_eq!(Meta::from_byte(0b1101_0000).kind(), Kind::Set);
1070 // And the flag itself reads off the byte rather than off the tag.
1071 assert!(Meta::from_byte(0b0100_0000).has_access());
1072 assert!(!Meta::from_byte(0b1011_1111).has_access());
1073 }
1074
1075 /// Every record this crate writes has room for an access field, and the
1076 /// field starts empty.
1077 #[test]
1078 fn a_fresh_record_has_an_unstamped_access_field() {
1079 for expire in [None, Some(1_700_000_000_000)] {
1080 for (enc, bytes) in [
1081 (Encoding::Int, &b"42"[..]),
1082 (Encoding::Embstr, b"hello"),
1083 (Encoding::Raw, &[b'x'; 64][..]),
1084 ] {
1085 let mut rec = vec![0u8; record_len(enc, bytes.len(), expire.is_some())];
1086 write_record(&mut rec, enc, bytes, expire);
1087 let a = access(&rec).expect("a record we just wrote has the field");
1088 assert!(a.is_unset(), "{enc:?} came out stamped");
1089 assert_eq!(expire_at(&rec), expire, "{enc:?} lost its deadline");
1090 match enc {
1091 Encoding::Int => assert_eq!(read(&rec), Str::Int(42)),
1092 _ => assert_eq!(read(&rec), Str::Bytes(bytes)),
1093 }
1094 }
1095 }
1096 }
1097
1098 /// The same for the other two writers, which is every record shape there is.
1099 #[test]
1100 fn slot_and_int_records_have_the_field_too() {
1101 for expire in [None, Some(9_000)] {
1102 let mut rec = vec![0u8; slot_record_len(expire.is_some())];
1103 write_slot_record(&mut rec, Kind::Set, 77, expire);
1104 assert!(access(&rec).expect("the field").is_unset());
1105 assert_eq!(slot(&rec), 77);
1106 assert_eq!(kind(&rec), Kind::Set);
1107 assert_eq!(expire_at(&rec), expire);
1108
1109 let mut rec = vec![0u8; record_len(Encoding::Int, 0, expire.is_some())];
1110 write_int_record(&mut rec, -5, expire);
1111 assert!(access(&rec).expect("the field").is_unset());
1112 assert_eq!(read(&rec), Str::Int(-5));
1113 assert_eq!(expire_at(&rec), expire);
1114 }
1115 }
1116
1117 /// Stamping the field does not disturb anything either side of it.
1118 ///
1119 /// It sits between the deadline and the payload and it is written in place
1120 /// on a path that is usually a read, so an off by one here would corrupt a
1121 /// value quietly rather than fail.
1122 #[test]
1123 fn stamping_the_field_leaves_the_deadline_and_the_payload_alone() {
1124 let deadline = 1_700_000_000_123u64;
1125 let body = b"the payload nobody should touch";
1126 let mut rec = vec![0u8; record_len(Encoding::Raw, body.len(), true)];
1127 write_record(&mut rec, Encoding::Raw, body, Some(deadline));
1128
1129 for bits in [1u32, 0xff, 0x00ff_ffff, 0x0012_3456] {
1130 let a = Access::from_bits(bits);
1131 assert!(set_access(&mut rec, a));
1132 assert_eq!(access(&rec), Some(a), "{bits:#x} did not survive");
1133 assert_eq!(
1134 expire_at(&rec),
1135 Some(deadline),
1136 "{bits:#x} hit the deadline"
1137 );
1138 assert_eq!(read(&rec), Str::Bytes(body), "{bits:#x} hit the payload");
1139 }
1140 }
1141
1142 /// A record written before the field existed still reads correctly, and
1143 /// refuses to be stamped rather than being stamped over its payload.
1144 ///
1145 /// This is the whole reason the field is behind a tag bit instead of just
1146 /// always being there. A file written by an older build has records with the
1147 /// bit clear, and their payload starts three bytes earlier.
1148 #[test]
1149 fn a_record_from_before_the_field_still_reads() {
1150 // Built by hand, the way the old writer did it: meta, deadline, payload,
1151 // and no access field.
1152 let body = b"older";
1153 let mut old = vec![Meta::string(Encoding::Raw, true).byte()];
1154 old.extend_from_slice(&7_000u64.to_le_bytes());
1155 old.extend_from_slice(body);
1156
1157 assert!(!Meta::from_byte(old[0]).has_access());
1158 assert_eq!(access(&old), None, "there is no field to read");
1159 assert_eq!(expire_at(&old), Some(7_000));
1160 assert_eq!(read(&old), Str::Bytes(body));
1161
1162 let before = old.clone();
1163 assert!(
1164 !set_access(&mut old, Access::from_bits(0xabcdef)),
1165 "it should refuse rather than write over the payload"
1166 );
1167 assert_eq!(old, before, "it wrote something anyway");
1168 }
1169
1170 /// The field costs three bytes on every record and no more.
1171 #[test]
1172 fn the_field_costs_three_bytes() {
1173 assert_eq!(record_len(Encoding::Raw, 10, false), 1 + 3 + 10);
1174 assert_eq!(record_len(Encoding::Raw, 10, true), 1 + 8 + 3 + 10);
1175 assert_eq!(record_len(Encoding::Int, 10, false), 1 + 3 + 8);
1176 assert_eq!(slot_record_len(false), 1 + 3 + 4);
1177 assert_eq!(slot_record_len(true), 1 + 8 + 3 + 4);
1178 }
1179
1180 /// A place on the file to demote a value to.
1181 fn somewhere() -> Addr {
1182 Addr::new(yo_common::Space::Log, 4096)
1183 }
1184
1185 #[test]
1186 fn a_demoted_record_says_so_in_the_byte_the_lookup_already_read() {
1187 let mut rec = vec![0u8; cold_record_len(false)];
1188 write_cold_record(
1189 &mut rec,
1190 Kind::String,
1191 Encoding::Raw,
1192 somewhere(),
1193 900,
1194 None,
1195 );
1196 // The one branch a point read makes, and it is a bit in rec[0].
1197 let c = cold(&rec).expect("the record is demoted");
1198 assert_eq!(c.at, somewhere());
1199 assert_eq!(c.len, 900);
1200 }
1201
1202 #[test]
1203 fn a_resident_record_is_not_demoted() {
1204 let mut rec = vec![0u8; record_len(Encoding::Raw, 3, false)];
1205 write_record(&mut rec, Encoding::Raw, b"abc", None);
1206 assert_eq!(cold(&rec), None);
1207 }
1208
1209 #[test]
1210 fn a_demoted_record_answers_everything_that_is_not_the_bytes() {
1211 let deadline = 1_700_000_000_000u64;
1212 let mut rec = vec![0u8; cold_record_len(true)];
1213 write_cold_record(
1214 &mut rec,
1215 Kind::String,
1216 Encoding::Raw,
1217 somewhere(),
1218 77,
1219 Some(deadline),
1220 );
1221 // None of these is allowed to want the device.
1222 assert_eq!(kind(&rec), Kind::String);
1223 assert_eq!(expire_at(&rec), Some(deadline));
1224 assert_eq!(str_len(&rec), Some(77));
1225 assert_eq!(Meta::from_byte(rec[0]).encoding(), Encoding::Raw);
1226 assert!(access(&rec).is_some());
1227 }
1228
1229 #[test]
1230 fn a_demoted_record_can_still_be_stamped_and_ranked() {
1231 // Eviction has to be able to score a key whose value is on the file,
1232 // or the policy would fault in the keys it is trying to get rid of.
1233 let mut rec = vec![0u8; cold_record_len(false)];
1234 write_cold_record(
1235 &mut rec,
1236 Kind::String,
1237 Encoding::Embstr,
1238 somewhere(),
1239 5,
1240 None,
1241 );
1242 let a = Access::from_bits(12345);
1243 assert!(set_access(&mut rec, a));
1244 assert_eq!(access(&rec), Some(a));
1245 // And the address survived being written around.
1246 assert_eq!(cold(&rec).map(|c| c.at), Some(somewhere()));
1247 }
1248
1249 #[test]
1250 fn the_in_place_writers_refuse_a_demoted_record() {
1251 let mut rec = vec![0u8; cold_record_len(false)];
1252 write_cold_record(&mut rec, Kind::String, Encoding::Int, somewhere(), 2, None);
1253 // The payload of this record parses as an int if you do not look at the
1254 // tier tag first, which is exactly the bug the check is here to stop.
1255 assert_eq!(read_int_in_place(&rec), None);
1256 assert_eq!(raw_in_place(&mut rec), None);
1257 }
1258
1259 #[test]
1260 fn demoting_a_long_value_is_what_buys_the_memory() {
1261 // Twelve bytes of payload whatever the value was. A short value is not
1262 // worth demoting and the arithmetic says so rather than a comment.
1263 let long = record_len(Encoding::Raw, 4096, false);
1264 assert!(
1265 cold_record_len(false) < long / 100,
1266 "{} against {long}",
1267 cold_record_len(false)
1268 );
1269 assert!(cold_record_len(false) > record_len(Encoding::Raw, 8, false));
1270 }
1271
1272 #[test]
1273 fn str_len_on_a_resident_string_is_the_value_length() {
1274 let mut rec = vec![0u8; record_len(Encoding::Raw, 3, false)];
1275 write_record(&mut rec, Encoding::Raw, b"abc", None);
1276 assert_eq!(str_len(&rec), Some(3));
1277
1278 let mut n = vec![0u8; record_len(Encoding::Int, 0, false)];
1279 write_int_record(&mut n, -1234, None);
1280 assert_eq!(str_len(&n), Some(5));
1281 }
1282}