Skip to main content

yo_kv/
cold.rs

1//! A value that is too big to hold, cut into chunks that are addressed rather
2//! than walked.
3//!
4//! `05` section 4.4 and the bottom row of section 5. Two different problems get
5//! the same answer here. One is a value of a megabyte, which must never be a
6//! single arena allocation that then gets copied twice, because that shape is
7//! what killed aki's `HGETALL` (L22). The other is a collection body that has
8//! left memory, where a membership test should fault one chunk and a full
9//! enumeration should stream them.
10//!
11//! Both want the same thing: fixed size pieces in the log, and a way to get to
12//! piece `i` without reading pieces `0` through `i - 1`.
13//!
14//! # The layout
15//!
16//! A value of at most one chunk is one record and nothing else. That is the
17//! common case for a demoted string and it costs one read, which matters,
18//! because G9 is a gate on device reads per point read and a value that needs
19//! two reads has already spent it.
20//!
21//! Anything longer is `n` chunk records plus a directory record holding their
22//! addresses in order, eight bytes each:
23//!
24//! ```text
25//!   directory                chunks
26//! +----------+----------+   +--------- 64 KiB ---------+
27//! | addr 0   | addr 1   |...| bytes 0 .. 65536         |
28//! +----------+----------+   +--------------------------+
29//!                           | bytes 65536 .. 131072    |
30//!                           +--------------------------+
31//! ```
32//!
33//! There is no second directory level and there does not need to be one. A
34//! directory is itself at most one chunk, which is 65536 bytes, which is 8192
35//! addresses, which is 8192 chunks, which is 512 MiB. That is exactly the
36//! largest string Redis will hold, so the arithmetic closes and a chain is
37//! always either one read or two.
38//!
39//! # What this module is not
40//!
41//! It does not decide when a value should be chunked, it does not own the log,
42//! and it does not know what a key is. [`Blocks`] is the whole of its contact
43//! with storage: somewhere to put bytes that hands back an address, and
44//! somewhere to read an address back from. The log in `yo-record` is one
45//! implementation of that and a vector in a test is another, which is what
46//! keeps this testable without a file.
47
48use yo_common::{Addr, Code, Error, Result, Space};
49
50/// One chunk, and the unit everything here counts in.
51///
52/// 64 KiB is `05` section 4.4's number. It is large enough that a megabyte is
53/// sixteen reads rather than two hundred and fifty, and small enough that a
54/// membership test on a spilled collection faults kilobytes rather than
55/// megabytes.
56pub const CHUNK: usize = 64 * 1024;
57
58/// How many addresses fit in a directory, which is how many chunks a chain can
59/// have.
60pub const FANOUT: usize = CHUNK / 8;
61
62/// The longest value a chain can hold, which is one directory's worth of chunks.
63///
64/// 512 MiB, and not a coincidence: it is also the largest string Redis accepts,
65/// so nothing that fits in the protocol fails to fit in a chain.
66pub const MAX_LEN: u64 = (FANOUT * CHUNK) as u64;
67
68/// Somewhere chunks go, and come back from.
69///
70/// Deliberately two methods. Everything this module needs from a log is an
71/// append that hands back an address and a read that takes one, and writing the
72/// trait that small is what lets the tests run against a vector instead of a
73/// file.
74pub trait Blocks {
75    /// Put these bytes somewhere and say where they went.
76    fn put(&mut self, bytes: &[u8]) -> Result<Addr>;
77
78    /// Read back what was put at `at`.
79    ///
80    /// The length is not passed in because the store knows it. A log record
81    /// carries its own length, and a caller that had to remember it would be
82    /// keeping a second copy of something that is already written down.
83    fn get(&self, at: Addr) -> Result<&[u8]>;
84
85    /// How many bytes the store is holding, for the storage limit.
86    ///
87    /// The store's own size and not the sum of what was put in it. A log that
88    /// has been written to and compacted knows what it occupies and nothing
89    /// above it does, and `maxstore` is a limit on the file rather than on the
90    /// payload that went into it.
91    fn bytes(&self) -> u64;
92
93    /// Says that every borrow handed out by [`Blocks::get`] is finished with.
94    ///
95    /// A store that borrows from something it already holds has nothing to do
96    /// here and takes the default. A store that has to copy the bytes somewhere
97    /// before it can lend them out needs a moment when that somewhere is known
98    /// to be unused, because `get` takes `&self` and cannot free anything, and
99    /// this is that moment: it takes `&mut self`, which is the proof that no
100    /// borrow is alive.
101    ///
102    /// A caller that never calls it is correct and grows. [`Reader`] holds
103    /// several chunks of one value at once by design, so the call belongs
104    /// before a read and not inside one, which is where
105    /// [`Tier`](crate::tier::Tier) puts it.
106    fn release(&mut self) {}
107}
108
109/// A store chosen at run time, which is how every real one arrives.
110///
111/// `Send` because the stripe it ends up in is worked on by whichever thread has
112/// taken that stripe's lock. Which thread that is changes from one command to
113/// the next, so a store that could not be sent could not be attached. Every
114/// store there is, which is a file and a vector, meets the bound already.
115pub type Store = Box<dyn Blocks + Send>;
116
117/// So that a store can be chosen at run time rather than at compile time.
118///
119/// [`Keyspace`](crate::Keyspace) holds its tier behind this box, and the reason
120/// is that the alternative is a type parameter on `Keyspace`, which would spread
121/// to `yo-resp` and to every caller of either, all to name a type that only the
122/// code opening the file knows. The dispatch it costs is one indirect call on a
123/// path that is about to read a device, and nothing at all on a warm read, which
124/// never reaches this trait.
125impl Blocks for Store {
126    fn put(&mut self, bytes: &[u8]) -> Result<Addr> {
127        (**self).put(bytes)
128    }
129
130    fn get(&self, at: Addr) -> Result<&[u8]> {
131        (**self).get(at)
132    }
133
134    fn bytes(&self) -> u64 {
135        (**self).bytes()
136    }
137
138    fn release(&mut self) {
139        (**self).release();
140    }
141}
142
143/// Where a value went, and how much of it there is.
144///
145/// Twelve bytes, which is what [`value::write_cold_record`](crate::value) puts
146/// in a demoted record. `at` is the single chunk when the value fits in one and
147/// the directory when it does not, and `len` is what says which.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct Chain {
150    /// The one chunk, or the directory.
151    pub at: Addr,
152    /// The value's length in bytes, not the chain's.
153    pub len: u64,
154}
155
156/// How many chunks a value of this length needs.
157///
158/// One for an empty value, because a chain always points at something. A zero
159/// length value that pointed at nothing would need a third case in every reader
160/// and there is nothing to gain from it.
161#[must_use]
162pub const fn chunks_for(len: u64) -> u64 {
163    if len == 0 {
164        1
165    } else {
166        len.div_ceil(CHUNK as u64)
167    }
168}
169
170/// A place to build a directory, owned by the shard and used again every time.
171///
172/// Y7 says a command path does not allocate, and a chain of the largest value
173/// Redis takes has a 64 KiB directory that has to be laid out somewhere before
174/// it is written. So the shard keeps one of these and hands it in.
175pub struct Scratch {
176    dir: Vec<u8>,
177}
178
179impl Scratch {
180    /// One directory's worth of room, allocated once.
181    #[must_use]
182    pub fn new() -> Scratch {
183        Scratch {
184            dir: Vec::with_capacity(CHUNK),
185        }
186    }
187
188    /// What it costs to keep, for the memory report.
189    #[must_use]
190    pub fn memory_bytes(&self) -> usize {
191        self.dir.capacity()
192    }
193}
194
195impl Default for Scratch {
196    fn default() -> Scratch {
197        Scratch::new()
198    }
199}
200
201/// Write a value as a chain and say where it went.
202///
203/// The chunks go down before the directory does, so a directory that is
204/// readable is a directory whose chunks are all readable. A reader that arrives
205/// after a crash either finds no directory, in which case the chunks are
206/// garbage that compaction will notice nobody points at, or finds one and can
207/// trust every address in it.
208pub fn write<B: Blocks>(blocks: &mut B, value: &[u8], scratch: &mut Scratch) -> Result<Chain> {
209    let len = value.len() as u64;
210    if len > MAX_LEN {
211        return Err(Error::fmt(
212            Code::Full,
213            format_args!("a value of {len} bytes is longer than a chain holds"),
214        ));
215    }
216
217    if value.len() <= CHUNK {
218        return Ok(Chain {
219            at: blocks.put(value)?,
220            len,
221        });
222    }
223
224    scratch.dir.clear();
225    for piece in value.chunks(CHUNK) {
226        let at = blocks.put(piece)?;
227        scratch.dir.extend_from_slice(&at.to_bits().to_le_bytes());
228    }
229    let at = blocks.put(&scratch.dir)?;
230    Ok(Chain { at, len })
231}
232
233/// A chain opened for reading.
234///
235/// Holding one means the directory has been read, so every chunk after that is
236/// a single fetch. That is the whole reason this is a type rather than a
237/// function taking a [`Chain`]: an enumeration that walks a spilled collection
238/// reads the directory once and not once per chunk.
239pub struct Reader<'a, B: Blocks> {
240    blocks: &'a B,
241    len: u64,
242    /// The directory, or the address of the only chunk when there is no
243    /// directory to have.
244    dir: Dir<'a>,
245}
246
247enum Dir<'a> {
248    One(Addr),
249    Many(&'a [u8]),
250}
251
252impl<'a, B: Blocks> Reader<'a, B> {
253    /// Read the directory, if there is one, and get ready to fetch chunks.
254    pub fn open(blocks: &'a B, chain: Chain) -> Result<Reader<'a, B>> {
255        let want = chunks_for(chain.len);
256        let dir = if want == 1 {
257            Dir::One(chain.at)
258        } else {
259            let bytes = blocks.get(chain.at)?;
260            if bytes.len() as u64 != want * 8 {
261                return Err(Error::fmt(
262                    Code::Corrupt,
263                    format_args!(
264                        "a chain of {} bytes wants {want} addresses and its directory has {}",
265                        chain.len,
266                        bytes.len() / 8
267                    ),
268                ));
269            }
270            Dir::Many(bytes)
271        };
272        Ok(Reader {
273            blocks,
274            len: chain.len,
275            dir,
276        })
277    }
278
279    /// The value's length in bytes.
280    #[must_use]
281    pub const fn len(&self) -> u64 {
282        self.len
283    }
284
285    /// Whether the value has no bytes in it, which is not the same as having no
286    /// chunks.
287    #[must_use]
288    pub const fn is_empty(&self) -> bool {
289        self.len == 0
290    }
291
292    /// How many chunks the value is in.
293    #[must_use]
294    pub const fn chunks(&self) -> u64 {
295        chunks_for(self.len)
296    }
297
298    /// Fetch chunk `i`, which is one read and never a walk.
299    pub fn chunk(&self, i: u64) -> Result<&'a [u8]> {
300        let at = match self.dir {
301            Dir::One(at) if i == 0 => at,
302            Dir::One(_) => {
303                return Err(Error::new(Code::Invalid, "there is only one chunk"));
304            }
305            Dir::Many(bytes) => {
306                let start = (i as usize)
307                    .checked_mul(8)
308                    .filter(|s| s + 8 <= bytes.len())
309                    .ok_or_else(|| Error::new(Code::Invalid, "no such chunk"))?;
310                let mut bits = [0u8; 8];
311                bits.copy_from_slice(&bytes[start..start + 8]);
312                Addr::from_bits(u64::from_le_bytes(bits))
313            }
314        };
315        if at.space() != Some(Space::Log) {
316            return Err(Error::fmt(
317                Code::Corrupt,
318                format_args!("chunk {i} is not in the log"),
319            ));
320        }
321        self.blocks.get(at)
322    }
323
324    /// Walk the pieces of `from .. to`, in order, without touching a chunk the
325    /// range does not reach.
326    ///
327    /// This is what `GETRANGE` on a spilled value wants, and what a membership
328    /// test on a spilled collection wants, and they are the same walk. A range
329    /// inside one chunk is one fetch whatever the value's size is.
330    pub fn range(&self, from: u64, to: u64) -> Pieces<'a, '_, B> {
331        let to = to.min(self.len);
332        let from = from.min(to);
333        Pieces {
334            reader: self,
335            at: from,
336            end: to,
337        }
338    }
339}
340
341/// The pieces of a byte range, one chunk at a time.
342pub struct Pieces<'a, 'r, B: Blocks> {
343    reader: &'r Reader<'a, B>,
344    at: u64,
345    end: u64,
346}
347
348impl<'a, B: Blocks> Iterator for Pieces<'a, '_, B> {
349    type Item = Result<&'a [u8]>;
350
351    fn next(&mut self) -> Option<Result<&'a [u8]>> {
352        if self.at >= self.end {
353            return None;
354        }
355        let chunk = self.at / CHUNK as u64;
356        let start = (self.at % CHUNK as u64) as usize;
357        let take = (self.end - self.at).min(CHUNK as u64 - start as u64) as usize;
358        self.at += take as u64;
359        Some(match self.reader.chunk(chunk) {
360            Ok(bytes) if start + take <= bytes.len() => Ok(&bytes[start..start + take]),
361            Ok(bytes) => Err(Error::fmt(
362                Code::Corrupt,
363                format_args!(
364                    "chunk {chunk} is {} bytes and the range wants {}",
365                    bytes.len(),
366                    start + take
367                ),
368            )),
369            Err(e) => Err(e),
370        })
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    /// A store that keeps what it is given in memory and hands back the index
379    /// as the address. Enough to exercise every path in here without a file,
380    /// and it counts its reads, which is the number the gate is about.
381    struct Mem {
382        blobs: Vec<Vec<u8>>,
383        reads: std::cell::Cell<usize>,
384    }
385
386    impl Mem {
387        fn new() -> Mem {
388            Mem {
389                blobs: Vec::new(),
390                reads: std::cell::Cell::new(0),
391            }
392        }
393
394        fn reads(&self) -> usize {
395            self.reads.get()
396        }
397    }
398
399    impl Blocks for Mem {
400        fn put(&mut self, bytes: &[u8]) -> Result<Addr> {
401            self.blobs.push(bytes.to_vec());
402            Ok(Addr::new(Space::Log, (self.blobs.len() - 1) as u64))
403        }
404
405        fn get(&self, at: Addr) -> Result<&[u8]> {
406            self.reads.set(self.reads.get() + 1);
407            self.blobs
408                .get(at.offset() as usize)
409                .map(Vec::as_slice)
410                .ok_or_else(|| Error::new(Code::NotFound, "no such block"))
411        }
412
413        fn bytes(&self) -> u64 {
414            self.blobs.iter().map(|b| b.len() as u64).sum()
415        }
416    }
417
418    fn pattern(len: usize) -> Vec<u8> {
419        (0..len).map(|i| (i % 251) as u8).collect()
420    }
421
422    fn whole<B: Blocks>(r: &Reader<'_, B>) -> Vec<u8> {
423        let mut out = Vec::new();
424        for piece in r.range(0, r.len()) {
425            out.extend_from_slice(piece.expect("a piece the value has"));
426        }
427        out
428    }
429
430    #[test]
431    fn a_value_that_fits_in_one_chunk_has_no_directory() {
432        let mut m = Mem::new();
433        let value = pattern(1000);
434        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
435        assert_eq!(
436            m.blobs.len(),
437            1,
438            "a directory was written and should not be"
439        );
440        assert_eq!(chain.len, 1000);
441
442        let r = Reader::open(&m, chain).expect("opened");
443        assert_eq!(r.chunks(), 1);
444        assert_eq!(whole(&r), value);
445        // The gate is device reads per point read, so this is the number that
446        // matters and it is one.
447        assert_eq!(
448            m.reads(),
449            1,
450            "reading a short value took more than one read"
451        );
452    }
453
454    #[test]
455    fn exactly_one_chunk_still_has_no_directory() {
456        let mut m = Mem::new();
457        let value = pattern(CHUNK);
458        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
459        assert_eq!(m.blobs.len(), 1);
460        let r = Reader::open(&m, chain).expect("opened");
461        assert_eq!(r.chunks(), 1);
462        assert_eq!(whole(&r), value);
463    }
464
465    #[test]
466    fn one_byte_more_than_a_chunk_is_two_chunks_and_a_directory() {
467        let mut m = Mem::new();
468        let value = pattern(CHUNK + 1);
469        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
470        assert_eq!(m.blobs.len(), 3, "two chunks and a directory");
471        let r = Reader::open(&m, chain).expect("opened");
472        assert_eq!(r.chunks(), 2);
473        assert_eq!(r.chunk(1).expect("the second chunk").len(), 1);
474        assert_eq!(whole(&r), value);
475    }
476
477    #[test]
478    fn an_empty_value_is_one_empty_chunk() {
479        let mut m = Mem::new();
480        let chain = write(&mut m, b"", &mut Scratch::new()).expect("written");
481        let r = Reader::open(&m, chain).expect("opened");
482        assert!(r.is_empty());
483        assert_eq!(r.chunks(), 1, "a chain always points at something");
484        assert_eq!(whole(&r), b"");
485    }
486
487    #[test]
488    fn a_range_inside_one_chunk_only_fetches_that_chunk() {
489        let mut m = Mem::new();
490        let value = pattern(10 * CHUNK);
491        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
492
493        let r = Reader::open(&m, chain).expect("opened");
494        let before = m.reads();
495        let mut got = Vec::new();
496        // Somewhere in the middle of chunk 7, which is the case that would go
497        // wrong if the walk started at the beginning.
498        let (from, to) = (7 * CHUNK as u64 + 100, 7 * CHUNK as u64 + 300);
499        for piece in r.range(from, to) {
500            got.extend_from_slice(piece.expect("a piece"));
501        }
502        assert_eq!(got, value[from as usize..to as usize]);
503        assert_eq!(
504            m.reads() - before,
505            1,
506            "a range inside one chunk of a ten chunk value should be one fetch"
507        );
508    }
509
510    #[test]
511    fn a_range_across_a_boundary_comes_back_in_two_pieces() {
512        let mut m = Mem::new();
513        let value = pattern(3 * CHUNK);
514        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
515        let r = Reader::open(&m, chain).expect("opened");
516
517        let (from, to) = (CHUNK as u64 - 5, CHUNK as u64 + 5);
518        let pieces: Vec<usize> = r
519            .range(from, to)
520            .map(|p| p.expect("a piece").len())
521            .collect();
522        assert_eq!(
523            pieces,
524            vec![5, 5],
525            "the boundary was not where it should be"
526        );
527    }
528
529    #[test]
530    fn a_range_past_the_end_stops_at_the_end() {
531        let mut m = Mem::new();
532        let value = pattern(100);
533        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
534        let r = Reader::open(&m, chain).expect("opened");
535        let mut got = Vec::new();
536        for piece in r.range(50, 1_000_000) {
537            got.extend_from_slice(piece.expect("a piece"));
538        }
539        assert_eq!(got, value[50..]);
540        assert_eq!(r.range(200, 300).count(), 0, "there is nothing out there");
541        assert_eq!(r.range(80, 20).count(), 0, "a backwards range is empty");
542    }
543
544    #[test]
545    fn every_chunk_but_the_last_is_full() {
546        let mut m = Mem::new();
547        let value = pattern(2 * CHUNK + 7);
548        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
549        let r = Reader::open(&m, chain).expect("opened");
550        assert_eq!(r.chunks(), 3);
551        assert_eq!(r.chunk(0).expect("chunk 0").len(), CHUNK);
552        assert_eq!(r.chunk(1).expect("chunk 1").len(), CHUNK);
553        assert_eq!(r.chunk(2).expect("chunk 2").len(), 7);
554        assert!(r.chunk(3).is_err(), "there is no fourth chunk");
555    }
556
557    #[test]
558    fn a_multi_chunk_value_is_two_reads_and_not_more() {
559        let mut m = Mem::new();
560        let value = pattern(5 * CHUNK);
561        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
562        let before = m.reads();
563        let r = Reader::open(&m, chain).expect("opened");
564        // Opening read the directory. Any one chunk after that is one more,
565        // whatever the value's size, which is the property the whole layout
566        // exists for.
567        assert_eq!(m.reads() - before, 1);
568        r.chunk(4).expect("the last chunk");
569        assert_eq!(m.reads() - before, 2);
570    }
571
572    #[test]
573    fn a_directory_that_does_not_match_the_length_is_refused() {
574        let mut m = Mem::new();
575        let value = pattern(2 * CHUNK);
576        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
577        // Say the value is longer than the directory can account for, which is
578        // what a torn write or a stale address looks like from here.
579        let lying = Chain {
580            at: chain.at,
581            len: 9 * CHUNK as u64,
582        };
583        assert!(
584            Reader::open(&m, lying).is_err(),
585            "a directory that is the wrong size was accepted"
586        );
587    }
588
589    #[test]
590    fn a_value_longer_than_a_chain_holds_is_refused_rather_than_truncated() {
591        // Not by building one, which would be half a gigabyte of test. The
592        // arithmetic is the thing being checked: one directory is one chunk, so
593        // a chain tops out at exactly the largest string the protocol carries.
594        assert_eq!(MAX_LEN, 512 * 1024 * 1024);
595        assert_eq!(chunks_for(MAX_LEN), FANOUT as u64);
596        assert_eq!(chunks_for(MAX_LEN + 1), FANOUT as u64 + 1);
597    }
598
599    #[test]
600    fn the_scratch_is_reused_and_does_not_grow_with_every_write() {
601        let mut m = Mem::new();
602        let mut scratch = Scratch::new();
603        let value = pattern(4 * CHUNK);
604        for _ in 0..8 {
605            write(&mut m, &value, &mut scratch).expect("written");
606        }
607        assert_eq!(
608            scratch.memory_bytes(),
609            CHUNK,
610            "the directory buffer grew, so a command path is allocating"
611        );
612    }
613
614    #[test]
615    fn what_went_in_comes_back_at_every_awkward_size() {
616        let mut m = Mem::new();
617        let mut scratch = Scratch::new();
618        for len in [
619            0,
620            1,
621            CHUNK - 1,
622            CHUNK,
623            CHUNK + 1,
624            2 * CHUNK - 1,
625            2 * CHUNK,
626            2 * CHUNK + 1,
627            3 * CHUNK + 123,
628        ] {
629            let value = pattern(len);
630            let chain = write(&mut m, &value, &mut scratch).expect("written");
631            let r = Reader::open(&m, chain).expect("opened");
632            assert_eq!(r.len(), len as u64);
633            assert_eq!(whole(&r), value, "a value of {len} bytes came back wrong");
634        }
635    }
636}