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};
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;