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}