Skip to main content

mz_compute/render/join/
linear_join.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// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Rendering of linear join plans.
11//!
12//! Consult [LinearJoinPlan] documentation for details.
13
14use std::time::{Duration, Instant};
15
16use differential_dataflow::consolidation::ConsolidatingContainerBuilder;
17use differential_dataflow::lattice::Lattice;
18use differential_dataflow::operators::arrange::arrangement::Arranged;
19use differential_dataflow::trace::cursor::{BatchCursor, BatchKey, BatchVal};
20use differential_dataflow::trace::{Cursor, Navigable, TraceReader};
21use differential_dataflow::{AsCollection, Data, VecCollection};
22use mz_compute_types::dyncfgs::{
23    ENABLE_COLUMN_PAGED_BATCHER, ENABLE_MZ_JOIN_CORE, LINEAR_JOIN_YIELDING,
24};
25use mz_compute_types::plan::join::JoinClosure;
26use mz_compute_types::plan::join::linear_join::{LinearJoinPlan, LinearStagePlan};
27use mz_dyncfg::ConfigSet;
28use mz_expr::Eval;
29use mz_repr::fixed_length::ExtendDatums;
30use mz_repr::{DatumVec, Diff, Row, RowArena, SharedRow};
31use mz_timely_util::columnar::batcher;
32use mz_timely_util::columnar::builder::ColumnBuilder;
33use mz_timely_util::columnar::{Col2ValBatcher, Col2ValPagedBatcher, columnar_exchange};
34use mz_timely_util::operator::{CollectionExt, StreamExt};
35use timely::dataflow::Scope;
36use timely::dataflow::channels::pact::{ExchangeCore, Pipeline};
37use timely::dataflow::operators::OkErr;
38
39use crate::extensions::arrange::MzArrangeCore;
40use crate::render::RenderTimestamp;
41use crate::render::context::{ArrangementFlavor, CollectionBundle, Context};
42use crate::render::errors::DataflowErrorSer;
43use crate::render::join::mz_join_core::mz_join_core;
44use crate::typedefs::{RowRowAgent, RowRowEnter};
45use mz_row_spine::{RowRowBuilder, RowRowColPagedBuilder, RowRowSpine};
46
47/// Available linear join implementations.
48///
49/// See the `mz_join_core` module docs for our rationale for providing two join implementations.
50#[derive(Clone, Copy)]
51enum LinearJoinImpl {
52    Materialize,
53    DifferentialDataflow,
54}
55
56/// Specification of how linear joins are to be executed.
57///
58/// Note that currently `yielding` only affects the `Materialize` join implementation, as the DD
59/// join doesn't allow configuring its yielding behavior. Merging [#390] would fix this.
60///
61/// [#390]: https://github.com/TimelyDataflow/differential-dataflow/pull/390
62#[derive(Clone, Copy)]
63pub struct LinearJoinSpec {
64    implementation: LinearJoinImpl,
65    yielding: YieldSpec,
66}
67
68impl Default for LinearJoinSpec {
69    fn default() -> Self {
70        Self {
71            implementation: LinearJoinImpl::Materialize,
72            yielding: Default::default(),
73        }
74    }
75}
76
77impl LinearJoinSpec {
78    /// Create a `LinearJoinSpec` based on the given config.
79    pub fn from_config(config: &ConfigSet) -> Self {
80        let implementation = if ENABLE_MZ_JOIN_CORE.get(config) {
81            LinearJoinImpl::Materialize
82        } else {
83            LinearJoinImpl::DifferentialDataflow
84        };
85
86        let yielding_raw = LINEAR_JOIN_YIELDING.get(config);
87        let yielding = YieldSpec::try_from_str(&yielding_raw).unwrap_or_else(|| {
88            tracing::error!("invalid LINEAR_JOIN_YIELDING config: {yielding_raw}");
89            YieldSpec::default()
90        });
91
92        Self {
93            implementation,
94            yielding,
95        }
96    }
97
98    /// Render a join operator according to this specification.
99    fn render<'s, T, Tr1, Tr2, L, I>(
100        &self,
101        arranged1: Arranged<'s, Tr1>,
102        arranged2: Arranged<'s, Tr2>,
103        result: L,
104    ) -> VecCollection<'s, T, I::Item, Diff>
105    where
106        T: Lattice + timely::progress::Timestamp,
107        Tr1: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
108        Tr2: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
109        BatchCursor<Tr1>: Cursor<Time = T, Diff = Diff>,
110        for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = BatchKey<'a, Tr1>, Time = T, Diff = Diff>,
111        L: FnMut(BatchKey<'_, Tr1>, BatchVal<'_, Tr1>, BatchVal<'_, Tr2>) -> I + 'static,
112        I: IntoIterator<Item: Data> + 'static,
113    {
114        use LinearJoinImpl::*;
115
116        match (
117            self.implementation,
118            self.yielding.after_work,
119            self.yielding.after_time,
120        ) {
121            (DifferentialDataflow, _, _) => arranged1.join_core(arranged2, result),
122            (Materialize, Some(work_limit), Some(time_limit)) => {
123                let yield_fn =
124                    move |start: Instant, work| work >= work_limit || start.elapsed() >= time_limit;
125                mz_join_core(arranged1, arranged2, result, yield_fn).as_collection()
126            }
127            (Materialize, Some(work_limit), None) => {
128                let yield_fn = move |_start, work| work >= work_limit;
129                mz_join_core(arranged1, arranged2, result, yield_fn).as_collection()
130            }
131            (Materialize, None, Some(time_limit)) => {
132                let yield_fn = move |start: Instant, _work| start.elapsed() >= time_limit;
133                mz_join_core(arranged1, arranged2, result, yield_fn).as_collection()
134            }
135            (Materialize, None, None) => {
136                let yield_fn = |_start, _work| false;
137                mz_join_core(arranged1, arranged2, result, yield_fn).as_collection()
138            }
139        }
140    }
141}
142
143/// Specification of a dataflow operator's yielding behavior.
144#[derive(Clone, Copy)]
145struct YieldSpec {
146    /// Yield after the given amount of work was performed.
147    after_work: Option<usize>,
148    /// Yield after the given amount of time has elapsed.
149    after_time: Option<Duration>,
150}
151
152impl Default for YieldSpec {
153    fn default() -> Self {
154        Self {
155            after_work: Some(1_000_000),
156            after_time: Some(Duration::from_millis(100)),
157        }
158    }
159}
160
161impl YieldSpec {
162    fn try_from_str(s: &str) -> Option<Self> {
163        let mut after_work = None;
164        let mut after_time = None;
165
166        let options = s.split(',').map(|o| o.trim());
167        for option in options {
168            let mut iter = option.split(':').map(|p| p.trim());
169            match std::array::from_fn(|_| iter.next()) {
170                [Some("work"), Some(amount), None] => {
171                    let amount = amount.parse().ok()?;
172                    after_work = Some(amount);
173                }
174                [Some("time"), Some(millis), None] => {
175                    let millis = millis.parse().ok()?;
176                    let duration = Duration::from_millis(millis);
177                    after_time = Some(duration);
178                }
179                _ => return None,
180            }
181        }
182
183        Some(Self {
184            after_work,
185            after_time,
186        })
187    }
188}
189
190/// Different forms the streamed data might take.
191enum JoinedFlavor<'scope, T: RenderTimestamp> {
192    /// Streamed data as a collection.
193    Collection(VecCollection<'scope, T, Row, Diff>),
194    /// A dataflow-local arrangement.
195    Local(Arranged<'scope, RowRowAgent<T, Diff>>),
196    /// An imported arrangement.
197    Trace(Arranged<'scope, RowRowEnter<mz_repr::Timestamp, Diff, T>>),
198}
199
200impl<'scope, T> Context<'scope, T>
201where
202    T: Lattice + RenderTimestamp,
203{
204    pub(crate) fn render_join(
205        &self,
206        inputs: Vec<CollectionBundle<'scope, T>>,
207        linear_plan: LinearJoinPlan,
208    ) -> CollectionBundle<'scope, T> {
209        self.scope.clone().region_named("Join(Linear)", |inner| {
210            self.render_join_inner(inputs, linear_plan, inner)
211        })
212    }
213
214    fn render_join_inner(
215        &self,
216        inputs: Vec<CollectionBundle<'scope, T>>,
217        linear_plan: LinearJoinPlan,
218        inner: Scope<'_, T>,
219    ) -> CollectionBundle<'scope, T> {
220        // Collect all error streams, and concatenate them at the end.
221        let mut errors = Vec::new();
222
223        // Determine which form our maintained spine of updates will initially take.
224        // First, just check out the availability of an appropriate arrangement.
225        // This will be `None` in the degenerate single-input join case, which ensures
226        // that we do not panic if we never go around the `stage_plans` loop.
227        let arrangement = linear_plan
228            .stage_plans
229            .get(0)
230            .and_then(|stage| inputs[linear_plan.source_relation].arrangement(&stage.stream_key));
231        // We can use an arrangement if it exists and an initial closure does not.
232        let mut joined = match (arrangement, linear_plan.initial_closure) {
233            (Some(ArrangementFlavor::Local(oks, errs)), None) => {
234                errors.push(errs.as_collection(|k, _v| k.clone()).enter_region(inner));
235                JoinedFlavor::Local(oks.enter_region(inner))
236            }
237            (Some(ArrangementFlavor::Trace(_gid, oks, errs)), None) => {
238                errors.push(errs.as_collection(|k, _v| k.clone()).enter_region(inner));
239                JoinedFlavor::Trace(oks.enter_region(inner))
240            }
241            (_, initial_closure) => {
242                // TODO: extract closure from the first stage in the join plan, should it exist.
243                // TODO: apply that closure in `flat_map_ref` rather than calling `.collection`.
244                let (joined, errs) = inputs[linear_plan.source_relation]
245                    .as_specific_collection(linear_plan.source_key.as_deref(), &self.config_set);
246                errors.push(errs.enter_region(inner));
247                let mut joined = joined.enter_region(inner);
248
249                // In the current code this should always be `None`, but we have this here should
250                // we change that and want to know what we should be doing.
251                if let Some(closure) = initial_closure {
252                    // If there is no starting arrangement, then we can run filters
253                    // directly on the starting collection.
254                    // If there is only one input, we are done joining, so run filters
255                    let name = "LinearJoinInitialization";
256                    type CB<C> = ConsolidatingContainerBuilder<C>;
257                    let (j, errs) = joined.flat_map_fallible::<CB<_>, CB<_>, _, _, _, _>(name, {
258                        // Reuseable allocation for unpacking.
259                        let mut datums = DatumVec::new();
260                        move |row| {
261                            let mut row_builder = SharedRow::get();
262                            let temp_storage = RowArena::new();
263                            let mut datums_local = datums.borrow_with(&row);
264                            // TODO(mcsherry): re-use `row` allocation.
265                            closure
266                                .apply(&mut datums_local, &temp_storage, &mut row_builder)
267                                .map(|row| row.cloned())
268                                .map_err(DataflowErrorSer::from)
269                                .transpose()
270                        }
271                    });
272                    joined = j;
273                    errors.push(errs);
274                }
275
276                JoinedFlavor::Collection(joined)
277            }
278        };
279
280        // progress through stages, updating partial results and errors.
281        for stage_plan in linear_plan.stage_plans.into_iter() {
282            // Different variants of `joined` implement this differently,
283            // and the logic is centralized there.
284            let stream = self.differential_join(
285                joined,
286                inputs[stage_plan.lookup_relation].enter_region(inner),
287                stage_plan,
288                &mut errors,
289            );
290            // Update joined results and capture any errors.
291            joined = JoinedFlavor::Collection(stream);
292        }
293
294        // We have completed the join building, but may have work remaining.
295        // For example, we may have expressions not pushed down (e.g. literals)
296        // and projections that could not be applied (e.g. column repetition).
297        let bundle = if let JoinedFlavor::Collection(mut joined) = joined {
298            if let Some(closure) = linear_plan.final_closure {
299                let name = "LinearJoinFinalization";
300                type CB<C> = ConsolidatingContainerBuilder<C>;
301                let (updates, errs) = joined.flat_map_fallible::<CB<_>, CB<_>, _, _, _, _>(name, {
302                    // Reuseable allocation for unpacking.
303                    let mut datums = DatumVec::new();
304                    move |row| {
305                        let mut row_builder = SharedRow::get();
306                        let temp_storage = RowArena::new();
307                        let mut datums_local = datums.borrow_with(&row);
308                        // TODO(mcsherry): re-use `row` allocation.
309                        closure
310                            .apply(&mut datums_local, &temp_storage, &mut row_builder)
311                            .map(|row| row.cloned())
312                            .map_err(DataflowErrorSer::from)
313                            .transpose()
314                    }
315                });
316
317                joined = updates;
318                errors.push(errs);
319            }
320
321            // Return joined results and all produced errors collected together.
322            CollectionBundle::from_collections(
323                joined,
324                differential_dataflow::collection::concatenate(inner, errors),
325            )
326        } else {
327            panic!("Unexpectedly arranged join output");
328        };
329        bundle.leave_region(self.scope)
330    }
331
332    /// Looks up the arrangement for the next input and joins it to the arranged
333    /// version of the join of previous inputs.
334    fn differential_join<'s>(
335        &self,
336        mut joined: JoinedFlavor<'s, T>,
337        lookup_relation: CollectionBundle<'s, T>,
338        LinearStagePlan {
339            stream_key,
340            stream_thinning,
341            lookup_key,
342            closure,
343            lookup_relation: _,
344        }: LinearStagePlan,
345        errors: &mut Vec<VecCollection<'s, T, DataflowErrorSer, Diff>>,
346    ) -> VecCollection<'s, T, Row, Diff> {
347        // If we have only a streamed collection, we must first form an arrangement.
348        if let JoinedFlavor::Collection(stream) = joined {
349            let name = "LinearJoinKeyPreparation";
350            let (keyed, errs) = stream
351                .inner
352                .unary_fallible::<ColumnBuilder<((Row, Row), T, Diff)>, _, _, _>(
353                    Pipeline,
354                    name,
355                    |_, _| {
356                        Box::new(move |input, ok, errs| {
357                            let mut temp_storage = RowArena::new();
358                            let mut key_buf = Row::default();
359                            let mut val_buf = Row::default();
360                            let mut datums = DatumVec::new();
361                            input.for_each(|time, data| {
362                                let mut ok_session = ok.session_with_builder(&time);
363                                let mut err_session = errs.session(&time);
364                                for (row, time, diff) in data.iter() {
365                                    temp_storage.clear();
366                                    let datums_local = datums.borrow_with(row);
367                                    let datums = stream_key
368                                        .iter()
369                                        .map(|e| e.eval(&datums_local, &temp_storage));
370                                    let result = key_buf.packer().try_extend(datums);
371                                    match result {
372                                        Ok(()) => {
373                                            val_buf.packer().extend(
374                                                stream_thinning.iter().map(|e| datums_local[*e]),
375                                            );
376                                            ok_session.give(((&key_buf, &val_buf), time, diff));
377                                        }
378                                        Err(e) => {
379                                            err_session.give((e.into(), time.clone(), *diff));
380                                        }
381                                    }
382                                }
383                            });
384                        })
385                    },
386                );
387
388            errors.push(errs.as_collection());
389
390            let exchange = ExchangeCore::<ColumnBuilder<_>, _>::new_core(
391                columnar_exchange::<Row, Row, T, Diff>,
392            );
393            let arranged = if ENABLE_COLUMN_PAGED_BATCHER.get(&self.config_set) {
394                keyed.mz_arrange_core::<
395                    _,
396                    batcher::ColumnChunker<_>,
397                    Col2ValPagedBatcher<_, _, _, _>,
398                    RowRowColPagedBuilder<_, _>,
399                    RowRowSpine<_, _>,
400                >(exchange, "JoinStage")
401            } else {
402                keyed.mz_arrange_core::<
403                    _,
404                    batcher::Chunker<_>,
405                    Col2ValBatcher<_, _, _, _>,
406                    RowRowBuilder<_, _>,
407                    RowRowSpine<_, _>,
408                >(exchange, "JoinStage")
409            };
410            joined = JoinedFlavor::Local(arranged);
411        }
412
413        // Demultiplex the four different cross products of arrangement types we might have.
414        let arrangement = lookup_relation
415            .arrangement(&lookup_key[..])
416            .expect("Arrangement absent despite explicit construction");
417
418        match joined {
419            JoinedFlavor::Collection(_) => {
420                unreachable!("JoinedFlavor::VecCollection variant avoided at top of method");
421            }
422            JoinedFlavor::Local(local) => match arrangement {
423                ArrangementFlavor::Local(oks, errs1) => {
424                    let (oks, errs2) = self
425                        .differential_join_inner::<RowRowAgent<_, _>, RowRowAgent<_, _>>(
426                            local, oks, closure,
427                        );
428
429                    errors.push(errs1.as_collection(|k, _v| k.clone()));
430                    errors.extend(errs2);
431                    oks
432                }
433                ArrangementFlavor::Trace(_gid, oks, errs1) => {
434                    let (oks, errs2) = self
435                        .differential_join_inner::<RowRowAgent<_, _>, RowRowEnter<_, _, _>>(
436                            local, oks, closure,
437                        );
438
439                    errors.push(errs1.as_collection(|k, _v| k.clone()));
440                    errors.extend(errs2);
441                    oks
442                }
443            },
444            JoinedFlavor::Trace(trace) => match arrangement {
445                ArrangementFlavor::Local(oks, errs1) => {
446                    let (oks, errs2) = self
447                        .differential_join_inner::<RowRowEnter<_, _, _>, RowRowAgent<_, _>>(
448                            trace, oks, closure,
449                        );
450
451                    errors.push(errs1.as_collection(|k, _v| k.clone()));
452                    errors.extend(errs2);
453                    oks
454                }
455                ArrangementFlavor::Trace(_gid, oks, errs1) => {
456                    let (oks, errs2) = self
457                        .differential_join_inner::<RowRowEnter<_, _, _>, RowRowEnter<_, _, _>>(
458                            trace, oks, closure,
459                        );
460
461                    errors.push(errs1.as_collection(|k, _v| k.clone()));
462                    errors.extend(errs2);
463                    oks
464                }
465            },
466        }
467    }
468
469    /// Joins the arrangement for `next_input` to the arranged version of the
470    /// join of previous inputs. This is split into its own method to enable
471    /// reuse of code with different types of `next_input`.
472    ///
473    /// The return type includes an optional error collection, which may be
474    /// `None` if we can determine that `closure` cannot error.
475    fn differential_join_inner<'s, Tr1, Tr2>(
476        &self,
477        prev_keyed: Arranged<'s, Tr1>,
478        next_input: Arranged<'s, Tr2>,
479        closure: JoinClosure,
480    ) -> (
481        VecCollection<'s, T, Row, Diff>,
482        Option<VecCollection<'s, T, DataflowErrorSer, Diff>>,
483    )
484    where
485        Tr1: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
486        Tr2: TraceReader<Batch: Navigable, Time = T> + Clone + 'static,
487        for<'a> BatchCursor<Tr1>:
488            Cursor<Key<'a>: ExtendDatums, Val<'a>: ExtendDatums, Time = T, Diff = Diff>,
489        for<'a> BatchCursor<Tr2>:
490            Cursor<Key<'a> = BatchKey<'a, Tr1>, Val<'a>: ExtendDatums, Time = T, Diff = Diff>,
491    {
492        // Reuseable allocation for unpacking.
493        let mut datums = DatumVec::new();
494
495        if closure.could_error() {
496            let (oks, err) = self
497                .linear_join_spec
498                .render(prev_keyed, next_input, move |key, old, new| {
499                    let mut row_builder = SharedRow::get();
500                    let temp_storage = RowArena::new();
501
502                    let mut datums_local = datums.borrow();
503                    key.extend_datums(&temp_storage, &mut datums_local, None);
504                    old.extend_datums(&temp_storage, &mut datums_local, None);
505                    new.extend_datums(&temp_storage, &mut datums_local, None);
506
507                    closure
508                        .apply(&mut datums_local, &temp_storage, &mut row_builder)
509                        .map(|row| row.cloned())
510                        .map_err(DataflowErrorSer::from)
511                        .transpose()
512                })
513                .inner
514                .ok_err(|(x, t, d)| {
515                    // TODO(mcsherry): consider `ok_err()` for `Collection`.
516                    match x {
517                        Ok(x) => Ok((x, t, d)),
518                        Err(x) => Err((x, t, d)),
519                    }
520                });
521
522            (oks.as_collection(), Some(err.as_collection()))
523        } else {
524            let oks = self
525                .linear_join_spec
526                .render(prev_keyed, next_input, move |key, old, new| {
527                    let mut row_builder = SharedRow::get();
528                    let temp_storage = RowArena::new();
529
530                    let mut datums_local = datums.borrow();
531                    key.extend_datums(&temp_storage, &mut datums_local, None);
532                    old.extend_datums(&temp_storage, &mut datums_local, None);
533                    new.extend_datums(&temp_storage, &mut datums_local, None);
534
535                    closure
536                        .apply(&mut datums_local, &temp_storage, &mut row_builder)
537                        .expect("Closure claimed to never error")
538                        .cloned()
539                });
540
541            (oks, None)
542        }
543    }
544}