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