Skip to main content

acta/read/
budget.rs

1//! The memory allowance one block decode spends.
2
3use crate::error::{Error, ErrorContext, Result};
4
5/// A running allowance of decoded bytes, shared by every column of one block.
6///
7/// Every declaration a block makes about itself is small: a row count, an
8/// element count, a stream length. Decoding multiplies them, and a decoded
9/// block holds all of its columns at once, so bounding each declaration alone
10/// leaves the total unbounded. Each buffer a decode materializes is charged
11/// here before it is allocated.
12///
13/// The allowance is spent, not borrowed: releasing a buffer does not refund
14/// it. That makes one decode's cost a single number the caller can reason
15/// about instead of a peak that depends on drop order.
16#[derive(Debug)]
17pub(crate) struct Budget<'scan> {
18    remaining: u64,
19    scan: Option<&'scan mut ScanBudget>,
20}
21
22impl<'scan> Budget<'scan> {
23    pub(crate) fn new(allowance: u64) -> Self {
24        Self {
25            remaining: allowance,
26            scan: None,
27        }
28    }
29
30    pub(crate) fn with_scan(mut self, scan: &'scan mut ScanBudget) -> Self {
31        self.scan = Some(scan);
32        self
33    }
34
35    /// Charge for `count` values of `width` bytes each.
36    pub(crate) fn charge_elements(&mut self, count: usize, width: usize) -> Result<()> {
37        let bytes = count.checked_mul(width).ok_or_else(exceeded)?;
38        self.charge(bytes)
39    }
40
41    /// Charge for one buffer of `bytes` bytes.
42    pub(crate) fn charge(&mut self, bytes: usize) -> Result<()> {
43        let bytes = u64::try_from(bytes).map_err(|_| exceeded())?;
44        if self.remaining < bytes {
45            return Err(exceeded());
46        }
47        // The scan allowance is only present on the lazy scan path.
48        if let Some(scan) = self.scan.as_deref_mut() {
49            if scan.remaining_bytes < bytes {
50                return Err(scan_exceeded());
51            }
52        }
53        self.remaining -= bytes;
54        if let Some(scan) = self.scan.as_deref_mut() {
55            scan.remaining_bytes -= bytes;
56        }
57        Ok(())
58    }
59
60    /// Record one decoded stream of `bytes` stored bytes.
61    pub(crate) fn record_stream(&mut self, bytes: u64) -> Result<()> {
62        if let Some(scan) = self.scan.as_deref_mut() {
63            scan.record_stream(bytes)?;
64        }
65        Ok(())
66    }
67
68    /// Record bytes this scan read from the file that are not stream payloads:
69    /// frame envelopes, block headers, and statistics.
70    pub(crate) fn record_bytes_read(&mut self, bytes: u64) -> Result<()> {
71        if let Some(scan) = self.scan.as_deref_mut() {
72            scan.record_bytes_read(bytes)?;
73        }
74        Ok(())
75    }
76}
77
78/// Aggregate work counters for one scan. These expose only logical scan
79/// accounting; physical descriptor tables and decoder state remain private.
80///
81/// The two byte counters answer different questions and are deliberately kept
82/// apart. [`Self::stream_bytes_decoded`] is the work projection removes: the
83/// stored size of the streams the scan actually decoded. [`Self::bytes_read`]
84/// is what the scan cost the file system, and it is larger, because reading a
85/// block verifies the whole frame body against its commit trailer before any
86/// stream is decoded. Projection narrows the first number, not the second.
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub struct ScanMetrics {
89    blocks_considered: u64,
90    blocks_pruned: u64,
91    streams_decoded: u64,
92    stream_bytes_decoded: u64,
93    bytes_read: u64,
94    rows_returned: u64,
95}
96
97impl ScanMetrics {
98    /// Number of committed blocks examined by the scan planner.
99    pub fn blocks_considered(&self) -> u64 {
100        self.blocks_considered
101    }
102
103    /// Number of blocks skipped using primary bounds before stream decoding.
104    pub fn blocks_pruned(&self) -> u64 {
105        self.blocks_pruned
106    }
107
108    /// Number of physical streams successfully decoded.
109    pub fn streams_decoded(&self) -> u64 {
110        self.streams_decoded
111    }
112
113    /// Stored byte count of the streams this scan decoded.
114    ///
115    /// This is the measure projection and pruning reduce. It excludes every
116    /// byte read for framing, block headers, and statistics; for the total
117    /// cost of the scan use [`Self::bytes_read`].
118    pub fn stream_bytes_decoded(&self) -> u64 {
119        self.stream_bytes_decoded
120    }
121
122    /// Total bytes this scan read from the file.
123    ///
124    /// This counts every byte of every candidate block the scan touched: the
125    /// frame envelope streamed to verify the body checksum, the block header,
126    /// the decoded streams, and any statistics that were verified. Pruned
127    /// blocks contribute nothing. It is a byte count taken from the lengths
128    /// the reader actually reads, not an estimate from the file size.
129    pub fn bytes_read(&self) -> u64 {
130        self.bytes_read
131    }
132
133    /// Number of rows returned in yielded batches.
134    pub fn rows_returned(&self) -> u64 {
135        self.rows_returned
136    }
137}
138
139/// The cumulative allowances owned by one lazy scan.
140#[derive(Debug)]
141pub(crate) struct ScanBudget {
142    remaining_rows: u64,
143    remaining_bytes: u64,
144    metrics: ScanMetrics,
145}
146
147impl ScanBudget {
148    pub(crate) fn new(rows: u64, bytes: u64) -> Self {
149        Self {
150            remaining_rows: rows,
151            remaining_bytes: bytes,
152            metrics: ScanMetrics::default(),
153        }
154    }
155
156    pub(crate) fn charge_rows(&mut self, rows: u64) -> Result<()> {
157        self.remaining_rows = self
158            .remaining_rows
159            .checked_sub(rows)
160            .ok_or_else(scan_rows_exceeded)?;
161        Ok(())
162    }
163
164    pub(crate) fn record_block(&mut self, pruned: bool) -> Result<()> {
165        self.metrics.blocks_considered = self
166            .metrics
167            .blocks_considered
168            .checked_add(1)
169            .ok_or_else(metrics_overflow)?;
170        if pruned {
171            self.metrics.blocks_pruned = self
172                .metrics
173                .blocks_pruned
174                .checked_add(1)
175                .ok_or_else(metrics_overflow)?;
176        }
177        Ok(())
178    }
179
180    pub(crate) fn record_stream(&mut self, bytes: u64) -> Result<()> {
181        self.metrics.streams_decoded = self
182            .metrics
183            .streams_decoded
184            .checked_add(1)
185            .ok_or_else(metrics_overflow)?;
186        self.metrics.stream_bytes_decoded = self
187            .metrics
188            .stream_bytes_decoded
189            .checked_add(bytes)
190            .ok_or_else(metrics_overflow)?;
191        self.record_bytes_read(bytes)
192    }
193
194    pub(crate) fn record_bytes_read(&mut self, bytes: u64) -> Result<()> {
195        self.metrics.bytes_read = self
196            .metrics
197            .bytes_read
198            .checked_add(bytes)
199            .ok_or_else(metrics_overflow)?;
200        Ok(())
201    }
202
203    pub(crate) fn record_rows(&mut self, rows: usize) -> Result<()> {
204        let rows = u64::try_from(rows).map_err(|_| metrics_overflow())?;
205        self.metrics.rows_returned = self
206            .metrics
207            .rows_returned
208            .checked_add(rows)
209            .ok_or_else(metrics_overflow)?;
210        Ok(())
211    }
212
213    pub(crate) fn metrics(&self) -> ScanMetrics {
214        self.metrics
215    }
216}
217
218fn exceeded() -> Error {
219    Error::resource_limit(
220        "decoding this block exceeds the configured decoded-byte limit",
221        None,
222    )
223    .with_context(ErrorContext::Payload)
224}
225
226fn scan_exceeded() -> Error {
227    Error::resource_limit(
228        "the scan exceeds the configured cumulative decoded-byte limit",
229        None,
230    )
231    .with_context(ErrorContext::Payload)
232}
233
234fn scan_rows_exceeded() -> Error {
235    Error::resource_limit("the scan exceeds the configured cumulative row limit", None)
236        .with_context(ErrorContext::Payload)
237}
238
239fn metrics_overflow() -> Error {
240    Error::resource_limit("scan metrics arithmetic overflow", None)
241}
242
243#[cfg(test)]
244mod tests {
245    use super::Budget;
246    use crate::ErrorKind;
247
248    #[test]
249    fn charges_accumulate_across_calls() {
250        let mut budget = Budget::new(16);
251        budget.charge(8).expect("the first charge fits");
252
253        assert_eq!(
254            budget
255                .charge(9)
256                .expect_err("the second charge exceeds the allowance")
257                .kind(),
258            ErrorKind::ResourceLimit
259        );
260    }
261
262    #[test]
263    fn an_element_count_that_overflows_is_a_resource_limit() {
264        let mut budget = Budget::new(u64::MAX);
265
266        assert_eq!(
267            budget
268                .charge_elements(usize::MAX, 16)
269                .expect_err("the element byte count overflows")
270                .kind(),
271            ErrorKind::ResourceLimit
272        );
273    }
274}