Skip to main content

mz_compute/compute_state/
peek_scan.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5
6//! An index peek's walk over the two traces that answer it, as one suspendable object.
7//!
8//! A [`PeekScan`] owns both cursors, the rows it has accumulated, and the accounting that bounds
9//! them, and it spends a single budget across both phases. It performs no IO and never awaits, so
10//! the same scan runs wherever its driver puts it, and a driver that stops it between two cursor
11//! positions picks it up again without repeating work.
12
13use std::mem;
14use std::num::{NonZeroI64, NonZeroUsize};
15use std::time::{Duration, Instant};
16
17use differential_dataflow::trace::cursor::BatchCursor;
18use differential_dataflow::trace::implementations::BatchContainer;
19use differential_dataflow::trace::{Cursor, Navigable, TraceReader};
20use mz_compute_client::protocol::command::Peek;
21use mz_compute_client::protocol::response::{PeekError, PeekResponse};
22use mz_expr::row::RowCollection;
23use mz_expr::{ColumnOrder, RowComparator};
24use mz_ore::cast::CastFrom;
25use mz_ore::soft_panic_or_log;
26use mz_repr::fixed_length::ExtendDatums;
27use mz_repr::{Diff, GlobalId, Row, Timestamp};
28use timely::order::PartialOrder;
29
30use crate::compute_state::error_scan::{ErrorScan, ErrorScanStep, ErrsHandle};
31use crate::compute_state::peek_result_iterator::{PeekResultIterator, Step};
32
33/// The scan an index peek builds, over the ok trace of the arrangement that answers it.
34pub(super) type IndexPeekScan = PeekScan<
35    crate::arrangement::manager::PaddedTrace<crate::typedefs::RowRowAgent<Timestamp, Diff>>,
36>;
37
38/// Rows a scan hands to its driver, in the order the scan produced them.
39///
40/// The form [`PeekResultIterator`] yields and the form the peek stash carries, so the path that
41/// moves large volumes never converts.
42pub(super) type RowBatch = Vec<(Row, NonZeroI64)>;
43
44/// Builds the peek's answer out of the rows a completed walk produced, sorted by `order_by`.
45pub(super) fn rows_response(rows: RowBatch, order_by: &[ColumnOrder]) -> PeekResponse {
46    let rows = rows
47        .into_iter()
48        .map(|(row, copies)| {
49            let copies = NonZeroUsize::try_from(copies).expect("fits into usize");
50            (row, copies)
51        })
52        .collect();
53    PeekResponse::Rows(vec![RowCollection::new(rows, order_by)])
54}
55
56/// The byte size of a row's count, as an answer built from a [`RowBatch`] stores it.
57pub(super) const COUNT_BYTE_SIZE: usize = size_of::<NonZeroUsize>();
58
59/// The byte size of a row's offset into the answer's packed row data.
60const OFFSET_BYTE_SIZE: usize = size_of::<usize>();
61
62/// The bytes one `(row, count)` entry contributes to the answer it ends up in.
63///
64/// This is `RowCollection::byte_len` per entry, the ruler `max_result_size` is applied with
65/// wherever a result is measured against it: the row's packed data, its offset into that data,
66/// and its count. The `Row` struct's own bytes are not part of it, because no answer carries them,
67/// and charging them measures a narrow row at up to twice what the client receives.
68pub(super) fn entry_byte_len(row: &Row) -> usize {
69    row.data_len()
70        .saturating_add(OFFSET_BYTE_SIZE)
71        .saturating_add(COUNT_BYTE_SIZE)
72}
73
74/// What a walk has spent, in the phases the peek metrics report.
75///
76/// Every number is cumulative over the slices the walk was cut into, wherever those slices ran.
77#[derive(Clone, Copy, Debug)]
78pub(super) struct WalkPhases {
79    /// Worker time the error walk spent.
80    pub error_scan: Duration,
81    /// Worker time spent opening the ok cursor.
82    pub cursor_setup: Duration,
83    /// Whether the error walk ended without finding an error. The two numbers above describe a
84    /// finished phase only when it did.
85    pub error_trace_clean: bool,
86    /// Worker time the ok walk spent, including the time thinning spent.
87    pub row_iteration: Duration,
88    /// Cursor positions the ok walk evaluated.
89    pub rows_processed: usize,
90    /// Worker time thinning spent.
91    pub thinning: Duration,
92    /// Rows handed to thinning, summed over the times it ran.
93    pub rows_thinned: usize,
94}
95
96/// How a scan's rows may leave for the peek stash.
97#[derive(Clone, Copy, Debug)]
98pub(super) struct StashBounds {
99    /// Whether the peek may divert its rows to the stash at all.
100    pub eligible: bool,
101    /// The accumulated size past which the first batch is handed over, which is where the answer
102    /// stops being an inline one.
103    pub threshold_bytes: usize,
104    /// The accumulated size past which every later batch is handed over. A batch is never smaller
105    /// than the threshold, so a value below it changes nothing.
106    pub batch_bytes: usize,
107}
108
109/// The outcome of a fueled [`PeekScan::step`].
110#[derive(Clone, Debug, PartialEq)]
111pub(super) enum ScanOutcome {
112    /// Stopped with work left, because the budget ran out or because the accumulated rows have
113    /// grown into a full batch.
114    ///
115    /// The scan retains what it accumulated. A driver that can write rows collects them through
116    /// [`PeekScan::take_batch`], and one that cannot is never handed rows it would have to drop.
117    ///
118    /// A driver must take every batch it is offered. A scan holding one makes no progress when
119    /// stepped, so a driver that steps without taking spins forever.
120    Suspended,
121    /// The walk is over. `Ok` carries the rows accumulated since the last batch was taken, which
122    /// together with the batches already taken are the peek's answer. `Err` is the peek's answer
123    /// instead, and the scan has dropped the rows it had accumulated, since they are part of no
124    /// answer.
125    Finished(Result<RowBatch, PeekError>),
126}
127
128/// The state of a [`PeekScan`]'s walk over its error trace.
129///
130/// Both ended states drop the walk, so a peek pins error batches only while it reads them.
131enum ErrorPhase {
132    /// The walk is under way, and resumes from the cursor position it stopped on.
133    Scanning(ErrorScan),
134    /// The error trace holds no error at the peek's timestamp, which is the only way to the ok
135    /// trace. The rows the walk examined have been handed to the ok walk.
136    Clean,
137    /// The error trace answered the peek. The answer is the scan's latched outcome.
138    Failed,
139}
140
141/// An index peek's walk over its error trace and its ok trace.
142///
143/// The walk suspends between any two cursor positions. Both phases spend one budget: the ok walk
144/// gets what the error walk leaves.
145///
146/// A stash-eligible scan retains at most the threshold before its first batch and the batch size
147/// after, plus the row that crossed either. A scan that cannot use the stash fills no batch, and
148/// `max_result_size` alone bounds its prefix.
149pub(super) struct PeekScan<Tr>
150where
151    Tr: TraceReader<Batch: Navigable>,
152{
153    /// The time at which the error trace is read.
154    peek_timestamp: Timestamp,
155    /// The collection the peek reads, for logging.
156    target_id: GlobalId,
157    error_phase: ErrorPhase,
158    /// The walk over the ok trace, reached only once the error walk reports the error trace
159    /// clean. Its cursor is opened with the scan, and nothing advances it before then.
160    oks: PeekResultIterator<Tr>,
161    /// The outcome the scan ended with, or `None` while it can still be stepped. The `Ok` carries
162    /// no rows: those left with the [`ScanOutcome::Finished`] that reported the end.
163    ended: Option<ScanOutcome>,
164    /// Rows accumulated since the last batch was taken.
165    results: RowBatch,
166    /// The byte size of `results`, as an answer built from them would store them.
167    total_size: usize,
168    /// The rows the answer holds so far, batches already handed to a driver included, counted in
169    /// copies because that is what the finishing's limit counts. Thinning takes its drops back off.
170    answer_rows: u64,
171    /// The ceiling on what the scan may hold, above which the peek fails.
172    max_result_size: usize,
173    stash: StashBounds,
174    /// Whether a batch has been handed over, which is where the answer stopped being an inline
175    /// one and the batch size took over from the threshold.
176    stash_bound: bool,
177    /// A bound on the rows the peek's finishing needs, `limit + offset`.
178    ///
179    /// Further limiting happens when the results are collected, so the scan does not have to hold
180    /// exactly this many rows, just at least those that would have been returned.
181    max_results: Option<usize>,
182    /// Orders the rows that thinning keeps. `None` when the finishing imposes no ordering, in
183    /// which case the walk ends at the limit rather than thinning at all.
184    comparator: Option<RowComparator>,
185    /// Worker time the error walk spent, summed over the slices it was cut into.
186    pub(super) error_scan_time: Duration,
187    /// Worker time spent opening the ok cursor.
188    pub(super) cursor_setup_time: Duration,
189    /// Worker time the ok walk spent, summed over the slices it was cut into. Includes the time
190    /// thinning spent.
191    pub(super) row_iteration_time: Duration,
192    /// Worker time thinning spent, summed over the times it ran.
193    pub(super) thinning_time: Duration,
194    /// Rows handed to thinning, summed over the times it ran.
195    pub(super) rows_thinned: usize,
196}
197
198impl<Tr> PeekScan<Tr>
199where
200    Tr: TraceReader<Batch: Navigable>,
201    for<'a> BatchCursor<Tr>: Cursor<
202            Key<'a>: ExtendDatums + Eq,
203            KeyContainer: BatchContainer<Owned = Row>,
204            Val<'a>: ExtendDatums,
205            TimeGat<'a>: PartialOrder<Timestamp>,
206            DiffGat<'a> = &'a Diff,
207        >,
208{
209    /// Opens a scan of `peek` over the traces that answer it.
210    ///
211    /// Both cursors are opened here, so that the scan holds everything it reads and needs neither
212    /// trace handle again. The walks start without a row-iteration limit. The limit in effect is
213    /// the caller's to supply to each [`PeekScan::step`].
214    pub(super) fn new(
215        peek: &Peek,
216        errs_handle: &mut ErrsHandle,
217        oks_handle: &mut Tr,
218        max_result_size: u64,
219        stash: StashBounds,
220    ) -> Self {
221        let error_scan = ErrorScan::new(errs_handle);
222        let error_scan_time = error_scan.scan_time;
223
224        let cursor_setup_start = Instant::now();
225        // The literal constraints are cloned rather than moved out of the peek, which outlives
226        // this scan.
227        let oks = PeekResultIterator::new(
228            peek.target.id(),
229            peek.map_filter_project.clone(),
230            peek.timestamp,
231            peek.literal_constraints.clone().as_deref_mut(),
232            oks_handle,
233            None,
234            0,
235        );
236        let cursor_setup_time = cursor_setup_start.elapsed();
237
238        let comparator = (!peek.finishing.order_by.is_empty())
239            .then(|| RowComparator::new(peek.finishing.order_by.clone()));
240
241        Self {
242            peek_timestamp: peek.timestamp,
243            target_id: peek.target.id(),
244            error_phase: ErrorPhase::Scanning(error_scan),
245            oks,
246            ended: None,
247            results: Vec::new(),
248            total_size: 0,
249            answer_rows: 0,
250            max_result_size: usize::cast_from(max_result_size),
251            stash,
252            stash_bound: false,
253            max_results: peek.finishing.num_rows_needed(),
254            comparator,
255            error_scan_time,
256            cursor_setup_time,
257            row_iteration_time: Duration::ZERO,
258            thinning_time: Duration::ZERO,
259            rows_thinned: 0,
260        }
261    }
262
263    /// Advances the scan until it has an answer for the peek, the accumulated rows make a full
264    /// batch, or `fuel` runs out, whichever comes first. Decrements `fuel` by the number of cursor
265    /// positions visited, in either phase.
266    ///
267    /// `row_iteration_limit` is the limit in effect now rather than at the scan's start, and the
268    /// count it bounds spans both phases.
269    ///
270    /// [`ScanOutcome::Suspended`] is not an end of scan: stepping again resumes where this call
271    /// stopped. [`ScanOutcome::Finished`] is, and stepping past one is a defect in the driver.
272    pub(super) fn step(
273        &mut self,
274        row_iteration_limit: Option<usize>,
275        fuel: &mut usize,
276    ) -> ScanOutcome {
277        // The repeat is not the answer this scan gave: the rows left with the first `Finished`.
278        // Reporting it rather than panicking keeps a driver that loses track of its scan from
279        // taking the replica with it.
280        if let Some(ended) = &self.ended {
281            soft_panic_or_log!("index peek scan stepped after it ended");
282            return ended.clone();
283        }
284
285        let outcome = match self.step_error_phase(row_iteration_limit, fuel) {
286            Some(outcome) => outcome,
287            None => self.step_ok_phase(row_iteration_limit, fuel),
288        };
289
290        // Latched here rather than in the arms of either walk, so every way the scan can end
291        // passes one place.
292        match &outcome {
293            ScanOutcome::Suspended => {}
294            ScanOutcome::Finished(Ok(_)) => {
295                self.ended = Some(ScanOutcome::Finished(Ok(RowBatch::new())));
296            }
297            ScanOutcome::Finished(Err(error)) => {
298                self.ended = Some(ScanOutcome::Finished(Err(error.clone())));
299            }
300        }
301
302        outcome
303    }
304
305    /// Takes the accumulated rows once they have crossed the stash threshold.
306    ///
307    /// Returns `None` while they have not, and for a peek that cannot use the stash at all, so a
308    /// driver with nowhere to write rows is never handed any.
309    pub(super) fn take_batch(&mut self) -> Option<RowBatch> {
310        if !self.batch_ready() {
311            return None;
312        }
313        self.stash_bound = true;
314        Some(self.take_results())
315    }
316
317    /// The number of cursor positions the ok walk has evaluated.
318    pub(super) fn rows_processed(&self) -> usize {
319        self.oks.rows_processed()
320    }
321
322    /// What the walk has spent so far, in the phases the peek metrics report.
323    pub(super) fn phases(&self) -> WalkPhases {
324        WalkPhases {
325            error_scan: self.error_scan_time,
326            cursor_setup: self.cursor_setup_time,
327            error_trace_clean: self.error_trace_clean(),
328            row_iteration: self.row_iteration_time,
329            rows_processed: self.rows_processed(),
330            thinning: self.thinning_time,
331            rows_thinned: self.rows_thinned,
332        }
333    }
334
335    /// Whether the walk over the error trace has ended without finding an error, which is the only
336    /// way the ok walk runs at all.
337    ///
338    /// False while that walk is under way, and false once it has answered the peek.
339    pub(super) fn error_trace_clean(&self) -> bool {
340        matches!(self.error_phase, ErrorPhase::Clean)
341    }
342
343    /// Whether this scan may divert rows to the peek stash, and so whether it ever fills a batch.
344    ///
345    /// A driver reads this rather than deciding eligibility again, so it cannot end up holding a
346    /// batch it has nowhere to write. A scan holding an untaken batch makes no progress.
347    pub(super) fn stash_eligible(&self) -> bool {
348        self.stash.eligible
349    }
350
351    /// Whether the accumulated rows have grown past what this peek may answer with inline, which
352    /// is when [`PeekScan::take_batch`] hands them over.
353    ///
354    /// A scan whose batch is ready stays where it stands until the batch is taken, so this is also
355    /// whether stepping the scan again can make progress.
356    pub(super) fn batch_ready(&self) -> bool {
357        let cut = if self.stash_bound {
358            self.stash.threshold_bytes.max(self.stash.batch_bytes)
359        } else {
360            self.stash.threshold_bytes
361        };
362        self.stash.eligible && self.total_size > cut
363    }
364
365    /// Takes the accumulated rows and the size accounted to them.
366    ///
367    /// Every path that hands rows out goes through here, so `total_size` stays an account of
368    /// `results`.
369    fn take_results(&mut self) -> RowBatch {
370        self.total_size = 0;
371        mem::take(&mut self.results)
372    }
373
374    /// Fails the peek with `error`, dropping the rows the scan had accumulated, so that
375    /// [`PeekScan::take_batch`] never hands a driver the prefix of an answer that will not be
376    /// given.
377    fn fail(&mut self, error: PeekError) -> ScanOutcome {
378        let _dropped = self.take_results();
379        ScanOutcome::Finished(Err(error))
380    }
381
382    /// Advances the walk over the error trace.
383    ///
384    /// Returns `None` once the error trace is known to hold no error at the peek's timestamp,
385    /// which is the only way to the ok trace.
386    fn step_error_phase(
387        &mut self,
388        row_iteration_limit: Option<usize>,
389        fuel: &mut usize,
390    ) -> Option<ScanOutcome> {
391        let scan = match &mut self.error_phase {
392            ErrorPhase::Scanning(scan) => scan,
393            // `step` reports an ended scan before it reaches here, so a failed phase is never
394            // seen here.
395            ErrorPhase::Clean | ErrorPhase::Failed => return None,
396        };
397
398        // The limit bounds the peek, not the call, so a walk already under way adopts the limit
399        // that is in effect now rather than the one that was in effect when it started.
400        scan.set_row_iteration_limit(row_iteration_limit);
401        let outcome = scan.step(self.peek_timestamp, self.target_id, fuel);
402        self.error_scan_time = scan.scan_time;
403
404        match outcome {
405            ErrorScanStep::Finished(Ok(rows_iterated)) => {
406                // The rows the error walk examined count against the peek's limit, so the ok walk
407                // continues that count. Runs once per scan, since `Clean` never steps the walk.
408                self.oks.add_rows_iterated(rows_iterated);
409                self.error_phase = ErrorPhase::Clean;
410                None
411            }
412            ErrorScanStep::Finished(Err(error)) => {
413                self.error_phase = ErrorPhase::Failed;
414                Some(self.fail(error))
415            }
416            ErrorScanStep::OutOfFuel => Some(ScanOutcome::Suspended),
417        }
418    }
419
420    /// Whether the answer holds every row the peek's finishing can use.
421    ///
422    /// Only ever true without an ordering: an ordered finishing ranks rows against the whole
423    /// trace, so no prefix of the walk satisfies it.
424    fn finishing_satisfied(&self) -> bool {
425        self.comparator.is_none()
426            && self
427                .max_results
428                .is_some_and(|max_results| self.answer_rows >= u64::cast_from(max_results))
429    }
430
431    /// Advances the walk over the ok trace, accumulating the rows it produces.
432    fn step_ok_phase(
433        &mut self,
434        row_iteration_limit: Option<usize>,
435        fuel: &mut usize,
436    ) -> ScanOutcome {
437        // Ahead of the batch guard, so a scan whose last batch completed the answer ends here
438        // rather than walking one more row into a batch of its own.
439        if self.finishing_satisfied() {
440            return ScanOutcome::Finished(Ok(self.take_results()));
441        }
442
443        // A scan holding a full batch stays where it is until the batch is taken, so the bound on
444        // what one scan retains is the scan's own rather than a rule each driver keeps. Past the
445        // stash threshold the result-size ceiling no longer bounds that growth either.
446        if self.batch_ready() {
447            return ScanOutcome::Suspended;
448        }
449
450        self.oks.set_row_iteration_limit(row_iteration_limit);
451
452        let row_iteration_start = Instant::now();
453
454        let outcome = loop {
455            let (row, copies) = match self.oks.step(fuel) {
456                Step::Row(Ok(row)) => row,
457                Step::Row(Err(error)) => break self.fail(error),
458                Step::Done => break ScanOutcome::Finished(Ok(self.take_results())),
459                Step::OutOfFuel => break ScanOutcome::Suspended,
460            };
461
462            self.total_size = self.total_size.saturating_add(entry_byte_len(&row));
463            let batch_ready = self.batch_ready();
464
465            // Rows bound for the stash are answered by a handle rather than by themselves, so the
466            // ceiling on an inline answer does not apply to a prefix that has grown past the
467            // stash threshold, nor to a scan whose first batch has already left: what that scan
468            // retains is bounded by the batch size, which may sit above the ceiling.
469            if !self.stash_bound && !batch_ready && self.total_size > self.max_result_size {
470                break self.fail(PeekError::ResultExceedsMaxSize {
471                    max_result_size: self.max_result_size,
472                });
473            }
474
475            // Positive here: the walk errors on a negative multiplicity rather than yielding it.
476            self.answer_rows = self.answer_rows.saturating_add(copies.get().unsigned_abs());
477            self.results.push((row, copies));
478
479            // Ahead of thinning, so that a row which both fills a batch and completes a thinned
480            // answer leaves the peek to the stash rather than answering it from the prefix.
481            if batch_ready {
482                break ScanOutcome::Suspended;
483            }
484
485            if self.finishing_satisfied() {
486                break ScanOutcome::Finished(Ok(self.take_results()));
487            }
488
489            if let Some(outcome) = self.thin() {
490                break outcome;
491            }
492        };
493
494        self.row_iteration_time += row_iteration_start.elapsed();
495
496        outcome
497    }
498
499    /// Thins the accumulated rows down to the ones an ordered finishing ranks first, once the scan
500    /// holds many more than it needs.
501    ///
502    /// Does nothing without an ordering: such a scan ends at [`PeekScan::finishing_satisfied`]
503    /// instead of accumulating past its limit.
504    fn thin(&mut self) -> Option<ScanOutcome> {
505        let max_results = self.max_results?;
506        let Some(comparator) = &self.comparator else {
507            return None;
508        };
509
510        // We use a threshold twice what we intend, to amortize the work across all of the
511        // insertions. We could tighten this, but it works for the moment.
512        //
513        // `max_results` is `limit + offset`, so a `LIMIT` near `i64::MAX` makes the doubling
514        // overflow. We then hold fewer rows than the threshold no matter what, and never thin.
515        // That is the right answer: such a peek cannot accumulate that many rows anyway, the
516        // result size limit stops it long before. Wrapping instead would make the threshold tiny,
517        // and we would thin while holding almost nothing, dropping rows past the end of the
518        // buffer.
519        let thin = max_results
520            .checked_mul(2)
521            .is_some_and(|threshold| self.results.len() >= threshold);
522        if !thin {
523            return None;
524        }
525
526        // Partitioned rather than sorted, because only which rows fall outside the first
527        // `max_results` matters here. The order among those that stay is established once, when
528        // the answer is collected.
529        //
530        // Partitioning is unstable, and entries carry counts, so which entry survives a tie
531        // across the cut changes the retained multiset. The client cannot tell: tied rows are
532        // byte-identical, since the tiebreaker compares the whole encoded row, and the
533        // `max_results` entries kept expand to at least `max_results` rows, of which the finishing
534        // reads `offset..offset + limit`.
535        //
536        // NOTE: a peek result must not be consumed without applying the finishing's limit.
537        //
538        // TODO: Had we left these as `Vec<Datum>` we would avoid the unpacking; we should consider
539        // doing that, although it will require a re-pivot of the code to branch on this inner test
540        // (as we prefer not to maintain `Vec<Datum>` in the other case).
541        let thinning_start = Instant::now();
542        self.rows_thinned = self.rows_thinned.saturating_add(self.results.len());
543        self.results
544            .select_nth_unstable_by(max_results, |left, right| {
545                comparator.compare_rows(&left.0, &right.0, || left.0.cmp(&right.0))
546            });
547        self.thinning_time += thinning_start.elapsed();
548
549        let dropped = self.results.drain(max_results..);
550        let (dropped_size, dropped_rows) = dropped.into_iter().fold(
551            (0usize, 0u64),
552            |(size, rows), (row, count): (Row, NonZeroI64)| {
553                (
554                    size.saturating_add(entry_byte_len(&row)),
555                    rows.saturating_add(count.get().unsigned_abs()),
556                )
557            },
558        );
559        self.total_size = self.total_size.saturating_sub(dropped_size);
560        self.answer_rows = self.answer_rows.saturating_sub(dropped_rows);
561
562        None
563    }
564}
565
566#[cfg(test)]
567mod tests;