Skip to main content

common_cache/
page.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Fixed-size byte page layout for immutable cached content.
16
17use std::io;
18use std::num::NonZeroU64;
19use std::ops::Range;
20
21/// Fixed-size logical byte pages within one immutable file.
22#[derive(Debug, Clone, Copy)]
23pub struct PageLayout {
24    page_size: NonZeroU64,
25    file_size: u64,
26}
27
28impl PageLayout {
29    /// Creates a layout. The final page is clipped to the file size.
30    pub fn new(page_size: NonZeroU64, file_size: u64) -> Self {
31        Self {
32            page_size,
33            file_size,
34        }
35    }
36
37    /// Returns the half-open page-ID range intersecting a byte request.
38    ///
39    /// Requests are clipped at EOF. Empty or wholly out-of-file requests return
40    /// an empty range; reversed ranges are rejected. This does not allocate one
41    /// entry per page, even for large files.
42    pub fn page_ids(&self, request: Range<u64>) -> io::Result<Range<u64>> {
43        validate_range(&request)?;
44        let start = request.start.min(self.file_size);
45        let end = request.end.min(self.file_size);
46        let first = start / self.page_size.get();
47        if start == end {
48            return Ok(first..first);
49        }
50        // end > 0 here. Unlike (end + page_size - 1) / page_size,
51        // this expression cannot overflow at the end of a large file.
52        let last = (end - 1) / self.page_size.get();
53        Ok(first..last + 1)
54    }
55
56    /// Returns a page's file byte range, or `None` for a page outside the file.
57    pub fn page_range(&self, page_id: u64) -> Option<Range<u64>> {
58        let start = page_id.checked_mul(self.page_size.get())?;
59        if start >= self.file_size {
60            return None;
61        }
62        let len = self.page_size.get().min(self.file_size - start);
63        Some(start..start + len)
64    }
65
66    /// Returns the requested slice relative to a page's buffer.
67    ///
68    /// Returns `None` if the page and request do not intersect. The caller can
69    /// borrow this slice for an immediate response; caching a slice of a much
70    /// larger I/O buffer may retain that entire allocation and needs a copy.
71    pub fn request_slice(
72        &self,
73        page_id: u64,
74        request: Range<u64>,
75    ) -> io::Result<Option<Range<usize>>> {
76        validate_range(&request)?;
77        let Some(page) = self.page_range(page_id) else {
78            return Ok(None);
79        };
80        let start = request.start.max(page.start);
81        let end = request.end.min(page.end);
82        if start >= end {
83            return Ok(None);
84        }
85        Ok(Some(
86            to_usize(start - page.start)?..to_usize(end - page.start)?,
87        ))
88    }
89}
90
91fn validate_range(range: &Range<u64>) -> io::Result<()> {
92    if range.start > range.end {
93        return Err(invalid_input("Byte range start exceeds end"));
94    }
95    Ok(())
96}
97
98fn to_usize(value: u64) -> io::Result<usize> {
99    usize::try_from(value).map_err(|_| invalid_input("Byte offset exceeds addressable memory"))
100}
101
102fn invalid_input(message: &'static str) -> io::Error {
103    io::Error::new(io::ErrorKind::InvalidInput, message)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn nonzero(value: u64) -> NonZeroU64 {
111        NonZeroU64::new(value).unwrap()
112    }
113
114    #[test]
115    fn test_page_alignment_and_eof() {
116        let layout = PageLayout::new(nonzero(64), 150);
117        for (request, expected) in [
118            (0..0, 0..0),
119            (0..64, 0..1),
120            (32..96, 0..2),
121            (64..128, 1..2),
122            (128..200, 2..3),
123            (150..200, 2..2),
124        ] {
125            assert_eq!(layout.page_ids(request).unwrap(), expected);
126        }
127        assert_eq!(layout.page_range(2), Some(128..150));
128        assert_eq!(layout.page_range(3), None);
129        assert_eq!(layout.request_slice(0, 32..96).unwrap(), Some(32..64));
130        assert_eq!(layout.request_slice(1, 32..96).unwrap(), Some(0..32));
131        assert_eq!(layout.request_slice(2, 128..200).unwrap(), Some(0..22));
132        assert_eq!(layout.request_slice(1, 0..64).unwrap(), None);
133        assert_eq!(PageLayout::new(nonzero(64), 0).page_range(0), None);
134        assert!(layout.page_ids(Range { start: 2, end: 1 }).is_err());
135        assert!(layout.request_slice(0, Range { start: 2, end: 1 }).is_err());
136    }
137
138    #[test]
139    fn test_large_file_boundaries() {
140        let layout = PageLayout::new(nonzero(1), u64::MAX);
141        assert_eq!(layout.page_ids(0..u64::MAX).unwrap(), 0..u64::MAX);
142        assert_eq!(
143            layout.page_range(u64::MAX - 1),
144            Some(u64::MAX - 1..u64::MAX)
145        );
146        assert_eq!(layout.page_range(u64::MAX), None);
147        let layout = PageLayout::new(nonzero(64), u64::MAX);
148        assert_eq!(layout.page_range(u64::MAX), None);
149        let last = u64::MAX / 64;
150        assert_eq!(layout.page_range(last), Some(last * 64..u64::MAX));
151        assert_eq!(
152            layout.page_ids(u64::MAX - 1..u64::MAX).unwrap(),
153            last..last + 1
154        );
155    }
156}