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;