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