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}