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}