1use std::cell::RefCell;
13use std::collections::BTreeMap;
14use std::collections::VecDeque;
15use std::collections::hash_map::DefaultHasher;
16use std::convert::Infallible;
17use std::fmt::{Debug, Formatter};
18use std::future::Future;
19use std::hash::{Hash, Hasher};
20use std::pin::pin;
21use std::rc::Rc;
22use std::sync::Arc;
23use std::time::Instant;
24
25use anyhow::anyhow;
26use arrow::array::ArrayRef;
27use differential_dataflow::Hashable;
28use differential_dataflow::difference::Monoid;
29use differential_dataflow::lattice::Lattice;
30use futures::stream::FuturesUnordered;
31use futures_util::StreamExt;
32use mz_ore::cast::CastFrom;
33use mz_ore::collections::CollectionExt;
34use mz_persist_types::stats::PartStats;
35use mz_persist_types::{Codec, Codec64};
36use mz_timely_util::builder_async::{
37 Event, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
38};
39use timely::PartialOrder;
40use timely::container::CapacityContainerBuilder;
41use timely::dataflow::channels::pact::{Exchange, Pipeline};
42use timely::dataflow::operators::{Capability, CapabilitySet, ConnectLoop, Enter, Feedback, Leave};
43use timely::dataflow::{Scope, StreamVec};
44use timely::order::TotalOrder;
45use timely::progress::frontier::AntichainRef;
46use timely::progress::{Antichain, Timestamp, timestamp::Refines};
47use tracing::{debug, trace};
48
49use crate::batch::BLOB_TARGET_SIZE;
50use crate::cfg::{
51 RetryParameters, SOURCE_FETCH_CONCURRENCY, SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES,
52 USE_CRITICAL_SINCE_SOURCE,
53};
54use crate::fetch::{ExchangeableBatchPart, FetchedBlob, Lease};
55use crate::internal::state::BatchPart;
56use crate::stats::{STATS_AUDIT_PERCENT, STATS_FILTER_ENABLED};
57use crate::{Diagnostics, PersistClient, ShardId};
58
59#[derive(Debug, Clone, PartialEq, Default)]
61pub enum FilterResult {
62 #[default]
64 Keep,
65 Discard,
67 ReplaceWith {
70 key: ArrayRef,
72 val: ArrayRef,
74 },
75}
76
77impl FilterResult {
78 pub fn keep_all<T>(_stats: &PartStats, _frontier: AntichainRef<T>) -> FilterResult {
80 Self::Keep
81 }
82}
83
84#[derive(Clone)]
95pub enum ErrorHandler {
96 Halt(&'static str),
98 Signal(Rc<dyn Fn(anyhow::Error) + 'static>),
100}
101
102impl Debug for ErrorHandler {
103 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
104 match self {
105 ErrorHandler::Halt(name) => f.debug_tuple("ErrorHandler::Halt").field(name).finish(),
106 ErrorHandler::Signal(_) => f.write_str("ErrorHandler::Signal"),
107 }
108 }
109}
110
111impl ErrorHandler {
112 pub fn signal(signal_fn: impl Fn(anyhow::Error) + 'static) -> Self {
114 Self::Signal(Rc::new(signal_fn))
115 }
116
117 pub async fn report_and_stop(&self, error: anyhow::Error) -> ! {
121 match self {
122 ErrorHandler::Halt(name) => {
123 mz_ore::halt!("unhandled error in {name}: {error:#}")
124 }
125 ErrorHandler::Signal(callback) => {
126 let () = callback(error);
127 std::future::pending().await
128 }
129 }
130 }
131}
132
133pub fn shard_source<'inner, 'outer, K, V, T, D, DT, TOuter, C>(
147 outer: Scope<'outer, TOuter>,
148 scope: Scope<'inner, T>,
149 name: &str,
150 client: impl Fn() -> C,
151 shard_id: ShardId,
152 as_of: Option<Antichain<TOuter>>,
153 snapshot_mode: SnapshotMode,
154 until: Antichain<TOuter>,
155 desc_transformer: Option<DT>,
156 key_schema: Arc<K::Schema>,
157 val_schema: Arc<V::Schema>,
158 filter_fn: impl FnMut(&PartStats, AntichainRef<TOuter>) -> FilterResult + 'static,
159 listen_sleep: Option<impl Fn() -> RetryParameters + 'static>,
161 start_signal: impl Future<Output = ()> + 'static,
162 error_handler: ErrorHandler,
163) -> (
164 StreamVec<'inner, T, FetchedBlob<K, V, TOuter, D>>,
165 Vec<PressOnDropButton>,
166)
167where
168 K: Debug + Codec,
169 V: Debug + Codec,
170 D: Monoid + Codec64 + Send + Sync,
171 TOuter: Timestamp + Lattice + Codec64 + TotalOrder + Sync,
173 T: Refines<TOuter>,
174 DT: FnOnce(
175 Scope<'inner, T>,
176 StreamVec<'inner, T, (usize, ExchangeableBatchPart<TOuter>)>,
177 usize,
178 ) -> (
179 StreamVec<'inner, T, (usize, ExchangeableBatchPart<TOuter>)>,
180 Vec<PressOnDropButton>,
181 ),
182 C: Future<Output = PersistClient> + Send + 'static,
183{
184 let chosen_worker = usize::cast_from(name.hashed()) % scope.peers();
200
201 let mut tokens = vec![];
202
203 let (completed_fetches_feedback_handle, completed_fetches_feedback_stream) =
206 scope.feedback(T::Summary::default());
207
208 let is_transient = !until.is_empty();
212
213 let (descs, descs_token) = shard_source_descs::<K, V, D, TOuter>(
214 outer,
215 name,
216 client(),
217 shard_id.clone(),
218 as_of,
219 snapshot_mode,
220 until,
221 completed_fetches_feedback_stream.leave(outer),
222 chosen_worker,
223 Arc::clone(&key_schema),
224 Arc::clone(&val_schema),
225 filter_fn,
226 listen_sleep,
227 start_signal,
228 error_handler.clone(),
229 );
230 tokens.push(descs_token);
231
232 let descs = descs.enter(scope);
233 let descs = match desc_transformer {
234 Some(desc_transformer) => {
235 let (descs, extra_tokens) = desc_transformer(scope, descs, chosen_worker);
236 tokens.extend(extra_tokens);
237 descs
238 }
239 None => descs,
240 };
241
242 let (parts, completed_fetches_stream, fetch_token) = shard_source_fetch::<K, V, TOuter, D, T>(
243 descs,
244 name,
245 client(),
246 shard_id,
247 key_schema,
248 val_schema,
249 is_transient,
250 error_handler,
251 );
252 completed_fetches_stream.connect_loop(completed_fetches_feedback_handle);
253 tokens.push(fetch_token);
254
255 (parts, tokens)
256}
257
258#[derive(Debug, Clone, Copy)]
260pub enum SnapshotMode {
261 Include,
263 Exclude,
265}
266
267#[derive(Debug)]
268struct LeaseManager<T> {
269 leases: BTreeMap<T, Vec<Lease>>,
270}
271
272impl<T: Timestamp + Codec64> LeaseManager<T> {
273 fn new() -> Self {
274 Self {
275 leases: BTreeMap::new(),
276 }
277 }
278
279 fn push_at(&mut self, time: T, lease: Lease) {
281 self.leases.entry(time).or_default().push(lease);
282 }
283
284 fn advance_to(&mut self, frontier: AntichainRef<T>)
286 where
287 T: TotalOrder,
289 {
290 while let Some(first) = self.leases.first_entry() {
291 if frontier.less_equal(first.key()) {
292 break; }
294 drop(first.remove());
295 }
296 }
297}
298
299pub(crate) fn shard_source_descs<'outer, K, V, D, TOuter>(
300 scope: Scope<'outer, TOuter>,
301 name: &str,
302 client: impl Future<Output = PersistClient> + Send + 'static,
303 shard_id: ShardId,
304 as_of: Option<Antichain<TOuter>>,
305 snapshot_mode: SnapshotMode,
306 until: Antichain<TOuter>,
307 completed_fetches_stream: StreamVec<'outer, TOuter, Infallible>,
308 chosen_worker: usize,
309 key_schema: Arc<K::Schema>,
310 val_schema: Arc<V::Schema>,
311 mut filter_fn: impl FnMut(&PartStats, AntichainRef<TOuter>) -> FilterResult + 'static,
312 listen_sleep: Option<impl Fn() -> RetryParameters + 'static>,
314 start_signal: impl Future<Output = ()> + 'static,
315 error_handler: ErrorHandler,
316) -> (
317 StreamVec<'outer, TOuter, (usize, ExchangeableBatchPart<TOuter>)>,
318 PressOnDropButton,
319)
320where
321 K: Debug + Codec,
322 V: Debug + Codec,
323 D: Monoid + Codec64 + Send + Sync,
324 TOuter: Timestamp + Lattice + Codec64 + TotalOrder + Sync,
326{
327 let worker_index = scope.index();
328 let num_workers = scope.peers();
329
330 let name_owned = name.to_owned();
333
334 let listen_handle = Rc::new(RefCell::new(None));
336 let return_listen_handle = Rc::clone(&listen_handle);
337
338 let (tx, rx) = tokio::sync::oneshot::channel::<Rc<RefCell<LeaseManager<TOuter>>>>();
340 let mut builder = AsyncOperatorBuilder::new(
341 format!("shard_source_descs_return({})", name),
342 scope.clone(),
343 );
344 let mut completed_fetches = builder.new_disconnected_input(completed_fetches_stream, Pipeline);
345 builder.build(move |_caps| async move {
348 let Ok(leases) = rx.await else {
349 return;
352 };
353 while let Some(event) = completed_fetches.next().await {
354 let Event::Progress(frontier) = event else {
355 continue;
356 };
357 leases.borrow_mut().advance_to(frontier.borrow());
358 }
359 drop(return_listen_handle);
361 });
362
363 let mut builder =
364 AsyncOperatorBuilder::new(format!("shard_source_descs({})", name), scope.clone());
365 let (descs_output, descs_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
366
367 #[allow(clippy::await_holding_refcell_ref)]
368 let shutdown_button = builder.build(move |caps| async move {
369 let mut cap_set = CapabilitySet::from_elem(caps.into_element());
370
371 if worker_index != chosen_worker {
373 trace!(
374 "We are not the chosen worker ({}), exiting...",
375 chosen_worker
376 );
377 return;
378 }
379
380 let mut read = mz_ore::task::spawn(|| format!("shard_source_reader({})", name_owned), {
390 let diagnostics = Diagnostics {
391 handle_purpose: format!("shard_source({})", name_owned),
392 shard_name: name_owned.clone(),
393 };
394 async move {
395 let client = client.await;
396 client
397 .open_leased_reader::<K, V, TOuter, D>(
398 shard_id,
399 key_schema,
400 val_schema,
401 diagnostics,
402 USE_CRITICAL_SINCE_SOURCE.get(client.dyncfgs()),
403 )
404 .await
405 }
406 })
407 .await
408 .expect("could not open persist shard");
409
410 let () = start_signal.await;
414
415 let cfg = read.cfg.clone();
416 let metrics = Arc::clone(&read.metrics);
417
418 let as_of = as_of.unwrap_or_else(|| read.since().clone());
419
420 cap_set.downgrade(as_of.clone());
436
437 let mut snapshot_parts =
438 match snapshot_mode {
439 SnapshotMode::Include => match read.snapshot(as_of.clone()).await {
440 Ok(parts) => parts,
441 Err(e) => error_handler
442 .report_and_stop(anyhow!(
443 "{name_owned}: {shard_id} cannot serve requested as_of {as_of:?}: {e:?}"
444 ))
445 .await,
446 },
447 SnapshotMode::Exclude => vec![],
448 };
449
450 let leases = Rc::new(RefCell::new(LeaseManager::new()));
454 tx.send(Rc::clone(&leases))
455 .expect("lease returner exited before desc producer");
456
457 let replay_upper = read.shared_upper();
463
464 let mut listen = listen_handle.borrow_mut();
467 let listen = match read.listen(as_of.clone()).await {
468 Ok(handle) => listen.insert(handle),
469 Err(e) => {
470 error_handler
471 .report_and_stop(anyhow!(
472 "{name_owned}: {shard_id} cannot serve requested as_of {as_of:?}: {e:?}"
473 ))
474 .await
475 }
476 };
477
478 let listen_retry = listen_sleep.as_ref().map(|retry| retry());
479
480 let listen_head = if !snapshot_parts.is_empty() {
482 let (mut parts, progress) = listen.next(listen_retry).await;
483 snapshot_parts.append(&mut parts);
484 futures::stream::iter(Some((snapshot_parts, progress)))
485 } else {
486 futures::stream::iter(None)
487 };
488
489 let listen_tail = futures::stream::unfold(listen, |listen| async move {
491 Some((listen.next(listen_retry).await, listen))
492 });
493
494 let mut shard_stream = pin!(listen_head.chain(listen_tail));
495
496 let mut audit_budget_bytes = u64::cast_from(BLOB_TARGET_SIZE.get(&cfg).saturating_mul(2));
500
501 let mut current_frontier = as_of.clone();
503
504 let coalesce_target = u64::cast_from(SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES.get(&cfg));
512 let mut coalesced_bytes: u64 = 0;
514
515 while !PartialOrder::less_equal(&until, ¤t_frontier) {
518 let (parts, progress) = shard_stream.next().await.expect("infinite stream");
519
520 let mut batch_bytes: u64 = 0;
521
522 let current_ts = current_frontier
525 .as_option()
526 .expect("until should always be <= the empty frontier");
527 let session_cap = cap_set.delayed(current_ts);
528
529 for mut part_desc in parts {
530 if STATS_FILTER_ENABLED.get(&cfg) {
533 let filter_result = match &part_desc.part {
534 BatchPart::Hollow(x) => {
535 let should_fetch =
536 x.stats.as_ref().map_or(FilterResult::Keep, |stats| {
537 match stats.try_decode() {
541 Ok(stats) => filter_fn(&stats, current_frontier.borrow()),
542 Err(err) => {
543 tracing::warn!(
544 %err,
545 "could not decode part stats, fetching part"
546 );
547 FilterResult::Keep
548 }
549 }
550 });
551 should_fetch
552 }
553 BatchPart::Inline { .. } => FilterResult::Keep,
554 };
555 let bytes = u64::cast_from(part_desc.encoded_size_bytes());
557 match filter_result {
558 FilterResult::Keep => {
559 audit_budget_bytes = audit_budget_bytes.saturating_add(bytes);
560 }
561 FilterResult::Discard => {
562 metrics.pushdown.parts_filtered_count.inc();
563 metrics.pushdown.parts_filtered_bytes.inc_by(bytes);
564 let should_audit = match &part_desc.part {
565 BatchPart::Hollow(x) => {
566 let mut h = DefaultHasher::new();
567 x.key.hash(&mut h);
568 usize::cast_from(h.finish()) % 100
569 < STATS_AUDIT_PERCENT.get(&cfg)
570 }
571 BatchPart::Inline { .. } => false,
572 };
573 if should_audit && bytes < audit_budget_bytes {
574 audit_budget_bytes -= bytes;
575 metrics.pushdown.parts_audited_count.inc();
576 metrics.pushdown.parts_audited_bytes.inc_by(bytes);
577 part_desc.request_filter_pushdown_audit();
578 } else {
579 debug!(
580 "skipping part because of stats filter {:?}",
581 part_desc.part.stats()
582 );
583 continue;
584 }
585 }
586 FilterResult::ReplaceWith { key, val } => {
587 part_desc.maybe_optimize(&cfg, key, val);
588 audit_budget_bytes = audit_budget_bytes.saturating_add(bytes);
589 }
590 }
591 let bytes = u64::cast_from(part_desc.encoded_size_bytes());
592 if part_desc.part.is_inline() {
593 metrics.pushdown.parts_inline_count.inc();
594 metrics.pushdown.parts_inline_bytes.inc_by(bytes);
595 } else {
596 metrics.pushdown.parts_fetched_count.inc();
597 metrics.pushdown.parts_fetched_bytes.inc_by(bytes);
598 }
599 }
600
601 let worker_idx = usize::cast_from(Instant::now().hashed()) % num_workers;
608 batch_bytes =
609 batch_bytes.saturating_add(u64::cast_from(part_desc.encoded_size_bytes()));
610 let (part, lease) = part_desc.into_exchangeable_part();
611 leases.borrow_mut().push_at(current_ts.clone(), lease);
612 descs_output.give(&session_cap, (worker_idx, part));
613 }
614
615 current_frontier.join_assign(&progress);
616 coalesced_bytes = coalesced_bytes.saturating_add(batch_bytes);
617
618 let caught_up = PartialOrder::less_equal(&replay_upper, ¤t_frontier);
624 let coalesce = coalesce_target > 0 && !caught_up && coalesced_bytes < coalesce_target;
625 if !coalesce {
626 coalesced_bytes = 0;
627 cap_set.downgrade(current_frontier.iter());
628 }
629 }
630 });
631
632 (descs_stream, shutdown_button.press_on_drop())
633}
634
635pub(crate) fn shard_source_fetch<'inner, K, V, T, D, TInner>(
636 descs: StreamVec<'inner, TInner, (usize, ExchangeableBatchPart<T>)>,
637 name: &str,
638 client: impl Future<Output = PersistClient> + Send + 'static,
639 shard_id: ShardId,
640 key_schema: Arc<K::Schema>,
641 val_schema: Arc<V::Schema>,
642 is_transient: bool,
643 error_handler: ErrorHandler,
644) -> (
645 StreamVec<'inner, TInner, FetchedBlob<K, V, T, D>>,
646 StreamVec<'inner, TInner, Infallible>,
647 PressOnDropButton,
648)
649where
650 K: Debug + Codec,
651 V: Debug + Codec,
652 T: Timestamp + Lattice + Codec64 + Sync,
653 D: Monoid + Codec64 + Send + Sync,
654 TInner: Timestamp + Refines<T>,
655{
656 let mut builder =
657 AsyncOperatorBuilder::new(format!("shard_source_fetch({})", name), descs.scope());
658 let (fetched_output, fetched_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
659 let (completed_fetches_output, completed_fetches_stream) =
660 builder.new_output::<CapacityContainerBuilder<Vec<Infallible>>>();
661 let mut descs_input = builder.new_input_for_many(
662 descs,
663 Exchange::new(|&(i, _): &(usize, _)| u64::cast_from(i)),
664 [&fetched_output, &completed_fetches_output],
665 );
666 let name_owned = name.to_owned();
667
668 let shutdown_button = builder.build(move |_capabilities| async move {
669 let (fetcher, max_concurrency) =
673 mz_ore::task::spawn(|| format!("shard_source_fetch({})", name_owned), {
674 let diagnostics = Diagnostics {
675 shard_name: name_owned.clone(),
676 handle_purpose: format!("shard_source_fetch batch fetcher {}", name_owned),
677 };
678 async move {
679 let client = client.await;
680 let max_concurrency = SOURCE_FETCH_CONCURRENCY.get(client.dyncfgs()).max(1);
687 let fetcher = client
688 .create_batch_fetcher::<K, V, T, D>(
689 shard_id,
690 key_schema,
691 val_schema,
692 is_transient,
693 diagnostics,
694 )
695 .await
696 .expect("shard codecs should not change");
697 (fetcher, max_concurrency)
698 }
699 })
700 .await;
701
702 let fetch_one = |caps: [Capability<TInner>; 2], part: ExchangeableBatchPart<T>| {
708 let mut fetcher = fetcher.clone();
709 async move {
710 let reader_id = part.reader_id().clone();
711 let fetched = fetcher
712 .fetch_leased_part(part)
713 .await
714 .expect("shard_id should match across all workers");
715 let fetched = match fetched {
716 Ok(fetched) => Ok(fetched),
717 Err(blob_key) => {
718 let diagnostics = fetcher.missing_blob_diagnostics(&reader_id).await;
730 Err(anyhow!(
731 "batch fetcher could not fetch batch part {}: {}",
732 blob_key,
733 diagnostics
734 ))
735 }
736 };
737 (caps, fetched)
738 }
739 };
740
741 let mut pending: VecDeque<([Capability<TInner>; 2], ExchangeableBatchPart<T>)> =
752 VecDeque::new();
753 let mut in_flight = FuturesUnordered::new();
754 let mut input_done = false;
755
756 loop {
757 while in_flight.len() < max_concurrency {
759 let Some((caps, part)) = pending.pop_front() else {
760 break;
761 };
762 in_flight.push(fetch_one(caps, part));
763 }
764
765 tokio::select! {
766 biased;
769 Some(([cap, _], fetched)) = in_flight.next(), if !in_flight.is_empty() => {
770 match fetched {
771 Ok(fetched) => fetched_output.give(&cap, fetched),
772 Err(e) => {
773 error_handler.report_and_stop(e).await;
778 }
779 }
780 }
781 event = descs_input.next(), if !input_done => {
784 match event {
785 Some(Event::Data(caps, data)) => {
786 for (_idx, part) in data {
789 pending.push_back((caps.clone(), part));
790 }
791 }
792 Some(Event::Progress(_)) => {}
793 None => input_done = true,
794 }
795 }
796 else => break,
798 }
799 }
800 });
801
802 (
803 fetched_stream,
804 completed_fetches_stream,
805 shutdown_button.press_on_drop(),
806 )
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use std::sync::Arc;
813
814 use mz_persist::location::{Blob, SeqNo};
815 use mz_persist_types::codec_impls::StringSchema;
816 use timely::dataflow::operators::Leave;
817 use timely::dataflow::operators::Probe;
818 use timely::dataflow::operators::capture::{Capture, Event as CaptureEvent};
819 use timely::dataflow::operators::probe::Handle as ProbeHandle;
820 use timely::progress::Antichain;
821
822 use crate::batch::{INLINE_WRITES_SINGLE_MAX_BYTES, INLINE_WRITES_TOTAL_MAX_BYTES};
823 use crate::cache::PersistClientCache;
824 use crate::internal::paths::{BlobKey, PartialBlobKey};
825 use crate::operators::shard_source::shard_source;
826 use crate::{Diagnostics, PersistLocation, ShardId};
827
828 #[mz_ore::test]
829 fn test_lease_manager() {
830 let lease = Lease::new(SeqNo::minimum());
831 let mut manager = LeaseManager::new();
832 for t in 0u64..10 {
833 manager.push_at(t, lease.clone());
834 }
835 assert_eq!(lease.count(), 11);
836 manager.advance_to(AntichainRef::new(&[5]));
837 assert_eq!(lease.count(), 6);
838 manager.advance_to(AntichainRef::new(&[3]));
839 assert_eq!(lease.count(), 6);
840 manager.advance_to(AntichainRef::new(&[9]));
841 assert_eq!(lease.count(), 2);
842 manager.advance_to(AntichainRef::new(&[10]));
843 assert_eq!(lease.count(), 1);
844 }
845
846 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
854 #[cfg_attr(miri, ignore)] async fn test_shard_source_implicit_initial_as_of() {
856 let persist_client = PersistClient::new_for_tests().await;
857
858 let expected_frontier = 42;
859 let shard_id = ShardId::new();
860
861 initialize_shard(
862 &persist_client,
863 shard_id,
864 Antichain::from_elem(expected_frontier),
865 )
866 .await;
867
868 let res = timely::execute::execute_directly(move |worker| {
869 let until = Antichain::new();
870
871 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
872 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
873 let transformer = move |_, descs, _| (descs, vec![]);
874 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
875 outer,
876 scope,
877 "test_source",
878 move || std::future::ready(persist_client.clone()),
879 shard_id,
880 None, SnapshotMode::Include,
882 until,
883 Some(transformer),
884 Arc::new(
885 <std::string::String as mz_persist_types::Codec>::Schema::default(),
886 ),
887 Arc::new(
888 <std::string::String as mz_persist_types::Codec>::Schema::default(),
889 ),
890 FilterResult::keep_all,
891 false.then_some(|| unreachable!()),
892 async {},
893 ErrorHandler::Halt("test"),
894 );
895 (stream.leave(outer), tokens)
896 });
897
898 let probe = ProbeHandle::new();
899 let _stream = stream.probe_with(&probe);
900
901 (probe, token)
902 });
903
904 while probe.less_than(&expected_frontier) {
905 worker.step();
906 }
907
908 let mut probe_frontier = Antichain::new();
909 probe.with_frontier(|f| probe_frontier.extend(f.iter().cloned()));
910
911 probe_frontier
912 });
913
914 assert_eq!(res, Antichain::from_elem(expected_frontier));
915 }
916
917 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
924 #[cfg_attr(miri, ignore)] async fn test_shard_source_explicit_initial_as_of() {
926 let persist_client = PersistClient::new_for_tests().await;
927
928 let expected_frontier = 42;
929 let shard_id = ShardId::new();
930
931 initialize_shard(
932 &persist_client,
933 shard_id,
934 Antichain::from_elem(expected_frontier),
935 )
936 .await;
937
938 let res = timely::execute::execute_directly(move |worker| {
939 let as_of = Antichain::from_elem(expected_frontier);
940 let until = Antichain::new();
941
942 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
943 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
944 let transformer = move |_, descs, _| (descs, vec![]);
945 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
946 outer,
947 scope,
948 "test_source",
949 move || std::future::ready(persist_client.clone()),
950 shard_id,
951 Some(as_of), SnapshotMode::Include,
953 until,
954 Some(transformer),
955 Arc::new(
956 <std::string::String as mz_persist_types::Codec>::Schema::default(),
957 ),
958 Arc::new(
959 <std::string::String as mz_persist_types::Codec>::Schema::default(),
960 ),
961 FilterResult::keep_all,
962 false.then_some(|| unreachable!()),
963 async {},
964 ErrorHandler::Halt("test"),
965 );
966 (stream.leave(outer), tokens)
967 });
968
969 let probe = ProbeHandle::new();
970 let _stream = stream.probe_with(&probe);
971
972 (probe, token)
973 });
974
975 while probe.less_than(&expected_frontier) {
976 worker.step();
977 }
978
979 let mut probe_frontier = Antichain::new();
980 probe.with_frontier(|f| probe_frontier.extend(f.iter().cloned()));
981
982 probe_frontier
983 });
984
985 assert_eq!(res, Antichain::from_elem(expected_frontier));
986 }
987
988 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1004 #[cfg_attr(miri, ignore)] async fn test_shard_source_hydration_frontier_coalesce() {
1006 const N_BATCHES: u64 = 64;
1007
1008 async fn run(coalesce_bytes: usize) -> Option<u64> {
1013 let mut cache = PersistClientCache::new_no_metrics();
1014 cache.cfg.compaction_enabled = false;
1017 cache
1018 .cfg
1019 .set_config(&SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES, coalesce_bytes);
1020 let persist_client = cache
1021 .open(PersistLocation::new_in_mem())
1022 .await
1023 .expect("in-mem location is valid");
1024 let shard_id = ShardId::new();
1025
1026 let mut write = persist_client
1027 .open_writer::<String, String, u64, u64>(
1028 shard_id,
1029 Arc::new(StringSchema),
1030 Arc::new(StringSchema),
1031 Diagnostics::for_tests(),
1032 )
1033 .await
1034 .expect("invalid usage");
1035
1036 for t in 0..N_BATCHES {
1039 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1040 write.expect_compare_and_append(&[row], t, t + 1).await;
1041 }
1042
1043 timely::execute::execute_directly(move |worker| {
1044 let (probe, receiver, _token) = worker.dataflow::<u64, _, _>(|outer| {
1045 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1046 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1047 outer,
1048 scope,
1049 "test_source",
1050 move || std::future::ready(persist_client.clone()),
1051 shard_id,
1052 Some(Antichain::from_elem(0)),
1053 SnapshotMode::Include,
1054 Antichain::from_elem(N_BATCHES),
1057 Some(move |_, descs, _| (descs, vec![])),
1058 Arc::new(StringSchema),
1059 Arc::new(StringSchema),
1060 FilterResult::keep_all,
1061 false.then_some(|| unreachable!()),
1062 async {},
1063 ErrorHandler::Halt("test"),
1064 );
1065 (stream.leave(outer), tokens)
1066 });
1067 let probe = ProbeHandle::new();
1068 let receiver = stream.probe_with(&probe).capture();
1072 (probe, receiver, token)
1073 });
1074
1075 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1078 while !probe.with_frontier(|f| f.is_empty()) {
1079 assert!(Instant::now() < deadline, "timed out hydrating shard");
1080 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1081 }
1082
1083 let mut max_msg_time: Option<u64> = None;
1088 while let Ok(event) = receiver.try_recv() {
1089 if let CaptureEvent::Messages(time, _) = event {
1090 max_msg_time = max_msg_time.max(Some(time));
1091 }
1092 }
1093 max_msg_time
1094 })
1095 }
1096
1097 assert_eq!(run(0).await, Some(N_BATCHES - 1));
1105 assert_eq!(run(1 << 30).await, Some(N_BATCHES - 1));
1106 }
1107
1108 #[mz_ore::test]
1113 fn test_frontier_coalesce_decision() {
1114 fn forwards(coalesce_target: u64, n: u64, bytes_per_batch: u64) -> usize {
1115 let replay_upper = n;
1116 let mut current = 0u64;
1117 let mut coalesced = 0u64;
1118 let mut forwarded = 0usize;
1119 for _ in 0..n {
1120 current += 1;
1121 coalesced += bytes_per_batch;
1122 let caught_up = replay_upper <= current;
1123 let coalesce = coalesce_target > 0 && !caught_up && coalesced < coalesce_target;
1125 if !coalesce {
1126 forwarded += 1;
1127 coalesced = 0;
1128 }
1129 }
1130 forwarded
1131 }
1132
1133 assert_eq!(forwards(0, 64, 10), 64);
1135 assert_eq!(forwards(1 << 30, 64, 10), 1);
1137 let partial = forwards(100, 64, 10);
1140 assert!(
1141 partial > 1 && partial < 64,
1142 "expected partial coalescing, got {partial}"
1143 );
1144 }
1145
1146 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1150 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetches_data() {
1152 let persist_client = PersistClient::new_for_tests().await;
1153 let shard_id = ShardId::new();
1154
1155 let mut write = persist_client
1156 .open_writer::<String, String, u64, u64>(
1157 shard_id,
1158 Arc::new(StringSchema),
1159 Arc::new(StringSchema),
1160 Diagnostics::for_tests(),
1161 )
1162 .await
1163 .expect("invalid usage");
1164 let data = [
1165 (("k1".to_owned(), "v1".to_owned()), 0u64, 1u64),
1166 (("k2".to_owned(), "v2".to_owned()), 1u64, 1u64),
1167 ];
1168 write.expect_compare_and_append(&data[..], 0, 5).await;
1169
1170 let expected_frontier = 5;
1171 let (blob_count, frontier) = timely::execute::execute_directly(move |worker| {
1172 let as_of = Antichain::from_elem(0);
1173 let until = Antichain::new();
1174
1175 let (capture, probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1176 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1177 let transformer = move |_, descs, _| (descs, vec![]);
1178 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1179 outer,
1180 scope,
1181 "test_source",
1182 move || std::future::ready(persist_client.clone()),
1183 shard_id,
1184 Some(as_of),
1185 SnapshotMode::Include,
1186 until,
1187 Some(transformer),
1188 Arc::new(StringSchema),
1189 Arc::new(StringSchema),
1190 FilterResult::keep_all,
1191 false.then_some(|| unreachable!()),
1192 async {},
1193 ErrorHandler::Halt("test"),
1194 );
1195 (stream.leave(outer), tokens)
1196 });
1197
1198 let probe = ProbeHandle::new();
1199 let stream = stream.probe_with(&probe);
1200 (stream.capture(), probe, token)
1201 });
1202
1203 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1204 while probe.less_than(&expected_frontier) {
1205 assert!(
1206 Instant::now() < deadline,
1207 "timed out waiting for output frontier {expected_frontier}"
1208 );
1209 worker.step();
1210 }
1211 drop(token);
1212
1213 let mut blob_count = 0;
1214 while let Ok(event) = capture.try_recv() {
1215 if let CaptureEvent::Messages(_, msgs) = event {
1216 blob_count += msgs.len();
1217 }
1218 }
1219 let mut frontier = Antichain::new();
1220 probe.with_frontier(|f| frontier.extend(f.iter().cloned()));
1221 (blob_count, frontier)
1222 });
1223
1224 assert!(blob_count >= 1, "expected at least one fetched blob");
1225 assert_eq!(frontier, Antichain::from_elem(expected_frontier));
1226 }
1227
1228 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1232 #[cfg_attr(miri, ignore)] async fn test_shard_source_shutdown_mid_stream() {
1234 let persist_client = PersistClient::new_for_tests().await;
1235 let shard_id = ShardId::new();
1236
1237 let mut write = persist_client
1238 .open_writer::<String, String, u64, u64>(
1239 shard_id,
1240 Arc::new(StringSchema),
1241 Arc::new(StringSchema),
1242 Diagnostics::for_tests(),
1243 )
1244 .await
1245 .expect("invalid usage");
1246 let data = [(("k1".to_owned(), "v1".to_owned()), 0u64, 1u64)];
1247 write.expect_compare_and_append(&data[..], 0, 5).await;
1248
1249 timely::execute::execute_directly(move |worker| {
1250 let as_of = Antichain::from_elem(0);
1251 let until = Antichain::new();
1254
1255 let (probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1256 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1257 let transformer = move |_, descs, _| (descs, vec![]);
1258 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1259 outer,
1260 scope,
1261 "test_source",
1262 move || std::future::ready(persist_client.clone()),
1263 shard_id,
1264 Some(as_of),
1265 SnapshotMode::Include,
1266 until,
1267 Some(transformer),
1268 Arc::new(StringSchema),
1269 Arc::new(StringSchema),
1270 FilterResult::keep_all,
1271 false.then_some(|| unreachable!()),
1272 async {},
1273 ErrorHandler::Halt("test"),
1274 );
1275 (stream.leave(outer), tokens)
1276 });
1277
1278 let probe = ProbeHandle::new();
1279 let _stream = stream.probe_with(&probe);
1280 (probe, token)
1281 });
1282
1283 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1286 while probe.less_than(&1) {
1287 assert!(Instant::now() < deadline, "timed out waiting for progress");
1288 worker.step();
1289 }
1290
1291 drop(token);
1295 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1296 loop {
1297 assert!(Instant::now() < deadline, "timed out waiting for shutdown");
1298 worker.step();
1299 if probe.with_frontier(|f| f.is_empty()) {
1300 break;
1301 }
1302 }
1303 });
1304 }
1305
1306 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1310 #[cfg_attr(miri, ignore)] async fn test_shard_source_error_freeze() {
1312 let persist_client = PersistClient::new_for_tests().await;
1313 let shard_id = ShardId::new();
1314
1315 let mut write = persist_client
1319 .open_writer::<String, String, u64, u64>(
1320 shard_id,
1321 Arc::new(StringSchema),
1322 Arc::new(StringSchema),
1323 Diagnostics::for_tests(),
1324 )
1325 .await
1326 .expect("invalid usage");
1327 let data = [(("k1".to_owned(), "v1".to_owned()), 0u64, 1u64)];
1328 write.expect_compare_and_append(&data[..], 0, 5).await;
1329 initialize_shard(&persist_client, shard_id, Antichain::from_elem(3)).await;
1330
1331 let (errored, frontier) = timely::execute::execute_directly(move |worker| {
1332 let as_of = Antichain::from_elem(1);
1333 let until = Antichain::new();
1334
1335 let errored = Rc::new(std::cell::Cell::new(false));
1336 let error_handler = ErrorHandler::signal({
1337 let errored = Rc::clone(&errored);
1338 move |_err| errored.set(true)
1339 });
1340
1341 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1342 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1343 let transformer = move |_, descs, _| (descs, vec![]);
1344 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1345 outer,
1346 scope,
1347 "test_source",
1348 move || std::future::ready(persist_client.clone()),
1349 shard_id,
1350 Some(as_of),
1351 SnapshotMode::Include,
1352 until,
1353 Some(transformer),
1354 Arc::new(StringSchema),
1355 Arc::new(StringSchema),
1356 FilterResult::keep_all,
1357 false.then_some(|| unreachable!()),
1358 async {},
1359 error_handler,
1360 );
1361 (stream.leave(outer), tokens)
1362 });
1363
1364 let probe = ProbeHandle::new();
1365 let _stream = stream.probe_with(&probe);
1366 (probe, token)
1367 });
1368
1369 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1370 while !errored.get() {
1371 assert!(Instant::now() < deadline, "timed out waiting for error");
1372 worker.step();
1373 }
1374 for _ in 0..100 {
1376 worker.step();
1377 }
1378
1379 let mut frontier = Antichain::new();
1380 probe.with_frontier(|f| frontier.extend(f.iter().cloned()));
1381 (errored.get(), frontier)
1382 });
1383
1384 assert!(errored);
1385 assert_eq!(frontier, Antichain::from_elem(1));
1386 }
1387
1388 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1400 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_error_freeze() {
1402 let mut cache = PersistClientCache::new_no_metrics();
1405 cache.cfg.compaction_enabled = false;
1406 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1407 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1408 let persist_client = cache
1409 .open(PersistLocation::new_in_mem())
1410 .await
1411 .expect("in-mem location is valid");
1412 let shard_id = ShardId::new();
1413 let blob = Arc::clone(&persist_client.blob);
1416
1417 let mut write = persist_client
1418 .open_writer::<String, String, u64, u64>(
1419 shard_id,
1420 Arc::new(StringSchema),
1421 Arc::new(StringSchema),
1422 Diagnostics::for_tests(),
1423 )
1424 .await
1425 .expect("invalid usage");
1426
1427 async fn batch_keys(blob: &dyn Blob) -> std::collections::BTreeSet<String> {
1429 let mut keys = std::collections::BTreeSet::new();
1430 blob.list_keys_and_metadata("", &mut |meta| {
1431 if let Ok((_, PartialBlobKey::Batch(..))) = BlobKey::parse_ids(meta.key) {
1432 keys.insert(meta.key.to_owned());
1433 }
1434 })
1435 .await
1436 .expect("list keys");
1437 keys
1438 }
1439
1440 let row = |t: u64| ((format!("k{t}"), format!("v{t}")), t, 1u64);
1441 let before = batch_keys(blob.as_ref()).await;
1442 write.expect_compare_and_append(&[row(0)], 0, 1).await;
1443 let after = batch_keys(blob.as_ref()).await;
1444 write.expect_compare_and_append(&[row(1)], 1, 2).await;
1445 write.expect_compare_and_append(&[row(2)], 2, 3).await;
1446
1447 let missing: Vec<_> = after.difference(&before).cloned().collect();
1450 assert!(!missing.is_empty(), "first batch wrote no blob part");
1451 for key in &missing {
1452 blob.delete(key).await.expect("delete");
1453 }
1454
1455 let frontier = timely::execute::execute_directly(move |worker| {
1456 let errored = Rc::new(std::cell::Cell::new(false));
1457 let error_handler = ErrorHandler::signal({
1458 let errored = Rc::clone(&errored);
1459 move |_err| errored.set(true)
1460 });
1461
1462 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1463 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1464 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1465 outer,
1466 scope,
1467 "test_source",
1468 move || std::future::ready(persist_client.clone()),
1469 shard_id,
1470 Some(Antichain::from_elem(0)),
1471 SnapshotMode::Include,
1472 Antichain::new(),
1473 Some(move |_, descs, _| (descs, vec![])),
1474 Arc::new(StringSchema),
1475 Arc::new(StringSchema),
1476 FilterResult::keep_all,
1477 false.then_some(|| unreachable!()),
1478 async {},
1479 error_handler,
1480 );
1481 (stream.leave(outer), tokens)
1482 });
1483 let probe = ProbeHandle::new();
1484 stream.probe_with(&probe);
1485 (probe, token)
1486 });
1487
1488 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1491 while !errored.get() {
1492 assert!(
1493 Instant::now() < deadline,
1494 "timed out waiting for fetch error"
1495 );
1496 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1497 }
1498 let mut last = probe.with_frontier(|f| f.to_owned());
1499 let mut stable = 0;
1500 while stable < 100 {
1501 assert!(Instant::now() < deadline, "timed out waiting for quiesce");
1502 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1503 let now = probe.with_frontier(|f| f.to_owned());
1504 if now == last {
1505 stable += 1;
1506 } else {
1507 stable = 0;
1508 last = now;
1509 }
1510 }
1511 last
1512 });
1513
1514 assert_eq!(frontier, Antichain::from_elem(0));
1516 }
1517
1518 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1523 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_concurrent() {
1525 const N_BATCHES: u64 = 16;
1526
1527 let mut cache = PersistClientCache::new_no_metrics();
1528 cache.cfg.compaction_enabled = false;
1529 cache.cfg.set_config(&SOURCE_FETCH_CONCURRENCY, 8);
1530 let persist_client = cache
1531 .open(PersistLocation::new_in_mem())
1532 .await
1533 .expect("in-mem location is valid");
1534 let shard_id = ShardId::new();
1535
1536 let mut write = persist_client
1537 .open_writer::<String, String, u64, u64>(
1538 shard_id,
1539 Arc::new(StringSchema),
1540 Arc::new(StringSchema),
1541 Diagnostics::for_tests(),
1542 )
1543 .await
1544 .expect("invalid usage");
1545 for t in 0..N_BATCHES {
1546 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1547 write.expect_compare_and_append(&[row], t, t + 1).await;
1548 }
1549
1550 let (blob_count, max_time) = timely::execute::execute_directly(move |worker| {
1551 let (capture, probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1552 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1553 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1554 outer,
1555 scope,
1556 "test_source",
1557 move || std::future::ready(persist_client.clone()),
1558 shard_id,
1559 Some(Antichain::from_elem(0)),
1560 SnapshotMode::Include,
1561 Antichain::from_elem(N_BATCHES),
1562 Some(move |_, descs, _| (descs, vec![])),
1563 Arc::new(StringSchema),
1564 Arc::new(StringSchema),
1565 FilterResult::keep_all,
1566 false.then_some(|| unreachable!()),
1567 async {},
1568 ErrorHandler::Halt("test"),
1569 );
1570 (stream.leave(outer), tokens)
1571 });
1572 let probe = ProbeHandle::new();
1573 let stream = stream.probe_with(&probe);
1574 (stream.capture(), probe, token)
1575 });
1576
1577 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1581 while !probe.with_frontier(|f| f.is_empty()) {
1582 assert!(
1583 Instant::now() < deadline,
1584 "timed out waiting for completion"
1585 );
1586 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1587 }
1588 drop(token);
1589
1590 let mut blob_count = 0;
1591 let mut max_time: Option<u64> = None;
1592 while let Ok(event) = capture.try_recv() {
1593 if let CaptureEvent::Messages(time, msgs) = event {
1594 blob_count += msgs.len();
1595 max_time = max_time.max(Some(time));
1596 }
1597 }
1598 (blob_count, max_time)
1599 });
1600
1601 assert!(blob_count >= 1, "expected at least one fetched blob");
1602 assert_eq!(max_time, Some(N_BATCHES - 1));
1604 }
1605
1606 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1613 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_concurrent_error_freeze() {
1615 const N_BATCHES: u64 = 12;
1616 const MISSING_TS: u64 = 5;
1617
1618 let mut cache = PersistClientCache::new_no_metrics();
1619 cache.cfg.compaction_enabled = false;
1620 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1621 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1622 cache.cfg.set_config(&SOURCE_FETCH_CONCURRENCY, 8);
1623 let persist_client = cache
1624 .open(PersistLocation::new_in_mem())
1625 .await
1626 .expect("in-mem location is valid");
1627 let shard_id = ShardId::new();
1628 let blob = Arc::clone(&persist_client.blob);
1629
1630 let mut write = persist_client
1631 .open_writer::<String, String, u64, u64>(
1632 shard_id,
1633 Arc::new(StringSchema),
1634 Arc::new(StringSchema),
1635 Diagnostics::for_tests(),
1636 )
1637 .await
1638 .expect("invalid usage");
1639
1640 async fn batch_keys(blob: &dyn Blob) -> std::collections::BTreeSet<String> {
1641 let mut keys = std::collections::BTreeSet::new();
1642 blob.list_keys_and_metadata("", &mut |meta| {
1643 if let Ok((_, PartialBlobKey::Batch(..))) = BlobKey::parse_ids(meta.key) {
1644 keys.insert(meta.key.to_owned());
1645 }
1646 })
1647 .await
1648 .expect("list keys");
1649 keys
1650 }
1651
1652 let mut missing = Vec::new();
1655 for t in 0..N_BATCHES {
1656 let before = batch_keys(blob.as_ref()).await;
1657 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1658 write.expect_compare_and_append(&[row], t, t + 1).await;
1659 if t == MISSING_TS {
1660 let after = batch_keys(blob.as_ref()).await;
1661 missing = after.difference(&before).cloned().collect();
1662 }
1663 }
1664 assert!(!missing.is_empty(), "missing batch wrote no blob part");
1665 for key in &missing {
1666 blob.delete(key).await.expect("delete");
1667 }
1668
1669 let frontier = timely::execute::execute_directly(move |worker| {
1670 let errored = Rc::new(std::cell::Cell::new(false));
1671 let error_handler = ErrorHandler::signal({
1672 let errored = Rc::clone(&errored);
1673 move |_err| errored.set(true)
1674 });
1675
1676 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1677 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1678 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1679 outer,
1680 scope,
1681 "test_source",
1682 move || std::future::ready(persist_client.clone()),
1683 shard_id,
1684 Some(Antichain::from_elem(0)),
1685 SnapshotMode::Include,
1686 Antichain::from_elem(N_BATCHES),
1687 Some(move |_, descs, _| (descs, vec![])),
1688 Arc::new(StringSchema),
1689 Arc::new(StringSchema),
1690 FilterResult::keep_all,
1691 false.then_some(|| unreachable!()),
1692 async {},
1693 error_handler,
1694 );
1695 (stream.leave(outer), tokens)
1696 });
1697 let probe = ProbeHandle::new();
1698 stream.probe_with(&probe);
1699 (probe, token)
1700 });
1701
1702 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1703 while !errored.get() {
1704 assert!(
1705 Instant::now() < deadline,
1706 "timed out waiting for fetch error"
1707 );
1708 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1709 }
1710 let mut last = probe.with_frontier(|f| f.to_owned());
1711 let mut stable = 0;
1712 while stable < 100 {
1713 assert!(Instant::now() < deadline, "timed out waiting for quiesce");
1714 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1715 let now = probe.with_frontier(|f| f.to_owned());
1716 if now == last {
1717 stable += 1;
1718 } else {
1719 stable = 0;
1720 last = now;
1721 }
1722 }
1723 last
1724 });
1725
1726 assert_eq!(frontier, Antichain::from_elem(MISSING_TS));
1729 }
1730
1731 async fn initialize_shard(
1732 persist_client: &PersistClient,
1733 shard_id: ShardId,
1734 since: Antichain<u64>,
1735 ) {
1736 let mut read_handle = persist_client
1737 .open_leased_reader::<String, String, u64, u64>(
1738 shard_id,
1739 Arc::new(<std::string::String as mz_persist_types::Codec>::Schema::default()),
1740 Arc::new(<std::string::String as mz_persist_types::Codec>::Schema::default()),
1741 Diagnostics::for_tests(),
1742 true,
1743 )
1744 .await
1745 .expect("invalid usage");
1746
1747 read_handle.downgrade_since(&since).await;
1748 }
1749}