1#![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#[derive(Clone)]
80pub struct RawSourceCreationConfig {
81 pub name: String,
83 pub id: GlobalId,
85 pub source_exports: BTreeMap<GlobalId, SourceExport<CollectionMetadata>>,
87 pub worker_id: usize,
89 pub worker_count: usize,
91 pub timestamp_interval: Duration,
94 pub now_fn: NowFn,
96 pub metrics: StorageMetrics,
98 pub as_of: Antichain<mz_repr::Timestamp>,
100 pub resume_uppers: BTreeMap<GlobalId, Antichain<mz_repr::Timestamp>>,
103 pub source_resume_uppers: BTreeMap<GlobalId, Vec<Row>>,
110 pub persist_clients: Arc<PersistClientCache>,
112 pub statistics: BTreeMap<GlobalId, SourceStatistics>,
114 pub shared_remap_upper: Rc<RefCell<Antichain<mz_repr::Timestamp>>>,
116 pub config: StorageConfiguration,
118 pub remap_collection_id: GlobalId,
120 pub remap_metadata: CollectionMetadata,
122 pub busy_signal: Arc<Semaphore>,
125}
126
127#[derive(Clone)]
130pub struct SourceExportCreationConfig {
131 pub id: GlobalId,
133 pub worker_id: usize,
135 pub metrics: StorageMetrics,
137 pub source_statistics: SourceStatistics,
139}
140
141impl RawSourceCreationConfig {
142 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 pub fn responsible_for<P: Hash>(&self, partition: P) -> bool {
152 self.responsible_worker(partition) == self.worker_id
153 }
154}
155
156pub 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 let remap_collection = remap_collection.inner.broadcast().as_collection();
216 tokens.push(remap_token);
217
218 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
275fn 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 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 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 probe_stream.broadcast().inspect_container(move |event| {
411 if let Ok((_, data)) = event {
412 for probe in data {
413 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
426fn 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 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 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 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
575fn 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 struct ExportState<T, FromTime> {
592 id: GlobalId,
593 input_index: usize,
594 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 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 use timely::progress::ChangeBatch;
640 let mut accepted_times: ChangeBatch<(T, FromTime)> = ChangeBatch::new();
641 let mut bindings_upper = Antichain::from_elem(Timestamp::minimum());
643 let mut ready_times = VecDeque::new();
647 let mut drained = 0;
650 let mut touched = Vec::new();
652
653 move |frontiers| {
654 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 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 let mut pending_times = std::mem::take(&mut accepted_times).into_inner();
669 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 if as_of.iter().all(|t| !bindings_upper.less_equal(t)) {
683 let mut all_beyond_as_of = true;
684 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 match committed_upper.as_option() {
741 Some(t_next) => {
742 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 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 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 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 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 fn step(&mut self, worker: &mut Worker) -> Option<ResumeUppers<MzOffset>> {
908 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 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}