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 filter_fn(&stats.decode(), current_frontier.borrow())
538 });
539 should_fetch
540 }
541 BatchPart::Inline { .. } => FilterResult::Keep,
542 };
543 let bytes = u64::cast_from(part_desc.encoded_size_bytes());
545 match filter_result {
546 FilterResult::Keep => {
547 audit_budget_bytes = audit_budget_bytes.saturating_add(bytes);
548 }
549 FilterResult::Discard => {
550 metrics.pushdown.parts_filtered_count.inc();
551 metrics.pushdown.parts_filtered_bytes.inc_by(bytes);
552 let should_audit = match &part_desc.part {
553 BatchPart::Hollow(x) => {
554 let mut h = DefaultHasher::new();
555 x.key.hash(&mut h);
556 usize::cast_from(h.finish()) % 100
557 < STATS_AUDIT_PERCENT.get(&cfg)
558 }
559 BatchPart::Inline { .. } => false,
560 };
561 if should_audit && bytes < audit_budget_bytes {
562 audit_budget_bytes -= bytes;
563 metrics.pushdown.parts_audited_count.inc();
564 metrics.pushdown.parts_audited_bytes.inc_by(bytes);
565 part_desc.request_filter_pushdown_audit();
566 } else {
567 debug!(
568 "skipping part because of stats filter {:?}",
569 part_desc.part.stats()
570 );
571 continue;
572 }
573 }
574 FilterResult::ReplaceWith { key, val } => {
575 part_desc.maybe_optimize(&cfg, key, val);
576 audit_budget_bytes = audit_budget_bytes.saturating_add(bytes);
577 }
578 }
579 let bytes = u64::cast_from(part_desc.encoded_size_bytes());
580 if part_desc.part.is_inline() {
581 metrics.pushdown.parts_inline_count.inc();
582 metrics.pushdown.parts_inline_bytes.inc_by(bytes);
583 } else {
584 metrics.pushdown.parts_fetched_count.inc();
585 metrics.pushdown.parts_fetched_bytes.inc_by(bytes);
586 }
587 }
588
589 let worker_idx = usize::cast_from(Instant::now().hashed()) % num_workers;
596 batch_bytes =
597 batch_bytes.saturating_add(u64::cast_from(part_desc.encoded_size_bytes()));
598 let (part, lease) = part_desc.into_exchangeable_part();
599 leases.borrow_mut().push_at(current_ts.clone(), lease);
600 descs_output.give(&session_cap, (worker_idx, part));
601 }
602
603 current_frontier.join_assign(&progress);
604 coalesced_bytes = coalesced_bytes.saturating_add(batch_bytes);
605
606 let caught_up = PartialOrder::less_equal(&replay_upper, ¤t_frontier);
612 let coalesce = coalesce_target > 0 && !caught_up && coalesced_bytes < coalesce_target;
613 if !coalesce {
614 coalesced_bytes = 0;
615 cap_set.downgrade(current_frontier.iter());
616 }
617 }
618 });
619
620 (descs_stream, shutdown_button.press_on_drop())
621}
622
623pub(crate) fn shard_source_fetch<'inner, K, V, T, D, TInner>(
624 descs: StreamVec<'inner, TInner, (usize, ExchangeableBatchPart<T>)>,
625 name: &str,
626 client: impl Future<Output = PersistClient> + Send + 'static,
627 shard_id: ShardId,
628 key_schema: Arc<K::Schema>,
629 val_schema: Arc<V::Schema>,
630 is_transient: bool,
631 error_handler: ErrorHandler,
632) -> (
633 StreamVec<'inner, TInner, FetchedBlob<K, V, T, D>>,
634 StreamVec<'inner, TInner, Infallible>,
635 PressOnDropButton,
636)
637where
638 K: Debug + Codec,
639 V: Debug + Codec,
640 T: Timestamp + Lattice + Codec64 + Sync,
641 D: Monoid + Codec64 + Send + Sync,
642 TInner: Timestamp + Refines<T>,
643{
644 let mut builder =
645 AsyncOperatorBuilder::new(format!("shard_source_fetch({})", name), descs.scope());
646 let (fetched_output, fetched_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
647 let (completed_fetches_output, completed_fetches_stream) =
648 builder.new_output::<CapacityContainerBuilder<Vec<Infallible>>>();
649 let mut descs_input = builder.new_input_for_many(
650 descs,
651 Exchange::new(|&(i, _): &(usize, _)| u64::cast_from(i)),
652 [&fetched_output, &completed_fetches_output],
653 );
654 let name_owned = name.to_owned();
655
656 let shutdown_button = builder.build(move |_capabilities| async move {
657 let (fetcher, max_concurrency) =
661 mz_ore::task::spawn(|| format!("shard_source_fetch({})", name_owned), {
662 let diagnostics = Diagnostics {
663 shard_name: name_owned.clone(),
664 handle_purpose: format!("shard_source_fetch batch fetcher {}", name_owned),
665 };
666 async move {
667 let client = client.await;
668 let max_concurrency = SOURCE_FETCH_CONCURRENCY.get(client.dyncfgs()).max(1);
675 let fetcher = client
676 .create_batch_fetcher::<K, V, T, D>(
677 shard_id,
678 key_schema,
679 val_schema,
680 is_transient,
681 diagnostics,
682 )
683 .await
684 .expect("shard codecs should not change");
685 (fetcher, max_concurrency)
686 }
687 })
688 .await;
689
690 let fetch_one = |caps: [Capability<TInner>; 2], part: ExchangeableBatchPart<T>| {
696 let mut fetcher = fetcher.clone();
697 async move {
698 let reader_id = part.reader_id().clone();
699 let fetched = fetcher
700 .fetch_leased_part(part)
701 .await
702 .expect("shard_id should match across all workers");
703 let fetched = match fetched {
704 Ok(fetched) => Ok(fetched),
705 Err(blob_key) => {
706 let diagnostics = fetcher.missing_blob_diagnostics(&reader_id).await;
718 Err(anyhow!(
719 "batch fetcher could not fetch batch part {}: {}",
720 blob_key,
721 diagnostics
722 ))
723 }
724 };
725 (caps, fetched)
726 }
727 };
728
729 let mut pending: VecDeque<([Capability<TInner>; 2], ExchangeableBatchPart<T>)> =
740 VecDeque::new();
741 let mut in_flight = FuturesUnordered::new();
742 let mut input_done = false;
743
744 loop {
745 while in_flight.len() < max_concurrency {
747 let Some((caps, part)) = pending.pop_front() else {
748 break;
749 };
750 in_flight.push(fetch_one(caps, part));
751 }
752
753 tokio::select! {
754 biased;
757 Some(([cap, _], fetched)) = in_flight.next(), if !in_flight.is_empty() => {
758 match fetched {
759 Ok(fetched) => fetched_output.give(&cap, fetched),
760 Err(e) => {
761 error_handler.report_and_stop(e).await;
766 }
767 }
768 }
769 event = descs_input.next(), if !input_done => {
772 match event {
773 Some(Event::Data(caps, data)) => {
774 for (_idx, part) in data {
777 pending.push_back((caps.clone(), part));
778 }
779 }
780 Some(Event::Progress(_)) => {}
781 None => input_done = true,
782 }
783 }
784 else => break,
786 }
787 }
788 });
789
790 (
791 fetched_stream,
792 completed_fetches_stream,
793 shutdown_button.press_on_drop(),
794 )
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use std::sync::Arc;
801
802 use mz_persist::location::{Blob, SeqNo};
803 use mz_persist_types::codec_impls::StringSchema;
804 use timely::dataflow::operators::Leave;
805 use timely::dataflow::operators::Probe;
806 use timely::dataflow::operators::capture::{Capture, Event as CaptureEvent};
807 use timely::dataflow::operators::probe::Handle as ProbeHandle;
808 use timely::progress::Antichain;
809
810 use crate::batch::{INLINE_WRITES_SINGLE_MAX_BYTES, INLINE_WRITES_TOTAL_MAX_BYTES};
811 use crate::cache::PersistClientCache;
812 use crate::internal::paths::{BlobKey, PartialBlobKey};
813 use crate::operators::shard_source::shard_source;
814 use crate::{Diagnostics, PersistLocation, ShardId};
815
816 #[mz_ore::test]
817 fn test_lease_manager() {
818 let lease = Lease::new(SeqNo::minimum());
819 let mut manager = LeaseManager::new();
820 for t in 0u64..10 {
821 manager.push_at(t, lease.clone());
822 }
823 assert_eq!(lease.count(), 11);
824 manager.advance_to(AntichainRef::new(&[5]));
825 assert_eq!(lease.count(), 6);
826 manager.advance_to(AntichainRef::new(&[3]));
827 assert_eq!(lease.count(), 6);
828 manager.advance_to(AntichainRef::new(&[9]));
829 assert_eq!(lease.count(), 2);
830 manager.advance_to(AntichainRef::new(&[10]));
831 assert_eq!(lease.count(), 1);
832 }
833
834 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
842 #[cfg_attr(miri, ignore)] async fn test_shard_source_implicit_initial_as_of() {
844 let persist_client = PersistClient::new_for_tests().await;
845
846 let expected_frontier = 42;
847 let shard_id = ShardId::new();
848
849 initialize_shard(
850 &persist_client,
851 shard_id,
852 Antichain::from_elem(expected_frontier),
853 )
854 .await;
855
856 let res = timely::execute::execute_directly(move |worker| {
857 let until = Antichain::new();
858
859 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
860 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
861 let transformer = move |_, descs, _| (descs, vec![]);
862 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
863 outer,
864 scope,
865 "test_source",
866 move || std::future::ready(persist_client.clone()),
867 shard_id,
868 None, SnapshotMode::Include,
870 until,
871 Some(transformer),
872 Arc::new(
873 <std::string::String as mz_persist_types::Codec>::Schema::default(),
874 ),
875 Arc::new(
876 <std::string::String as mz_persist_types::Codec>::Schema::default(),
877 ),
878 FilterResult::keep_all,
879 false.then_some(|| unreachable!()),
880 async {},
881 ErrorHandler::Halt("test"),
882 );
883 (stream.leave(outer), tokens)
884 });
885
886 let probe = ProbeHandle::new();
887 let _stream = stream.probe_with(&probe);
888
889 (probe, token)
890 });
891
892 while probe.less_than(&expected_frontier) {
893 worker.step();
894 }
895
896 let mut probe_frontier = Antichain::new();
897 probe.with_frontier(|f| probe_frontier.extend(f.iter().cloned()));
898
899 probe_frontier
900 });
901
902 assert_eq!(res, Antichain::from_elem(expected_frontier));
903 }
904
905 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
912 #[cfg_attr(miri, ignore)] async fn test_shard_source_explicit_initial_as_of() {
914 let persist_client = PersistClient::new_for_tests().await;
915
916 let expected_frontier = 42;
917 let shard_id = ShardId::new();
918
919 initialize_shard(
920 &persist_client,
921 shard_id,
922 Antichain::from_elem(expected_frontier),
923 )
924 .await;
925
926 let res = timely::execute::execute_directly(move |worker| {
927 let as_of = Antichain::from_elem(expected_frontier);
928 let until = Antichain::new();
929
930 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
931 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
932 let transformer = move |_, descs, _| (descs, vec![]);
933 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
934 outer,
935 scope,
936 "test_source",
937 move || std::future::ready(persist_client.clone()),
938 shard_id,
939 Some(as_of), SnapshotMode::Include,
941 until,
942 Some(transformer),
943 Arc::new(
944 <std::string::String as mz_persist_types::Codec>::Schema::default(),
945 ),
946 Arc::new(
947 <std::string::String as mz_persist_types::Codec>::Schema::default(),
948 ),
949 FilterResult::keep_all,
950 false.then_some(|| unreachable!()),
951 async {},
952 ErrorHandler::Halt("test"),
953 );
954 (stream.leave(outer), tokens)
955 });
956
957 let probe = ProbeHandle::new();
958 let _stream = stream.probe_with(&probe);
959
960 (probe, token)
961 });
962
963 while probe.less_than(&expected_frontier) {
964 worker.step();
965 }
966
967 let mut probe_frontier = Antichain::new();
968 probe.with_frontier(|f| probe_frontier.extend(f.iter().cloned()));
969
970 probe_frontier
971 });
972
973 assert_eq!(res, Antichain::from_elem(expected_frontier));
974 }
975
976 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
992 #[cfg_attr(miri, ignore)] async fn test_shard_source_hydration_frontier_coalesce() {
994 const N_BATCHES: u64 = 64;
995
996 async fn run(coalesce_bytes: usize) -> Option<u64> {
1001 let mut cache = PersistClientCache::new_no_metrics();
1002 cache.cfg.compaction_enabled = false;
1005 cache
1006 .cfg
1007 .set_config(&SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES, coalesce_bytes);
1008 let persist_client = cache
1009 .open(PersistLocation::new_in_mem())
1010 .await
1011 .expect("in-mem location is valid");
1012 let shard_id = ShardId::new();
1013
1014 let mut write = persist_client
1015 .open_writer::<String, String, u64, u64>(
1016 shard_id,
1017 Arc::new(StringSchema),
1018 Arc::new(StringSchema),
1019 Diagnostics::for_tests(),
1020 )
1021 .await
1022 .expect("invalid usage");
1023
1024 for t in 0..N_BATCHES {
1027 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1028 write.expect_compare_and_append(&[row], t, t + 1).await;
1029 }
1030
1031 timely::execute::execute_directly(move |worker| {
1032 let (probe, receiver, _token) = worker.dataflow::<u64, _, _>(|outer| {
1033 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1034 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1035 outer,
1036 scope,
1037 "test_source",
1038 move || std::future::ready(persist_client.clone()),
1039 shard_id,
1040 Some(Antichain::from_elem(0)),
1041 SnapshotMode::Include,
1042 Antichain::from_elem(N_BATCHES),
1045 Some(move |_, descs, _| (descs, vec![])),
1046 Arc::new(StringSchema),
1047 Arc::new(StringSchema),
1048 FilterResult::keep_all,
1049 false.then_some(|| unreachable!()),
1050 async {},
1051 ErrorHandler::Halt("test"),
1052 );
1053 (stream.leave(outer), tokens)
1054 });
1055 let probe = ProbeHandle::new();
1056 let receiver = stream.probe_with(&probe).capture();
1060 (probe, receiver, token)
1061 });
1062
1063 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1066 while !probe.with_frontier(|f| f.is_empty()) {
1067 assert!(Instant::now() < deadline, "timed out hydrating shard");
1068 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1069 }
1070
1071 let mut max_msg_time: Option<u64> = None;
1076 while let Ok(event) = receiver.try_recv() {
1077 if let CaptureEvent::Messages(time, _) = event {
1078 max_msg_time = max_msg_time.max(Some(time));
1079 }
1080 }
1081 max_msg_time
1082 })
1083 }
1084
1085 assert_eq!(run(0).await, Some(N_BATCHES - 1));
1093 assert_eq!(run(1 << 30).await, Some(N_BATCHES - 1));
1094 }
1095
1096 #[mz_ore::test]
1101 fn test_frontier_coalesce_decision() {
1102 fn forwards(coalesce_target: u64, n: u64, bytes_per_batch: u64) -> usize {
1103 let replay_upper = n;
1104 let mut current = 0u64;
1105 let mut coalesced = 0u64;
1106 let mut forwarded = 0usize;
1107 for _ in 0..n {
1108 current += 1;
1109 coalesced += bytes_per_batch;
1110 let caught_up = replay_upper <= current;
1111 let coalesce = coalesce_target > 0 && !caught_up && coalesced < coalesce_target;
1113 if !coalesce {
1114 forwarded += 1;
1115 coalesced = 0;
1116 }
1117 }
1118 forwarded
1119 }
1120
1121 assert_eq!(forwards(0, 64, 10), 64);
1123 assert_eq!(forwards(1 << 30, 64, 10), 1);
1125 let partial = forwards(100, 64, 10);
1128 assert!(
1129 partial > 1 && partial < 64,
1130 "expected partial coalescing, got {partial}"
1131 );
1132 }
1133
1134 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1138 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetches_data() {
1140 let persist_client = PersistClient::new_for_tests().await;
1141 let shard_id = ShardId::new();
1142
1143 let mut write = persist_client
1144 .open_writer::<String, String, u64, u64>(
1145 shard_id,
1146 Arc::new(StringSchema),
1147 Arc::new(StringSchema),
1148 Diagnostics::for_tests(),
1149 )
1150 .await
1151 .expect("invalid usage");
1152 let data = [
1153 (("k1".to_owned(), "v1".to_owned()), 0u64, 1u64),
1154 (("k2".to_owned(), "v2".to_owned()), 1u64, 1u64),
1155 ];
1156 write.expect_compare_and_append(&data[..], 0, 5).await;
1157
1158 let expected_frontier = 5;
1159 let (blob_count, frontier) = timely::execute::execute_directly(move |worker| {
1160 let as_of = Antichain::from_elem(0);
1161 let until = Antichain::new();
1162
1163 let (capture, probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1164 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1165 let transformer = move |_, descs, _| (descs, vec![]);
1166 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1167 outer,
1168 scope,
1169 "test_source",
1170 move || std::future::ready(persist_client.clone()),
1171 shard_id,
1172 Some(as_of),
1173 SnapshotMode::Include,
1174 until,
1175 Some(transformer),
1176 Arc::new(StringSchema),
1177 Arc::new(StringSchema),
1178 FilterResult::keep_all,
1179 false.then_some(|| unreachable!()),
1180 async {},
1181 ErrorHandler::Halt("test"),
1182 );
1183 (stream.leave(outer), tokens)
1184 });
1185
1186 let probe = ProbeHandle::new();
1187 let stream = stream.probe_with(&probe);
1188 (stream.capture(), probe, token)
1189 });
1190
1191 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1192 while probe.less_than(&expected_frontier) {
1193 assert!(
1194 Instant::now() < deadline,
1195 "timed out waiting for output frontier {expected_frontier}"
1196 );
1197 worker.step();
1198 }
1199 drop(token);
1200
1201 let mut blob_count = 0;
1202 while let Ok(event) = capture.try_recv() {
1203 if let CaptureEvent::Messages(_, msgs) = event {
1204 blob_count += msgs.len();
1205 }
1206 }
1207 let mut frontier = Antichain::new();
1208 probe.with_frontier(|f| frontier.extend(f.iter().cloned()));
1209 (blob_count, frontier)
1210 });
1211
1212 assert!(blob_count >= 1, "expected at least one fetched blob");
1213 assert_eq!(frontier, Antichain::from_elem(expected_frontier));
1214 }
1215
1216 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1220 #[cfg_attr(miri, ignore)] async fn test_shard_source_shutdown_mid_stream() {
1222 let persist_client = PersistClient::new_for_tests().await;
1223 let shard_id = ShardId::new();
1224
1225 let mut write = persist_client
1226 .open_writer::<String, String, u64, u64>(
1227 shard_id,
1228 Arc::new(StringSchema),
1229 Arc::new(StringSchema),
1230 Diagnostics::for_tests(),
1231 )
1232 .await
1233 .expect("invalid usage");
1234 let data = [(("k1".to_owned(), "v1".to_owned()), 0u64, 1u64)];
1235 write.expect_compare_and_append(&data[..], 0, 5).await;
1236
1237 timely::execute::execute_directly(move |worker| {
1238 let as_of = Antichain::from_elem(0);
1239 let until = Antichain::new();
1242
1243 let (probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1244 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1245 let transformer = move |_, descs, _| (descs, vec![]);
1246 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1247 outer,
1248 scope,
1249 "test_source",
1250 move || std::future::ready(persist_client.clone()),
1251 shard_id,
1252 Some(as_of),
1253 SnapshotMode::Include,
1254 until,
1255 Some(transformer),
1256 Arc::new(StringSchema),
1257 Arc::new(StringSchema),
1258 FilterResult::keep_all,
1259 false.then_some(|| unreachable!()),
1260 async {},
1261 ErrorHandler::Halt("test"),
1262 );
1263 (stream.leave(outer), tokens)
1264 });
1265
1266 let probe = ProbeHandle::new();
1267 let _stream = stream.probe_with(&probe);
1268 (probe, token)
1269 });
1270
1271 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1274 while probe.less_than(&1) {
1275 assert!(Instant::now() < deadline, "timed out waiting for progress");
1276 worker.step();
1277 }
1278
1279 drop(token);
1283 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1284 loop {
1285 assert!(Instant::now() < deadline, "timed out waiting for shutdown");
1286 worker.step();
1287 if probe.with_frontier(|f| f.is_empty()) {
1288 break;
1289 }
1290 }
1291 });
1292 }
1293
1294 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1298 #[cfg_attr(miri, ignore)] async fn test_shard_source_error_freeze() {
1300 let persist_client = PersistClient::new_for_tests().await;
1301 let shard_id = ShardId::new();
1302
1303 let mut write = persist_client
1307 .open_writer::<String, String, u64, u64>(
1308 shard_id,
1309 Arc::new(StringSchema),
1310 Arc::new(StringSchema),
1311 Diagnostics::for_tests(),
1312 )
1313 .await
1314 .expect("invalid usage");
1315 let data = [(("k1".to_owned(), "v1".to_owned()), 0u64, 1u64)];
1316 write.expect_compare_and_append(&data[..], 0, 5).await;
1317 initialize_shard(&persist_client, shard_id, Antichain::from_elem(3)).await;
1318
1319 let (errored, frontier) = timely::execute::execute_directly(move |worker| {
1320 let as_of = Antichain::from_elem(1);
1321 let until = Antichain::new();
1322
1323 let errored = Rc::new(std::cell::Cell::new(false));
1324 let error_handler = ErrorHandler::signal({
1325 let errored = Rc::clone(&errored);
1326 move |_err| errored.set(true)
1327 });
1328
1329 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1330 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1331 let transformer = move |_, descs, _| (descs, vec![]);
1332 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1333 outer,
1334 scope,
1335 "test_source",
1336 move || std::future::ready(persist_client.clone()),
1337 shard_id,
1338 Some(as_of),
1339 SnapshotMode::Include,
1340 until,
1341 Some(transformer),
1342 Arc::new(StringSchema),
1343 Arc::new(StringSchema),
1344 FilterResult::keep_all,
1345 false.then_some(|| unreachable!()),
1346 async {},
1347 error_handler,
1348 );
1349 (stream.leave(outer), tokens)
1350 });
1351
1352 let probe = ProbeHandle::new();
1353 let _stream = stream.probe_with(&probe);
1354 (probe, token)
1355 });
1356
1357 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1358 while !errored.get() {
1359 assert!(Instant::now() < deadline, "timed out waiting for error");
1360 worker.step();
1361 }
1362 for _ in 0..100 {
1364 worker.step();
1365 }
1366
1367 let mut frontier = Antichain::new();
1368 probe.with_frontier(|f| frontier.extend(f.iter().cloned()));
1369 (errored.get(), frontier)
1370 });
1371
1372 assert!(errored);
1373 assert_eq!(frontier, Antichain::from_elem(1));
1374 }
1375
1376 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1388 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_error_freeze() {
1390 let mut cache = PersistClientCache::new_no_metrics();
1393 cache.cfg.compaction_enabled = false;
1394 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1395 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1396 let persist_client = cache
1397 .open(PersistLocation::new_in_mem())
1398 .await
1399 .expect("in-mem location is valid");
1400 let shard_id = ShardId::new();
1401 let blob = Arc::clone(&persist_client.blob);
1404
1405 let mut write = persist_client
1406 .open_writer::<String, String, u64, u64>(
1407 shard_id,
1408 Arc::new(StringSchema),
1409 Arc::new(StringSchema),
1410 Diagnostics::for_tests(),
1411 )
1412 .await
1413 .expect("invalid usage");
1414
1415 async fn batch_keys(blob: &dyn Blob) -> std::collections::BTreeSet<String> {
1417 let mut keys = std::collections::BTreeSet::new();
1418 blob.list_keys_and_metadata("", &mut |meta| {
1419 if let Ok((_, PartialBlobKey::Batch(..))) = BlobKey::parse_ids(meta.key) {
1420 keys.insert(meta.key.to_owned());
1421 }
1422 })
1423 .await
1424 .expect("list keys");
1425 keys
1426 }
1427
1428 let row = |t: u64| ((format!("k{t}"), format!("v{t}")), t, 1u64);
1429 let before = batch_keys(blob.as_ref()).await;
1430 write.expect_compare_and_append(&[row(0)], 0, 1).await;
1431 let after = batch_keys(blob.as_ref()).await;
1432 write.expect_compare_and_append(&[row(1)], 1, 2).await;
1433 write.expect_compare_and_append(&[row(2)], 2, 3).await;
1434
1435 let missing: Vec<_> = after.difference(&before).cloned().collect();
1438 assert!(!missing.is_empty(), "first batch wrote no blob part");
1439 for key in &missing {
1440 blob.delete(key).await.expect("delete");
1441 }
1442
1443 let frontier = timely::execute::execute_directly(move |worker| {
1444 let errored = Rc::new(std::cell::Cell::new(false));
1445 let error_handler = ErrorHandler::signal({
1446 let errored = Rc::clone(&errored);
1447 move |_err| errored.set(true)
1448 });
1449
1450 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1451 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1452 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1453 outer,
1454 scope,
1455 "test_source",
1456 move || std::future::ready(persist_client.clone()),
1457 shard_id,
1458 Some(Antichain::from_elem(0)),
1459 SnapshotMode::Include,
1460 Antichain::new(),
1461 Some(move |_, descs, _| (descs, vec![])),
1462 Arc::new(StringSchema),
1463 Arc::new(StringSchema),
1464 FilterResult::keep_all,
1465 false.then_some(|| unreachable!()),
1466 async {},
1467 error_handler,
1468 );
1469 (stream.leave(outer), tokens)
1470 });
1471 let probe = ProbeHandle::new();
1472 stream.probe_with(&probe);
1473 (probe, token)
1474 });
1475
1476 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1479 while !errored.get() {
1480 assert!(
1481 Instant::now() < deadline,
1482 "timed out waiting for fetch error"
1483 );
1484 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1485 }
1486 let mut last = probe.with_frontier(|f| f.to_owned());
1487 let mut stable = 0;
1488 while stable < 100 {
1489 assert!(Instant::now() < deadline, "timed out waiting for quiesce");
1490 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1491 let now = probe.with_frontier(|f| f.to_owned());
1492 if now == last {
1493 stable += 1;
1494 } else {
1495 stable = 0;
1496 last = now;
1497 }
1498 }
1499 last
1500 });
1501
1502 assert_eq!(frontier, Antichain::from_elem(0));
1504 }
1505
1506 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1511 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_concurrent() {
1513 const N_BATCHES: u64 = 16;
1514
1515 let mut cache = PersistClientCache::new_no_metrics();
1516 cache.cfg.compaction_enabled = false;
1517 cache.cfg.set_config(&SOURCE_FETCH_CONCURRENCY, 8);
1518 let persist_client = cache
1519 .open(PersistLocation::new_in_mem())
1520 .await
1521 .expect("in-mem location is valid");
1522 let shard_id = ShardId::new();
1523
1524 let mut write = persist_client
1525 .open_writer::<String, String, u64, u64>(
1526 shard_id,
1527 Arc::new(StringSchema),
1528 Arc::new(StringSchema),
1529 Diagnostics::for_tests(),
1530 )
1531 .await
1532 .expect("invalid usage");
1533 for t in 0..N_BATCHES {
1534 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1535 write.expect_compare_and_append(&[row], t, t + 1).await;
1536 }
1537
1538 let (blob_count, max_time) = timely::execute::execute_directly(move |worker| {
1539 let (capture, probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1540 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1541 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1542 outer,
1543 scope,
1544 "test_source",
1545 move || std::future::ready(persist_client.clone()),
1546 shard_id,
1547 Some(Antichain::from_elem(0)),
1548 SnapshotMode::Include,
1549 Antichain::from_elem(N_BATCHES),
1550 Some(move |_, descs, _| (descs, vec![])),
1551 Arc::new(StringSchema),
1552 Arc::new(StringSchema),
1553 FilterResult::keep_all,
1554 false.then_some(|| unreachable!()),
1555 async {},
1556 ErrorHandler::Halt("test"),
1557 );
1558 (stream.leave(outer), tokens)
1559 });
1560 let probe = ProbeHandle::new();
1561 let stream = stream.probe_with(&probe);
1562 (stream.capture(), probe, token)
1563 });
1564
1565 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1569 while !probe.with_frontier(|f| f.is_empty()) {
1570 assert!(
1571 Instant::now() < deadline,
1572 "timed out waiting for completion"
1573 );
1574 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1575 }
1576 drop(token);
1577
1578 let mut blob_count = 0;
1579 let mut max_time: Option<u64> = None;
1580 while let Ok(event) = capture.try_recv() {
1581 if let CaptureEvent::Messages(time, msgs) = event {
1582 blob_count += msgs.len();
1583 max_time = max_time.max(Some(time));
1584 }
1585 }
1586 (blob_count, max_time)
1587 });
1588
1589 assert!(blob_count >= 1, "expected at least one fetched blob");
1590 assert_eq!(max_time, Some(N_BATCHES - 1));
1592 }
1593
1594 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1601 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_concurrent_error_freeze() {
1603 const N_BATCHES: u64 = 12;
1604 const MISSING_TS: u64 = 5;
1605
1606 let mut cache = PersistClientCache::new_no_metrics();
1607 cache.cfg.compaction_enabled = false;
1608 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1609 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1610 cache.cfg.set_config(&SOURCE_FETCH_CONCURRENCY, 8);
1611 let persist_client = cache
1612 .open(PersistLocation::new_in_mem())
1613 .await
1614 .expect("in-mem location is valid");
1615 let shard_id = ShardId::new();
1616 let blob = Arc::clone(&persist_client.blob);
1617
1618 let mut write = persist_client
1619 .open_writer::<String, String, u64, u64>(
1620 shard_id,
1621 Arc::new(StringSchema),
1622 Arc::new(StringSchema),
1623 Diagnostics::for_tests(),
1624 )
1625 .await
1626 .expect("invalid usage");
1627
1628 async fn batch_keys(blob: &dyn Blob) -> std::collections::BTreeSet<String> {
1629 let mut keys = std::collections::BTreeSet::new();
1630 blob.list_keys_and_metadata("", &mut |meta| {
1631 if let Ok((_, PartialBlobKey::Batch(..))) = BlobKey::parse_ids(meta.key) {
1632 keys.insert(meta.key.to_owned());
1633 }
1634 })
1635 .await
1636 .expect("list keys");
1637 keys
1638 }
1639
1640 let mut missing = Vec::new();
1643 for t in 0..N_BATCHES {
1644 let before = batch_keys(blob.as_ref()).await;
1645 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1646 write.expect_compare_and_append(&[row], t, t + 1).await;
1647 if t == MISSING_TS {
1648 let after = batch_keys(blob.as_ref()).await;
1649 missing = after.difference(&before).cloned().collect();
1650 }
1651 }
1652 assert!(!missing.is_empty(), "missing batch wrote no blob part");
1653 for key in &missing {
1654 blob.delete(key).await.expect("delete");
1655 }
1656
1657 let frontier = timely::execute::execute_directly(move |worker| {
1658 let errored = Rc::new(std::cell::Cell::new(false));
1659 let error_handler = ErrorHandler::signal({
1660 let errored = Rc::clone(&errored);
1661 move |_err| errored.set(true)
1662 });
1663
1664 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1665 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1666 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1667 outer,
1668 scope,
1669 "test_source",
1670 move || std::future::ready(persist_client.clone()),
1671 shard_id,
1672 Some(Antichain::from_elem(0)),
1673 SnapshotMode::Include,
1674 Antichain::from_elem(N_BATCHES),
1675 Some(move |_, descs, _| (descs, vec![])),
1676 Arc::new(StringSchema),
1677 Arc::new(StringSchema),
1678 FilterResult::keep_all,
1679 false.then_some(|| unreachable!()),
1680 async {},
1681 error_handler,
1682 );
1683 (stream.leave(outer), tokens)
1684 });
1685 let probe = ProbeHandle::new();
1686 stream.probe_with(&probe);
1687 (probe, token)
1688 });
1689
1690 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1691 while !errored.get() {
1692 assert!(
1693 Instant::now() < deadline,
1694 "timed out waiting for fetch error"
1695 );
1696 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1697 }
1698 let mut last = probe.with_frontier(|f| f.to_owned());
1699 let mut stable = 0;
1700 while stable < 100 {
1701 assert!(Instant::now() < deadline, "timed out waiting for quiesce");
1702 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1703 let now = probe.with_frontier(|f| f.to_owned());
1704 if now == last {
1705 stable += 1;
1706 } else {
1707 stable = 0;
1708 last = now;
1709 }
1710 }
1711 last
1712 });
1713
1714 assert_eq!(frontier, Antichain::from_elem(MISSING_TS));
1717 }
1718
1719 async fn initialize_shard(
1720 persist_client: &PersistClient,
1721 shard_id: ShardId,
1722 since: Antichain<u64>,
1723 ) {
1724 let mut read_handle = persist_client
1725 .open_leased_reader::<String, String, u64, u64>(
1726 shard_id,
1727 Arc::new(<std::string::String as mz_persist_types::Codec>::Schema::default()),
1728 Arc::new(<std::string::String as mz_persist_types::Codec>::Schema::default()),
1729 Diagnostics::for_tests(),
1730 true,
1731 )
1732 .await
1733 .expect("invalid usage");
1734
1735 read_handle.downgrade_since(&since).await;
1736 }
1737}