mz_compute/compute_state/peek_result_iterator.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//! Code for extracting a peek result out of compute state/an arrangement.
7
8use std::iter::FusedIterator;
9use std::num::NonZeroI64;
10use std::ops::Range;
11
12use differential_dataflow::trace::cursor::{BatchCursor, BatchKey, CursorList};
13use differential_dataflow::trace::implementations::BatchContainer;
14use differential_dataflow::trace::{Cursor, Navigable, TraceReader};
15use mz_compute_client::protocol::response::PeekError;
16use mz_repr::fixed_length::ExtendDatums;
17use mz_repr::{DatumVec, Diff, GlobalId, Row, RowArena};
18use timely::order::PartialOrder;
19
20use crate::compute_state::PeekRowIterationTracker;
21
22/// The merged cursor a [`TraceReader::cursor`] hands out over all of a trace's batches: a
23/// [`CursorList`] over the per-batch cursors.
24pub(super) type TraceCursor<Tr> = CursorList<BatchCursor<Tr>>;
25/// Backing storage for a [`TraceCursor`]: the batches the cursor borrows from.
26pub(super) type TraceStorage<Tr> = Vec<<Tr as TraceReader>::Batch>;
27
28pub(super) struct PeekResultIterator<Tr>
29where
30 Tr: TraceReader<Batch: Navigable>,
31{
32 // For debug/trace logging.
33 target_id: GlobalId,
34 cursor: TraceCursor<Tr>,
35 storage: TraceStorage<Tr>,
36 map_filter_project: mz_expr::SafeMfpPlan,
37 peek_timestamp: mz_repr::Timestamp,
38 row_builder: Row,
39 datum_vec: DatumVec,
40 literals: Option<Literals<Tr>>,
41 rows_processed: usize,
42 row_iteration_tracker: PeekRowIterationTracker,
43 exhausted: bool,
44}
45
46/// Helper to handle literals in peeks
47struct Literals<Tr: TraceReader<Batch: Navigable>> {
48 /// The literals in a container, sorted by `Ord`.
49 literals: <BatchCursor<Tr> as Cursor>::KeyContainer,
50 /// The range of the literals that are still available.
51 range: Range<usize>,
52 /// Where the cursor sits relative to the literal list.
53 position: LiteralPosition,
54}
55
56/// Where a [`Literals`]' cursor sits relative to the literal list.
57///
58/// `Seeking` and `Exhausted` stay distinct: the former still owes rows, the latter is the end of
59/// the scan. Collapsing them would empty out every literal-constrained peek.
60enum LiteralPosition {
61 /// A seek is outstanding, either not started or suspended part-way through the literal list.
62 /// The cursor is parked on a key no literal has claimed, so no row may be read until the
63 /// seek completes. [`Literals::range`] holds the literals left to try.
64 Seeking,
65 /// The cursor sits on the key of the literal at this index.
66 At(usize),
67 /// Every literal has been tried. The scan is done.
68 Exhausted,
69}
70
71/// The outcome of a fueled [`Literals::seek_next_literal_key`].
72enum SeekOutcome {
73 /// The seek finished: the cursor sits on a matching literal, or the literals are exhausted.
74 Complete,
75 /// Fuel ran out with literals left to try. Seeking again resumes at the next untried
76 /// literal.
77 OutOfFuel,
78}
79
80impl<Tr> Literals<Tr>
81where
82 Tr: TraceReader<Batch: Navigable>,
83 BatchCursor<Tr>: Cursor<KeyContainer: BatchContainer<Owned: Ord>>,
84{
85 /// Construct a new `Literals` from a mutable slice of literals. Sorts contents.
86 ///
87 /// The literals must be distinct. A repeated literal seeks to the same key twice and
88 /// returns its rows twice, since `seek_key` seeks forward only. `MirRelationExpr` literal
89 /// constraints are deduplicated by the optimizer (`mz_transform::literal_constraints`).
90 ///
91 /// Does not seek the trace cursor. The initial seek runs on the first fueled step instead, so
92 /// its cost is charged to a budget instead of paid before any budget exists.
93 fn new(
94 literals: &mut [<<BatchCursor<Tr> as Cursor>::KeyContainer as BatchContainer>::Owned],
95 ) -> Self {
96 // We have to sort the literal constraints because cursor.seek_key can
97 // seek only forward.
98 literals.sort();
99 let mut container =
100 <BatchCursor<Tr> as Cursor>::KeyContainer::with_capacity(literals.len());
101 for constraint in literals {
102 container.push_own(constraint)
103 }
104 let range = 0..container.len();
105 Self {
106 literals: container,
107 range,
108 position: LiteralPosition::Seeking,
109 }
110 }
111
112 /// Returns the current literal, if the cursor sits on one.
113 ///
114 /// Returns `None` while a seek is outstanding and once the literals are exhausted. In
115 /// neither case does the cursor point at a row that belongs to a literal.
116 fn peek(&self) -> Option<BatchKey<'_, Tr>> {
117 match self.position {
118 LiteralPosition::At(index) => self.literals.get(index),
119 LiteralPosition::Seeking | LiteralPosition::Exhausted => None,
120 }
121 }
122
123 /// Returns `true` if a seek has to run before the cursor sits on a matching literal.
124 fn seek_pending(&self) -> bool {
125 matches!(self.position, LiteralPosition::Seeking)
126 }
127
128 /// Returns `true` if there are no more literals to process.
129 fn is_exhausted(&self) -> bool {
130 matches!(self.position, LiteralPosition::Exhausted)
131 }
132
133 /// Seeks the cursor to the next key of a matching literal, if any, charging one unit of
134 /// `fuel` per `seek_key` call.
135 ///
136 /// Returns [`SeekOutcome::OutOfFuel`] if literals remain untried when the fuel runs out.
137 /// The walk resumes from that literal on the next call, so a caller must not treat a
138 /// suspended seek as an end of scan.
139 ///
140 /// A literal list whose entries are mostly absent from the trace costs one seek per absent
141 /// literal, so the walk is fueled instead of run to completion.
142 fn seek_next_literal_key(
143 &mut self,
144 cursor: &mut TraceCursor<Tr>,
145 storage: &TraceStorage<Tr>,
146 fuel: &mut usize,
147 ) -> SeekOutcome {
148 // Until a literal matches, the cursor is parked on a key no literal claims. Recording
149 // that keeps a suspended seek from being read as "sitting on the previous literal".
150 self.position = LiteralPosition::Seeking;
151 while !self.range.is_empty() {
152 if *fuel == 0 {
153 return SeekOutcome::OutOfFuel;
154 }
155 *fuel -= 1;
156 let index = self.range.next().expect("range is not empty");
157 let literal = self.literals.get(index).expect("index out of bounds");
158 cursor.seek_key(storage, literal);
159 if cursor.get_key(storage).map_or(true, |key| key == literal) {
160 self.position = LiteralPosition::At(index);
161 return SeekOutcome::Complete;
162 }
163 // The cursor landed on a record that has a different key,
164 // meaning that there is no record whose key would match the
165 // current literal.
166 }
167 self.position = LiteralPosition::Exhausted;
168 SeekOutcome::Complete
169 }
170}
171
172/// An [Iterator] that extracts a peek result from a [TraceReader].
173///
174/// The iterator will apply a given `MapFilterProject` and obey literal
175/// constraints, if any.
176impl<Tr> PeekResultIterator<Tr>
177where
178 Tr: TraceReader<Batch: Navigable>,
179 for<'a> BatchCursor<Tr>: Cursor<
180 Key<'a>: ExtendDatums + Eq,
181 KeyContainer: BatchContainer<Owned = Row>,
182 Val<'a>: ExtendDatums,
183 TimeGat<'a>: PartialOrder<mz_repr::Timestamp>,
184 DiffGat<'a> = &'a Diff,
185 >,
186{
187 pub(super) fn new(
188 target_id: GlobalId,
189 map_filter_project: mz_expr::SafeMfpPlan,
190 peek_timestamp: mz_repr::Timestamp,
191 literal_constraints: Option<&mut [Row]>,
192 trace_reader: &mut Tr,
193 row_iteration_limit: Option<usize>,
194 rows_iterated: usize,
195 ) -> Self {
196 let (cursor, storage) = trace_reader.cursor();
197 Self::from_cursor(
198 target_id,
199 map_filter_project,
200 peek_timestamp,
201 literal_constraints,
202 cursor,
203 storage,
204 row_iteration_limit,
205 rows_iterated,
206 )
207 }
208
209 /// Builds an iterator over an already-opened cursor.
210 pub(super) fn from_cursor(
211 target_id: GlobalId,
212 map_filter_project: mz_expr::SafeMfpPlan,
213 peek_timestamp: mz_repr::Timestamp,
214 literal_constraints: Option<&mut [Row]>,
215 cursor: TraceCursor<Tr>,
216 storage: TraceStorage<Tr>,
217 row_iteration_limit: Option<usize>,
218 rows_iterated: usize,
219 ) -> Self {
220 let literals = literal_constraints.map(Literals::new);
221
222 Self {
223 target_id,
224 cursor,
225 storage,
226 map_filter_project,
227 peek_timestamp,
228 row_builder: Row::default(),
229 datum_vec: DatumVec::new(),
230 literals,
231 rows_processed: 0,
232 row_iteration_tracker: PeekRowIterationTracker::new(row_iteration_limit, rows_iterated),
233 exhausted: false,
234 }
235 }
236
237 /// Returns the number of rows evaluated by the iterator.
238 pub fn rows_processed(&self) -> usize {
239 self.rows_processed
240 }
241
242 /// Adopts the row-iteration limit that is in effect, without forgetting the rows the walk has
243 /// already examined.
244 pub(super) fn set_row_iteration_limit(&mut self, limit: Option<usize>) {
245 self.row_iteration_tracker.set_limit(limit);
246 }
247
248 /// Adopts the rows a walk that ran before this one examined, so that the row-iteration limit
249 /// bounds the peek rather than either walk alone.
250 pub(super) fn add_rows_iterated(&mut self, rows_iterated: usize) {
251 self.row_iteration_tracker.add_rows_iterated(rows_iterated);
252 }
253
254 /// Returns `true` if the iterator has no more literals to process, or if there are no literals at all.
255 fn literals_exhausted(&self) -> bool {
256 self.literals.as_ref().map_or(false, Literals::is_exhausted)
257 }
258}
259
260impl<Tr> FusedIterator for PeekResultIterator<Tr>
261where
262 Tr: TraceReader<Batch: Navigable>,
263 for<'a> BatchCursor<Tr>: Cursor<
264 Key<'a>: ExtendDatums + Eq,
265 KeyContainer: BatchContainer<Owned = Row>,
266 Val<'a>: ExtendDatums,
267 TimeGat<'a>: PartialOrder<mz_repr::Timestamp>,
268 DiffGat<'a> = &'a Diff,
269 >,
270{
271}
272
273impl<Tr> Iterator for PeekResultIterator<Tr>
274where
275 Tr: TraceReader<Batch: Navigable>,
276 for<'a> BatchCursor<Tr>: Cursor<
277 Key<'a>: ExtendDatums + Eq,
278 KeyContainer: BatchContainer<Owned = Row>,
279 Val<'a>: ExtendDatums,
280 TimeGat<'a>: PartialOrder<mz_repr::Timestamp>,
281 DiffGat<'a> = &'a Diff,
282 >,
283{
284 type Item = Result<(Row, NonZeroI64), PeekError>;
285
286 fn next(&mut self) -> Option<Self::Item> {
287 let mut fuel = usize::MAX;
288 match self.step(&mut fuel) {
289 Step::Row(row) => Some(row),
290 Step::Done => None,
291 Step::OutOfFuel => unreachable!("stepped with unbounded fuel"),
292 }
293 }
294}
295
296/// The outcome of a single fueled [`PeekResultIterator::step`].
297pub enum Step {
298 /// A result row, or the error that ends the scan.
299 ///
300 /// An error is the peek's whole answer, so the iterator latches shut on one and a caller that
301 /// steps again gets [`Step::Done`] rather than the next value or the same error again.
302 Row(Result<(Row, NonZeroI64), PeekError>),
303 /// The cursor is exhausted, or an error already ended the scan. Further steps also return
304 /// `Done`, and cost no fuel.
305 Done,
306 /// The budget is spent. Whether the scan has work left is not implied: a walk whose last
307 /// position was rejected by the `map_filter_project`, or whose last literal seek landed
308 /// past the end of the trace, spends its budget and returns [`Step::Done`] on the next
309 /// call.
310 ///
311 /// The iterator resumes exactly where it stopped. The cursor itself may sit on an
312 /// arbitrary intermediate key if a literal seek was suspended, because resumption is
313 /// driven by `Literals::range` rather than by cursor position.
314 OutOfFuel,
315}
316
317/// The outcome of a [`PeekResultIterator::step_key`].
318enum KeyStep {
319 /// The cursor sits on a new key, which has at least one value.
320 Advanced,
321 /// No key remains.
322 Exhausted,
323 /// The fuel ran out inside the literal seek. The seek resumes at the next untried literal.
324 OutOfFuel,
325}
326
327impl<Tr> PeekResultIterator<Tr>
328where
329 Tr: TraceReader<Batch: Navigable>,
330 for<'a> BatchCursor<Tr>: Cursor<
331 Key<'a>: ExtendDatums + Eq,
332 KeyContainer: BatchContainer<Owned = Row>,
333 Val<'a>: ExtendDatums,
334 TimeGat<'a>: PartialOrder<mz_repr::Timestamp>,
335 DiffGat<'a> = &'a Diff,
336 >,
337{
338 /// Advances the cursor until it produces a row, the cursor is exhausted,
339 /// or `fuel` runs out, whichever comes first. Decrements `fuel` by the
340 /// number of cursor positions visited, a literal seek's `seek_key` calls
341 /// included.
342 ///
343 /// Fuel is charged per cursor position, not per row returned, so a
344 /// selective `map_filter_project` cannot starve the caller of yield
345 /// points. The charge does not depend on how the walk is sliced: the same
346 /// walk costs the same total whether it runs in one call or in
347 /// single-unit steps.
348 ///
349 /// A zero budget makes no progress and returns [`Step::OutOfFuel`], so a
350 /// caller that derives the budget from a configuration has to floor it at
351 /// one to avoid rescheduling the same peek forever.
352 pub fn step(&mut self, fuel: &mut usize) -> Step {
353 if self.exhausted {
354 return Step::Done;
355 }
356
357 let result = loop {
358 // Every advance that can suspend runs before the per-position charge below, so a
359 // slice that suspends has paid only for work it kept. Charging first would buy a
360 // position the suspended advance never reached, and the resumed call would buy the
361 // same position again, so a sliced walk would cost more than an unsliced one.
362 if let Some(literals) = &mut self.literals
363 && literals.seek_pending()
364 {
365 match literals.seek_next_literal_key(&mut self.cursor, &self.storage, fuel) {
366 SeekOutcome::Complete => {}
367 // The seek stopped part-way through the literal list: the cursor is at a
368 // valid intermediate position and no row came out of it. `Done` would drop
369 // the rows of the literals not yet tried, and there is no row to hand back,
370 // so report the budget. The literal list position is retained, so stepping
371 // again resumes with the next untried literal.
372 SeekOutcome::OutOfFuel => return Step::OutOfFuel,
373 }
374 }
375
376 if self.literals_exhausted() {
377 return Step::Done;
378 }
379
380 if !self.cursor.key_valid(&self.storage) {
381 return Step::Done;
382 }
383
384 if !self.cursor.val_valid(&self.storage) {
385 match self.step_key(fuel) {
386 KeyStep::Advanced => {}
387 KeyStep::Exhausted => return Step::Done,
388 KeyStep::OutOfFuel => return Step::OutOfFuel,
389 }
390 }
391
392 if *fuel == 0 {
393 return Step::OutOfFuel;
394 }
395 *fuel -= 1;
396
397 // Filtered and zero-multiplicity rows still consume worker time, so
398 // they count against the budget before evaluation.
399 //
400 // Latches for the same reason the tail below does, and does it here because this
401 // error returns without passing through it.
402 if let Err(error) = self.row_iteration_tracker.track_next() {
403 self.exhausted = true;
404 return Step::Row(Err(error));
405 }
406
407 self.rows_processed = self.rows_processed.saturating_add(1);
408 match self.extract_current_row() {
409 Ok(Some(row)) => break Ok(row),
410 Ok(None) => {
411 // Have to keep stepping and try with the next val.
412 self.cursor.step_val(&self.storage);
413 }
414 Err(err) => break Err(err),
415 }
416 };
417
418 if result.is_err() {
419 // The peek is answered with this error, so the values after it are not part of any
420 // answer. Latching leaves the cursor where it stands and reports the end to a caller
421 // that steps again, rather than resuming the walk or repeating the error forever.
422 self.exhausted = true;
423 } else {
424 self.cursor.step_val(&self.storage);
425 }
426
427 Step::Row(result)
428 }
429
430 /// Extracts and returns the row currently pointed at by our cursor. Returns
431 /// `Ok(None)` if our MapFilterProject evaluates to `None`. Also returns any
432 /// errors that arise from evaluating the MapFilterProject.
433 fn extract_current_row(&mut self) -> Result<Option<(Row, NonZeroI64)>, PeekError> {
434 // TODO: This arena could be maintained and reused for longer,
435 // but it wasn't clear at what interval we should flush
436 // it to ensure we don't accidentally spike our memory use.
437 // This choice is conservative, and not the end of the world
438 // from a performance perspective.
439 let arena = RowArena::new();
440
441 let key_item = self.cursor.key(&self.storage);
442 let row_item = self.cursor.val(&self.storage);
443
444 // An optional literal that we might have added to the borrow. Needs to be declared
445 // before the borrow to ensure correct drop order.
446 let maybe_literal;
447 let mut borrow = self.datum_vec.borrow();
448 key_item.extend_datums(&arena, &mut borrow, None);
449 row_item.extend_datums(&arena, &mut borrow, None);
450
451 if let Some(literals) = &mut self.literals {
452 // The peek was created from an IndexedFilter join. We have to add those columns
453 // here that the join would add in a dataflow.
454 //
455 // `step` reaches this only with a completed seek and literals left, so the cursor
456 // sits on a matching literal. Reading a `None` as "no literal to add" would leave
457 // the datum vec one column short and apply the MFP at the wrong arity.
458 maybe_literal = literals
459 .peek()
460 .expect("literal position must be at a matching literal during row extraction");
461 maybe_literal.extend_datums(&arena, &mut borrow, None);
462 }
463 if let Some(result) = self
464 .map_filter_project
465 .evaluate_into(&mut borrow, &arena, &mut self.row_builder)
466 .map(|row| row.cloned())
467 .map_err(PeekError::from)?
468 {
469 let mut copies = Diff::ZERO;
470 self.cursor.map_times(&self.storage, |time, diff| {
471 if time.less_equal(&self.peek_timestamp) {
472 copies += diff;
473 }
474 });
475 let copies: i64 = if copies.is_negative() {
476 let row = &*borrow;
477 tracing::error!(
478 target = %self.target_id, diff = %copies, ?row,
479 "index peek encountered negative multiplicities in ok trace",
480 );
481 return Err(PeekError::unstructured(format!(
482 "Invalid data in source, \
483 saw retractions ({}) for row that does not exist: {:?}",
484 -copies, row,
485 )));
486 } else {
487 copies.into_inner()
488 };
489 // if copies > 0 ... otherwise skip
490 if let Some(copies) = NonZeroI64::new(copies) {
491 Ok(Some((result, copies)))
492 } else {
493 Ok(None)
494 }
495 } else {
496 Ok(None)
497 }
498 }
499
500 /// Steps the key forward, respecting literal constraints and charging `fuel` for the
501 /// literal seek.
502 fn step_key(&mut self, fuel: &mut usize) -> KeyStep {
503 assert!(
504 !self.cursor.val_valid(&self.storage),
505 "must only step key when the vals for a key are exhausted"
506 );
507
508 if let Some(literals) = &mut self.literals {
509 match literals.seek_next_literal_key(&mut self.cursor, &self.storage, fuel) {
510 SeekOutcome::Complete => {}
511 SeekOutcome::OutOfFuel => return KeyStep::OutOfFuel,
512 }
513
514 if literals.is_exhausted() {
515 return KeyStep::Exhausted;
516 }
517 } else {
518 self.cursor.step_key(&self.storage);
519 }
520
521 if !self.cursor.key_valid(&self.storage) {
522 // We're exhausted!
523 return KeyStep::Exhausted;
524 }
525
526 assert!(
527 self.cursor.val_valid(&self.storage),
528 "there must always be at least one val per key"
529 );
530
531 KeyStep::Advanced
532 }
533}
534
535#[cfg(test)]
536mod tests;