Skip to main content

differential_dataflow/operators/arrange/
arrangement.rs

1//! Arranges a collection into a re-usable trace structure.
2//!
3//! The `arrange` operator applies to a differential dataflow `Collection` and returns an `Arranged`
4//! structure, provides access to both an indexed form of accepted updates as well as a stream of
5//! batches of newly arranged updates.
6//!
7//! Several operators (`join`, `reduce`, and `count`, among others) are implemented against `Arranged`,
8//! and can be applied directly to arranged data instead of the collection. Internally, the operators
9//! will borrow the shared state, and listen on the timely stream for shared batches of data. The
10//! resources to index the collection---communication, computation, and memory---are spent only once,
11//! and only one copy of the index needs to be maintained as the collection changes.
12//!
13//! The arranged collection is stored in a trace, whose append-only operation means that it is safe to
14//! share between the single `arrange` writer and multiple readers. Each reader is expected to interrogate
15//! the trace only at times for which it knows the trace is complete, as indicated by the frontiers on its
16//! incoming channels. Failing to do this is "safe" in the Rust sense of memory safety, but the reader may
17//! see ill-defined data at times for which the trace is not complete. (All current implementations
18//! commit only completed data to the trace).
19
20use timely::dataflow::operators::{Enter, vec::Map};
21use timely::order::PartialOrder;
22use timely::dataflow::{Scope, Stream};
23use timely::dataflow::operators::generic::Operator;
24use timely::dataflow::channels::pact::{ParallelizationContract, Pipeline};
25use timely::progress::Timestamp;
26use timely::progress::Antichain;
27use timely::container::PushInto;
28use timely::dataflow::operators::{Capability, CapabilitySet};
29use timely::progress::Stamp;
30
31use crate::{Data, VecCollection, AsCollection};
32use crate::difference::Semigroup;
33use crate::lattice::Lattice;
34use crate::logging::Logger;
35use crate::batcher::Batcher;
36use crate::trace::{self, Description, SpanOf, Trace, TraceReader, Navigable, Builder, Cursor, BatchCursor, BatchDiff, BatchKey, BatchVal, BatchValOwn};
37
38use trace::wrappers::enter::{TraceEnter, enter_span};
39
40use super::TraceAgent;
41
42/// An arranged collection of `(K,V)` values.
43///
44/// An `Arranged` allows multiple differential operators to share the resources (communication,
45/// computation, memory) required to produce and maintain an indexed representation of a collection.
46pub struct Arranged<'scope, Tr: TraceReader> {
47    /// A stream containing arranged updates.
48    ///
49    /// This stream contains the same batches of updates the trace itself accepts, so there should
50    /// be no additional overhead to receiving these records. The batches can be navigated just as
51    /// the batches in the trace, by key and by value.
52    ///
53    /// Unchecked assumption: each update in a batch has a time greater or equal to some time in the
54    /// stamp of the message carrying it. In particular, a message with an empty stamp carries no updates.
55    pub stream: Stream<'scope, Tr::Time, Vec<SpanOf<Tr>>>,
56    /// A shared trace, updated by the `Arrange` operator and readable by others.
57    pub trace: Tr,
58}
59
60impl<'scope, Tr: TraceReader+Clone> Clone for Arranged<'scope, Tr> {
61    fn clone(&self) -> Self {
62        Arranged {
63            stream: self.stream.clone(),
64            trace: self.trace.clone(),
65        }
66    }
67}
68
69use ::timely::progress::timestamp::Refines;
70use timely::Container;
71
72impl<'scope, Tr: TraceReader> Arranged<'scope, Tr> {
73    /// Brings an arranged collection into a nested scope.
74    ///
75    /// This method produces a proxy trace handle that uses the same backing data, but acts as if the timestamps
76    /// have all been extended with an additional coordinate with the default value. The resulting collection does
77    /// not vary with the new timestamp coordinate.
78    pub fn enter<'inner, TInner>(self, child: Scope<'inner, TInner>) -> Arranged<'inner, TraceEnter<Tr, TInner>>
79    where
80        TInner: Refines<Tr::Time>+Lattice,
81    {
82        Arranged {
83            stream: self.stream.enter(child).map(enter_span),
84            trace: TraceEnter::make_from(self.trace),
85        }
86    }
87
88    /// Brings an arranged collection into a nested region.
89    ///
90    /// This method only applies to *regions*, which are subscopes with the same timestamp
91    /// as their containing scope. In this case, the trace type does not need to change.
92    pub fn enter_region<'inner>(self, child: Scope<'inner, Tr::Time>) -> Arranged<'inner, Tr> {
93        Arranged {
94            stream: self.stream.enter(child),
95            trace: self.trace,
96        }
97    }
98
99    /// Extracts a collection of any container from the stream of batches.
100    ///
101    /// This method is like `self.stream.flat_map`, except that it produces containers
102    /// directly, rather than form a container of containers as `flat_map` would.
103    pub fn as_container<I, L>(self, mut logic: L) -> crate::Collection<'scope, Tr::Time, I::Item>
104    where
105        I: IntoIterator<Item: Container>,
106        L: FnMut(SpanOf<Tr>) -> I+'static,
107    {
108        self.stream.unary(Pipeline, "AsContainer", move |_,_| move |input, output| {
109            input.for_each(|time, data| {
110                let mut session = output.session(&time);
111                for wrapper in data.drain(..) {
112                    for mut container in logic(wrapper) {
113                        session.give_container(&mut container);
114                    }
115                }
116            });
117        })
118        .as_collection()
119    }
120
121    /// Flattens the stream into a `VecCollection`.
122    ///
123    /// The underlying `Stream<T, Vec<SpanOf<T>>>` is a much more efficient way to access the data,
124    /// and this method should only be used when the data need to be transformed or exchanged, rather than
125    /// supplied as arguments to an operator using the same key-value structure.
126    pub fn as_collection<D: Data, L>(self, mut logic: L) -> VecCollection<'scope, Tr::Time, D, BatchDiff<Tr>>
127        where
128            Tr::Batch: Navigable,
129            BatchCursor<Tr>: Cursor<Time = Tr::Time>,
130            L: FnMut(BatchKey<'_, Tr>, BatchVal<'_, Tr>) -> D+'static,
131    {
132        self.flat_map_ref(move |key, val| Some(logic(key,val)))
133    }
134
135    /// Flattens the stream into a `VecCollection`.
136    ///
137    /// The underlying `Stream<T, Vec<SpanOf<T>>>` is a much more efficient way to access the data,
138    /// and this method should only be used when the data need to be transformed or exchanged, rather than
139    /// supplied as arguments to an operator using the same key-value structure.
140    ///
141    /// The method takes `K` and `V` as generic arguments, in order to constrain the reference types to support
142    /// cloning into owned types. If this bound does not work, the `as_collection` method allows arbitrary logic
143    /// on the reference types.
144    pub fn as_vecs<K, V>(self) -> VecCollection<'scope, Tr::Time, (K, V), BatchDiff<Tr>>
145    where
146        K: crate::ExchangeData,
147        V: crate::ExchangeData,
148        Tr::Batch: Navigable,
149        BatchCursor<Tr>: Cursor<Time = Tr::Time>,
150        for<'a> BatchCursor<Tr>: Cursor<Key<'a> = &'a K, Val<'a> = &'a V>,
151    {
152        self.flat_map_ref(move |key, val| [(key.clone(), val.clone())])
153    }
154
155    /// Extracts elements from an arrangement as a `VecCollection`.
156    ///
157    /// The supplied logic may produce an iterator over output values, allowing either
158    /// filtering or flat mapping as part of the extraction.
159    pub fn flat_map_ref<I, L>(self, logic: L) -> VecCollection<'scope, Tr::Time, I::Item, BatchDiff<Tr>>
160        where
161            Tr::Batch: Navigable,
162            BatchCursor<Tr>: Cursor<Time = Tr::Time>,
163            I: IntoIterator<Item: Data>,
164            L: FnMut(BatchKey<'_, Tr>, BatchVal<'_, Tr>) -> I+'static,
165    {
166        Self::flat_map_batches(self.stream, logic)
167    }
168
169    /// Extracts elements from a stream of batches as a `VecCollection`.
170    ///
171    /// The supplied logic may produce an iterator over output values, allowing either
172    /// filtering or flat mapping as part of the extraction.
173    ///
174    /// This method exists for streams of batches without the corresponding arrangement.
175    /// If you have the arrangement, its `flat_map_ref` method is equivalent to this.
176    pub fn flat_map_batches<I, L>(stream: Stream<'scope, Tr::Time, Vec<SpanOf<Tr>>>, mut logic: L) -> VecCollection<'scope, Tr::Time, I::Item, BatchDiff<Tr>>
177    where
178        Tr::Batch: Navigable,
179        BatchCursor<Tr>: Cursor<Time = Tr::Time>,
180        I: IntoIterator<Item: Data>,
181        L: FnMut(BatchKey<'_, Tr>, BatchVal<'_, Tr>) -> I+'static,
182    {
183        stream.unary(Pipeline, "AsCollection", move |_,_| move |input, output| {
184            input.for_each(|time, data| {
185                let mut session = output.session(&time);
186                for wrapper in data.iter() {
187                    let Some(batch) = wrapper.inner.as_ref() else { continue };
188                    let mut cursor = batch.cursor();
189                    while let Some(key) = cursor.get_key(batch) {
190                        while let Some(val) = cursor.get_val(batch) {
191                            for datum in logic(key, val) {
192                                cursor.map_times(batch, |time, diff| {
193                                    session.give((datum.clone(), <BatchCursor<Tr> as Cursor>::owned_time(time), <BatchCursor<Tr> as Cursor>::owned_diff(diff)));
194                                });
195                            }
196                            cursor.step_val(batch);
197                        }
198                        cursor.step_key(batch);
199                    }
200                }
201            });
202        })
203        .as_collection()
204    }
205}
206
207
208use crate::difference::Multiply;
209// Direct join implementations.
210impl<'scope, Tr1: TraceReader<Batch: Navigable>+'static> Arranged<'scope, Tr1> {
211    /// A convenience method to join and produce `VecCollection` output.
212    ///
213    /// Avoid this method, as it is likely to evolve into one without the `VecCollection` opinion.
214    pub fn join_core<Tr2,I,L,R1,R2,KC>(self, other: Arranged<'scope, Tr2>, mut result: L) -> VecCollection<'scope, Tr1::Time,I::Item,<R1 as Multiply<R2>>::Output>
215    where
216        Tr2: TraceReader<Batch: Navigable, Time=Tr1::Time>+Clone+'static,
217        // Pin the cursor diffs to named params `R1`/`R2`: a `Multiply` bound on a projection
218        // does not connect to its use-site (the solver normalizes the use but not the bound's
219        // subject), so we constrain plain params instead.
220        BatchCursor<Tr1>: Cursor<Diff = R1, Time = Tr1::Time, KeyContainer = KC>,
221        BatchCursor<Tr2>: Cursor<Diff = R2, Time = Tr1::Time>,
222        KC: BatchContainer,
223        for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
224        for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>>,
225        R1: Multiply<R2, Output: Semigroup+'static> + Clone,
226        I: IntoIterator<Item: Data>,
227        L: FnMut(KC::ReadItem<'_>,BatchVal<'_, Tr1>,BatchVal<'_, Tr2>)->I+'static
228    {
229        let mut result = move |k: KC::ReadItem<'_>, v1: BatchVal<'_, Tr1>, v2: BatchVal<'_, Tr2>, t: Tr1::Time, r1: &R1, r2: &R2| {
230            let r = (r1.clone()).multiply(r2);
231            result(k, v1, v2).into_iter().map(move |d| (d, t.clone(), r.clone()))
232        };
233
234        use crate::operators::join::join_traces;
235        join_traces::<_, _, _, _, crate::consolidation::ConsolidatingContainerBuilder<_>>(
236            self,
237            other,
238            "Join",
239            move |k, v1, v2, t, d1, d2, c| {
240                for datum in result(k, v1, v2, t, d1, d2) {
241                    c.push_into(datum);
242                }
243            }
244        )
245            .as_collection()
246    }
247}
248
249// Direct reduce implementations.
250use crate::difference::Abelian;
251use crate::trace::implementations::containers::BatchContainer;
252impl<'scope, Tr1: TraceReader<Batch: Navigable>+'static> Arranged<'scope, Tr1> {
253    /// A direct implementation of `ReduceCore::reduce_abelian`.
254    pub fn reduce_abelian<L, Bu, Tr2, KC, P>(self, name: &str, mut logic: L, push: P) -> Arranged<'scope, TraceAgent<Tr2>>
255    where
256        Tr2: Trace<Batch: Navigable, Time=Tr1::Time>+'static,
257        KC: BatchContainer,
258        BatchCursor<Tr1>: Cursor<Time = Tr1::Time, KeyContainer = KC>,
259        for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
260        for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>, ValOwn: Data, Time = Tr2::Time, Diff: Abelian>,
261        Bu: Builder<Time=Tr1::Time, Output: Into<Tr2::Batch>, Input: Default> + 'static,
262        L: FnMut(KC::ReadItem<'_>, &[(BatchVal<'_, Tr1>, BatchDiff<Tr1>)], &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>)+'static,
263        P: FnMut(&mut Bu::Input, KC::ReadItem<'_>, &mut Vec<(BatchValOwn<Tr2>, Tr2::Time, BatchDiff<Tr2>)>) + 'static,
264    {
265        self.reduce_core::<_,Bu,Tr2,KC,_>(name, move |key, input, output, change| {
266            if !input.is_empty() {
267                logic(key, input, change);
268            }
269            change.extend(output.drain(..).map(|(x,mut d)| { d.negate(); (x, d) }));
270            crate::consolidation::consolidate(change);
271        }, push)
272    }
273
274    /// A direct implementation of `ReduceCore::reduce_core`.
275    pub fn reduce_core<L, Bu, Tr2, KC, P>(self, name: &str, logic: L, push: P) -> Arranged<'scope, TraceAgent<Tr2>>
276    where
277        Tr2: Trace<Batch: Navigable, Time=Tr1::Time>+'static,
278        KC: BatchContainer,
279        BatchCursor<Tr1>: Cursor<Time = Tr1::Time, KeyContainer = KC>,
280        for<'a> BatchCursor<Tr1>: Cursor<Key<'a> = KC::ReadItem<'a>>,
281        for<'a> BatchCursor<Tr2>: Cursor<Key<'a> = KC::ReadItem<'a>, ValOwn: Data, Time = Tr2::Time>,
282        Bu: Builder<Time=Tr1::Time, Output: Into<Tr2::Batch>, Input: Default> + 'static,
283        L: FnMut(KC::ReadItem<'_>, &[(BatchVal<'_, Tr1>, BatchDiff<Tr1>)], &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>, &mut Vec<(BatchValOwn<Tr2>, BatchDiff<Tr2>)>)+'static,
284        P: FnMut(&mut Bu::Input, KC::ReadItem<'_>, &mut Vec<(BatchValOwn<Tr2>, Tr2::Time, BatchDiff<Tr2>)>) + 'static,
285    {
286        use crate::operators::reduce::reduce_trace;
287        reduce_trace::<_,Bu,_,KC,_,_>(self, name, logic, push)
288    }
289}
290
291impl<'scope, Tr: TraceReader> Arranged<'scope, Tr> {
292    /// Brings an arranged collection out of a nested region.
293    ///
294    /// This method only applies to *regions*, which are subscopes with the same timestamp
295    /// as their containing scope. In this case, the trace type does not need to change.
296    pub fn leave_region<'outer>(self, outer: Scope<'outer, Tr::Time>) -> Arranged<'outer, Tr> {
297        use timely::dataflow::operators::Leave;
298        Arranged {
299            stream: self.stream.leave(outer),
300            trace: self.trace,
301        }
302    }
303}
304
305/// Arranges a stream of updates by a key, configured with a name and a parallelization contract.
306///
307/// This operator arranges a stream of values into a shared trace, whose contents it maintains.
308/// It uses the supplied parallelization contract to distribute the data, which does not need to
309/// be consistently by key (though this is the most common).
310pub fn arrange_core<'scope, P, C, Ba, Tr>(
311    stream: Stream<'scope, Tr::Time, C>,
312    pact: P,
313    name: &str,
314    batcher: impl FnOnce(Option<Logger>, usize) -> Ba,
315) -> Arranged<'scope, TraceAgent<Tr>>
316where
317    C: Container + Clone + 'static,
318    P: ParallelizationContract<Tr::Time, C>,
319    Ba: Batcher<C, Time = Tr::Time, Output: Into<Tr::Batch>> + 'static,
320    Tr: Trace+'static,
321{
322    // The `Arrange` operator is tasked with reacting to an advancing input
323    // frontier by producing the sequence of batches whose lower and upper
324    // bounds are those frontiers, containing updates at times greater or
325    // equal to lower and not greater or equal to upper.
326    //
327    // The operator uses its batch type's `Batcher`, which accepts update
328    // triples and responds to requests to "seal" batches (presented as new
329    // upper frontiers).
330    //
331    // Each sealed batch is presented to the trace, and if at all possible
332    // transmitted along the outgoing channel. Empty batches may not have
333    // a corresponding capability, as they are only retained for actual data
334    // held by the batcher, which may prevents the operator from sending an
335    // empty batch.
336
337    let mut reader: Option<TraceAgent<Tr>> = None;
338
339    // fabricate a data-parallel operator that holds capabilities and consults its input frontier.
340    let reader_ref = &mut reader;
341    let scope = stream.scope();
342
343    let stream = stream.unary_frontier(pact, name, move |_capability, info| {
344
345        // Acquire a logger for arrange events.
346        let logger = scope.worker().logger_for::<crate::logging::DifferentialEventBuilder>("differential/arrange").map(Into::into);
347
348        // Where we will deposit received updates, and from which we extract batches.
349        let mut batcher = batcher(logger.clone(), info.global_id);
350
351        // Capabilities for the lower envelope of updates in `batcher`.
352        let mut capabilities = Antichain::<Capability<Tr::Time>>::new();
353
354        let activator = Some(scope.activator_for(std::rc::Rc::clone(&info.address)));
355        let mut empty_trace = Tr::new(info.clone(), logger.clone(), activator);
356        // If there is default exertion logic set, install it.
357        if let Some(exert_logic) = scope.worker().config().get::<trace::ExertionLogic>("differential/default_exert_logic").cloned() {
358            empty_trace.set_exert_logic(exert_logic);
359        }
360
361        let (reader_local, mut writer) = TraceAgent::new(empty_trace, info, logger);
362
363        *reader_ref = Some(reader_local);
364
365        // Initialize to the minimal input frontier.
366        let mut prev_frontier = Antichain::from_elem(Tr::Time::minimum());
367
368        move |(input, frontier), output| {
369
370            // As we receive data, we need to (i) stash the data and (ii) keep *enough* capabilities.
371            // We don't have to keep all capabilities, but we need to be able to form output messages
372            // when we realize that time intervals are complete.
373
374            input.for_each(|cap, data| {
375                for capability in cap.retain_stamp(0).iter() {
376                    capabilities.insert(capability.clone());
377                }
378                batcher.insert(data);
379            });
380
381            // The frontier may have advanced by multiple elements. We extract one batch for the
382            // whole advance, and send it stamped with every held capability it retires.
383
384            // Assert that the frontier never regresses.
385            assert!(PartialOrder::less_equal(&prev_frontier.borrow(), &frontier.frontier()));
386
387            // Test to see if strict progress has occurred, which happens whenever the new
388            // frontier isn't equal to the previous. It is only in this case that we have any
389            // data processing to do.
390            if prev_frontier.borrow() != frontier.frontier() {
391                // There are two cases to handle with some care:
392                //
393                // 1. If any held capabilities are not in advance of the new input frontier,
394                //    we must carve out updates now in advance of the new input frontier and
395                //    transmit them as a batch, stamped with the capabilities they retire.
396                //
397                // 2. If there are no held capabilities in advance of the new input frontier,
398                //    then there are no updates not in advance of the new input frontier and
399                //    we can simply create an empty input batch with the new upper frontier
400                //    and feed this to the trace agent (but not along the timely output).
401
402                // If there is at least one capability not in advance of the input frontier ...
403                if capabilities.elements().iter().any(|c| !frontier.less_equal(c.time())) {
404
405                    // The capabilities to retire: those not in advance of the input frontier.
406                    // Each update extracted below is greater or equal to one of them, as updates
407                    // supported only by the remaining capabilities are in advance of the input
408                    // frontier and remain in the batcher.
409                    let retired = capabilities
410                        .elements()
411                        .iter()
412                        .filter(|c| !frontier.less_equal(c.time()))
413                        .cloned()
414                        .collect::<CapabilitySet<_>>();
415
416                    // Extract all updates not in advance of the input frontier, as one batch.
417                    // The batch spans the interval from the previously reported frontier to the
418                    // current one, which is exactly the interval the batcher carves out.
419                    let description = Description::new(
420                        prev_frontier.clone(),
421                        frontier.frontier().to_owned(),
422                        Antichain::from_elem(Tr::Time::minimum()),
423                    );
424                    let (extracted, retained) = batcher.extract(frontier.frontier());
425
426                    let batch = trace::Span::new(description, extracted.map(Into::into));
427
428                    let stamp = retired.iter().map(|c| c.time().clone()).collect::<Stamp<_>>();
429                    writer.insert(batch.clone(), stamp);
430
431                    // send the batch to downstream consumers, empty or not.
432                    output.session(&retired).give(batch);
433
434                    // Having extracted and sent the batch of updates not in advance of the input
435                    // frontier, we downgrade all capabilities to match the batcher's lower update
436                    // frontier.
437                    // This may involve discarding capabilities, which is fine as any new updates arrive
438                    // in messages with new capabilities.
439                    let mut new_capabilities = Antichain::new();
440                    for time in retained.iter() {
441                        if let Some(capability) = capabilities.elements().iter().find(|c| c.time().less_equal(time)) {
442                            new_capabilities.insert(capability.delayed(time));
443                        }
444                        else {
445                            panic!("failed to find capability");
446                        }
447                    }
448                    capabilities = new_capabilities;
449                }
450                else {
451                    // Announce progress updates, even without data. No held capability precedes
452                    // the input frontier, so no update does either, and the batcher has nothing
453                    // to extract.
454                    writer.seal(frontier.frontier().to_owned());
455                }
456
457                prev_frontier.clear();
458                prev_frontier.extend(frontier.frontier().iter().cloned());
459            }
460
461            writer.exert();
462        }
463    });
464
465    Arranged { stream, trace: reader.unwrap() }
466}