Skip to main content

yo_format/
page.rs

1//! The log page header.
2//!
3//! `07` section 4. Thirty two bytes at the front of every segment that holds
4//! log records, and its checksum covers the header only.
5//!
6//! That last part is a decision rather than an oversight. A whole page checksum
7//! would have to be recomputed on every append, which turns an append into a
8//! read of the whole page, and the append is the commit (`06` section 3). Torn
9//! tails are found by walking records instead: the `len == 0` sentinel says
10//! where the writing stopped, and the per record trailer says whether what came
11//! before it survived.
12
13use crate::{checksum_skipping, get_u32, get_u64, put_u32, put_u64};
14use yo_common::{Code, Error, Result};
15
16/// The header, in bytes. Records start here.
17pub const PAGE_HEADER_LEN: usize = 32;
18
19/// `YOLG`, little endian, so it reads as those four characters in a hex dump.
20pub const PAGE_MAGIC: u32 = 0x594F_4C47;
21
22/// Where the checksum lives.
23const CRC_OFFSET: usize = 28;
24
25/// The header at the front of a log page.
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
27pub struct PageHeader {
28    /// Which shard owns this page. Two shards never touch each other's pages,
29    /// so this is a check rather than a lookup, and it is the check that catches
30    /// a segment allocated to two shards at once.
31    pub shard: u32,
32    /// The log address of byte zero of the payload, meaning of byte 32 of the
33    /// page. Log addresses are logically infinite, so this is what maps a
34    /// physical segment back to a position in the log.
35    pub page_addr: u64,
36    /// Bytes of the payload that have been written, header excluded.
37    pub used: u32,
38    /// Of those, how many belong to records the index no longer points at.
39    /// Compaction reads this and nothing else to decide what to do (`06`
40    /// section 5).
41    pub dead_bytes: u32,
42    /// The epoch the page was last written in.
43    pub epoch: u32,
44}
45
46impl PageHeader {
47    /// Writes the header and its checksum into the first 32 bytes of `page`.
48    ///
49    /// # Panics
50    ///
51    /// If `page` is shorter than [`PAGE_HEADER_LEN`].
52    pub fn encode(&self, page: &mut [u8]) {
53        assert!(page.len() >= PAGE_HEADER_LEN, "a page holds its own header");
54        put_u32(page, 0, PAGE_MAGIC);
55        put_u32(page, 4, self.shard);
56        put_u64(page, 8, self.page_addr);
57        put_u32(page, 16, self.used);
58        put_u32(page, 20, self.dead_bytes);
59        put_u32(page, 24, self.epoch);
60        let crc = checksum_skipping(&page[..CRC_OFFSET + 4], CRC_OFFSET);
61        put_u32(page, CRC_OFFSET, crc);
62    }
63
64    /// Reads the header back, checking the magic and the checksum.
65    pub fn decode(page: &[u8]) -> Result<PageHeader> {
66        if page.len() < PAGE_HEADER_LEN {
67            return Err(Error::new(Code::Invalid, "shorter than a page header"));
68        }
69        let magic = get_u32(page, 0);
70        if magic != PAGE_MAGIC {
71            return Err(Error::new(Code::Corrupt, "this segment is not a log page")
72                .with_detail(format!("magic={magic:#010x} want={PAGE_MAGIC:#010x}")));
73        }
74        let want = get_u32(page, CRC_OFFSET);
75        let got = checksum_skipping(&page[..CRC_OFFSET + 4], CRC_OFFSET);
76        if want != got {
77            return Err(
78                Error::new(Code::Corrupt, "log page header checksum mismatch")
79                    .with_detail(format!("stored={want:#010x} computed={got:#010x}")),
80            );
81        }
82        let h = PageHeader {
83            shard: get_u32(page, 4),
84            page_addr: get_u64(page, 8),
85            used: get_u32(page, 16),
86            dead_bytes: get_u32(page, 20),
87            epoch: get_u32(page, 24),
88        };
89        if h.dead_bytes > h.used {
90            return Err(
91                Error::new(Code::Corrupt, "more dead bytes than written bytes")
92                    .with_detail(format!("dead_bytes={} used={}", h.dead_bytes, h.used)),
93            );
94        }
95        Ok(h)
96    }
97
98    /// The fraction of this page that is dead, between 0 and 1.
99    ///
100    /// An empty page is 0 rather than a division by zero, because an empty page
101    /// is not worth compacting and that is the only question this answers.
102    #[must_use]
103    pub fn dead_fraction(&self) -> f64 {
104        if self.used == 0 {
105            return 0.0;
106        }
107        f64::from(self.dead_bytes) / f64::from(self.used)
108    }
109
110    /// Whether this page is worth compacting, at `06` section 5's threshold.
111    #[must_use]
112    pub fn wants_compaction(&self) -> bool {
113        self.dead_fraction() > 0.5
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::DEFAULT_PAGE_SIZE;
121
122    fn a_page() -> Vec<u8> {
123        let mut page = vec![0u8; DEFAULT_PAGE_SIZE as usize];
124        PageHeader {
125            shard: 3,
126            page_addr: 32 * 1024 * 1024,
127            used: 4096,
128            dead_bytes: 1024,
129            epoch: 9,
130        }
131        .encode(&mut page);
132        page
133    }
134
135    #[test]
136    fn the_magic_reads_as_yolg_in_a_hex_dump() {
137        let page = a_page();
138        assert_eq!(&page[..4], b"GLOY", "little endian, so reversed on disk");
139        assert_eq!(PAGE_MAGIC.to_be_bytes(), *b"YOLG");
140    }
141
142    #[test]
143    fn a_page_header_round_trips() {
144        let page = a_page();
145        let h = PageHeader::decode(&page).unwrap();
146        assert_eq!(h.shard, 3);
147        assert_eq!(h.page_addr, 32 * 1024 * 1024);
148        assert_eq!(h.used, 4096);
149        assert_eq!(h.dead_bytes, 1024);
150        assert_eq!(h.epoch, 9);
151    }
152
153    #[test]
154    fn every_field_lands_where_the_specification_says() {
155        let page = a_page();
156        assert_eq!(get_u32(&page, 0), PAGE_MAGIC);
157        assert_eq!(get_u32(&page, 4), 3);
158        assert_eq!(get_u64(&page, 8), 32 * 1024 * 1024);
159        assert_eq!(get_u32(&page, 16), 4096);
160        assert_eq!(get_u32(&page, 20), 1024);
161        assert_eq!(get_u32(&page, 24), 9);
162    }
163
164    #[test]
165    fn the_checksum_covers_the_header_and_stops_there() {
166        let mut page = a_page();
167        // Writing a record does not invalidate the header, which is the whole
168        // reason the checksum is scoped this way.
169        page[PAGE_HEADER_LEN] = 0xff;
170        page[9000] = 0xff;
171        assert!(PageHeader::decode(&page).is_ok());
172
173        for i in 0..PAGE_HEADER_LEN {
174            let mut bad = page.clone();
175            bad[i] ^= 0x10;
176            assert!(PageHeader::decode(&bad).is_err(), "byte {i} was not caught");
177        }
178    }
179
180    #[test]
181    fn a_segment_that_is_not_a_log_page_says_so() {
182        let mut page = a_page();
183        put_u32(&mut page, 0, 0x1234_5678);
184        let err = PageHeader::decode(&page).unwrap_err();
185        assert_eq!(err.code(), Code::Corrupt);
186        assert!(err.detail().unwrap().contains("0x12345678"));
187    }
188
189    #[test]
190    fn more_dead_than_written_is_refused() {
191        let mut page = vec![0u8; 64];
192        PageHeader {
193            used: 100,
194            dead_bytes: 200,
195            ..PageHeader::default()
196        }
197        .encode(&mut page);
198        let err = PageHeader::decode(&page).unwrap_err();
199        assert_eq!(err.code(), Code::Corrupt);
200    }
201
202    #[test]
203    fn the_compaction_trigger_is_a_dead_fraction_over_a_half() {
204        let mut h = PageHeader {
205            used: 1000,
206            ..PageHeader::default()
207        };
208        h.dead_bytes = 500;
209        assert!(!h.wants_compaction(), "exactly a half does not trigger");
210        h.dead_bytes = 501;
211        assert!(h.wants_compaction());
212
213        let empty = PageHeader::default();
214        assert_eq!(empty.dead_fraction(), 0.0);
215        assert!(!empty.wants_compaction());
216    }
217
218    #[test]
219    fn a_short_buffer_is_an_error_and_not_a_panic() {
220        assert_eq!(
221            PageHeader::decode(&[0u8; 8]).unwrap_err().code(),
222            Code::Invalid
223        );
224    }
225}