Skip to main content

mz_storage/source/
source_reader_pipeline.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Types related to the creation of dataflow raw sources.
11//!
12//! Raw sources are differential dataflow  collections of data directly produced by the
13//! upstream service. The main export of this module is [`create_raw_source`],
14//! which turns [`RawSourceCreationConfig`]s into the aforementioned streams.
15//!
16//! The full source, which is the _differential_ stream that represents the actual object
17//! created by a `CREATE SOURCE` statement, is created by composing
18//! [`create_raw_source`] with
19//! decoding, `SourceEnvelope` rendering, and more.
20//!
21
22// https://github.com/tokio-rs/prost/issues/237
23#![allow(missing_docs)]
24#![allow(clippy::needless_borrow)]
25
26use std::cell::RefCell;
27use std::collections::btree_map::Entry;
28use std::collections::{BTreeMap, VecDeque};
29use std::hash::{Hash, Hasher};
30use std::rc::Rc;
31use std::sync::Arc;
32use std::time::Duration;
33
34use differential_dataflow::lattice::Lattice;
35use differential_dataflow::{AsCollection, Hashable, VecCollection};
36use futures::stream::StreamExt;
37use mz_ore::cast::CastFrom;
38use mz_ore::collections::CollectionExt;
39use mz_ore::now::NowFn;
40use mz_persist_client::cache::PersistClientCache;
41use mz_repr::{Diff, GlobalId, RelationDesc, Row};
42use mz_storage_types::configuration::StorageConfiguration;
43use mz_storage_types::controller::CollectionMetadata;
44use mz_storage_types::errors::DataflowError;
45use mz_storage_types::sources::{SourceConnection, SourceExport, SourceTimestamp};
46use mz_timely_util::antichain::AntichainExt;
47use mz_timely_util::builder_async::{OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton};
48use mz_timely_util::capture::PusherCapture;
49use mz_timely_util::operator::ConcatenateFlatten;
50use mz_timely_util::reclock::reclock;
51use timely::PartialOrder;
52use timely::container::CapacityContainerBuilder;
53use timely::dataflow::channels::pact::Pipeline;
54use timely::dataflow::operators::capture::capture::Capture;
55use timely::dataflow::operators::core::Map as _;
56use timely::dataflow::operators::generic::OutputBuilder;
57use timely::dataflow::operators::generic::builder_rc::OperatorBuilder as OperatorBuilderRc;
58use timely::dataflow::operators::vec::Broadcast;
59use timely::dataflow::operators::{CapabilitySet, InspectCore, Leave};
60use timely::dataflow::{Scope, StreamVec};
61use timely::order::TotalOrder;
62use timely::progress::frontier::MutableAntichain;
63use timely::progress::{Antichain, Timestamp};
64use tokio::sync::{Semaphore, watch};
65use tokio_stream::wrappers::WatchStream;
66use tracing::trace;
67
68use crate::healthcheck::{HealthStatusMessage, HealthStatusUpdate};
69use crate::metrics::StorageMetrics;
70use crate::metrics::source::SourceMetrics;
71use crate::source::reclock::ReclockOperator;
72use crate::source::types::{
73    Probe, ResumeUppers, SourceMessage, SourceOutput, SourceRender, StackedCollection,
74};
75use crate::statistics::SourceStatistics;
76
77/// Shared configuration information for all source types. This is used in the
78/// `create_raw_source` functions, which produce raw sources.
79#[derive(Clone)]
80pub struct RawSourceCreationConfig {
81    /// The name to attach to the underlying timely operator.
82    pub name: String,
83    /// The ID of this instantiation of this source.
84    pub id: GlobalId,
85    /// The details of the outputs from this ingestion.
86    pub source_exports: BTreeMap<GlobalId, SourceExport<CollectionMetadata>>,
87    /// The ID of the worker on which this operator is executing
88    pub worker_id: usize,
89    /// The total count of workers
90    pub worker_count: usize,
91    /// Granularity with which timestamps should be closed (and capabilities
92    /// downgraded).
93    pub timestamp_interval: Duration,
94    /// The function to return a now time.
95    pub now_fn: NowFn,
96    /// The metrics & registry that each source instantiates.
97    pub metrics: StorageMetrics,
98    /// The upper frontier this source should resume ingestion at
99    pub as_of: Antichain<mz_repr::Timestamp>,
100    /// For each source export, the upper frontier this source should resume ingestion at in the
101    /// system time domain.
102    pub resume_uppers: BTreeMap<GlobalId, Antichain<mz_repr::Timestamp>>,
103    /// For each source export, the upper frontier this source should resume ingestion at in the
104    /// source time domain.
105    ///
106    /// Since every source has a different timestamp type we carry the timestamps of this frontier
107    /// in an encoded `Vec<Row>` form which will get decoded once we reach the connection
108    /// specialized functions.
109    pub source_resume_uppers: BTreeMap<GlobalId, Vec<Row>>,
110    /// A handle to the persist client cache
111    pub persist_clients: Arc<PersistClientCache>,
112    /// Collection of `SourceStatistics` for source and exports to share updates.
113    pub statistics: BTreeMap<GlobalId, SourceStatistics>,
114    /// Enables reporting the remap operator's write frontier.
115    pub shared_remap_upper: Rc<RefCell<Antichain<mz_repr::Timestamp>>>,
116    /// Configuration parameters, possibly from LaunchDarkly
117    pub config: StorageConfiguration,
118    /// The ID of this source remap/progress collection.
119    pub remap_collection_id: GlobalId,
120    /// The storage metadata for the remap/progress collection
121    pub remap_metadata: CollectionMetadata,
122    // A semaphore that should be acquired by async operators in order to signal that upstream
123    // operators should slow down.
124    pub busy_signal: Arc<Semaphore>,
125}
126
127/// Reduced version of [`RawSourceCreationConfig`] that is used when rendering
128/// each export.
129#[derive(Clone)]
130pub struct SourceExportCreationConfig {
131    /// The ID of this instantiation of this source.
132    pub id: GlobalId,
133    /// The ID of the worker on which this operator is executing
134    pub worker_id: usize,
135    /// The metrics & registry that each source instantiates.
136    pub metrics: StorageMetrics,
137    /// Place to share statistics updates with storage state.
138    pub source_statistics: SourceStatistics,
139}
140
141impl RawSourceCreationConfig {
142    /// Returns the worker id responsible for handling the given partition.
143    pub fn responsible_worker<P: Hash>(&self, partition: P) -> usize {
144        let mut h = std::hash::DefaultHasher::default();
145        (self.id, partition).hash(&mut h);
146        let key = usize::cast_from(h.finish());
147        key % self.worker_count
148    }
149
150    /// Returns true if this worker is responsible for handling the given partition.
151    pub fn responsible_for<P: Hash>(&self, partition: P) -> bool {
152        self.responsible_worker(partition) == self.worker_id
153    }
154}
155
156/// Creates a source dataflow operator graph from a source connection. The type of SourceConnection
157/// determines the type of connection that _should_ be created.
158///
159/// This is also the place where _reclocking_
160/// (<https://github.com/MaterializeInc/materialize/blob/main/doc/developer/design/20210714_reclocking.md>)
161/// happens.
162///
163/// See the [`source` module docs](crate::source) for more details about how raw
164/// sources are used.
165///
166/// The `committed_uppers` parameter contains one stream per export whose frontier advances
167/// whenever times are durably recorded for that export. They are reclocked into the
168/// [`ResumeUppers`] the source observes.
169///
170/// Alongside the reclocked exports this returns a no-data stream whose frontier is the remap upper.
171pub fn create_raw_source<'scope, 'root, C>(
172    scope: Scope<'scope, mz_repr::Timestamp>,
173    root_scope: Scope<'root, ()>,
174    storage_state: &crate::storage_state::StorageState,
175    committed_uppers: BTreeMap<GlobalId, StreamVec<'scope, mz_repr::Timestamp, ()>>,
176    config: &RawSourceCreationConfig,
177    source_connection: C,
178    start_signal: impl std::future::Future<Output = ()> + 'static,
179) -> (
180    BTreeMap<
181        GlobalId,
182        VecCollection<
183            'scope,
184            mz_repr::Timestamp,
185            Result<SourceOutput<C::Time>, DataflowError>,
186            Diff,
187        >,
188    >,
189    StreamVec<'root, (), HealthStatusMessage>,
190    StreamVec<'scope, mz_repr::Timestamp, ()>,
191    Vec<PressOnDropButton>,
192)
193where
194    C: SourceConnection + SourceRender + Clone + 'static,
195{
196    let worker_id = config.worker_id;
197    let id = config.id;
198
199    let mut tokens = vec![];
200
201    let (probed_upper_tx, probed_upper_rx) = watch::channel(None);
202
203    let source_metrics = Arc::new(config.metrics.get_source_metrics(id, worker_id));
204
205    let timestamp_desc = source_connection.timestamp_desc();
206
207    let (remap_collection, remap_token) = remap_operator(
208        scope,
209        storage_state,
210        config.clone(),
211        probed_upper_rx,
212        timestamp_desc,
213    );
214    // Need to broadcast the remap changes to all workers.
215    let remap_collection = remap_collection.inner.broadcast().as_collection();
216    tokens.push(remap_token);
217
218    // Drops the bidings, as this stream is only used to track the remap upper, which drives
219    // ceiling calculation in the persist sink during snapshots.
220    let remap_upper = remap_collection
221        .inner
222        .clone()
223        .flat_map::<Vec<()>, _, _>(|_| None::<()>);
224
225    let resume_uppers = reclock_committed_upper(
226        remap_collection.clone(),
227        config.as_of.clone(),
228        committed_uppers,
229        id,
230        Arc::clone(&source_metrics),
231    );
232
233    let mut reclocked_exports = BTreeMap::new();
234
235    let reclocked_exports2 = &mut reclocked_exports;
236    let (health, source_tokens) = root_scope.scoped("SourceTimeDomain", move |scope| {
237        let (exports, health_stream, source_tokens) = source_render_operator(
238            scope,
239            config,
240            source_connection,
241            probed_upper_tx,
242            resume_uppers,
243            start_signal,
244        );
245
246        for (id, export) in exports {
247            let (reclock_pusher, reclocked) =
248                reclock(remap_collection.clone(), config.as_of.clone());
249            export
250                .inner
251                .map(move |(result, from_time, diff)| {
252                    let result = match result {
253                        Ok(msg) => Ok(SourceOutput {
254                            key: msg.key,
255                            value: msg.value,
256                            metadata: msg.metadata,
257                            from_time: from_time.clone(),
258                        }),
259                        Err(err) => Err(err),
260                    };
261                    (result, from_time, diff)
262                })
263                .capture_into(PusherCapture(reclock_pusher));
264            reclocked_exports2.insert(id, reclocked);
265        }
266
267        (health_stream.leave(root_scope), source_tokens)
268    });
269
270    tokens.extend(source_tokens);
271
272    (reclocked_exports, health, remap_upper, tokens)
273}
274
275/// Renders the source dataflow fragment from the given [SourceConnection]. This returns a
276/// collection timestamped with the source specific timestamp type.
277fn source_render_operator<'scope, C>(
278    scope: Scope<'scope, C::Time>,
279    config: &RawSourceCreationConfig,
280    source_connection: C,
281    probed_upper_tx: watch::Sender<Option<Probe<C::Time>>>,
282    resume_uppers: impl futures::Stream<Item = ResumeUppers<C::Time>> + 'static,
283    start_signal: impl std::future::Future<Output = ()> + 'static,
284) -> (
285    BTreeMap<GlobalId, StackedCollection<'scope, C::Time, Result<SourceMessage, DataflowError>>>,
286    StreamVec<'scope, C::Time, HealthStatusMessage>,
287    Vec<PressOnDropButton>,
288)
289where
290    C: SourceRender + 'static,
291{
292    let source_id = config.id;
293    let worker_id = config.worker_id;
294
295    let resume_uppers = resume_uppers.inspect(move |uppers| {
296        trace!(
297            %uppers,
298            "timely-{worker_id} source({source_id}) received resume uppers"
299        );
300    });
301
302    let (exports, health, probe_stream, tokens) =
303        source_connection.render(scope, config, resume_uppers, start_signal);
304
305    let mut export_collections = BTreeMap::new();
306
307    let source_metrics = config.metrics.get_source_metrics(config.id, worker_id);
308
309    // Compute the overall resume upper to report for the ingestion
310    let resume_upper = Antichain::from_iter(
311        config
312            .resume_uppers
313            .values()
314            .flat_map(|f| f.iter().cloned()),
315    );
316    source_metrics
317        .resume_upper
318        .set(mz_persist_client::metrics::encode_ts_metric(&resume_upper));
319
320    let mut health_streams = vec![];
321
322    for (id, export) in exports {
323        let name = format!("SourceGenericStats({})", id);
324        let mut builder = OperatorBuilderRc::new(name, scope.clone());
325
326        let (health_output, derived_health) = builder.new_output();
327        let mut health_output =
328            OutputBuilder::<_, CapacityContainerBuilder<_>>::from(health_output);
329        health_streams.push(derived_health);
330
331        let (output, new_export) = builder.new_output();
332        let mut output = OutputBuilder::<_, CapacityContainerBuilder<_>>::from(output);
333
334        let mut input = builder.new_input(export.inner, Pipeline);
335        export_collections.insert(id, new_export.as_collection());
336
337        let bytes_read_counter = config.metrics.source_defs.bytes_read.clone();
338        let source_statistics = config
339            .statistics
340            .get(&id)
341            .expect("statistics initialized")
342            .clone();
343
344        builder.build(move |mut caps| {
345            let mut health_cap = Some(caps.remove(0));
346
347            move |frontiers| {
348                let mut last_status = None;
349                let mut health_output = health_output.activate();
350
351                if frontiers[0].is_empty() {
352                    health_cap = None;
353                    return;
354                }
355                let health_cap = health_cap.as_mut().unwrap();
356
357                input.for_each(|cap, data| {
358                    for (message, _, _) in data.iter() {
359                        match message {
360                            Ok(message) => {
361                                source_statistics.inc_messages_received_by(1);
362                                let key_len = u64::cast_from(message.key.byte_len());
363                                let value_len = u64::cast_from(message.value.byte_len());
364                                bytes_read_counter.inc_by(key_len + value_len);
365                                source_statistics.inc_bytes_received_by(key_len + value_len);
366                            }
367                            Err(error) => {
368                                // All errors coming into the data stream are definite.
369                                // Downstream consumers of this data will preserve this
370                                // status.
371                                let hint = match error {
372                                    DataflowError::SourceError(e) if e.hint.is_some() => {
373                                        e.hint.as_deref().map(str::to_string)
374                                    }
375                                    _ => Some(
376                                        "retracting the errored value may resume the source"
377                                            .to_string(),
378                                    ),
379                                };
380                                let update = HealthStatusUpdate::stalled(error.to_string(), hint);
381                                let status = HealthStatusMessage {
382                                    id: Some(id),
383                                    namespace: C::STATUS_NAMESPACE.clone(),
384                                    update,
385                                };
386                                if last_status.as_ref() != Some(&status) {
387                                    last_status = Some(status.clone());
388                                    health_output.session(&health_cap).give(status);
389                                }
390                            }
391                        }
392                    }
393                    let mut output = output.activate();
394                    output.session(&cap).give_container(data);
395                });
396            }
397        });
398    }
399
400    // Broadcasting does more work than necessary, which would be to exchange the probes to the
401    // worker that will be the one minting the bindings but we'd have to thread this information
402    // through and couple the two functions enough that it's not worth the optimization (I think).
403    // Use `InspectCore::inspect_container` instead of `Inspect::inspect`.
404    // `Inspect` carries a `where for<'a> &'a C: IntoIterator` bound, and on
405    // macOS the solver can satisfy that bound by chasing objc2's
406    // `&Retained<T>: IntoIterator` blanket impl into an endless
407    // `Retained<Retained<…>>` chain, overflowing the recursion limit.
408    // `InspectCore` has no such bound, so the cascade never starts. We
409    // iterate the container by hand to recover the per-item callback.
410    probe_stream.broadcast().inspect_container(move |event| {
411        if let Ok((_, data)) = event {
412            for probe in data {
413                // We don't care if the receiver is gone
414                let _ = probed_upper_tx.send(Some(probe.clone()));
415            }
416        }
417    });
418
419    (
420        export_collections,
421        health.concatenate_flatten::<_, CapacityContainerBuilder<_>>(health_streams),
422        tokens,
423    )
424}
425
426/// Mints new contents for the remap shard based on summaries about the source
427/// upper it receives from the raw reader operators.
428///
429/// Only one worker will be active and write to the remap shard. All source
430/// upper summaries will be exchanged to it.
431fn remap_operator<'scope, FromTime>(
432    scope: Scope<'scope, mz_repr::Timestamp>,
433    storage_state: &crate::storage_state::StorageState,
434    config: RawSourceCreationConfig,
435    mut probed_upper: watch::Receiver<Option<Probe<FromTime>>>,
436    remap_relation_desc: RelationDesc,
437) -> (
438    VecCollection<'scope, mz_repr::Timestamp, FromTime, Diff>,
439    PressOnDropButton,
440)
441where
442    FromTime: SourceTimestamp,
443{
444    let RawSourceCreationConfig {
445        name,
446        id,
447        source_exports: _,
448        worker_id,
449        worker_count,
450        timestamp_interval: _,
451        remap_metadata,
452        as_of,
453        resume_uppers: _,
454        source_resume_uppers: _,
455        metrics: _,
456        now_fn,
457        persist_clients,
458        statistics: _,
459        shared_remap_upper,
460        config: _,
461        remap_collection_id,
462        busy_signal: _,
463    } = config;
464
465    let read_only_rx = storage_state.read_only_rx.clone();
466    let error_handler = storage_state.error_handler("remap_operator", id);
467
468    let chosen_worker = usize::cast_from(id.hashed() % u64::cast_from(worker_count));
469    let active_worker = chosen_worker == worker_id;
470
471    let operator_name = format!("remap({})", id);
472    let mut remap_op = AsyncOperatorBuilder::new(operator_name, scope.clone());
473    let (remap_output, remap_stream) = remap_op.new_output::<CapacityContainerBuilder<_>>();
474
475    let button = remap_op.build(move |capabilities| async move {
476        if !active_worker {
477            // This worker is not writing, so make sure it's "taken out" of the
478            // calculation by advancing to the empty frontier.
479            shared_remap_upper.borrow_mut().clear();
480            return;
481        }
482
483        let mut cap_set = CapabilitySet::from_elem(capabilities.into_element());
484
485        let remap_handle = crate::source::reclock::compat::PersistHandle::<FromTime, _>::new(
486            Arc::clone(&persist_clients),
487            read_only_rx,
488            remap_metadata.clone(),
489            as_of.clone(),
490            shared_remap_upper,
491            id,
492            "remap",
493            worker_id,
494            worker_count,
495            remap_relation_desc,
496            remap_collection_id,
497        )
498        .await;
499
500        let remap_handle = match remap_handle {
501            Ok(handle) => handle,
502            Err(e) => {
503                error_handler
504                    .report_and_stop(
505                        e.context(format!("Failed to create remap handle for source {name}")),
506                    )
507                    .await
508            }
509        };
510
511        let (mut timestamper, mut initial_batch) = ReclockOperator::new(remap_handle).await;
512
513        // Emit initial snapshot of the remap_shard, bootstrapping
514        // downstream reclock operators.
515        trace!(
516            "timely-{worker_id} remap({id}) emitting remap snapshot: trace_updates={:?}",
517            &initial_batch.updates
518        );
519
520        let cap = cap_set.delayed(cap_set.first().unwrap());
521        remap_output.give_container(&cap, &mut initial_batch.updates);
522        drop(cap);
523        cap_set.downgrade(initial_batch.upper);
524
525        let mut prev_probe_ts: Option<mz_repr::Timestamp> = None;
526
527        while !cap_set.is_empty() {
528            // We only mint bindings after a successful probe.
529            let new_probe = probed_upper
530                .wait_for(|new_probe| match (prev_probe_ts, new_probe) {
531                    (None, Some(_)) => true,
532                    (Some(prev_ts), Some(new)) => prev_ts < new.probe_ts,
533                    _ => false,
534                })
535                .await
536                .map(|probe| (*probe).clone())
537                .unwrap_or_else(|_| {
538                    Some(Probe {
539                        probe_ts: now_fn().into(),
540                        upstream_frontier: Antichain::new(),
541                    })
542                });
543
544            let probe = new_probe.expect("known to be Some");
545            prev_probe_ts = Some(probe.probe_ts);
546
547            let binding_ts = probe.probe_ts;
548            let cur_source_upper = probe.upstream_frontier;
549
550            let new_into_upper = Antichain::from_elem(binding_ts.step_forward());
551
552            let mut remap_trace_batch = timestamper
553                .mint(binding_ts, new_into_upper, cur_source_upper.borrow())
554                .await;
555
556            trace!(
557                "timely-{worker_id} remap({id}) minted new bindings: \
558                updates={:?} \
559                source_upper={} \
560                trace_upper={}",
561                &remap_trace_batch.updates,
562                cur_source_upper.pretty(),
563                remap_trace_batch.upper.pretty()
564            );
565
566            let cap = cap_set.delayed(cap_set.first().unwrap());
567            remap_output.give_container(&cap, &mut remap_trace_batch.updates);
568            cap_set.downgrade(remap_trace_batch.upper);
569        }
570    });
571
572    (remap_stream.as_collection(), button.press_on_drop())
573}
574
575/// Reclocks the per-export `IntoTime` committed upper streams into `FromTime` [`ResumeUppers`].
576/// This is used for the virtual (through persist) feedback edge so that we convert the `IntoTime`
577/// resumption frontiers into the `FromTime` frontiers that are used with the source's
578/// `OffsetCommiter`.
579fn reclock_committed_upper<'scope, T, FromTime>(
580    bindings: VecCollection<'scope, T, FromTime, Diff>,
581    as_of: Antichain<T>,
582    committed_uppers: BTreeMap<GlobalId, StreamVec<'scope, T, ()>>,
583    id: GlobalId,
584    metrics: Arc<SourceMetrics>,
585) -> impl futures::stream::Stream<Item = ResumeUppers<FromTime>> + 'static
586where
587    T: Timestamp + Lattice + TotalOrder,
588    FromTime: SourceTimestamp,
589{
590    // Only used within this function, rather than create a comment to explain the fields.
591    struct ExportState<T, FromTime> {
592        id: GlobalId,
593        input_index: usize,
594        /// The committed upper that `source_upper` and `applied` reflect.
595        committed_upper: Antichain<T>,
596        source_upper: MutableAntichain<FromTime>,
597        applied: usize,
598    }
599
600    impl<T: Timestamp, FromTime: Timestamp> ExportState<T, FromTime> {
601        fn reclocked_upper(&self) -> Antichain<FromTime> {
602            if self.committed_upper.is_empty() {
603                Antichain::new()
604            } else {
605                self.source_upper.frontier().to_owned()
606            }
607        }
608    }
609
610    let (tx, rx) = watch::channel(ResumeUppers {
611        source: None,
612        exports: BTreeMap::new(),
613    });
614    let scope = bindings.scope().clone();
615
616    let name = format!("ReclockCommitUpper({id})");
617    let mut builder = OperatorBuilderRc::new(name, scope);
618
619    let mut bindings = builder.new_input(bindings.inner.clone(), Pipeline);
620
621    let mut exports: Vec<_> = committed_uppers
622        .into_iter()
623        .map(|(export_id, committed_upper)| {
624            let input_index = builder.shape().inputs();
625            // not reading data, just the frontiers
626            let _ = builder.new_input(committed_upper, Pipeline);
627            ExportState {
628                id: export_id,
629                input_index,
630                committed_upper: Antichain::from_elem(T::minimum()),
631                source_upper: MutableAntichain::new(),
632                applied: 0,
633            }
634        })
635        .collect();
636
637    builder.build(move |_| {
638        // Remap bindings beyond the upper
639        use timely::progress::ChangeBatch;
640        let mut accepted_times: ChangeBatch<(T, FromTime)> = ChangeBatch::new();
641        // The upper frontier of the bindings
642        let mut bindings_upper = Antichain::from_elem(Timestamp::minimum());
643        // Remap bindings not beyond upper that some export has not yet applied, in `into` order.
644        // This is a shared queue of remap bindings. A binding is retained until every export has
645        // applied it.
646        let mut ready_times = VecDeque::new();
647        // The number of bindings dropped from the front of `ready_times`, which makes the
648        // per-export positions absolute rather than deque indices.
649        let mut drained = 0;
650        // Indices of the exports whose reclocked upper may have changed in this invocation.
651        let mut touched = Vec::new();
652
653        move |frontiers| {
654            // Accept new bindings
655            bindings.for_each(|_, data| {
656                accepted_times.extend(data.drain(..).map(|(from, mut into, diff)| {
657                    into.advance_by(as_of.borrow());
658                    ((into, from), diff.into_inner())
659                }));
660            });
661            // Extract ready bindings
662            let new_bindings_upper = frontiers[0].frontier();
663            let ready_before = ready_times.len();
664            if PartialOrder::less_than(&bindings_upper.borrow(), &new_bindings_upper) {
665                bindings_upper = new_bindings_upper.to_owned();
666                // Drain consolidated accepted times not greater or equal to `bindings_upper` into `ready_times`.
667                // Retain accepted times greater or equal to `bindings_upper` in
668                let mut pending_times = std::mem::take(&mut accepted_times).into_inner();
669                // These should already be sorted, as part of `.into_inner()`, but sort defensively in case.
670                pending_times.sort_unstable_by(|a, b| a.0.cmp(&b.0));
671                for ((into, from), diff) in pending_times.drain(..) {
672                    if !bindings_upper.less_equal(&into) {
673                        ready_times.push_back((from, into, diff));
674                    } else {
675                        accepted_times.update((into, from), diff);
676                    }
677                }
678            }
679            let bindings_ready = ready_times.len() > ready_before;
680
681            // The received times only accumulate correctly for times beyond the as_of.
682            if as_of.iter().all(|t| !bindings_upper.less_equal(t)) {
683                let mut all_beyond_as_of = true;
684                // The export with the least committed upper. Its reclocked upper is the
685                // source-wide one, because t1 <= t2 => remap[t1] <= remap[t2].
686                let mut least_committed_upper: Option<(T, usize)> = None;
687                let mut min_applied = drained + ready_times.len();
688                for (index, export) in exports.iter_mut().enumerate() {
689                    let committed_upper = frontiers[export.input_index].frontier();
690                    if !as_of.iter().all(|t| !committed_upper.less_equal(t)) {
691                        all_beyond_as_of = false;
692                        min_applied = min_applied.min(export.applied);
693                        continue;
694                    }
695                    let advanced =
696                        PartialOrder::less_than(&export.committed_upper.borrow(), &committed_upper);
697                    if advanced {
698                        export.committed_upper = committed_upper.to_owned();
699                    }
700                    // We have committed this export up until `committed_upper`. Because we have
701                    // required that IntoTime is a total order this will be either a singleton set
702                    // or the empty set.
703                    //
704                    // * Case 1: committed_upper is the empty set {}
705                    //
706                    // There won't be any future IntoTime timestamps that we will produce so we can
707                    // provide feedback to the source that it can forget about everything.
708                    //
709                    // * Case 2: committed_upper is a singleton set {t_next}
710                    //
711                    // We know that t_next cannot be the minimum timestamp because we have required
712                    // that all times of the as_of frontier are not beyond some time of
713                    // committed_upper. Therefore t_next has a predecessor timestamp t_prev.
714                    //
715                    // We don't know what remap[t_next] is yet, but we do know that we will have to
716                    // emit all source updates `u: remap[t_prev] <= time(u) <= remap[t_next]`.
717                    // Since `t_next` is the minimum undetermined timestamp and we know that t1 <=
718                    // t2 => remap[t1] <= remap[t2] we know that we will never need any source
719                    // updates `u: !(remap[t_prev] <= time(u))`.
720                    //
721                    // Therefore we can provide feedback to the source that it can forget about any
722                    // updates that are not beyond remap[t_prev].
723                    //
724                    // Important: We are *NOT* saying that the source can *compact* its data using
725                    // remap[t_prev] as the compaction frontier. If the source were to compact its
726                    // collection to remap[t_prev] we would lose the distinction between updates
727                    // that happened *at* t_prev versus updates that happened ealier and were
728                    // advanced to t_prev. If the source needs to communicate a compaction frontier
729                    // upstream then the specific source implementation needs to further adjust the
730                    // reclocked committed_upper and calculate a suitable compaction frontier in
731                    // the same way we adjust uppers of collections in the controller with the
732                    // LagWriteFrontier read policy.
733                    //
734                    // == What about IntoTime times that are general lattices?
735                    //
736                    // Reversing the upper for a general lattice is much more involved but it boils
737                    // down to computing the meet of all the times in `committed_upper` and then
738                    // treating that as `t_next` (I think). Until we need to deal with that though
739                    // we can just assume TotalOrder.
740                    match committed_upper.as_option() {
741                        Some(t_next) => {
742                            // The remap input and this export's committed upper input advance
743                            // independently, so bindings below `t_next` can become ready after
744                            // the export committed `t_next`. Either input moving can therefore
745                            // add bindings for this export to apply.
746                            if advanced || bindings_ready {
747                                let start = export.applied - drained;
748                                let end = ready_times.partition_point(|(_, t, _)| t < t_next);
749                                if end > start {
750                                    let updates = ready_times
751                                        .range(start..end)
752                                        .map(|(from_time, _, diff)| (from_time.clone(), *diff));
753                                    export.source_upper.update_iter(updates);
754                                    export.applied = drained + end;
755                                    touched.push(index);
756                                }
757                            }
758                            if least_committed_upper
759                                .as_ref()
760                                .is_none_or(|(t, _)| t_next < t)
761                            {
762                                least_committed_upper = Some((t_next.clone(), index));
763                            }
764                        }
765                        None => {
766                            export.applied = drained + ready_times.len();
767                            if advanced {
768                                touched.push(index);
769                            }
770                        }
771                    }
772                    min_applied = min_applied.min(export.applied);
773                }
774
775                // If all exports are beyond the as_of, the source upper is the meet of the exports
776                // committed uppers, or all exports are complete and it's the empty frontier.
777                let source = all_beyond_as_of.then(|| match least_committed_upper {
778                    Some((_, index)) => exports[index].reclocked_upper(),
779                    None => Antichain::new(),
780                });
781                ready_times.drain(..min_applied - drained);
782                drained = min_applied;
783                // From testing, the common case is seeing a few exports updated in a given
784                // invocation. The published value is updated in place for the touched exports,
785                // rather than wholly replace the existing map with a new allocation.
786                tx.send_if_modified(|published| {
787                    let mut modified = false;
788                    for export in touched.drain(..).map(|index| &exports[index]) {
789                        let reclocked_upper = export.reclocked_upper();
790                        match published.exports.entry(export.id) {
791                            Entry::Occupied(entry) if *entry.get() == reclocked_upper => {}
792                            Entry::Occupied(mut entry) => {
793                                entry.insert(reclocked_upper);
794                                modified = true;
795                            }
796                            Entry::Vacant(entry) => {
797                                entry.insert(reclocked_upper);
798                                modified = true;
799                            }
800                        }
801                    }
802                    if published.source != source {
803                        published.source = source;
804                        modified = true;
805                    }
806                    modified
807                });
808            }
809
810            metrics
811                .commit_upper_accepted_times
812                .set(u64::cast_from(accepted_times.len()));
813            metrics
814                .commit_upper_ready_times
815                .set(u64::cast_from(ready_times.len()));
816        }
817    });
818
819    WatchStream::from_changes(rx)
820}
821
822#[cfg(test)]
823mod tests {
824    use futures::FutureExt;
825    use futures::stream::LocalBoxStream;
826    use mz_ore::metrics::MetricsRegistry;
827    use mz_repr::Timestamp;
828    use mz_storage_types::sources::MzOffset;
829    use timely::dataflow::operators::Input;
830    use timely::dataflow::operators::vec::input::Handle;
831    use timely::worker::Worker;
832
833    use super::*;
834    use crate::metrics::source::GeneralSourceMetricDefs;
835
836    const A: GlobalId = GlobalId::User(1);
837    const B: GlobalId = GlobalId::User(2);
838
839    /// Drives `reclock_committed_upper` with an as_of of `{0}`. [`Harness::bind`] makes the source
840    /// frontier `10 * t` at each `IntoTime` `t`, so a committed upper of `{t}` reclocks to
841    /// `{10 * (t - 1)}`.
842    struct Harness {
843        bindings: Handle<Timestamp, (MzOffset, Timestamp, Diff)>,
844        committed_uppers: BTreeMap<GlobalId, Handle<Timestamp, ()>>,
845        resume_uppers: LocalBoxStream<'static, ResumeUppers<MzOffset>>,
846        metrics: Arc<SourceMetrics>,
847    }
848
849    impl Harness {
850        fn new(worker: &mut Worker, exports: &[GlobalId]) -> Self {
851            let defs = GeneralSourceMetricDefs::register_with(&MetricsRegistry::new());
852            let metrics = Arc::new(SourceMetrics::new(&defs, GlobalId::User(0), 0));
853            worker.dataflow::<Timestamp, _, _>(|scope| {
854                let (bindings, bindings_stream) = scope.new_input();
855                let mut committed_uppers = BTreeMap::new();
856                let mut streams = BTreeMap::new();
857                for id in exports {
858                    let (handle, stream) = scope.new_input();
859                    committed_uppers.insert(*id, handle);
860                    streams.insert(*id, stream);
861                }
862                let resume_uppers = reclock_committed_upper(
863                    bindings_stream.as_collection(),
864                    Antichain::from_elem(Timestamp::MIN),
865                    streams,
866                    GlobalId::User(0),
867                    Arc::clone(&metrics),
868                )
869                .boxed_local();
870                Harness {
871                    bindings,
872                    committed_uppers,
873                    resume_uppers,
874                    metrics,
875                }
876            })
877        }
878
879        /// Binds `IntoTime` `t` to the source frontier `{10 * t}` and closes the bindings through
880        /// `t`.
881        fn bind(&mut self, t: u64) {
882            if t > 0 {
883                self.bindings.send((
884                    MzOffset::from(10 * (t - 1)),
885                    Timestamp::from(t),
886                    Diff::MINUS_ONE,
887                ));
888            }
889            self.bindings
890                .send((MzOffset::from(10 * t), Timestamp::from(t), Diff::ONE));
891            self.bindings.advance_to(Timestamp::from(t + 1));
892        }
893
894        fn commit(&mut self, id: GlobalId, t: u64) {
895            self.committed_uppers
896                .get_mut(&id)
897                .expect("export is open")
898                .advance_to(Timestamp::from(t));
899        }
900
901        fn close(&mut self, id: GlobalId) {
902            self.committed_uppers.remove(&id);
903        }
904
905        /// Steps the dataflow until it is quiescent and returns the last value it published, if
906        /// it published any.
907        fn step(&mut self, worker: &mut Worker) -> Option<ResumeUppers<MzOffset>> {
908            // A single worker reaches quiescence within a few steps. Extra steps are no-ops.
909            for _ in 0..10 {
910                worker.step();
911            }
912            let mut latest = None;
913            while let Some(Some(uppers)) = self.resume_uppers.next().now_or_never() {
914                latest = Some(uppers);
915            }
916            latest
917        }
918
919        fn ready_times(&self) -> u64 {
920            self.metrics.commit_upper_ready_times.get()
921        }
922    }
923
924    fn uppers(
925        source: Option<Antichain<MzOffset>>,
926        exports: impl IntoIterator<Item = (GlobalId, Antichain<MzOffset>)>,
927    ) -> Option<ResumeUppers<MzOffset>> {
928        Some(ResumeUppers {
929            source,
930            exports: exports.into_iter().collect(),
931        })
932    }
933
934    fn offset(offset: u64) -> Antichain<MzOffset> {
935        Antichain::from_elem(MzOffset::from(offset))
936    }
937
938    #[mz_ore::test]
939    #[cfg_attr(miri, ignore)]
940    fn reclock_committed_upper_reports_each_export() {
941        timely::execute_directly(|worker| {
942            let mut h = Harness::new(worker, &[A, B]);
943            for t in 0..=3 {
944                h.bind(t);
945            }
946            assert_eq!(h.step(worker), None, "no export is beyond the as_of");
947
948            h.commit(A, 3);
949            assert_eq!(
950                h.step(worker),
951                uppers(None, [(A, offset(20))]),
952                "B holds back the source upper until it is beyond the as_of",
953            );
954
955            h.commit(B, 2);
956            assert_eq!(
957                h.step(worker),
958                uppers(Some(offset(10)), [(A, offset(20)), (B, offset(10))]),
959                "the source upper is B's, the least",
960            );
961
962            h.commit(B, 4);
963            assert_eq!(
964                h.step(worker),
965                uppers(Some(offset(20)), [(A, offset(20)), (B, offset(30))]),
966                "the source upper is A's once B passes it",
967            );
968        });
969    }
970
971    #[mz_ore::test]
972    #[cfg_attr(miri, ignore)]
973    fn reclock_committed_upper_retains_bindings_for_slowest_export() {
974        timely::execute_directly(|worker| {
975            let mut h = Harness::new(worker, &[A, B]);
976            for t in 0..=3 {
977                h.bind(t);
978            }
979            h.commit(A, 4);
980            h.commit(B, 2);
981            assert_eq!(
982                h.step(worker),
983                uppers(Some(offset(10)), [(A, offset(30)), (B, offset(10))]),
984            );
985            // Seven updates bind times 0 through 3. B has applied the three below time 2.
986            assert_eq!(h.ready_times(), 4);
987
988            h.bind(4);
989            h.commit(A, 5);
990            assert_eq!(
991                h.step(worker),
992                uppers(Some(offset(10)), [(A, offset(40)), (B, offset(10))]),
993            );
994            assert_eq!(h.ready_times(), 6, "B has not applied the new bindings");
995
996            h.commit(B, 3);
997            assert_eq!(
998                h.step(worker),
999                uppers(Some(offset(20)), [(A, offset(40)), (B, offset(20))]),
1000            );
1001            assert_eq!(h.ready_times(), 4);
1002
1003            h.commit(B, 5);
1004            assert_eq!(
1005                h.step(worker),
1006                uppers(Some(offset(40)), [(A, offset(40)), (B, offset(40))]),
1007            );
1008            assert_eq!(h.ready_times(), 0, "every export has applied every binding");
1009        });
1010    }
1011
1012    #[mz_ore::test]
1013    #[cfg_attr(miri, ignore)]
1014    fn reclock_committed_upper_applies_bindings_arriving_after_the_commit() {
1015        timely::execute_directly(|worker| {
1016            let mut h = Harness::new(worker, &[A]);
1017            for t in 0..=3 {
1018                h.bind(t);
1019            }
1020            h.commit(A, 5);
1021            assert_eq!(
1022                h.step(worker),
1023                uppers(Some(offset(30)), [(A, offset(30))]),
1024                "only bindings through time 3 exist",
1025            );
1026            assert_eq!(
1027                h.step(worker),
1028                None,
1029                "nothing changed, nothing is published"
1030            );
1031
1032            h.bind(4);
1033            assert_eq!(
1034                h.step(worker),
1035                uppers(Some(offset(40)), [(A, offset(40))]),
1036                "the binding for time 4 applies without A committing again",
1037            );
1038        });
1039    }
1040
1041    #[mz_ore::test]
1042    #[cfg_attr(miri, ignore)]
1043    fn reclock_committed_upper_closed_exports() {
1044        timely::execute_directly(|worker| {
1045            let mut h = Harness::new(worker, &[A, B]);
1046            for t in 0..=3 {
1047                h.bind(t);
1048            }
1049            h.commit(B, 2);
1050            h.close(A);
1051            assert_eq!(
1052                h.step(worker),
1053                uppers(Some(offset(10)), [(A, Antichain::new()), (B, offset(10))]),
1054                "a closed export does not constrain the source upper",
1055            );
1056
1057            h.close(B);
1058            assert_eq!(
1059                h.step(worker),
1060                uppers(
1061                    Some(Antichain::new()),
1062                    [(A, Antichain::new()), (B, Antichain::new())],
1063                ),
1064            );
1065            assert_eq!(h.ready_times(), 0);
1066        });
1067    }
1068}