Skip to main content

differential_dataflow/operators/
join.rs

1//! Match pairs of records based on a key.
2//!
3//! The various `join` implementations require that the units of each collection can be multiplied, and that
4//! the multiplication distributes over addition. That is, we will repeatedly evaluate (a + b) * c as (a * c)
5//! + (b * c), and if this is not equal to the former term, little is known about the actual output.
6use std::cmp::Ordering;
7use std::collections::VecDeque;
8
9use timely::{Container, ContainerBuilder};
10use timely::container::NoopBuilder;
11use timely::order::PartialOrder;
12use timely::progress::Timestamp;
13use timely::dataflow::Stream;
14use timely::dataflow::operators::generic::{Operator, OutputBuilderSession};
15use timely::dataflow::channels::pact::Pipeline;
16use timely::dataflow::operators::Capability;
17
18use crate::lattice::Lattice;
19use crate::operators::arrange::Arranged;
20use crate::trace::{BatchCursor, BatchDiff, BatchReader, BatchVal, Cursor, Navigable, TraceReader};
21use crate::trace::cursor::cursor_list;
22use crate::trace::implementations::containers::BatchContainer;
23use crate::operators::ValueHistory;
24
25/// A type that can manage the joining of lists of batches.
26///
27/// The trait is parameterized by the output container `C`, not by the builder that assembles it: a tactic
28/// yields finished containers, and how it produces them (pushing records into a [`ContainerBuilder`], or
29/// otherwise) is its own concern.
30pub trait JoinTactic<B0: BatchReader, B1: BatchReader<Time = B0::Time>, C> {
31    /// Prepare the join of two lists of batches into an iterator of output containers.
32    ///
33    /// The supplied `fresh` and `meet` indicate respectively which input is "novel", and should drive the
34    /// join, as well as a lower bound on that input's times, so that the other input can be loaded compacted.
35    fn prep(&mut self, input0: Vec<B0>, input1: Vec<B1>, fresh: Fresh, meet: B0::Time) -> Box<dyn Iterator<Item = C>>;
36}
37
38/// Which input contributed the freshly-arrived batch of a deferred join unit.
39///
40/// The fresh batch's times all lie at or beyond the capability, so its side is not advanced by the
41/// capability's meet; the opposing accumulated trace is. The marker also selects which queue a unit
42/// joins, so a burst on one input cannot starve the other.
43pub enum Fresh {
44    /// The first input (`B0`) contributed the fresh batch.
45    Input0,
46    /// The second input (`B1`) contributed the fresh batch.
47    Input1,
48}
49
50/// An equijoin of two traces, sharing a common key type.
51///
52/// This method exists to provide join functionality without opinions on the specific input types, keys and values,
53/// that should be presented. The two traces here can have arbitrary key and value types, which can be unsized and
54/// even potentially unrelated to the input collection data. Importantly, the key and value types could be generic
55/// associated types (GATs) of the traces, and we would seemingly struggle to frame these types as trait arguments.
56///
57/// The implementation produces a caller-specified container. Implementations can use [`AsCollection`] to wrap the
58/// output stream in a collection.
59///
60/// The "correctness" of this method depends heavily on the behavior of the supplied `result` function.
61///
62/// [`AsCollection`]: crate::collection::AsCollection
63pub fn join_traces<'scope, Tr1, Tr2, KC, L, CB>(arranged1: Arranged<'scope, Tr1>, arranged2: Arranged<'scope, Tr2>, result: L) -> Stream<'scope, Tr1::Time, CB::Container>
64where
65    Tr1: TraceReader<Batch: Navigable>+'static,
66    Tr2: TraceReader<Batch: Navigable, Time = Tr1::Time>+'static,
67    KC: BatchContainer,
68    BatchCursor<Tr1>: Cursor<Time = Tr1::Time, KeyContainer = KC>,
69    for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
70    for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>, Time = Tr1::Time>,
71    L: FnMut(KC::ReadItem<'_>,BatchVal<'_, Tr1>,BatchVal<'_, Tr2>,Tr1::Time,&BatchDiff<Tr1>,&BatchDiff<Tr2>,&mut CB)+'static,
72    CB: ContainerBuilder<Container: Default> + 'static,
73{
74    join_with_tactic(arranged1, arranged2, cursors::CursorTactic::<Tr1::Batch, Tr2::Batch, _, CB>::new(result))
75}
76
77/// Drives an equijoin of two traces using a supplied [`JoinTactic`].
78///
79/// This is the general join operator: it does the dataflow plumbing (frontiers, capabilities, trace
80/// compaction) and routes the per-batch work through the tactic. It requires only `TraceReader` of its
81/// inputs, never `Navigable`: it extracts trace batches via `batches_through`, and building cursors over
82/// them (if that is how the join proceeds) is the tactic's concern.
83pub fn join_with_tactic<'scope, Tr1, Tr2, T, C>(arranged1: Arranged<'scope, Tr1>, arranged2: Arranged<'scope, Tr2>, mut tactic: T) -> Stream<'scope, Tr1::Time, C>
84where
85    Tr1: TraceReader+'static,
86    Tr2: TraceReader<Time = Tr1::Time>+'static,
87    T: JoinTactic<Tr1::Batch, Tr2::Batch, C>+'static,
88    C: Container + 'static,
89{
90    // Rename traces for symmetry from here on out.
91    let mut trace1 = arranged1.trace;
92    let mut trace2 = arranged2.trace;
93
94    let scope = arranged1.stream.scope();
95    arranged1.stream.binary_frontier(arranged2.stream, Pipeline, Pipeline, "Join", move |capability, info| {
96
97        // Acquire an activator to reschedule the operator when it has unfinished work.
98        use timely::scheduling::Activator;
99        let activations = scope.activations();
100        let activator = Activator::new(info.address, activations);
101
102        // Our initial invariants are that for each trace, physical compaction is less or equal the trace's upper bound.
103        // These invariants ensure that we can reference observed batch frontiers from `_start_upper` onward, as long as
104        // we maintain our physical compaction capabilities appropriately. These assertions are tested as we load up the
105        // initial work for the two traces, and before the operator is constructed.
106
107        // Acknowledged frontier for each input.
108        // These two are used exclusively to track batch boundaries on which we may want/need to call `cursor_through`.
109        // They will drive our physical compaction of each trace, and we want to maintain at all times that each is beyond
110        // the physical compaction frontier of their corresponding trace.
111        // Should we ever *drop* a trace, these are 1. much harder to maintain correctly, but 2. no longer used.
112        use timely::progress::frontier::Antichain;
113        let mut acknowledged1 = Antichain::from_elem(Tr1::Time::minimum());
114        let mut acknowledged2 = Antichain::from_elem(Tr1::Time::minimum());
115
116        // Deferred work, as `(capability, iterator)` pairs bucketed by which input carried the fresh
117        // batch (so a burst on one input cannot starve the other). The driver owns the capabilities and
118        // the fuel budget; each iterator, prepared by the tactic, yields the output containers to ship
119        // under its paired capability, and is dropped once it goes dry.
120        let mut todo0: VecDeque<(Capability<Tr1::Time>, Box<dyn Iterator<Item = C>>)> = VecDeque::new();
121        let mut todo1: VecDeque<(Capability<Tr1::Time>, Box<dyn Iterator<Item = C>>)> = VecDeque::new();
122
123        // We'll unload the initial batches here, to put ourselves in a less non-deterministic state to start.
124        trace1.map_batches(|batch1| {
125            acknowledged1.clone_from(batch1.upper());
126            // No `todo1` work here, because we haven't accepted anything into `batches2` yet.
127            // It is effectively "empty", because we choose to drain `trace1` before `trace2`.
128            // Once we start streaming batches in, we will need to respond to new batches from
129            // `input1` with logic that would have otherwise been here. Check out the next loop
130            // for the structure.
131        });
132        // At this point, `ack1` should exactly equal `trace1.read_upper()`, as they are both determined by
133        // iterating through batches and capturing the upper bound. This is a great moment to assert that
134        // `trace1`'s physical compaction frontier is before the frontier of completed times in `trace1`.
135        // TODO: in the case that this does not hold, instead "upgrade" the physical compaction frontier.
136        assert!(PartialOrder::less_equal(&trace1.get_physical_compaction(), &acknowledged1.borrow()));
137
138        // We capture batch2's batches first and establish work second to avoid taking a `RefCell` lock
139        // on both traces at the same time, as they could be the same trace and this would panic.
140        let mut batch2_list = Vec::new();
141        trace2.map_batches(|batch2| {
142            acknowledged2.clone_from(batch2.upper());
143            batch2_list.push(batch2.clone());
144        });
145        // At this point, `ack2` should exactly equal `trace2.read_upper()`, as they are both determined by
146        // iterating through batches and capturing the upper bound. This is a great moment to assert that
147        // `trace2`'s physical compaction frontier is before the frontier of completed times in `trace2`.
148        // TODO: in the case that this does not hold, instead "upgrade" the physical compaction frontier.
149        assert!(PartialOrder::less_equal(&trace2.get_physical_compaction(), &acknowledged2.borrow()));
150
151        // Batches wholly at or before these frontiers were joined by the start-up loading
152        // above; batches arriving on the input streams are ignored up to them. Beyond them,
153        // every non-empty arriving batch must be joined, even when `acknowledged` has been
154        // advanced past it: `advance_upper` consults the shared trace, whose merges may have
155        // consolidated an in-flight batch's updates away (e.g. an add/remove pair collapsing
156        // once logical compaction equates their times). The trace's emptiness there is valid
157        // only for readers at or beyond the compaction frontier, while our consumers may read
158        // finer times; the raw batch still owes them its updates. (#801)
159        let preload_upper1 = acknowledged1.clone();
160        let preload_upper2 = acknowledged2.clone();
161
162        // Load up deferred work joining each captured `trace2` batch against `trace1`.
163        for batch2 in batch2_list.into_iter() {
164            // It is safe to ask for `ack1` because we have confirmed it to be in advance of `distinguish_since`.
165            let trace1_storage = trace1.batches_through(acknowledged1.borrow()).unwrap();
166            // We could downgrade the capability here, but doing so is a bit complicated mathematically.
167            // TODO: downgrade the capability by searching out the one time in `batch2.lower()` and not
168            // in `batch2.upper()`. Only necessary for non-empty batches, as empty batches may not have
169            // that property.
170            let work = tactic.prep(trace1_storage, vec![batch2], Fresh::Input1, capability.time().clone());
171            todo1.push_back((capability.clone(), work));
172        }
173
174        // Droppable handles to shared trace data structures.
175        let mut trace1_option = Some(trace1);
176        let mut trace2_option = Some(trace2);
177
178        move |(input1, frontier1), (input2, frontier2), output| {
179
180            // 1. Consuming input.
181            //
182            // The join computation repeatedly accepts batches of updates from each of its inputs.
183            //
184            // For each accepted batch, it prepares a work-item to join the batch against previously "accepted"
185            // updates from its other input. It is important to track which updates have been accepted, because
186            // we use a shared trace and there may be updates present that are in advance of this accepted bound.
187            //
188            // Batches are accepted: 1. in bulk at start-up (above), 2. as we observe them in the input stream,
189            // and 3. if the trace can confirm a region of empty space directly following our accepted bound.
190            // This last case is a consequence of our inability to transmit empty batches, as they may be formed
191            // in the absence of timely dataflow capabilities.
192
193            // Drain input 1, prepare work.
194            input1.for_each(|capability, data| {
195                // This test *should* always pass, as we only drop a trace in response to the other input emptying.
196                if let Some(ref mut trace2) = trace2_option {
197                    let capability = capability.retain(0);
198                    for batch1 in data.drain(..) {
199                        // An arriving batch must lie wholly on one side of the preload boundary,
200                        // and wholly on one side of `acknowledged1`: both frontiers are drawn from
201                        // the lattice of stream batch boundaries (received uppers, and uppers of
202                        // trace merges of whole stream batches). A batch spanning the former would
203                        // be partially double-processed; one spanning the latter mis-accounted.
204                        assert!(
205                            PartialOrder::less_equal(batch1.upper(), &preload_upper1) ||
206                            PartialOrder::less_equal(&preload_upper1, batch1.lower()),
207                            "batch spans the preload boundary",
208                        );
209                        assert!(
210                            PartialOrder::less_equal(&acknowledged1, batch1.lower()) ||
211                            PartialOrder::less_equal(batch1.upper(), &acknowledged1),
212                            "batch spans the acknowledged frontier",
213                        );
214
215                        // Ignore any pre-loaded data, which was joined at start-up. Note that this
216                        // is a test against the preload boundary, not against `acknowledged1`: the
217                        // latter can be advanced past an in-flight batch by `advance_upper`, when
218                        // trace merges consolidate the batch's updates away, and such a batch must
219                        // still be joined (its updates remain real at times finer than the trace's
220                        // compaction frontier, and no other work item has accounted for them).
221                        if !PartialOrder::less_equal(batch1.upper(), &preload_upper1) {
222                            if !batch1.is_empty() {
223                                // It is safe to ask for `ack2` as we validated that it was at least `get_physical_compaction()`
224                                // at start-up, and have held back physical compaction ever since.
225                                let trace2_storage = trace2.batches_through(acknowledged2.borrow()).unwrap();
226                                let work = tactic.prep(vec![batch1.clone()], trace2_storage, Fresh::Input0, capability.time().clone());
227                                todo0.push_back((capability.clone(), work));
228                            }
229
230                            // To update `acknowledged1` we might presume that `batch1.lower` should equal it, but we
231                            // may have skipped over empty batches. Still, the batches are in-order, and we should be
232                            // able to just assume the most recent `batch1.upper`, unless `advance_upper` has already
233                            // moved `acknowledged1` past this batch, in which case we keep the further frontier.
234                            if PartialOrder::less_equal(&acknowledged1, batch1.lower()) {
235                                debug_assert!(PartialOrder::less_equal(&acknowledged1, batch1.upper()));
236                                acknowledged1.clone_from(batch1.upper());
237                            }
238                        }
239                    }
240                }
241                else { panic!("`trace2_option` dropped before `input1` emptied!"); }
242            });
243
244            // Drain input 2, prepare work.
245            input2.for_each(|capability, data| {
246                // This test *should* always pass, as we only drop a trace in response to the other input emptying.
247                if let Some(ref mut trace1) = trace1_option {
248                    let capability = capability.retain(0);
249                    for batch2 in data.drain(..) {
250                        // An arriving batch must lie wholly on one side of the preload boundary,
251                        // and wholly on one side of `acknowledged2`: both frontiers are drawn from
252                        // the lattice of stream batch boundaries (received uppers, and uppers of
253                        // trace merges of whole stream batches). A batch spanning the former would
254                        // be partially double-processed; one spanning the latter mis-accounted.
255                        assert!(
256                            PartialOrder::less_equal(batch2.upper(), &preload_upper2) ||
257                            PartialOrder::less_equal(&preload_upper2, batch2.lower()),
258                            "batch spans the preload boundary",
259                        );
260                        assert!(
261                            PartialOrder::less_equal(&acknowledged2, batch2.lower()) ||
262                            PartialOrder::less_equal(batch2.upper(), &acknowledged2),
263                            "batch spans the acknowledged frontier",
264                        );
265
266                        // Ignore any pre-loaded data, which was joined at start-up. Note that this
267                        // is a test against the preload boundary, not against `acknowledged2`: the
268                        // latter can be advanced past an in-flight batch by `advance_upper`, when
269                        // trace merges consolidate the batch's updates away, and such a batch must
270                        // still be joined (its updates remain real at times finer than the trace's
271                        // compaction frontier, and no other work item has accounted for them).
272                        if !PartialOrder::less_equal(batch2.upper(), &preload_upper2) {
273                            if !batch2.is_empty() {
274                                // It is safe to ask for `ack1` as we validated that it was at least `get_physical_compaction()`
275                                // at start-up, and have held back physical compaction ever since.
276                                let trace1_storage = trace1.batches_through(acknowledged1.borrow()).unwrap();
277                                let work = tactic.prep(trace1_storage, vec![batch2.clone()], Fresh::Input1, capability.time().clone());
278                                todo1.push_back((capability.clone(), work));
279                            }
280
281                            // To update `acknowledged2` we might presume that `batch2.lower` should equal it, but we
282                            // may have skipped over empty batches. Still, the batches are in-order, and we should be
283                            // able to just assume the most recent `batch2.upper`, unless `advance_upper` has already
284                            // moved `acknowledged2` past this batch, in which case we keep the further frontier.
285                            if PartialOrder::less_equal(&acknowledged2, batch2.lower()) {
286                                debug_assert!(PartialOrder::less_equal(&acknowledged2, batch2.upper()));
287                                acknowledged2.clone_from(batch2.upper());
288                            }
289                        }
290                    }
291                }
292                else { panic!("`trace1_option` dropped before `input2` emptied!"); }
293            });
294
295            // Advance acknowledged frontiers through any empty regions that we may not receive as batches.
296            if let Some(trace1) = trace1_option.as_mut() {
297                trace1.advance_upper(&mut acknowledged1);
298            }
299            if let Some(trace2) = trace2_option.as_mut() {
300                trace2.advance_upper(&mut acknowledged2);
301            }
302
303            // 2. Join computation.
304            //
305            // For each of the inputs, we do some amount of work (measured in terms of number
306            // of output records produced). This is meant to yield control to allow downstream
307            // operators to consume and reduce the output, but it it also means to provide some
308            // degree of responsiveness. There is a potential risk here that if we fall behind
309            // then the increasing queues hold back physical compaction of the underlying traces
310            // which results in unintentionally quadratic processing time (each batch of either
311            // input must scan all batches from the other input).
312
313            // Perform some amount of outstanding work by pulling the deferred iterators and shipping the
314            // containers they yield. Each direction drains against its own half of the budget, so a burst
315            // on one input cannot starve the other. We reschedule the operator whenever any work remains,
316            // which is observable directly: an iterator has yet to yield `None`. The budget is split from
317            // `2_000_000` to preserve the historical `1_000_000` of progress per input each activation.
318            // The driver only ships finished containers (`give_container`), never pushing records, so it
319            // pins the operator output to `NoopBuilder<C>` — the builder for exactly this "containers ready
320            // to go" case, which is a `ContainerBuilder` for any `C` without further bounds.
321            let output: &mut OutputBuilderSession<'_, Tr1::Time, NoopBuilder<C>> = output;
322            let mut drain = |queue: &mut VecDeque<(Capability<Tr1::Time>, Box<dyn Iterator<Item = C>>)>, mut fuel: isize| {
323                while fuel >= 0 {
324                    let Some((capability, work)) = queue.front_mut() else { break };
325                    match work.next() {
326                        Some(mut container) => {
327                            fuel -= container.record_count() as isize;
328                            output.session_with_builder(&*capability).give_container(&mut container);
329                        }
330                        None => { queue.pop_front(); }
331                    }
332                }
333            };
334            let fuel = 2_000_000;
335            drain(&mut todo0, fuel / 2);
336            drain(&mut todo1, fuel / 2);
337            if !todo0.is_empty() || !todo1.is_empty() {
338                activator.activate();
339            }
340
341            // 3. Trace maintenance.
342            //
343            // Importantly, we use `input.frontier()` here rather than `acknowledged` to track
344            // the progress of an input, because should we ever drop one of the traces we will
345            // lose the ability to extract information from anything other than the input.
346            // For example, if we dropped `trace2` we would not be able to use `advance_upper`
347            // to keep `acknowledged2` up to date wrt empty batches, and would hold back logical
348            // compaction of `trace1`.
349
350            // Maintain `trace1`. Drop if `input2` is empty, or advance based on future needs.
351            if let Some(trace1) = trace1_option.as_mut() {
352                if frontier2.is_empty() { trace1_option = None; }
353                else {
354                    // Allow `trace1` to compact logically up to the frontier we may yet receive,
355                    // in the opposing input (`input2`). All `input2` times will be beyond this
356                    // frontier, and joined times only need to be accurate when advanced to it.
357                    trace1.set_logical_compaction(frontier2.frontier());
358                    // Allow `trace1` to compact physically up to the upper bound of batches we
359                    // have received in its input (`input1`). We will not require a cursor that
360                    // is not beyond this bound.
361                    trace1.set_physical_compaction(acknowledged1.borrow());
362                }
363            }
364
365            // Maintain `trace2`. Drop if `input1` is empty, or advance based on future needs.
366            if let Some(trace2) = trace2_option.as_mut() {
367                if frontier1.is_empty() { trace2_option = None;}
368                else {
369                    // Allow `trace2` to compact logically up to the frontier we may yet receive,
370                    // in the opposing input (`input1`). All `input1` times will be beyond this
371                    // frontier, and joined times only need to be accurate when advanced to it.
372                    trace2.set_logical_compaction(frontier1.frontier());
373                    // Allow `trace2` to compact physically up to the upper bound of batches we
374                    // have received in its input (`input2`). We will not require a cursor that
375                    // is not beyond this bound.
376                    trace2.set_physical_compaction(acknowledged2.borrow());
377                }
378            }
379        }
380    })
381}
382
383/// Cursor-based join: the conventional [`JoinTactic`] implementation and its per-batch worker.
384mod cursors {
385
386    use std::cell::RefCell;
387    use std::rc::Rc;
388
389    use super::*;
390
391    /// The conventional cursor-based [`JoinTactic`].
392    ///
393    /// It builds a [`CursorList`] over each input batch list and plays the merge-join out at whatever rate
394    /// the driver's fuel allows. Each prepared unit joins a `B0`-side cursor against a `B1`-side cursor,
395    /// emitting `(val0, val1)` to `logic` and yielding the output containers `logic` fills. `logic` is
396    /// shared across all outstanding units (an `Rc<RefCell<_>>`), preserving the single mutable-state
397    /// semantics of one closure threaded through every match — each unit is a self-contained `'static`
398    /// iterator, so it cannot borrow the tactic.
399    ///
400    /// It is parameterized by the builder `CB` into which `logic` pushes output; the [`JoinTactic`] it
401    /// implements is over the container `CB` yields (`CB::Container`).
402    pub struct CursorTactic<B0, B1, L, CB>
403    where
404        B0: BatchReader + Navigable,
405        B1: BatchReader<Time = B0::Time> + Navigable,
406        B0::Cursor: Cursor<Time = B0::Time>,
407        B1::Cursor: for<'a> Cursor<Key<'a> = <B0::Cursor as Cursor>::Key<'a>, Time = B0::Time>,
408    {
409        logic: Rc<RefCell<L>>,
410        _marker: std::marker::PhantomData<(B0, B1, CB)>,
411    }
412
413    impl<B0, B1, L, CB> CursorTactic<B0, B1, L, CB>
414    where
415        B0: BatchReader + Navigable,
416        B1: BatchReader<Time = B0::Time> + Navigable,
417        B0::Cursor: Cursor<Time = B0::Time>,
418        B1::Cursor: for<'a> Cursor<Key<'a> = <B0::Cursor as Cursor>::Key<'a>, Time = B0::Time>,
419    {
420        /// Construct a tactic that applies `logic` to each matched `(key, val0, val1)`.
421        pub fn new(logic: L) -> Self {
422            CursorTactic { logic: Rc::new(RefCell::new(logic)), _marker: std::marker::PhantomData }
423        }
424    }
425
426    impl<B0, B1, L, CB> JoinTactic<B0, B1, CB::Container> for CursorTactic<B0, B1, L, CB>
427    where
428        B0: BatchReader + Navigable + 'static,
429        B1: BatchReader<Time = B0::Time> + Navigable + 'static,
430        B0::Cursor: Cursor<Time = B0::Time>,
431        B1::Cursor: for<'a> Cursor<Key<'a> = <B0::Cursor as Cursor>::Key<'a>, Time = B0::Time>,
432        CB: ContainerBuilder<Container: Default> + 'static,
433        L: for<'a> FnMut(<B0::Cursor as Cursor>::Key<'a>, <B0::Cursor as Cursor>::Val<'a>, <B1::Cursor as Cursor>::Val<'a>, B0::Time, &<B0::Cursor as Cursor>::Diff, &<B1::Cursor as Cursor>::Diff, &mut CB) + 'static,
434    {
435        fn prep(&mut self, input0: Vec<B0>, input1: Vec<B1>, fresh: Fresh, meet: B0::Time) -> Box<dyn Iterator<Item = CB::Container>> {
436            // The accumulated side's history is advanced by `meet` to consolidate it before the
437            // cross-product; the fresh side is left, as its times already lie at or beyond `meet`. `fresh`
438            // fixes which side is which. The advance is output-neutral either way (the fresh side's times are
439            // at or beyond `meet`, so the joined time is too), so it is purely a consolidation: it pays off
440            // when the accumulated side carries times below `meet`, and is a wasted scan when it does not. A
441            // more precise rule would skip the scan when the side is already entirely at or beyond `meet`,
442            // but detecting that needs both frontiers, not just `lower`: a batch's times lie at or beyond
443            // both its `lower` and its `since`, so the side is entirely beyond `meet` exactly when
444            // `meet <= lower` or `meet <= since`. A fresh batch is caught by `lower` (its `since` is
445            // `minimum`), a compacted trace by `since` (its `lower` is `minimum`); checking `lower` alone
446            // would wrongly advance a compacted trace whose times are all already at or beyond `meet`. We
447            // keep the simpler fresh-based choice and accept the occasional no-op scan.
448            let (cursor1, storage1) = cursor_list(input0);
449            let (cursor2, storage2) = cursor_list(input1);
450            let (advance1, advance2) = match fresh {
451                Fresh::Input0 => (false, true),
452                Fresh::Input1 => (true, false),
453            };
454            Box::new(DeferredIter {
455                cursor1,
456                storage1,
457                cursor2,
458                storage2,
459                meet,
460                advance1,
461                advance2,
462                logic: Rc::clone(&self.logic),
463                builder: CB::default(),
464                ready: VecDeque::new(),
465                done: false,
466            })
467        }
468    }
469
470    /// Deferred join computation, as an iterator of output containers.
471    ///
472    /// The structure wraps cursors which allow us to play out join computation at whatever rate we like.
473    /// This allows us to avoid producing and buffering massive amounts of data, without giving the timely
474    /// dataflow system a chance to run operators that can consume and aggregate the data. Each `next` plays
475    /// the merge-join forward until the builder yields a container (or the cursors run dry), matching the
476    /// former per-unit `work` loop but suspending at container boundaries rather than under a fuel budget:
477    /// the driver stops pulling once its budget is spent and resumes the same iterator next activation.
478    struct DeferredIter<T, C1, C2, L, CB>
479    where
480        T: Timestamp+Lattice,
481        C1: Cursor<Time=T>,
482        C2: for<'a> Cursor<Key<'a>=C1::Key<'a>, Time=T>,
483        CB: ContainerBuilder,
484    {
485        cursor1: C1,
486        storage1: C1::Storage,
487        cursor2: C2,
488        storage2: C2::Storage,
489        /// The capability's time, at which this unit's output ships; the lower envelope for consolidation.
490        meet: T,
491        /// Whether to advance each side's history by `meet` before consolidation.
492        advance1: bool,
493        advance2: bool,
494        /// The output closure, shared across all outstanding units.
495        logic: Rc<RefCell<L>>,
496        /// The builder `logic` fills; drained into `ready` as containers complete.
497        builder: CB,
498        /// Completed containers awaiting a `next` call.
499        ready: VecDeque<CB::Container>,
500        done: bool,
501    }
502
503    impl<T, C1, C2, L, CB> Iterator for DeferredIter<T, C1, C2, L, CB>
504    where
505        T: Timestamp+Lattice,
506        C1: Cursor<Time=T>,
507        C2: for<'a> Cursor<Key<'a>=C1::Key<'a>, Time=T>,
508        CB: ContainerBuilder<Container: Default>,
509        L: for<'a> FnMut(C1::Key<'a>, C1::Val<'a>, C2::Val<'a>, T, &C1::Diff, &C2::Diff, &mut CB),
510    {
511        type Item = CB::Container;
512
513        /// Play the merge-join forward until a container is ready, or the cursors run dry.
514        #[inline(never)]
515        fn next(&mut self) -> Option<CB::Container> {
516            // Serve any container completed on an earlier call first.
517            if let Some(container) = self.ready.pop_front() { return Some(container); }
518            if self.done { return None; }
519
520            // The accumulated side is advanced by `meet` to consolidate its history; the fresh side is left,
521            // as its times already lie at or beyond `meet`. The choice was fixed per side at construction,
522            // from which input carried the fresh batch.
523            let meet1 = if self.advance1 { Some(&self.meet) } else { None };
524            let meet2 = if self.advance2 { Some(&self.meet) } else { None };
525
526            let storage1 = &self.storage1;
527            let storage2 = &self.storage2;
528            let cursor1 = &mut self.cursor1;
529            let cursor2 = &mut self.cursor2;
530            let builder = &mut self.builder;
531            let ready = &mut self.ready;
532            let mut logic = self.logic.borrow_mut();
533            let logic = &mut *logic;
534
535            let mut thinker = JoinThinker::new();
536            let mut exhausted = false;
537
538            while ready.is_empty() {
539                match (cursor1.get_key(storage1), cursor2.get_key(storage2)) {
540                    (Some(key1), Some(key2)) => match key1.cmp(&key2) {
541                        Ordering::Less => cursor1.seek_key(storage1, key2),
542                        Ordering::Greater => cursor2.seek_key(storage2, key1),
543                        Ordering::Equal => {
544
545                            thinker.history1.edits.load(cursor1, storage1, meet1);
546                            thinker.history2.edits.load(cursor2, storage2, meet2);
547
548                            thinker.think(|v1,v2,t,r1,r2| {
549                                logic(key1, v1, v2, t, r1, r2, builder);
550                            });
551
552                            cursor1.step_key(storage1);
553                            cursor2.step_key(storage2);
554
555                            thinker.history1.clear();
556                            thinker.history2.clear();
557
558                            // Move any completed containers aside; we yield them one at a time.
559                            while let Some(container) = builder.extract() {
560                                // Avoiding the mem::take would require a non-iterator trait.
561                                ready.push_back(std::mem::take(container));
562                            }
563                        }
564                    },
565                    // One side is exhausted; no further keys can match.
566                    _ => { exhausted = true; break; }
567                }
568            }
569
570            if exhausted {
571                self.done = true;
572                // Flush the final partial container.
573                while let Some(container) = builder.finish() {
574                    // Avoiding the mem::take would require a non-iterator trait.
575                    ready.push_back(std::mem::take(container));
576                }
577            }
578
579            ready.pop_front()
580        }
581    }
582
583    struct JoinThinker<V1, V2, T, D1, D2> {
584        pub history1: ValueHistory<V1, T, D1>,
585        pub history2: ValueHistory<V2, T, D2>,
586    }
587
588    impl<V1, V2, T, D1, D2> JoinThinker<V1, V2, T, D1, D2>
589    where
590        V1: Copy + Ord,
591        V2: Copy + Ord,
592        T: Ord + Clone + Lattice,
593        D1: Clone + crate::difference::Semigroup,
594        D2: Clone + crate::difference::Semigroup,
595    {
596        fn new() -> Self {
597            JoinThinker {
598                history1: ValueHistory::new(),
599                history2: ValueHistory::new(),
600            }
601        }
602
603        fn think<F: FnMut(V1, V2, T, &D1, &D2)>(&mut self, mut results: F) {
604
605            // for reasonably sized edits, do the dead-simple thing.
606            if self.history1.edits.len() < 10 || self.history2.edits.len() < 10 {
607                self.history1.edits.map(|v1, t1, d1| {
608                    self.history2.edits.map(|v2, t2, d2| {
609                        results(v1, v2, t1.join(t2), d1, d2);
610                    })
611                })
612            }
613            else {
614
615                let mut replay1 = self.history1.replay();
616                let mut replay2 = self.history2.replay();
617
618                // TODO: It seems like there is probably a good deal of redundant `advance_buffer_by`
619                //       in here. If a time is ever repeated, for example, the call will be identical
620                //       and accomplish nothing. If only a single record has been added, it may not
621                //       be worth the time to collapse (advance, re-sort) the data when a linear scan
622                //       is sufficient.
623
624                while !replay1.is_done() && !replay2.is_done() {
625
626                    if replay1.time().unwrap().cmp(replay2.time().unwrap()) == ::std::cmp::Ordering::Less {
627                        replay2.advance_buffer_by(replay1.meet().unwrap());
628                        for &((val2, ref time2), ref diff2) in replay2.buffer().iter() {
629                            let (val1, time1, diff1) = replay1.edit().unwrap();
630                            results(val1, val2, time1.join(time2), diff1, diff2);
631                        }
632                        replay1.step();
633                    }
634                    else {
635                        replay1.advance_buffer_by(replay2.meet().unwrap());
636                        for &((val1, ref time1), ref diff1) in replay1.buffer().iter() {
637                            let (val2, time2, diff2) = replay2.edit().unwrap();
638                            results(val1, val2, time1.join(time2), diff1, diff2);
639                        }
640                        replay2.step();
641                    }
642                }
643
644                while !replay1.is_done() {
645                    replay2.advance_buffer_by(replay1.meet().unwrap());
646                    for &((val2, ref time2), ref diff2) in replay2.buffer().iter() {
647                        let (val1, time1, diff1) = replay1.edit().unwrap();
648                        results(val1, val2, time1.join(time2), diff1, diff2);
649                    }
650                    replay1.step();
651                }
652                while !replay2.is_done() {
653                    replay1.advance_buffer_by(replay2.meet().unwrap());
654                    for &((val1, ref time1), ref diff1) in replay1.buffer().iter() {
655                        let (val2, time2, diff2) = replay2.edit().unwrap();
656                        results(val1, val2, time1.join(time2), diff1, diff2);
657                    }
658                    replay2.step();
659                }
660            }
661        }
662    }
663}