Skip to main content

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;