Skip to main content

mz_timely_util/
operator.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Common operator transformations on timely streams and differential collections.
17
18use std::hash::{BuildHasher, Hash, Hasher};
19
20use columnation::Columnation;
21use differential_dataflow::batcher::merge::{Extractor, MergeBatcher, MergeLadder};
22use differential_dataflow::batcher::{Batcher, Extract};
23use differential_dataflow::consolidation::ConsolidatingContainerBuilder;
24use differential_dataflow::difference::{Multiply, Semigroup};
25use differential_dataflow::lattice::Lattice;
26use differential_dataflow::logging::Logger;
27use differential_dataflow::{AsCollection, Collection, Hashable, VecCollection};
28use timely::container::{DrainContainer, PushInto};
29use timely::dataflow::channels::pact::{Exchange, ParallelizationContract, Pipeline};
30use timely::dataflow::operators::Capability;
31use timely::dataflow::operators::generic::builder_rc::{
32    OperatorBuilder as OperatorBuilderRc, OperatorBuilder,
33};
34use timely::dataflow::operators::generic::operator::{self, Operator};
35use timely::dataflow::operators::generic::{
36    InputHandleCore, OperatorInfo, OutputBuilder, OutputBuilderSession,
37};
38use timely::dataflow::{Scope, Stream, StreamVec};
39use timely::progress::frontier::AntichainRef;
40use timely::progress::operate::FrontierInterest;
41use timely::progress::{Antichain, Timestamp};
42use timely::{Container, ContainerBuilder, PartialOrder};
43
44use crate::columnation::ColumnationStack;
45
46/// Extension methods for timely [`Stream`]s.
47pub trait StreamExt<'scope, T, C1>
48where
49    T: Timestamp,
50    C1: Container + DrainContainer + Clone + 'static,
51{
52    /// Like `timely::dataflow::operators::generic::operator::Operator::unary`,
53    /// but the logic function can handle failures.
54    ///
55    /// Creates a new dataflow operator that partitions its input stream by a
56    /// parallelization strategy `pact` and repeatedly invokes `logic`, the
57    /// function returned by the function passed as `constructor`. The `logic`
58    /// function can read to the input stream and write to either of two output
59    /// streams, where the first output stream represents successful
60    /// computations and the second output stream represents failed
61    /// computations.
62    fn unary_fallible<DCB, ECB, B, P>(
63        self,
64        pact: P,
65        name: &str,
66        constructor: B,
67    ) -> (
68        Stream<'scope, T, DCB::Container>,
69        Stream<'scope, T, ECB::Container>,
70    )
71    where
72        DCB: ContainerBuilder,
73        ECB: ContainerBuilder,
74        B: FnOnce(
75            Capability<T>,
76            OperatorInfo,
77        ) -> Box<
78            dyn FnMut(
79                    &mut InputHandleCore<T, C1, P::Puller>,
80                    &mut OutputBuilderSession<'_, T, DCB>,
81                    &mut OutputBuilderSession<'_, T, ECB>,
82                ) + 'static,
83        >,
84        P: ParallelizationContract<T, C1>;
85
86    /// Like [`timely::dataflow::operators::vec::Map::flat_map`], but `logic`
87    /// is allowed to fail. The first returned stream will contain the
88    /// successful applications of `logic`, while the second returned stream
89    /// will contain the failed applications.
90    fn flat_map_fallible<DCB, ECB, D2, E, I, L>(
91        self,
92        name: &str,
93        logic: L,
94    ) -> (
95        Stream<'scope, T, DCB::Container>,
96        Stream<'scope, T, ECB::Container>,
97    )
98    where
99        DCB: ContainerBuilder + PushInto<D2>,
100        ECB: ContainerBuilder + PushInto<E>,
101        I: IntoIterator<Item = Result<D2, E>>,
102        L: for<'a> FnMut(C1::Item<'a>) -> I + 'static;
103
104    /// Routes each record to one of two output streams by a per-record predicate.
105    ///
106    /// Records for which `predicate` returns `true` go to the first output and all
107    /// others to the second. Both outputs are sent under the input capability. That
108    /// capability is only a lower bound on the times of the records it carries, so a
109    /// split that depends on a record's time must inspect the record, and cannot be
110    /// decided once per container.
111    fn partition_by<CB, L>(
112        self,
113        name: &str,
114        predicate: L,
115    ) -> (
116        Stream<'scope, T, CB::Container>,
117        Stream<'scope, T, CB::Container>,
118    )
119    where
120        CB: ContainerBuilder + for<'a> PushInto<C1::Item<'a>>,
121        L: for<'a> FnMut(&C1::Item<'a>) -> bool + 'static;
122
123    /// Block progress of the frontier at `expiration` time
124    fn expire_stream_at(self, name: &str, expiration: T) -> Stream<'scope, T, C1>;
125}
126
127/// Extension methods for differential [`Collection`]s.
128pub trait CollectionExt<'scope, T, D1, R>: Sized
129where
130    T: Timestamp,
131    R: Semigroup,
132{
133    /// Creates a new empty collection in `scope`.
134    fn empty(scope: Scope<'scope, T>) -> VecCollection<'scope, T, D1, R>;
135
136    /// Like [`Collection::map`], but `logic` is allowed to fail. The first
137    /// returned collection will contain successful applications of `logic`,
138    /// while the second returned collection will contain the failed
139    /// applications.
140    ///
141    /// Callers need to specify the following type parameters:
142    /// * `DCB`: The container builder for the `Ok` output.
143    /// * `ECB`: The container builder for the `Err` output.
144    fn map_fallible<DCB, ECB, D2, E, L>(
145        self,
146        name: &str,
147        mut logic: L,
148    ) -> (
149        VecCollection<'scope, T, D2, R>,
150        VecCollection<'scope, T, E, R>,
151    )
152    where
153        DCB: ContainerBuilder<Container = Vec<(D2, T, R)>> + PushInto<(D2, T, R)>,
154        ECB: ContainerBuilder<Container = Vec<(E, T, R)>> + PushInto<(E, T, R)>,
155        D2: Clone + 'static,
156        E: Clone + 'static,
157        L: FnMut(D1) -> Result<D2, E> + 'static,
158    {
159        self.flat_map_fallible::<DCB, ECB, _, _, _, _>(name, move |record| Some(logic(record)))
160    }
161
162    /// Like [`Collection::flat_map`], but `logic` is allowed to fail. The first
163    /// returned collection will contain the successful applications of `logic`,
164    /// while the second returned collection will contain the failed
165    /// applications.
166    fn flat_map_fallible<DCB, ECB, D2, E, I, L>(
167        self,
168        name: &str,
169        logic: L,
170    ) -> (
171        Collection<'scope, T, DCB::Container>,
172        Collection<'scope, T, ECB::Container>,
173    )
174    where
175        DCB: ContainerBuilder + PushInto<(D2, T, R)>,
176        ECB: ContainerBuilder + PushInto<(E, T, R)>,
177        D2: Clone + 'static,
178        E: Clone + 'static,
179        I: IntoIterator<Item = Result<D2, E>>,
180        L: FnMut(D1) -> I + 'static;
181
182    /// Block progress of the frontier at `expiration` time.
183    fn expire_collection_at(self, name: &str, expiration: T) -> VecCollection<'scope, T, D1, R>;
184
185    /// Replaces each record with another, with a new difference type.
186    ///
187    /// This method is most commonly used to take records containing aggregatable data (e.g. numbers to be summed)
188    /// and move the data into the difference component. This will allow differential dataflow to update in-place.
189    fn explode_one<D2, R2, L>(
190        self,
191        logic: L,
192    ) -> VecCollection<'scope, T, D2, <R2 as Multiply<R>>::Output>
193    where
194        D2: differential_dataflow::Data,
195        R2: Semigroup + Multiply<R>,
196        <R2 as Multiply<R>>::Output: Clone + 'static + Semigroup,
197        L: FnMut(D1) -> (D2, R2) + 'static,
198        T: Lattice;
199
200    /// Partitions the input into a monotonic collection and
201    /// non-monotone exceptions, with respect to differences.
202    ///
203    /// The exceptions are transformed by `into_err`.
204    fn ensure_monotonic<E, IE>(
205        self,
206        into_err: IE,
207    ) -> (
208        VecCollection<'scope, T, D1, R>,
209        VecCollection<'scope, T, E, R>,
210    )
211    where
212        E: Clone + 'static,
213        IE: Fn(D1, R) -> (E, R) + 'static,
214        R: num_traits::sign::Signed;
215
216    /// Consolidates the collection if `must_consolidate` is `true` and leaves it
217    /// untouched otherwise.
218    fn consolidate_named_if<Ba>(self, must_consolidate: bool, name: &str) -> Self
219    where
220        D1: differential_dataflow::ExchangeData + Hash + Columnation,
221        R: Semigroup + differential_dataflow::ExchangeData + Columnation,
222        T: Lattice + Columnation,
223        Ba: Batcher<
224                Vec<((D1, ()), T, R)>,
225                Time = T,
226                Output = Vec<ColumnationStack<((D1, ()), T, R)>>,
227            > + 'static,
228        Ba: BatcherNew;
229
230    /// Consolidates the collection.
231    fn consolidate_named<Ba>(self, name: &str) -> Self
232    where
233        D1: differential_dataflow::ExchangeData + Hash + Columnation,
234        R: Semigroup + differential_dataflow::ExchangeData + Columnation,
235        T: Lattice + Columnation,
236        Ba: Batcher<
237                Vec<((D1, ()), T, R)>,
238                Time = T,
239                Output = Vec<ColumnationStack<((D1, ()), T, R)>>,
240            > + 'static,
241        Ba: BatcherNew;
242}
243
244impl<'scope, T, C1> StreamExt<'scope, T, C1> for Stream<'scope, T, C1>
245where
246    T: Timestamp,
247    C1: Container + DrainContainer + Clone + 'static,
248{
249    fn unary_fallible<DCB, ECB, B, P>(
250        self,
251        pact: P,
252        name: &str,
253        constructor: B,
254    ) -> (
255        Stream<'scope, T, DCB::Container>,
256        Stream<'scope, T, ECB::Container>,
257    )
258    where
259        DCB: ContainerBuilder,
260        ECB: ContainerBuilder,
261        B: FnOnce(
262            Capability<T>,
263            OperatorInfo,
264        ) -> Box<
265            dyn FnMut(
266                    &mut InputHandleCore<T, C1, P::Puller>,
267                    &mut OutputBuilderSession<'_, T, DCB>,
268                    &mut OutputBuilderSession<'_, T, ECB>,
269                ) + 'static,
270        >,
271        P: ParallelizationContract<T, C1>,
272    {
273        let mut builder = OperatorBuilderRc::new(name.into(), self.scope());
274
275        let operator_info = builder.operator_info();
276
277        let mut input = builder.new_input(self.clone(), pact);
278        builder.set_notify_for(0, FrontierInterest::Never);
279        let (ok_output, ok_stream) = builder.new_output();
280        let mut ok_output = OutputBuilder::from(ok_output);
281        let (err_output, err_stream) = builder.new_output();
282        let mut err_output = OutputBuilder::from(err_output);
283
284        builder.build(move |mut capabilities| {
285            // `capabilities` should be a single-element vector.
286            let capability = capabilities.pop().unwrap();
287            let mut logic = constructor(capability, operator_info);
288            move |_frontiers| {
289                let mut ok_output_handle = ok_output.activate();
290                let mut err_output_handle = err_output.activate();
291                logic(&mut input, &mut ok_output_handle, &mut err_output_handle);
292            }
293        });
294
295        (ok_stream, err_stream)
296    }
297
298    // XXX(guswynn): file an minimization bug report for the logic flat_map
299    // false positive here
300    // TODO(guswynn): remove this after https://github.com/rust-lang/rust-clippy/issues/8098 is
301    // resolved. The `logic` `FnMut` needs to be borrowed in the `flat_map` call, not moved in
302    // so the simple `|d1| logic(d1)` closure is load-bearing
303    #[allow(clippy::redundant_closure)]
304    fn flat_map_fallible<DCB, ECB, D2, E, I, L>(
305        self,
306        name: &str,
307        mut logic: L,
308    ) -> (
309        Stream<'scope, T, DCB::Container>,
310        Stream<'scope, T, ECB::Container>,
311    )
312    where
313        DCB: ContainerBuilder + PushInto<D2>,
314        ECB: ContainerBuilder + PushInto<E>,
315        I: IntoIterator<Item = Result<D2, E>>,
316        L: for<'a> FnMut(C1::Item<'a>) -> I + 'static,
317    {
318        self.unary_fallible::<DCB, ECB, _, _>(Pipeline, name, move |_, _| {
319            Box::new(move |input, ok_output, err_output| {
320                input.for_each_stamp(|time, data| {
321                    let mut ok_session = ok_output.session_with_builder(&time);
322                    let mut err_session = err_output.session_with_builder(&time);
323                    for r in data
324                        .flat_map(DrainContainer::drain)
325                        .flat_map(|d1| logic(d1))
326                    {
327                        match r {
328                            Ok(d2) => ok_session.give(d2),
329                            Err(e) => err_session.give(e),
330                        }
331                    }
332                })
333            })
334        })
335    }
336
337    fn partition_by<CB, L>(
338        self,
339        name: &str,
340        mut predicate: L,
341    ) -> (
342        Stream<'scope, T, CB::Container>,
343        Stream<'scope, T, CB::Container>,
344    )
345    where
346        CB: ContainerBuilder + for<'a> PushInto<C1::Item<'a>>,
347        L: for<'a> FnMut(&C1::Item<'a>) -> bool + 'static,
348    {
349        self.unary_fallible::<CB, CB, _, _>(Pipeline, name, move |_, _| {
350            Box::new(move |input, matching_output, rest_output| {
351                input.for_each_stamp(|time, data| {
352                    let mut matching = matching_output.session_with_builder(&time);
353                    let mut rest = rest_output.session_with_builder(&time);
354                    for item in data.flat_map(DrainContainer::drain) {
355                        if predicate(&item) {
356                            matching.give(item);
357                        } else {
358                            rest.give(item);
359                        }
360                    }
361                })
362            })
363        })
364    }
365
366    fn expire_stream_at(self, name: &str, expiration: T) -> Stream<'scope, T, C1> {
367        let name = format!("expire_stream_at({name})");
368        self.unary_frontier(Pipeline, &name.clone(), move |cap, _| {
369            // Retain a capability for the expiration time, which we'll only drop if the token
370            // is dropped. Else, block progress at the expiration time to prevent downstream
371            // operators from making any statement about expiration time or any following time.
372            let cap = Some(cap.delayed(&expiration));
373            let mut warned = false;
374            move |(input, frontier), output| {
375                let _ = &cap;
376                let frontier = frontier.frontier();
377                if !frontier.less_than(&expiration) && !warned {
378                    // Here, we print a warning, not an error. The state is only a liveness
379                    // concern, but not relevant for correctness. Additionally, a race between
380                    // shutting down the dataflow and dropping the token can cause the dataflow
381                    // to shut down before we drop the token.  This can happen when dropping
382                    // the last remaining capability on a different worker.  We do not want to
383                    // log an error every time this happens.
384
385                    tracing::warn!(
386                        name = name,
387                        frontier = ?frontier,
388                        expiration = ?expiration,
389                        "frontier not less than expiration"
390                    );
391                    warned = true;
392                }
393                input.for_each(|time, data| {
394                    let mut session = output.session(&time);
395                    session.give_container(data);
396                });
397            }
398        })
399    }
400}
401
402impl<'scope, T, D1, R> CollectionExt<'scope, T, D1, R> for VecCollection<'scope, T, D1, R>
403where
404    T: Timestamp + Clone + 'static,
405    D1: Clone + 'static,
406    R: Semigroup + 'static,
407{
408    fn empty(scope: Scope<'scope, T>) -> VecCollection<'scope, T, D1, R> {
409        operator::empty(scope).as_collection()
410    }
411
412    fn flat_map_fallible<DCB, ECB, D2, E, I, L>(
413        self,
414        name: &str,
415        mut logic: L,
416    ) -> (
417        Collection<'scope, T, DCB::Container>,
418        Collection<'scope, T, ECB::Container>,
419    )
420    where
421        DCB: ContainerBuilder + PushInto<(D2, T, R)>,
422        ECB: ContainerBuilder + PushInto<(E, T, R)>,
423        D2: Clone + 'static,
424        E: Clone + 'static,
425        I: IntoIterator<Item = Result<D2, E>>,
426        L: FnMut(D1) -> I + 'static,
427    {
428        let (ok_stream, err_stream) =
429            self.inner
430                .flat_map_fallible::<DCB, ECB, _, _, _, _>(name, move |(d1, t, r)| {
431                    logic(d1).into_iter().map(move |res| match res {
432                        Ok(d2) => Ok((d2, t.clone(), r.clone())),
433                        Err(e) => Err((e, t.clone(), r.clone())),
434                    })
435                });
436        (ok_stream.as_collection(), err_stream.as_collection())
437    }
438
439    fn expire_collection_at(self, name: &str, expiration: T) -> VecCollection<'scope, T, D1, R> {
440        self.inner
441            .expire_stream_at(name, expiration)
442            .as_collection()
443    }
444
445    fn explode_one<D2, R2, L>(
446        self,
447        mut logic: L,
448    ) -> VecCollection<'scope, T, D2, <R2 as Multiply<R>>::Output>
449    where
450        D2: differential_dataflow::Data,
451        R2: Semigroup + Multiply<R>,
452        <R2 as Multiply<R>>::Output: Clone + 'static + Semigroup,
453        L: FnMut(D1) -> (D2, R2) + 'static,
454        T: Lattice,
455    {
456        self.inner
457            .clone()
458            .unary::<ConsolidatingContainerBuilder<_>, _, _, _>(
459                Pipeline,
460                "ExplodeOne",
461                move |_, _| {
462                    move |input, output| {
463                        input.for_each(|time, data| {
464                            output
465                                .session_with_builder(&time)
466                                .give_iterator(data.drain(..).map(|(x, t, d)| {
467                                    let (x, d2) = logic(x);
468                                    (x, t, d2.multiply(&d))
469                                }));
470                        });
471                    }
472                },
473            )
474            .as_collection()
475    }
476
477    fn ensure_monotonic<E, IE>(
478        self,
479        into_err: IE,
480    ) -> (
481        VecCollection<'scope, T, D1, R>,
482        VecCollection<'scope, T, E, R>,
483    )
484    where
485        E: Clone + 'static,
486        IE: Fn(D1, R) -> (E, R) + 'static,
487        R: num_traits::sign::Signed,
488    {
489        let (oks, errs) = self
490            .inner
491            .unary_fallible(Pipeline, "EnsureMonotonic", move |_, _| {
492                Box::new(move |input, ok_output, err_output| {
493                    input.for_each(|time, data| {
494                        let mut ok_session = ok_output.session(&time);
495                        let mut err_session = err_output.session(&time);
496                        for (x, t, d) in data.drain(..) {
497                            if d.is_positive() {
498                                ok_session.give((x, t, d))
499                            } else {
500                                let (e, d2) = into_err(x, d);
501                                err_session.give((e, t, d2))
502                            }
503                        }
504                    })
505                })
506            });
507        (oks.as_collection(), errs.as_collection())
508    }
509
510    fn consolidate_named_if<Ba>(self, must_consolidate: bool, name: &str) -> Self
511    where
512        D1: differential_dataflow::ExchangeData + Hash + Columnation,
513        R: Semigroup + differential_dataflow::ExchangeData + Columnation,
514        T: Lattice + Ord + Columnation,
515        Ba: Batcher<
516                Vec<((D1, ()), T, R)>,
517                Time = T,
518                Output = Vec<ColumnationStack<((D1, ()), T, R)>>,
519            > + 'static,
520        Ba: BatcherNew,
521    {
522        if must_consolidate {
523            // We employ AHash below instead of the default hasher in DD to obtain
524            // a better distribution of data to workers. AHash claims empirically
525            // both speed and high quality, according to
526            // https://github.com/tkaitchuck/aHash/blob/master/compare/readme.md.
527            // TODO(vmarcos): Consider here if it is worth it to spend the time to
528            // implement twisted tabulation hashing as proposed in Mihai Patrascu,
529            // Mikkel Thorup: Twisted Tabulation Hashing. SODA 2013: 209-228, available
530            // at https://epubs.siam.org/doi/epdf/10.1137/1.9781611973105.16. The latter
531            // would provide good bounds for balls-into-bins problems when the number of
532            // bins is small (as is our case), so we'd have a theoretical guarantee.
533            // The seeds are fixed for determinism across builds; see
534            // [`crate::hash::fixed_state`].
535            let random_state = crate::hash::fixed_state();
536            let exchange = Exchange::new(move |update: &((D1, _), T, R)| {
537                let data = &(update.0).0;
538                let mut h = random_state.build_hasher();
539                data.hash(&mut h);
540                h.finish()
541            });
542            consolidate_pact::<Ba, _, _>(
543                self.map(|k| (k, ())).inner,
544                exchange,
545                name,
546                Ba::new_batcher,
547            )
548            .unary(Pipeline, "unpack consolidated", |_, _| {
549                |input, output| {
550                    input.for_each(|time, data| {
551                        let mut session = output.session(&time);
552                        for ((k, ()), t, d) in data.iter().flatten().flat_map(|chunk| chunk.iter())
553                        {
554                            session.give((k.clone(), t.clone(), d.clone()))
555                        }
556                    })
557                }
558            })
559            .as_collection()
560        } else {
561            self
562        }
563    }
564
565    fn consolidate_named<Ba>(self, name: &str) -> Self
566    where
567        D1: differential_dataflow::ExchangeData + Hash + Columnation,
568        R: Semigroup + differential_dataflow::ExchangeData + Columnation,
569        T: Lattice + Ord + Columnation,
570        Ba: Batcher<
571                Vec<((D1, ()), T, R)>,
572                Time = T,
573                Output = Vec<ColumnationStack<((D1, ()), T, R)>>,
574            > + 'static,
575        Ba: BatcherNew,
576    {
577        let exchange = Exchange::new(move |update: &((D1, ()), T, R)| (update.0).0.hashed());
578
579        consolidate_pact::<Ba, _, _>(self.map(|k| (k, ())).inner, exchange, name, Ba::new_batcher)
580            .unary(Pipeline, &format!("Unpack {name}"), |_, _| {
581                |input, output| {
582                    input.for_each(|time, data| {
583                        let mut session = output.session(&time);
584                        for ((k, ()), t, d) in data.iter().flatten().flat_map(|chunk| chunk.iter())
585                        {
586                            session.give((k.clone(), t.clone(), d.clone()))
587                        }
588                    })
589                }
590            })
591            .as_collection()
592    }
593}
594
595/// Aggregates the weights of equal records into at most one record.
596///
597/// Produces a stream of chains of records, partitioned according to `pact`. The
598/// data is sorted according to `Ba`. For each timestamp, it produces at most one chain.
599///
600/// The data are accumulated in place, each held back until their timestamp has completed.
601pub fn consolidate_pact<'scope, Ba, C, P>(
602    stream: Stream<'scope, Ba::Time, C>,
603    pact: P,
604    name: &str,
605    batcher: impl FnOnce(Option<Logger>, usize) -> Ba + 'static,
606) -> StreamVec<'scope, Ba::Time, Ba::Output>
607where
608    Ba: Batcher<C> + 'static,
609    Ba::Time: Timestamp,
610    C: Container + Clone + 'static,
611    Ba::Output: Default + Clone + 'static,
612    P: ParallelizationContract<Ba::Time, C>,
613{
614    let logger = stream
615        .scope()
616        .worker()
617        .logger_for("differential/arrange")
618        .map(Into::into);
619    stream.unary_frontier(pact, name, move |_cap, info| {
620        let mut batcher = batcher(logger, info.global_id);
621        // Capabilities for the lower envelope of updates in `batcher`.
622        let mut capabilities = Antichain::<Capability<Ba::Time>>::new();
623        let mut prev_frontier = Antichain::from_elem(Ba::Time::minimum());
624        // `extract` reports the batcher's retained lower bound borrowed from the batcher
625        // itself, so it must be copied out before the next call reborrows the batcher.
626        let mut batcher_frontier = Antichain::<Ba::Time>::new();
627
628        move |(input, frontier), output| {
629            input.for_each(|cap, data| {
630                // A message's stamp need not be a singleton, so retain the whole set rather
631                // than asking for the one time `retain` would insist on.
632                for capability in cap.retain_stamp(0).iter() {
633                    capabilities.insert(capability.clone());
634                }
635                batcher.insert(data);
636            });
637
638            if prev_frontier.borrow() != frontier.frontier() {
639                if capabilities
640                    .elements()
641                    .iter()
642                    .any(|c| !frontier.less_equal(c.time()))
643                {
644                    let mut upper = Antichain::new(); // re-used allocation for sealing batches.
645
646                    // For each capability not in advance of the input frontier ...
647                    for (index, capability) in capabilities.elements().iter().enumerate() {
648                        if !frontier.less_equal(capability.time()) {
649                            // Assemble the upper bound on times we can commit with this capabilities.
650                            // We must respect the input frontier, and *subsequent* capabilities, as
651                            // we are pretending to retire the capability changes one by one.
652                            upper.clear();
653                            for time in frontier.frontier().iter() {
654                                upper.insert(time.clone());
655                            }
656                            for other_capability in &capabilities.elements()[(index + 1)..] {
657                                upper.insert(other_capability.time().clone());
658                            }
659
660                            // Extract updates not in advance of `upper`.
661                            let (chain, retained) = batcher.extract(upper.borrow());
662                            batcher_frontier.clear();
663                            batcher_frontier.extend(retained.iter().cloned());
664
665                            // send the batch to downstream consumers, empty or not.
666                            let mut session = output.session(&capabilities.elements()[index]);
667                            session.give(chain.unwrap_or_default());
668                        }
669                    }
670
671                    // Having extracted and sent batches between each capability and the input frontier,
672                    // we should downgrade all capabilities to match the batcher's lower update frontier.
673                    // This may involve discarding capabilities, which is fine as any new updates arrive
674                    // in messages with new capabilities.
675
676                    let mut new_capabilities = Antichain::new();
677                    for time in batcher_frontier.iter() {
678                        if let Some(capability) = capabilities
679                            .elements()
680                            .iter()
681                            .find(|c| c.time().less_equal(time))
682                        {
683                            new_capabilities.insert(capability.delayed(time));
684                        } else {
685                            panic!("failed to find capability");
686                        }
687                    }
688
689                    capabilities = new_capabilities;
690                }
691
692                prev_frontier.clear();
693                prev_frontier.extend(frontier.frontier().iter().cloned());
694            }
695        }
696    })
697}
698
699/// Accumulates updates into sorted, consolidated chains, and releases what a frontier unblocks.
700///
701/// This is the [`Batcher`] a consolidation wants. Differential's own `MergeBatcher` seals each
702/// extracted chain into a trace batch, which a caller that only needs the updates consolidated
703/// would immediately have to take apart again; this one hands the chain over as it is.
704///
705/// `Chu` melds raw input containers into sorted, consolidated chunks, and `M` merges those
706/// chunks and splits them by time. The chains live in differential's [`MergeLadder`], and the
707/// batcher's own work is the carve-by-frontier, mirroring `MergeBatcher`'s.
708pub struct ConsolidatingBatcher<Chu, M: Extractor> {
709    /// Melds input containers into sorted, consolidated chunks.
710    chunker: Chu,
711    /// Sorted, consolidated chains, with their size accounting.
712    ladder: MergeLadder<M>,
713    /// Splits the merged chain at a frontier.
714    ///
715    /// The ladder keeps its own merger private, so extraction uses a second instance.
716    extractor: M,
717    /// Stash of empty chunks, recycled through extraction.
718    stash: Vec<M::Chunk>,
719    /// The lower-bound frontier of the data retained after the last extract.
720    frontier: Antichain<M::Time>,
721}
722
723impl<Chu: Default, M: Extractor<Time: Timestamp>> ConsolidatingBatcher<Chu, M> {
724    /// Allocates a new empty batcher.
725    pub fn new(logger: Option<Logger>, operator_id: usize) -> Self {
726        Self {
727            chunker: Chu::default(),
728            ladder: MergeLadder::new(logger, operator_id),
729            extractor: M::default(),
730            stash: Vec::new(),
731            frontier: Antichain::new(),
732        }
733    }
734}
735
736impl<C, Chu, M> Batcher<C> for ConsolidatingBatcher<Chu, M>
737where
738    M: Extractor<Time: Timestamp>,
739    Chu: ContainerBuilder<Container = M::Chunk> + for<'a> PushInto<&'a mut C>,
740{
741    fn insert(&mut self, container: &mut C) {
742        self.chunker.push_into(container);
743        while let Some(chunk) = self.chunker.extract().map(std::mem::take) {
744            self.ladder.insert_chain(vec![chunk]);
745        }
746    }
747}
748
749impl<Chu, M> Extract for ConsolidatingBatcher<Chu, M>
750where
751    M: Extractor<Time: Timestamp>,
752    Chu: ContainerBuilder<Container = M::Chunk>,
753{
754    type Time = M::Time;
755    type Output = Vec<M::Chunk>;
756
757    fn extract<'a>(
758        &'a mut self,
759        upper: AntichainRef<'_, M::Time>,
760    ) -> (Option<Self::Output>, AntichainRef<'a, M::Time>) {
761        // Flush whatever the chunker is still accumulating: a partial final chunk would
762        // otherwise never reach the merge ladder.
763        while let Some(chunk) = self.chunker.finish().map(std::mem::take) {
764            self.ladder.insert_chain(vec![chunk]);
765        }
766
767        let merged = self.ladder.merge_all();
768
769        let mut kept = Vec::new();
770        let mut readied = Vec::new();
771        self.frontier.clear();
772        self.extractor.extract(
773            merged,
774            upper,
775            &mut self.frontier,
776            &mut readied,
777            &mut kept,
778            &mut self.stash,
779        );
780
781        // The ladder is empty after `merge_all`, so this restores it to the one kept chain.
782        self.ladder.insert_chain(kept);
783        self.stash.clear();
784
785        (
786            (!readied.is_empty()).then_some(readied),
787            self.frontier.borrow(),
788        )
789    }
790}
791
792impl<Chu, M: Extractor> ConsolidatingBatcher<Chu, M> {
793    /// Insert one already sorted and consolidated chunk, bypassing the chunker.
794    ///
795    /// The ladder assumes each chunk it holds is sorted and consolidated, so a caller that
796    /// prepared the chunk itself uses this; everything else goes through [`Batcher::insert`].
797    pub fn push_chunk(&mut self, chunk: M::Chunk) {
798        self.ladder.insert_chain(vec![chunk]);
799    }
800}
801
802/// A batcher that generic code can construct.
803///
804/// [`Batcher`] itself has no constructor, and differential's operators take one as an `FnOnce`
805/// argument. Wrappers such as [`CollectionExt::consolidate_named`] and `mz_arrange` build their
806/// batcher through this trait, so their callers only name the type.
807pub trait BatcherNew {
808    /// Allocates an empty batcher, reporting its footprint against `operator_id`.
809    fn new_batcher(logger: Option<Logger>, operator_id: usize) -> Self;
810}
811
812impl<Chu: Default, M: Extractor, S> BatcherNew for MergeBatcher<Chu, M, S> {
813    fn new_batcher(logger: Option<Logger>, operator_id: usize) -> Self {
814        Self::new(logger, operator_id)
815    }
816}
817
818impl<Chu: Default, M: Extractor<Time: Timestamp>> BatcherNew for ConsolidatingBatcher<Chu, M> {
819    fn new_batcher(logger: Option<Logger>, operator_id: usize) -> Self {
820        Self::new(logger, operator_id)
821    }
822}
823
824/// Merge the contents of multiple streams and combine the containers using a container builder.
825pub trait ConcatenateFlatten<'scope, T: Timestamp, C: Container + DrainContainer> {
826    /// Merge the contents of multiple streams and use the provided container builder to form
827    /// output containers.
828    ///
829    /// # Examples
830    /// ```
831    /// use timely::container::CapacityContainerBuilder;
832    /// use timely::dataflow::operators::{ToStream, Inspect};
833    /// use mz_timely_util::operator::ConcatenateFlatten;
834    ///
835    /// timely::example(|scope| {
836    ///
837    ///     let streams: Vec<timely::dataflow::StreamVec<_, i32>> =
838    ///         vec![(0..10).to_stream(scope),
839    ///              (0..10).to_stream(scope),
840    ///              (0..10).to_stream(scope)];
841    ///
842    ///     scope.concatenate_flatten::<_, CapacityContainerBuilder<Vec<i32>>>(streams)
843    ///          .inspect(|x| println!("seen: {:?}", x));
844    /// });
845    /// ```
846    fn concatenate_flatten<I, CB>(&self, sources: I) -> Stream<'scope, T, CB::Container>
847    where
848        I: IntoIterator<Item = Stream<'scope, T, C>>,
849        CB: ContainerBuilder + for<'a> PushInto<C::Item<'a>>;
850}
851
852impl<'scope, T, C> ConcatenateFlatten<'scope, T, C> for Stream<'scope, T, C>
853where
854    T: Timestamp,
855    C: Container + DrainContainer + Clone + 'static,
856{
857    fn concatenate_flatten<I, CB>(&self, sources: I) -> Stream<'scope, T, CB::Container>
858    where
859        I: IntoIterator<Item = Stream<'scope, T, C>>,
860        CB: ContainerBuilder + for<'a> PushInto<C::Item<'a>>,
861    {
862        self.scope()
863            .concatenate_flatten::<_, CB>(Some(Clone::clone(self)).into_iter().chain(sources))
864    }
865}
866
867impl<'scope, T, C> ConcatenateFlatten<'scope, T, C> for Scope<'scope, T>
868where
869    T: Timestamp,
870    C: Container + DrainContainer,
871{
872    fn concatenate_flatten<I, CB>(&self, sources: I) -> Stream<'scope, T, CB::Container>
873    where
874        I: IntoIterator<Item = Stream<'scope, T, C>>,
875        CB: ContainerBuilder + for<'a> PushInto<C::Item<'a>>,
876    {
877        let mut builder = OperatorBuilder::new("ConcatenateFlatten".to_string(), self.clone());
878
879        // create new input handles for each input stream.
880        let mut handles = sources
881            .into_iter()
882            .map(|s| builder.new_input(s, Pipeline))
883            .collect::<Vec<_>>();
884        for i in 0..handles.len() {
885            builder.set_notify_for(i, FrontierInterest::Never);
886        }
887
888        // create one output handle for the concatenated results.
889        let (output, result) = builder.new_output::<CB::Container>();
890        let mut output = OutputBuilder::<_, CB>::from(output);
891
892        builder.build(move |_capability| {
893            move |_frontier| {
894                let mut output = output.activate();
895                for handle in handles.iter_mut() {
896                    handle.for_each_stamp(|time, data| {
897                        output
898                            .session_with_builder(&time)
899                            .give_iterator(data.flat_map(DrainContainer::drain));
900                    })
901                }
902            }
903        });
904
905        result
906    }
907}
908
909/// A trait for containers that can be cleared.
910pub trait ClearContainer {
911    /// Clear the contents of the container.
912    fn clear(&mut self);
913}
914
915impl<T> ClearContainer for Vec<T> {
916    fn clear(&mut self) {
917        Vec::clear(self)
918    }
919}
920
921#[cfg(test)]
922mod tests {
923    use timely::container::CapacityContainerBuilder;
924    use timely::dataflow::operators::Capture;
925    use timely::dataflow::operators::capture::Extract;
926    use timely::dataflow::operators::core::to_stream::ToStreamBuilder;
927    use timely::dataflow::operators::vec::ToStream;
928
929    use crate::columnar::Column;
930    use crate::columnar::builder::ColumnBuilder;
931
932    use super::*;
933
934    /// Updates whose own times straddle `SPLIT`, all carried under the single input
935    /// capability at time zero. A split decided per container would route them together.
936    const UPDATES: [(u64, u64, i64); 4] = [(0, 0, 1), (1, 3, 1), (2, 5, -1), (3, 7, 1)];
937    const SPLIT: u64 = 5;
938    const MATCHING: [(u64, u64, i64); 2] = [(2, 5, -1), (3, 7, 1)];
939    const REST: [(u64, u64, i64); 2] = [(0, 0, 1), (1, 3, 1)];
940
941    #[mz_ore::test]
942    fn partition_by_routes_vec_records_by_their_own_time() {
943        let (matching, rest) = timely::execute_directly(|worker| {
944            worker.dataflow::<u64, _, _>(|scope| {
945                let (matching, rest) = UPDATES
946                    .to_vec()
947                    .to_stream(scope)
948                    .partition_by::<CapacityContainerBuilder<Vec<_>>, _>("Test", |(_, time, _)| {
949                        *time >= SPLIT
950                    });
951                (matching.capture(), rest.capture())
952            })
953        });
954        let flatten = |captured: Vec<(u64, Vec<(u64, u64, i64)>)>| {
955            captured
956                .into_iter()
957                .flat_map(|(capability, updates)| {
958                    assert_eq!(capability, 0, "outputs keep the input capability");
959                    updates
960                })
961                .collect::<Vec<_>>()
962        };
963        assert_eq!(flatten(matching.extract()), MATCHING);
964        assert_eq!(flatten(rest.extract()), REST);
965    }
966
967    #[mz_ore::test]
968    fn partition_by_routes_columnar_records_by_their_own_time() {
969        let (matching, rest) = timely::execute_directly(|worker| {
970            worker.dataflow::<u64, _, _>(|scope| {
971                let (matching, rest) = UPDATES
972                    .to_vec()
973                    .to_stream_with_builder::<_, ColumnBuilder<(u64, u64, i64)>>(scope)
974                    .partition_by::<ColumnBuilder<(u64, u64, i64)>, _>("Test", |(_, time, _)| {
975                        **time >= SPLIT
976                    });
977                (matching.capture(), rest.capture())
978            })
979        });
980        let flatten = |captured: Vec<(u64, Column<(u64, u64, i64)>)>| {
981            captured
982                .into_iter()
983                .flat_map(|(capability, mut column)| {
984                    assert_eq!(capability, 0, "outputs keep the input capability");
985                    column
986                        .drain()
987                        .map(|(data, time, diff)| (*data, *time, *diff))
988                        .collect::<Vec<_>>()
989                })
990                .collect::<Vec<_>>()
991        };
992        assert_eq!(flatten(matching.extract()), MATCHING);
993        assert_eq!(flatten(rest.extract()), REST);
994    }
995}