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/// So that a store can be chosen at run time rather than at compile time.
110///
111/// [`Keyspace`](crate::Keyspace) holds its tier behind this box, and the reason
112/// is that the alternative is a type parameter on `Keyspace`, which would spread
113/// to `yo-resp` and to every caller of either, all to name a type that only the
114/// code opening the file knows. The dispatch it costs is one indirect call on a
115/// path that is about to read a device, and nothing at all on a warm read, which
116/// never reaches this trait.
117impl Blocks for Box<dyn Blocks> {
118    fn put(&mut self, bytes: &[u8]) -> Result<Addr> {
119        (**self).put(bytes)
120    }
121
122    fn get(&self, at: Addr) -> Result<&[u8]> {
123        (**self).get(at)
124    }
125
126    fn bytes(&self) -> u64 {
127        (**self).bytes()
128    }
129
130    fn release(&mut self) {
131        (**self).release();
132    }
133}
134
135/// Where a value went, and how much of it there is.
136///
137/// Twelve bytes, which is what [`value::write_cold_record`](crate::value) puts
138/// in a demoted record. `at` is the single chunk when the value fits in one and
139/// the directory when it does not, and `len` is what says which.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub struct Chain {
142    /// The one chunk, or the directory.
143    pub at: Addr,
144    /// The value's length in bytes, not the chain's.
145    pub len: u64,
146}
147
148/// How many chunks a value of this length needs.
149///
150/// One for an empty value, because a chain always points at something. A zero
151/// length value that pointed at nothing would need a third case in every reader
152/// and there is nothing to gain from it.
153#[must_use]
154pub const fn chunks_for(len: u64) -> u64 {
155    if len == 0 {
156        1
157    } else {
158        len.div_ceil(CHUNK as u64)
159    }
160}
161
162/// A place to build a directory, owned by the shard and used again every time.
163///
164/// Y7 says a command path does not allocate, and a chain of the largest value
165/// Redis takes has a 64 KiB directory that has to be laid out somewhere before
166/// it is written. So the shard keeps one of these and hands it in.
167pub struct Scratch {
168    dir: Vec<u8>,
169}
170
171impl Scratch {
172    /// One directory's worth of room, allocated once.
173    #[must_use]
174    pub fn new() -> Scratch {
175        Scratch {
176            dir: Vec::with_capacity(CHUNK),
177        }
178    }
179
180    /// What it costs to keep, for the memory report.
181    #[must_use]
182    pub fn memory_bytes(&self) -> usize {
183        self.dir.capacity()
184    }
185}
186
187impl Default for Scratch {
188    fn default() -> Scratch {
189        Scratch::new()
190    }
191}
192
193/// Write a value as a chain and say where it went.
194///
195/// The chunks go down before the directory does, so a directory that is
196/// readable is a directory whose chunks are all readable. A reader that arrives
197/// after a crash either finds no directory, in which case the chunks are
198/// garbage that compaction will notice nobody points at, or finds one and can
199/// trust every address in it.
200pub fn write<B: Blocks>(blocks: &mut B, value: &[u8], scratch: &mut Scratch) -> Result<Chain> {
201    let len = value.len() as u64;
202    if len > MAX_LEN {
203        return Err(Error::fmt(
204            Code::Full,
205            format_args!("a value of {len} bytes is longer than a chain holds"),
206        ));
207    }
208
209    if value.len() <= CHUNK {
210        return Ok(Chain {
211            at: blocks.put(value)?,
212            len,
213        });
214    }
215
216    scratch.dir.clear();
217    for piece in value.chunks(CHUNK) {
218        let at = blocks.put(piece)?;
219        scratch.dir.extend_from_slice(&at.to_bits().to_le_bytes());
220    }
221    let at = blocks.put(&scratch.dir)?;
222    Ok(Chain { at, len })
223}
224
225/// A chain opened for reading.
226///
227/// Holding one means the directory has been read, so every chunk after that is
228/// a single fetch. That is the whole reason this is a type rather than a
229/// function taking a [`Chain`]: an enumeration that walks a spilled collection
230/// reads the directory once and not once per chunk.
231pub struct Reader<'a, B: Blocks> {
232    blocks: &'a B,
233    len: u64,
234    /// The directory, or the address of the only chunk when there is no
235    /// directory to have.
236    dir: Dir<'a>,
237}
238
239enum Dir<'a> {
240    One(Addr),
241    Many(&'a [u8]),
242}
243
244impl<'a, B: Blocks> Reader<'a, B> {
245    /// Read the directory, if there is one, and get ready to fetch chunks.
246    pub fn open(blocks: &'a B, chain: Chain) -> Result<Reader<'a, B>> {
247        let want = chunks_for(chain.len);
248        let dir = if want == 1 {
249            Dir::One(chain.at)
250        } else {
251            let bytes = blocks.get(chain.at)?;
252            if bytes.len() as u64 != want * 8 {
253                return Err(Error::fmt(
254                    Code::Corrupt,
255                    format_args!(
256                        "a chain of {} bytes wants {want} addresses and its directory has {}",
257                        chain.len,
258                        bytes.len() / 8
259                    ),
260                ));
261            }
262            Dir::Many(bytes)
263        };
264        Ok(Reader {
265            blocks,
266            len: chain.len,
267            dir,
268        })
269    }
270
271    /// The value's length in bytes.
272    #[must_use]
273    pub const fn len(&self) -> u64 {
274        self.len
275    }
276
277    /// Whether the value has no bytes in it, which is not the same as having no
278    /// chunks.
279    #[must_use]
280    pub const fn is_empty(&self) -> bool {
281        self.len == 0
282    }
283
284    /// How many chunks the value is in.
285    #[must_use]
286    pub const fn chunks(&self) -> u64 {
287        chunks_for(self.len)
288    }
289
290    /// Fetch chunk `i`, which is one read and never a walk.
291    pub fn chunk(&self, i: u64) -> Result<&'a [u8]> {
292        let at = match self.dir {
293            Dir::One(at) if i == 0 => at,
294            Dir::One(_) => {
295                return Err(Error::new(Code::Invalid, "there is only one chunk"));
296            }
297            Dir::Many(bytes) => {
298                let start = (i as usize)
299                    .checked_mul(8)
300                    .filter(|s| s + 8 <= bytes.len())
301                    .ok_or_else(|| Error::new(Code::Invalid, "no such chunk"))?;
302                let mut bits = [0u8; 8];
303                bits.copy_from_slice(&bytes[start..start + 8]);
304                Addr::from_bits(u64::from_le_bytes(bits))
305            }
306        };
307        if at.space() != Some(Space::Log) {
308            return Err(Error::fmt(
309                Code::Corrupt,
310                format_args!("chunk {i} is not in the log"),
311            ));
312        }
313        self.blocks.get(at)
314    }
315
316    /// Walk the pieces of `from .. to`, in order, without touching a chunk the
317    /// range does not reach.
318    ///
319    /// This is what `GETRANGE` on a spilled value wants, and what a membership
320    /// test on a spilled collection wants, and they are the same walk. A range
321    /// inside one chunk is one fetch whatever the value's size is.
322    pub fn range(&self, from: u64, to: u64) -> Pieces<'a, '_, B> {
323        let to = to.min(self.len);
324        let from = from.min(to);
325        Pieces {
326            reader: self,
327            at: from,
328            end: to,
329        }
330    }
331}
332
333/// The pieces of a byte range, one chunk at a time.
334pub struct Pieces<'a, 'r, B: Blocks> {
335    reader: &'r Reader<'a, B>,
336    at: u64,
337    end: u64,
338}
339
340impl<'a, B: Blocks> Iterator for Pieces<'a, '_, B> {
341    type Item = Result<&'a [u8]>;
342
343    fn next(&mut self) -> Option<Result<&'a [u8]>> {
344        if self.at >= self.end {
345            return None;
346        }
347        let chunk = self.at / CHUNK as u64;
348        let start = (self.at % CHUNK as u64) as usize;
349        let take = (self.end - self.at).min(CHUNK as u64 - start as u64) as usize;
350        self.at += take as u64;
351        Some(match self.reader.chunk(chunk) {
352            Ok(bytes) if start + take <= bytes.len() => Ok(&bytes[start..start + take]),
353            Ok(bytes) => Err(Error::fmt(
354                Code::Corrupt,
355                format_args!(
356                    "chunk {chunk} is {} bytes and the range wants {}",
357                    bytes.len(),
358                    start + take
359                ),
360            )),
361            Err(e) => Err(e),
362        })
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    /// A store that keeps what it is given in memory and hands back the index
371    /// as the address. Enough to exercise every path in here without a file,
372    /// and it counts its reads, which is the number the gate is about.
373    struct Mem {
374        blobs: Vec<Vec<u8>>,
375        reads: std::cell::Cell<usize>,
376    }
377
378    impl Mem {
379        fn new() -> Mem {
380            Mem {
381                blobs: Vec::new(),
382                reads: std::cell::Cell::new(0),
383            }
384        }
385
386        fn reads(&self) -> usize {
387            self.reads.get()
388        }
389    }
390
391    impl Blocks for Mem {
392        fn put(&mut self, bytes: &[u8]) -> Result<Addr> {
393            self.blobs.push(bytes.to_vec());
394            Ok(Addr::new(Space::Log, (self.blobs.len() - 1) as u64))
395        }
396
397        fn get(&self, at: Addr) -> Result<&[u8]> {
398            self.reads.set(self.reads.get() + 1);
399            self.blobs
400                .get(at.offset() as usize)
401                .map(Vec::as_slice)
402                .ok_or_else(|| Error::new(Code::NotFound, "no such block"))
403        }
404
405        fn bytes(&self) -> u64 {
406            self.blobs.iter().map(|b| b.len() as u64).sum()
407        }
408    }
409
410    fn pattern(len: usize) -> Vec<u8> {
411        (0..len).map(|i| (i % 251) as u8).collect()
412    }
413
414    fn whole<B: Blocks>(r: &Reader<'_, B>) -> Vec<u8> {
415        let mut out = Vec::new();
416        for piece in r.range(0, r.len()) {
417            out.extend_from_slice(piece.expect("a piece the value has"));
418        }
419        out
420    }
421
422    #[test]
423    fn a_value_that_fits_in_one_chunk_has_no_directory() {
424        let mut m = Mem::new();
425        let value = pattern(1000);
426        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
427        assert_eq!(
428            m.blobs.len(),
429            1,
430            "a directory was written and should not be"
431        );
432        assert_eq!(chain.len, 1000);
433
434        let r = Reader::open(&m, chain).expect("opened");
435        assert_eq!(r.chunks(), 1);
436        assert_eq!(whole(&r), value);
437        // The gate is device reads per point read, so this is the number that
438        // matters and it is one.
439        assert_eq!(
440            m.reads(),
441            1,
442            "reading a short value took more than one read"
443        );
444    }
445
446    #[test]
447    fn exactly_one_chunk_still_has_no_directory() {
448        let mut m = Mem::new();
449        let value = pattern(CHUNK);
450        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
451        assert_eq!(m.blobs.len(), 1);
452        let r = Reader::open(&m, chain).expect("opened");
453        assert_eq!(r.chunks(), 1);
454        assert_eq!(whole(&r), value);
455    }
456
457    #[test]
458    fn one_byte_more_than_a_chunk_is_two_chunks_and_a_directory() {
459        let mut m = Mem::new();
460        let value = pattern(CHUNK + 1);
461        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
462        assert_eq!(m.blobs.len(), 3, "two chunks and a directory");
463        let r = Reader::open(&m, chain).expect("opened");
464        assert_eq!(r.chunks(), 2);
465        assert_eq!(r.chunk(1).expect("the second chunk").len(), 1);
466        assert_eq!(whole(&r), value);
467    }
468
469    #[test]
470    fn an_empty_value_is_one_empty_chunk() {
471        let mut m = Mem::new();
472        let chain = write(&mut m, b"", &mut Scratch::new()).expect("written");
473        let r = Reader::open(&m, chain).expect("opened");
474        assert!(r.is_empty());
475        assert_eq!(r.chunks(), 1, "a chain always points at something");
476        assert_eq!(whole(&r), b"");
477    }
478
479    #[test]
480    fn a_range_inside_one_chunk_only_fetches_that_chunk() {
481        let mut m = Mem::new();
482        let value = pattern(10 * CHUNK);
483        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
484
485        let r = Reader::open(&m, chain).expect("opened");
486        let before = m.reads();
487        let mut got = Vec::new();
488        // Somewhere in the middle of chunk 7, which is the case that would go
489        // wrong if the walk started at the beginning.
490        let (from, to) = (7 * CHUNK as u64 + 100, 7 * CHUNK as u64 + 300);
491        for piece in r.range(from, to) {
492            got.extend_from_slice(piece.expect("a piece"));
493        }
494        assert_eq!(got, value[from as usize..to as usize]);
495        assert_eq!(
496            m.reads() - before,
497            1,
498            "a range inside one chunk of a ten chunk value should be one fetch"
499        );
500    }
501
502    #[test]
503    fn a_range_across_a_boundary_comes_back_in_two_pieces() {
504        let mut m = Mem::new();
505        let value = pattern(3 * CHUNK);
506        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
507        let r = Reader::open(&m, chain).expect("opened");
508
509        let (from, to) = (CHUNK as u64 - 5, CHUNK as u64 + 5);
510        let pieces: Vec<usize> = r
511            .range(from, to)
512            .map(|p| p.expect("a piece").len())
513            .collect();
514        assert_eq!(
515            pieces,
516            vec![5, 5],
517            "the boundary was not where it should be"
518        );
519    }
520
521    #[test]
522    fn a_range_past_the_end_stops_at_the_end() {
523        let mut m = Mem::new();
524        let value = pattern(100);
525        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
526        let r = Reader::open(&m, chain).expect("opened");
527        let mut got = Vec::new();
528        for piece in r.range(50, 1_000_000) {
529            got.extend_from_slice(piece.expect("a piece"));
530        }
531        assert_eq!(got, value[50..]);
532        assert_eq!(r.range(200, 300).count(), 0, "there is nothing out there");
533        assert_eq!(r.range(80, 20).count(), 0, "a backwards range is empty");
534    }
535
536    #[test]
537    fn every_chunk_but_the_last_is_full() {
538        let mut m = Mem::new();
539        let value = pattern(2 * CHUNK + 7);
540        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
541        let r = Reader::open(&m, chain).expect("opened");
542        assert_eq!(r.chunks(), 3);
543        assert_eq!(r.chunk(0).expect("chunk 0").len(), CHUNK);
544        assert_eq!(r.chunk(1).expect("chunk 1").len(), CHUNK);
545        assert_eq!(r.chunk(2).expect("chunk 2").len(), 7);
546        assert!(r.chunk(3).is_err(), "there is no fourth chunk");
547    }
548
549    #[test]
550    fn a_multi_chunk_value_is_two_reads_and_not_more() {
551        let mut m = Mem::new();
552        let value = pattern(5 * CHUNK);
553        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
554        let before = m.reads();
555        let r = Reader::open(&m, chain).expect("opened");
556        // Opening read the directory. Any one chunk after that is one more,
557        // whatever the value's size, which is the property the whole layout
558        // exists for.
559        assert_eq!(m.reads() - before, 1);
560        r.chunk(4).expect("the last chunk");
561        assert_eq!(m.reads() - before, 2);
562    }
563
564    #[test]
565    fn a_directory_that_does_not_match_the_length_is_refused() {
566        let mut m = Mem::new();
567        let value = pattern(2 * CHUNK);
568        let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
569        // Say the value is longer than the directory can account for, which is
570        // what a torn write or a stale address looks like from here.
571        let lying = Chain {
572            at: chain.at,
573            len: 9 * CHUNK as u64,
574        };
575        assert!(
576            Reader::open(&m, lying).is_err(),
577            "a directory that is the wrong size was accepted"
578        );
579    }
580
581    #[test]
582    fn a_value_longer_than_a_chain_holds_is_refused_rather_than_truncated() {
583        // Not by building one, which would be half a gigabyte of test. The
584        // arithmetic is the thing being checked: one directory is one chunk, so
585        // a chain tops out at exactly the largest string the protocol carries.
586        assert_eq!(MAX_LEN, 512 * 1024 * 1024);
587        assert_eq!(chunks_for(MAX_LEN), FANOUT as u64);
588        assert_eq!(chunks_for(MAX_LEN + 1), FANOUT as u64 + 1);
589    }
590
591    #[test]
592    fn the_scratch_is_reused_and_does_not_grow_with_every_write() {
593        let mut m = Mem::new();
594        let mut scratch = Scratch::new();
595        let value = pattern(4 * CHUNK);
596        for _ in 0..8 {
597            write(&mut m, &value, &mut scratch).expect("written");
598        }
599        assert_eq!(
600            scratch.memory_bytes(),
601            CHUNK,
602            "the directory buffer grew, so a command path is allocating"
603        );
604    }
605
606    #[test]
607    fn what_went_in_comes_back_at_every_awkward_size() {
608        let mut m = Mem::new();
609        let mut scratch = Scratch::new();
610        for len in [
611            0,
612            1,
613            CHUNK - 1,
614            CHUNK,
615            CHUNK + 1,
616            2 * CHUNK - 1,
617            2 * CHUNK,
618            2 * CHUNK + 1,
619            3 * CHUNK + 123,
620        ] {
621            let value = pattern(len);
622            let chain = write(&mut m, &value, &mut scratch).expect("written");
623            let r = Reader::open(&m, chain).expect("opened");
624            assert_eq!(r.len(), len as u64);
625            assert_eq!(whole(&r), value, "a value of {len} bytes came back wrong");
626        }
627    }
628}