Skip to main content

differential_dataflow/operators/cursor/
join.rs

1//! Cursor-based join implementation.
2
3use std::cell::RefCell;
4use std::cmp::Ordering;
5use std::collections::VecDeque;
6use std::rc::Rc;
7
8use timely::ContainerBuilder;
9use timely::dataflow::Stream;
10use timely::progress::Timestamp;
11
12use super::history::load_current;
13use crate::lattice::Lattice;
14use crate::operators::arrange::Arranged;
15use crate::operators::history::ValueHistory;
16use crate::operators::join::{Fresh, JoinTactic, join_with_tactic};
17use crate::trace::{BatchCursor, BatchDiff, BatchVal, Cursor, Navigable, TraceReader};
18use crate::trace::cursor::cursor_list;
19use crate::trace::implementations::containers::BatchContainer;
20
21/// An equijoin of two traces, sharing a common key type.
22///
23/// This method exists to provide join functionality without opinions on the specific input types, keys and values,
24/// that should be presented. The two traces here can have arbitrary key and value types, which can be unsized and
25/// even potentially unrelated to the input collection data. Importantly, the key and value types could be generic
26/// associated types (GATs) of the traces, and we would seemingly struggle to frame these types as trait arguments.
27///
28/// The implementation produces a caller-specified container. Implementations can use [`AsCollection`] to wrap the
29/// output stream in a collection.
30///
31/// The "correctness" of this method depends heavily on the behavior of the supplied `result` function.
32///
33/// [`AsCollection`]: crate::collection::AsCollection
34pub fn join_traces<'scope, Tr1, Tr2, KC, L, CB>(arranged1: Arranged<'scope, Tr1>, arranged2: Arranged<'scope, Tr2>, name: &str, result: L) -> Stream<'scope, Tr1::Time, CB::Container>
35where
36    Tr1: TraceReader<Batch: Navigable>+'static,
37    Tr2: TraceReader<Batch: Navigable, Time = Tr1::Time>+'static,
38    KC: BatchContainer,
39    BatchCursor<Tr1>: Cursor<Time = Tr1::Time, KeyContainer = KC>,
40    for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
41    for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>, Time = Tr1::Time>,
42    L: FnMut(KC::ReadItem<'_>,BatchVal<'_, Tr1>,BatchVal<'_, Tr2>,Tr1::Time,&BatchDiff<Tr1>,&BatchDiff<Tr2>,&mut CB)+'static,
43    CB: ContainerBuilder<Container: Default> + 'static,
44{
45    join_with_tactic(arranged1, arranged2, name, CursorTactic::<Tr1::Batch, Tr2::Batch, _, CB>::new(result))
46}
47
48
49/// The conventional cursor-based [`JoinTactic`].
50///
51/// It builds a [`CursorList`](crate::trace::cursor::CursorList) over each input batch list and plays the merge-join out at whatever rate
52/// the driver's fuel allows. Each prepared unit joins a `B0`-side cursor against a `B1`-side cursor,
53/// emitting `(val0, val1)` to `logic` and yielding the output containers `logic` fills. `logic` is
54/// shared across all outstanding units (an `Rc<RefCell<_>>`), preserving the single mutable-state
55/// semantics of one closure threaded through every match — each unit is a self-contained `'static`
56/// iterator, so it cannot borrow the tactic.
57///
58/// It is parameterized by the builder `CB` into which `logic` pushes output; the [`JoinTactic`] it
59/// implements is over the container `CB` yields (`CB::Container`).
60pub struct CursorTactic<B0, B1, L, CB>
61where
62    B0: Navigable,
63    B1: Navigable,
64    B1::Cursor: for<'a> Cursor<Key<'a> = <B0::Cursor as Cursor>::Key<'a>, Time = <B0::Cursor as Cursor>::Time>,
65{
66    logic: Rc<RefCell<L>>,
67    _marker: std::marker::PhantomData<(B0, B1, CB)>,
68}
69
70impl<B0, B1, L, CB> CursorTactic<B0, B1, L, CB>
71where
72    B0: Navigable,
73    B1: Navigable,
74    B1::Cursor: for<'a> Cursor<Key<'a> = <B0::Cursor as Cursor>::Key<'a>, Time = <B0::Cursor as Cursor>::Time>,
75{
76    /// Construct a tactic that applies `logic` to each matched `(key, val0, val1)`.
77    pub fn new(logic: L) -> Self {
78        CursorTactic { logic: Rc::new(RefCell::new(logic)), _marker: std::marker::PhantomData }
79    }
80}
81
82impl<B0, B1, L, CB> JoinTactic<<B0::Cursor as Cursor>::Time, B0, B1, CB::Container> for CursorTactic<B0, B1, L, CB>
83where
84    B0: Navigable + 'static,
85    B1: Navigable + 'static,
86    B1::Cursor: for<'a> Cursor<Key<'a> = <B0::Cursor as Cursor>::Key<'a>, Time = <B0::Cursor as Cursor>::Time>,
87    CB: ContainerBuilder<Container: Default> + 'static,
88    L: for<'a> FnMut(<B0::Cursor as Cursor>::Key<'a>, <B0::Cursor as Cursor>::Val<'a>, <B1::Cursor as Cursor>::Val<'a>, <B0::Cursor as Cursor>::Time, &<B0::Cursor as Cursor>::Diff, &<B1::Cursor as Cursor>::Diff, &mut CB) + 'static,
89{
90    fn prep(&mut self, input0: Vec<B0>, input1: Vec<B1>, fresh: Fresh, meet: <B0::Cursor as Cursor>::Time) -> Box<dyn Iterator<Item = CB::Container>> {
91        // The accumulated side's history is advanced by `meet` to consolidate it before the
92        // cross-product; the fresh side is left, as its times already lie at or beyond `meet`. `fresh`
93        // fixes which side is which. The advance is output-neutral either way (the fresh side's times are
94        // at or beyond `meet`, so the joined time is too), so it is purely a consolidation: it pays off
95        // when the accumulated side carries times below `meet`, and is a wasted scan when it does not. A
96        // more precise rule would skip the scan when the side is already entirely at or beyond `meet`,
97        // but detecting that needs both frontiers, not just `lower`: a batch's times lie at or beyond
98        // both its `lower` and its `since`, so the side is entirely beyond `meet` exactly when
99        // `meet <= lower` or `meet <= since`. A fresh batch is caught by `lower` (its `since` is
100        // `minimum`), a compacted trace by `since` (its `lower` is `minimum`); checking `lower` alone
101        // would wrongly advance a compacted trace whose times are all already at or beyond `meet`. We
102        // keep the simpler fresh-based choice and accept the occasional no-op scan.
103        let (cursor1, storage1) = cursor_list(input0);
104        let (cursor2, storage2) = cursor_list(input1);
105        let (advance1, advance2) = match fresh {
106            Fresh::Input0 => (false, true),
107            Fresh::Input1 => (true, false),
108        };
109        Box::new(DeferredIter {
110            cursor1,
111            storage1,
112            cursor2,
113            storage2,
114            meet,
115            advance1,
116            advance2,
117            logic: Rc::clone(&self.logic),
118            builder: CB::default(),
119            ready: VecDeque::new(),
120            done: false,
121        })
122    }
123}
124
125/// Deferred join computation, as an iterator of output containers.
126///
127/// The structure wraps cursors which allow us to play out join computation at whatever rate we like.
128/// This allows us to avoid producing and buffering massive amounts of data, without giving the timely
129/// dataflow system a chance to run operators that can consume and aggregate the data. Each `next` plays
130/// the merge-join forward until the builder yields a container (or the cursors run dry), matching the
131/// former per-unit `work` loop but suspending at container boundaries rather than under a fuel budget:
132/// the driver stops pulling once its budget is spent and resumes the same iterator next activation.
133struct DeferredIter<T, C1, C2, L, CB>
134where
135    T: Timestamp+Lattice,
136    C1: Cursor<Time=T>,
137    C2: for<'a> Cursor<Key<'a>=C1::Key<'a>, Time=T>,
138    CB: ContainerBuilder,
139{
140    cursor1: C1,
141    storage1: C1::Storage,
142    cursor2: C2,
143    storage2: C2::Storage,
144    /// The meet of the stamp's times, a lower bound on this unit's output times; the lower envelope for consolidation.
145    meet: T,
146    /// Whether to advance each side's history by `meet` before consolidation.
147    advance1: bool,
148    advance2: bool,
149    /// The output closure, shared across all outstanding units.
150    logic: Rc<RefCell<L>>,
151    /// The builder `logic` fills; drained into `ready` as containers complete.
152    builder: CB,
153    /// Completed containers awaiting a `next` call.
154    ready: VecDeque<CB::Container>,
155    done: bool,
156}
157
158impl<T, C1, C2, L, CB> Iterator for DeferredIter<T, C1, C2, L, CB>
159where
160    T: Timestamp+Lattice,
161    C1: Cursor<Time=T>,
162    C2: for<'a> Cursor<Key<'a>=C1::Key<'a>, Time=T>,
163    CB: ContainerBuilder<Container: Default>,
164    L: for<'a> FnMut(C1::Key<'a>, C1::Val<'a>, C2::Val<'a>, T, &C1::Diff, &C2::Diff, &mut CB),
165{
166    type Item = CB::Container;
167
168    /// Play the merge-join forward until a container is ready, or the cursors run dry.
169    #[inline(never)]
170    fn next(&mut self) -> Option<CB::Container> {
171        // Serve any container completed on an earlier call first.
172        if let Some(container) = self.ready.pop_front() { return Some(container); }
173        if self.done { return None; }
174
175        // The accumulated side is advanced by `meet` to consolidate its history; the fresh side is left,
176        // as its times already lie at or beyond `meet`. The choice was fixed per side at construction,
177        // from which input carried the fresh batch.
178        let meet1 = if self.advance1 { Some(&self.meet) } else { None };
179        let meet2 = if self.advance2 { Some(&self.meet) } else { None };
180
181        let storage1 = &self.storage1;
182        let storage2 = &self.storage2;
183        let cursor1 = &mut self.cursor1;
184        let cursor2 = &mut self.cursor2;
185        let builder = &mut self.builder;
186        let ready = &mut self.ready;
187        let mut logic = self.logic.borrow_mut();
188        let logic = &mut *logic;
189
190        let mut thinker = JoinThinker::new();
191        let mut exhausted = false;
192
193        while ready.is_empty() {
194            match (cursor1.get_key(storage1), cursor2.get_key(storage2)) {
195                (Some(key1), Some(key2)) => match key1.cmp(&key2) {
196                    Ordering::Less => cursor1.seek_key(storage1, key2),
197                    Ordering::Greater => cursor2.seek_key(storage2, key1),
198                    Ordering::Equal => {
199
200                        load_current(&mut thinker.history1, cursor1, storage1, meet1);
201                        load_current(&mut thinker.history2, cursor2, storage2, meet2);
202
203                        thinker.think(|v1,v2,t,r1,r2| {
204                            logic(key1, v1, v2, t, r1, r2, builder);
205                        });
206
207                        cursor1.step_key(storage1);
208                        cursor2.step_key(storage2);
209
210                        thinker.history1.clear();
211                        thinker.history2.clear();
212
213                        // Move any completed containers aside; we yield them one at a time.
214                        while let Some(container) = builder.extract() {
215                            // Avoiding the mem::take would require a non-iterator trait.
216                            ready.push_back(std::mem::take(container));
217                        }
218                    }
219                },
220                // One side is exhausted; no further keys can match.
221                _ => { exhausted = true; break; }
222            }
223        }
224
225        if exhausted {
226            self.done = true;
227            // Flush the final partial container.
228            while let Some(container) = builder.finish() {
229                // Avoiding the mem::take would require a non-iterator trait.
230                ready.push_back(std::mem::take(container));
231            }
232        }
233
234        ready.pop_front()
235    }
236}
237
238struct JoinThinker<V1, V2, T, D1, D2> {
239    pub history1: ValueHistory<V1, T, D1>,
240    pub history2: ValueHistory<V2, T, D2>,
241}
242
243impl<V1, V2, T, D1, D2> JoinThinker<V1, V2, T, D1, D2>
244where
245    V1: Copy + Ord,
246    V2: Copy + Ord,
247    T: Ord + Clone + Lattice,
248    D1: Clone + crate::difference::Semigroup,
249    D2: Clone + crate::difference::Semigroup,
250{
251    fn new() -> Self {
252        JoinThinker {
253            history1: ValueHistory::new(),
254            history2: ValueHistory::new(),
255        }
256    }
257
258    fn think<F: FnMut(V1, V2, T, &D1, &D2)>(&mut self, mut results: F) {
259
260        // for reasonably sized edits, do the dead-simple thing.
261        if self.history1.edit_len() < 10 || self.history2.edit_len() < 10 {
262            self.history1.map_edits(|v1, t1, d1| {
263                self.history2.map_edits(|v2, t2, d2| {
264                    results(v1, v2, t1.join(t2), d1, d2);
265                })
266            })
267        }
268        else {
269
270            let mut replay1 = self.history1.replay();
271            let mut replay2 = self.history2.replay();
272
273            // TODO: It seems like there is probably a good deal of redundant `advance_buffer_by`
274            //       in here. If a time is ever repeated, for example, the call will be identical
275            //       and accomplish nothing. If only a single record has been added, it may not
276            //       be worth the time to collapse (advance, re-sort) the data when a linear scan
277            //       is sufficient.
278
279            while !replay1.is_done() && !replay2.is_done() {
280
281                if replay1.time().unwrap().cmp(replay2.time().unwrap()) == ::std::cmp::Ordering::Less {
282                    replay2.advance_buffer_by(replay1.meet().unwrap());
283                    for &((val2, ref time2), ref diff2) in replay2.buffer().iter() {
284                        let (val1, time1, diff1) = replay1.edit().unwrap();
285                        results(val1, val2, time1.join(time2), diff1, diff2);
286                    }
287                    replay1.step();
288                }
289                else {
290                    replay1.advance_buffer_by(replay2.meet().unwrap());
291                    for &((val1, ref time1), ref diff1) in replay1.buffer().iter() {
292                        let (val2, time2, diff2) = replay2.edit().unwrap();
293                        results(val1, val2, time1.join(time2), diff1, diff2);
294                    }
295                    replay2.step();
296                }
297            }
298
299            while !replay1.is_done() {
300                replay2.advance_buffer_by(replay1.meet().unwrap());
301                for &((val2, ref time2), ref diff2) in replay2.buffer().iter() {
302                    let (val1, time1, diff1) = replay1.edit().unwrap();
303                    results(val1, val2, time1.join(time2), diff1, diff2);
304                }
305                replay1.step();
306            }
307            while !replay2.is_done() {
308                replay1.advance_buffer_by(replay2.meet().unwrap());
309                for &((val1, ref time1), ref diff1) in replay1.buffer().iter() {
310                    let (val2, time2, diff2) = replay2.edit().unwrap();
311                    results(val1, val2, time1.join(time2), diff1, diff2);
312                }
313                replay2.step();
314            }
315        }
316    }
317}