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_ore::metrics::IntCounter;
35use mz_ore::process::halt_with_code;
36use mz_persist_types::stats::PartStats;
37use mz_persist_types::{Codec, Codec64};
38use mz_timely_util::builder_async::{
39 Event, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
40};
41use timely::PartialOrder;
42use timely::container::CapacityContainerBuilder;
43use timely::dataflow::channels::pact::{Exchange, Pipeline};
44use timely::dataflow::operators::{Capability, CapabilitySet, ConnectLoop, Enter, Feedback, Leave};
45use timely::dataflow::{Scope, StreamVec};
46use timely::order::TotalOrder;
47use timely::progress::frontier::AntichainRef;
48use timely::progress::{Antichain, Timestamp, timestamp::Refines};
49use tracing::{debug, trace};
50
51use crate::batch::BLOB_TARGET_SIZE;
52use crate::cfg::{
53 RetryParameters, SOURCE_FETCH_CONCURRENCY, SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES,
54 USE_CRITICAL_SINCE_SOURCE,
55};
56use crate::fetch::{ExchangeableBatchPart, FetchedBlob, Lease, MissingBlobDiagnosis};
57use crate::internal::state::BatchPart;
58use crate::read::READER_LEASE_EXPIRED_EXIT_CODE;
59use crate::stats::{STATS_AUDIT_PERCENT, STATS_FILTER_ENABLED};
60use crate::{Diagnostics, PersistClient, ShardId};
61
62#[derive(Debug, Clone, PartialEq, Default)]
64pub enum FilterResult {
65 #[default]
67 Keep,
68 Discard,
70 ReplaceWith {
73 key: ArrayRef,
75 val: ArrayRef,
77 },
78}
79
80impl FilterResult {
81 pub fn keep_all<T>(_stats: &PartStats, _frontier: AntichainRef<T>) -> FilterResult {
83 Self::Keep
84 }
85}
86
87#[derive(Clone)]
98pub enum ErrorHandler {
99 Halt(&'static str),
102 Signal(Rc<dyn Fn(anyhow::Error) + 'static>),
104}
105
106impl Debug for ErrorHandler {
107 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
108 match self {
109 ErrorHandler::Halt(name) => f.debug_tuple("ErrorHandler::Halt").field(name).finish(),
110 ErrorHandler::Signal(_) => f.write_str("ErrorHandler::Signal"),
111 }
112 }
113}
114
115impl ErrorHandler {
116 pub fn signal(signal_fn: impl Fn(anyhow::Error) + 'static) -> Self {
118 Self::Signal(Rc::new(signal_fn))
119 }
120
121 pub async fn report_and_stop(&self, error: anyhow::Error) -> ! {
125 match self {
126 ErrorHandler::Halt(name) if error.is::<ReaderLeaseExpired>() => halt_with_code(
127 READER_LEASE_EXPIRED_EXIT_CODE,
128 format_args!("unhandled error in {name}: {error:#}"),
129 ),
130 ErrorHandler::Halt(name) => {
131 mz_ore::halt!("unhandled error in {name}: {error:#}")
132 }
133 ErrorHandler::Signal(callback) => {
134 let () = callback(error);
135 std::future::pending().await
136 }
137 }
138 }
139}
140
141#[derive(Debug)]
143struct ReaderLeaseExpired(String);
144
145impl std::fmt::Display for ReaderLeaseExpired {
146 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147 f.write_str(&self.0)
148 }
149}
150
151impl std::error::Error for ReaderLeaseExpired {}
152
153fn missing_blob_error(
156 blob_key: &str,
157 diagnosis: MissingBlobDiagnosis,
158 expired_live_read: &IntCounter,
159) -> anyhow::Error {
160 let message = format!(
161 "batch fetcher could not fetch batch part {}: {}",
162 blob_key, diagnosis
163 );
164 match diagnosis {
165 MissingBlobDiagnosis::LeaseExpired { .. } => {
166 expired_live_read.inc();
167 anyhow::Error::new(ReaderLeaseExpired(message))
168 }
169 _ => anyhow!(message),
170 }
171}
172
173pub fn shard_source<'inner, 'outer, K, V, T, D, DT, TOuter, C>(
187 outer: Scope<'outer, TOuter>,
188 scope: Scope<'inner, T>,
189 name: &str,
190 client: impl Fn() -> C,
191 shard_id: ShardId,
192 as_of: Option<Antichain<TOuter>>,
193 snapshot_mode: SnapshotMode,
194 until: Antichain<TOuter>,
195 desc_transformer: Option<DT>,
196 key_schema: Arc<K::Schema>,
197 val_schema: Arc<V::Schema>,
198 filter_fn: impl FnMut(&PartStats, AntichainRef<TOuter>) -> FilterResult + 'static,
199 listen_sleep: Option<impl Fn() -> RetryParameters + 'static>,
201 start_signal: impl Future<Output = ()> + 'static,
202 error_handler: ErrorHandler,
203) -> (
204 StreamVec<'inner, T, FetchedBlob<K, V, TOuter, D>>,
205 Vec<PressOnDropButton>,
206)
207where
208 K: Debug + Codec,
209 V: Debug + Codec,
210 D: Monoid + Codec64 + Send + Sync,
211 TOuter: Timestamp + Lattice + Codec64 + TotalOrder + Sync,
213 T: Refines<TOuter>,
214 DT: FnOnce(
215 Scope<'inner, T>,
216 StreamVec<'inner, T, (usize, ExchangeableBatchPart<TOuter>)>,
217 usize,
218 ) -> (
219 StreamVec<'inner, T, (usize, ExchangeableBatchPart<TOuter>)>,
220 Vec<PressOnDropButton>,
221 ),
222 C: Future<Output = PersistClient> + Send + 'static,
223{
224 let chosen_worker = usize::cast_from(name.hashed()) % scope.peers();
240
241 let mut tokens = vec![];
242
243 let (completed_fetches_feedback_handle, completed_fetches_feedback_stream) =
246 scope.feedback(T::Summary::default());
247
248 let is_transient = !until.is_empty();
252
253 let (descs, descs_token) = shard_source_descs::<K, V, D, TOuter>(
254 outer,
255 name,
256 client(),
257 shard_id.clone(),
258 as_of,
259 snapshot_mode,
260 until,
261 completed_fetches_feedback_stream.leave(outer),
262 chosen_worker,
263 Arc::clone(&key_schema),
264 Arc::clone(&val_schema),
265 filter_fn,
266 listen_sleep,
267 start_signal,
268 error_handler.clone(),
269 );
270 tokens.push(descs_token);
271
272 let descs = descs.enter(scope);
273 let descs = match desc_transformer {
274 Some(desc_transformer) => {
275 let (descs, extra_tokens) = desc_transformer(scope, descs, chosen_worker);
276 tokens.extend(extra_tokens);
277 descs
278 }
279 None => descs,
280 };
281
282 let (parts, completed_fetches_stream, fetch_token) = shard_source_fetch::<K, V, TOuter, D, T>(
283 descs,
284 name,
285 client(),
286 shard_id,
287 key_schema,
288 val_schema,
289 is_transient,
290 error_handler,
291 );
292 completed_fetches_stream.connect_loop(completed_fetches_feedback_handle);
293 tokens.push(fetch_token);
294
295 (parts, tokens)
296}
297
298#[derive(Debug, Clone, Copy)]
300pub enum SnapshotMode {
301 Include,
303 Exclude,
305}
306
307#[derive(Debug)]
308struct LeaseManager<T> {
309 leases: BTreeMap<T, Vec<Lease>>,
310}
311
312impl<T: Timestamp + Codec64> LeaseManager<T> {
313 fn new() -> Self {
314 Self {
315 leases: BTreeMap::new(),
316 }
317 }
318
319 fn push_at(&mut self, time: T, lease: Lease) {
321 self.leases.entry(time).or_default().push(lease);
322 }
323
324 fn advance_to(&mut self, frontier: AntichainRef<T>)
326 where
327 T: TotalOrder,
329 {
330 while let Some(first) = self.leases.first_entry() {
331 if frontier.less_equal(first.key()) {
332 break; }
334 drop(first.remove());
335 }
336 }
337}
338
339pub(crate) fn shard_source_descs<'outer, K, V, D, TOuter>(
340 scope: Scope<'outer, TOuter>,
341 name: &str,
342 client: impl Future<Output = PersistClient> + Send + 'static,
343 shard_id: ShardId,
344 as_of: Option<Antichain<TOuter>>,
345 snapshot_mode: SnapshotMode,
346 until: Antichain<TOuter>,
347 completed_fetches_stream: StreamVec<'outer, TOuter, Infallible>,
348 chosen_worker: usize,
349 key_schema: Arc<K::Schema>,
350 val_schema: Arc<V::Schema>,
351 mut filter_fn: impl FnMut(&PartStats, AntichainRef<TOuter>) -> FilterResult + 'static,
352 listen_sleep: Option<impl Fn() -> RetryParameters + 'static>,
354 start_signal: impl Future<Output = ()> + 'static,
355 error_handler: ErrorHandler,
356) -> (
357 StreamVec<'outer, TOuter, (usize, ExchangeableBatchPart<TOuter>)>,
358 PressOnDropButton,
359)
360where
361 K: Debug + Codec,
362 V: Debug + Codec,
363 D: Monoid + Codec64 + Send + Sync,
364 TOuter: Timestamp + Lattice + Codec64 + TotalOrder + Sync,
366{
367 let worker_index = scope.index();
368 let num_workers = scope.peers();
369
370 let name_owned = name.to_owned();
373
374 let listen_handle = Rc::new(RefCell::new(None));
376 let return_listen_handle = Rc::clone(&listen_handle);
377
378 let (tx, rx) = tokio::sync::oneshot::channel::<Rc<RefCell<LeaseManager<TOuter>>>>();
380 let mut builder = AsyncOperatorBuilder::new(
381 format!("shard_source_descs_return({})", name),
382 scope.clone(),
383 );
384 let mut completed_fetches = builder.new_disconnected_input(completed_fetches_stream, Pipeline);
385 builder.build(move |_caps| async move {
388 let Ok(leases) = rx.await else {
389 return;
392 };
393 while let Some(event) = completed_fetches.next().await {
394 let Event::Progress(frontier) = event else {
395 continue;
396 };
397 leases.borrow_mut().advance_to(frontier.borrow());
398 }
399 drop(return_listen_handle);
401 });
402
403 let mut builder =
404 AsyncOperatorBuilder::new(format!("shard_source_descs({})", name), scope.clone());
405 let (descs_output, descs_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
406
407 #[allow(clippy::await_holding_refcell_ref)]
408 let shutdown_button = builder.build(move |caps| async move {
409 let mut cap_set = CapabilitySet::from_elem(caps.into_element());
410
411 if worker_index != chosen_worker {
413 trace!(
414 "We are not the chosen worker ({}), exiting...",
415 chosen_worker
416 );
417 return;
418 }
419
420 let mut read = mz_ore::task::spawn(|| format!("shard_source_reader({})", name_owned), {
430 let diagnostics = Diagnostics {
431 handle_purpose: format!("shard_source({})", name_owned),
432 shard_name: name_owned.clone(),
433 };
434 async move {
435 let client = client.await;
436 client
437 .open_leased_reader::<K, V, TOuter, D>(
438 shard_id,
439 key_schema,
440 val_schema,
441 diagnostics,
442 USE_CRITICAL_SINCE_SOURCE.get(client.dyncfgs()),
443 )
444 .await
445 }
446 })
447 .await
448 .expect("could not open persist shard");
449
450 let () = start_signal.await;
454
455 let cfg = read.cfg.clone();
456 let metrics = Arc::clone(&read.metrics);
457
458 let as_of = as_of.unwrap_or_else(|| read.since().clone());
459
460 cap_set.downgrade(as_of.clone());
476
477 let mut snapshot_parts =
478 match snapshot_mode {
479 SnapshotMode::Include => match read.snapshot(as_of.clone()).await {
480 Ok(parts) => parts,
481 Err(e) => error_handler
482 .report_and_stop(anyhow!(
483 "{name_owned}: {shard_id} cannot serve requested as_of {as_of:?}: {e:?}"
484 ))
485 .await,
486 },
487 SnapshotMode::Exclude => vec![],
488 };
489
490 let leases = Rc::new(RefCell::new(LeaseManager::new()));
494 tx.send(Rc::clone(&leases))
495 .expect("lease returner exited before desc producer");
496
497 let replay_upper = read.shared_upper();
503
504 let mut listen = listen_handle.borrow_mut();
507 let listen = match read.listen(as_of.clone()).await {
508 Ok(handle) => listen.insert(handle),
509 Err(e) => {
510 error_handler
511 .report_and_stop(anyhow!(
512 "{name_owned}: {shard_id} cannot serve requested as_of {as_of:?}: {e:?}"
513 ))
514 .await
515 }
516 };
517
518 let listen_retry = listen_sleep.as_ref().map(|retry| retry());
519
520 let listen_head = if !snapshot_parts.is_empty() {
522 let (mut parts, progress) = listen.next(listen_retry).await;
523 snapshot_parts.append(&mut parts);
524 futures::stream::iter(Some((snapshot_parts, progress)))
525 } else {
526 futures::stream::iter(None)
527 };
528
529 let listen_tail = futures::stream::unfold(listen, |listen| async move {
531 Some((listen.next(listen_retry).await, listen))
532 });
533
534 let mut shard_stream = pin!(listen_head.chain(listen_tail));
535
536 let mut audit_budget_bytes = u64::cast_from(BLOB_TARGET_SIZE.get(&cfg).saturating_mul(2));
540
541 let mut current_frontier = as_of.clone();
543
544 let coalesce_target = u64::cast_from(SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES.get(&cfg));
552 let mut coalesced_bytes: u64 = 0;
554
555 while !PartialOrder::less_equal(&until, ¤t_frontier) {
558 let (parts, progress) = shard_stream.next().await.expect("infinite stream");
559
560 let mut batch_bytes: u64 = 0;
561
562 let current_ts = current_frontier
565 .as_option()
566 .expect("until should always be <= the empty frontier");
567 let session_cap = cap_set.delayed(current_ts);
568
569 for mut part_desc in parts {
570 if STATS_FILTER_ENABLED.get(&cfg) {
573 let filter_result = match &part_desc.part {
574 BatchPart::Hollow(x) => {
575 let should_fetch =
576 x.stats.as_ref().map_or(FilterResult::Keep, |stats| {
577 match stats.try_decode() {
581 Ok(stats) => filter_fn(&stats, current_frontier.borrow()),
582 Err(err) => {
583 tracing::warn!(
584 %err,
585 "could not decode part stats, fetching part"
586 );
587 FilterResult::Keep
588 }
589 }
590 });
591 should_fetch
592 }
593 BatchPart::Inline { .. } => FilterResult::Keep,
594 };
595 let bytes = u64::cast_from(part_desc.encoded_size_bytes());
597 match filter_result {
598 FilterResult::Keep => {
599 audit_budget_bytes = audit_budget_bytes.saturating_add(bytes);
600 }
601 FilterResult::Discard => {
602 metrics.pushdown.parts_filtered_count.inc();
603 metrics.pushdown.parts_filtered_bytes.inc_by(bytes);
604 let should_audit = match &part_desc.part {
605 BatchPart::Hollow(x) => {
606 let mut h = DefaultHasher::new();
607 x.key.hash(&mut h);
608 usize::cast_from(h.finish()) % 100
609 < STATS_AUDIT_PERCENT.get(&cfg)
610 }
611 BatchPart::Inline { .. } => false,
612 };
613 if should_audit && bytes < audit_budget_bytes {
614 audit_budget_bytes -= bytes;
615 metrics.pushdown.parts_audited_count.inc();
616 metrics.pushdown.parts_audited_bytes.inc_by(bytes);
617 part_desc.request_filter_pushdown_audit();
618 } else {
619 debug!(
620 "skipping part because of stats filter {:?}",
621 part_desc.part.stats()
622 );
623 continue;
624 }
625 }
626 FilterResult::ReplaceWith { key, val } => {
627 part_desc.maybe_optimize(&cfg, key, val);
628 audit_budget_bytes = audit_budget_bytes.saturating_add(bytes);
629 }
630 }
631 let bytes = u64::cast_from(part_desc.encoded_size_bytes());
632 if part_desc.part.is_inline() {
633 metrics.pushdown.parts_inline_count.inc();
634 metrics.pushdown.parts_inline_bytes.inc_by(bytes);
635 } else {
636 metrics.pushdown.parts_fetched_count.inc();
637 metrics.pushdown.parts_fetched_bytes.inc_by(bytes);
638 }
639 }
640
641 let worker_idx = usize::cast_from(Instant::now().hashed()) % num_workers;
648 batch_bytes =
649 batch_bytes.saturating_add(u64::cast_from(part_desc.encoded_size_bytes()));
650 let (part, lease) = part_desc.into_exchangeable_part();
651 leases.borrow_mut().push_at(current_ts.clone(), lease);
652 descs_output.give(&session_cap, (worker_idx, part));
653 }
654
655 current_frontier.join_assign(&progress);
656 coalesced_bytes = coalesced_bytes.saturating_add(batch_bytes);
657
658 let caught_up = PartialOrder::less_equal(&replay_upper, ¤t_frontier);
664 let coalesce = coalesce_target > 0 && !caught_up && coalesced_bytes < coalesce_target;
665 if !coalesce {
666 coalesced_bytes = 0;
667 cap_set.downgrade(current_frontier.iter());
668 }
669 }
670 });
671
672 (descs_stream, shutdown_button.press_on_drop())
673}
674
675pub(crate) fn shard_source_fetch<'inner, K, V, T, D, TInner>(
676 descs: StreamVec<'inner, TInner, (usize, ExchangeableBatchPart<T>)>,
677 name: &str,
678 client: impl Future<Output = PersistClient> + Send + 'static,
679 shard_id: ShardId,
680 key_schema: Arc<K::Schema>,
681 val_schema: Arc<V::Schema>,
682 is_transient: bool,
683 error_handler: ErrorHandler,
684) -> (
685 StreamVec<'inner, TInner, FetchedBlob<K, V, T, D>>,
686 StreamVec<'inner, TInner, Infallible>,
687 PressOnDropButton,
688)
689where
690 K: Debug + Codec,
691 V: Debug + Codec,
692 T: Timestamp + Lattice + Codec64 + Sync,
693 D: Monoid + Codec64 + Send + Sync,
694 TInner: Timestamp + Refines<T>,
695{
696 let mut builder =
697 AsyncOperatorBuilder::new(format!("shard_source_fetch({})", name), descs.scope());
698 let (fetched_output, fetched_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
699 let (completed_fetches_output, completed_fetches_stream) =
700 builder.new_output::<CapacityContainerBuilder<Vec<Infallible>>>();
701 let mut descs_input = builder.new_input_for_many(
702 descs,
703 Exchange::new(|&(i, _): &(usize, _)| u64::cast_from(i)),
704 [&fetched_output, &completed_fetches_output],
705 );
706 let name_owned = name.to_owned();
707
708 let shutdown_button = builder.build(move |_capabilities| async move {
709 let (fetcher, max_concurrency) =
713 mz_ore::task::spawn(|| format!("shard_source_fetch({})", name_owned), {
714 let diagnostics = Diagnostics {
715 shard_name: name_owned.clone(),
716 handle_purpose: format!("shard_source_fetch batch fetcher {}", name_owned),
717 };
718 async move {
719 let client = client.await;
720 let max_concurrency = SOURCE_FETCH_CONCURRENCY.get(client.dyncfgs()).max(1);
727 let fetcher = client
728 .create_batch_fetcher::<K, V, T, D>(
729 shard_id,
730 key_schema,
731 val_schema,
732 is_transient,
733 diagnostics,
734 )
735 .await
736 .expect("shard codecs should not change");
737 (fetcher, max_concurrency)
738 }
739 })
740 .await;
741
742 let fetch_one = |caps: [Capability<TInner>; 2], part: ExchangeableBatchPart<T>| {
748 let mut fetcher = fetcher.clone();
749 async move {
750 let reader_id = part.reader_id().clone();
751 let fetched = fetcher
752 .fetch_leased_part(part)
753 .await
754 .expect("shard_id should match across all workers");
755 let fetched = match fetched {
756 Ok(fetched) => Ok(fetched),
757 Err(blob_key) => {
758 let diagnosis = fetcher.missing_blob_diagnostics(&reader_id).await;
770 Err(missing_blob_error(
771 &blob_key,
772 diagnosis,
773 &fetcher.metrics.lease.expired_live_read,
774 ))
775 }
776 };
777 (caps, fetched)
778 }
779 };
780
781 let mut pending: VecDeque<([Capability<TInner>; 2], ExchangeableBatchPart<T>)> =
792 VecDeque::new();
793 let mut in_flight = FuturesUnordered::new();
794 let mut input_done = false;
795
796 loop {
797 while in_flight.len() < max_concurrency {
799 let Some((caps, part)) = pending.pop_front() else {
800 break;
801 };
802 in_flight.push(fetch_one(caps, part));
803 }
804
805 tokio::select! {
806 biased;
809 Some(([cap, _], fetched)) = in_flight.next(), if !in_flight.is_empty() => {
810 match fetched {
811 Ok(fetched) => fetched_output.give(&cap, fetched),
812 Err(e) => {
813 error_handler.report_and_stop(e).await;
818 }
819 }
820 }
821 event = descs_input.next(), if !input_done => {
824 match event {
825 Some(Event::Data(caps, data)) => {
826 for (_idx, part) in data {
829 pending.push_back((caps.clone(), part));
830 }
831 }
832 Some(Event::Progress(_)) => {}
833 None => input_done = true,
834 }
835 }
836 else => break,
838 }
839 }
840 });
841
842 (
843 fetched_stream,
844 completed_fetches_stream,
845 shutdown_button.press_on_drop(),
846 )
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852 use std::sync::Arc;
853
854 use mz_persist::location::{Blob, SeqNo};
855 use mz_persist_types::codec_impls::StringSchema;
856 use timely::dataflow::operators::Leave;
857 use timely::dataflow::operators::Probe;
858 use timely::dataflow::operators::capture::{Capture, Event as CaptureEvent};
859 use timely::dataflow::operators::probe::Handle as ProbeHandle;
860 use timely::progress::Antichain;
861
862 use crate::batch::{INLINE_WRITES_SINGLE_MAX_BYTES, INLINE_WRITES_TOTAL_MAX_BYTES};
863 use crate::cache::PersistClientCache;
864 use crate::internal::paths::{BlobKey, PartialBlobKey};
865 use crate::operators::shard_source::shard_source;
866 use crate::read::LeasedReaderId;
867 use crate::{Diagnostics, PersistLocation, ShardId};
868
869 #[mz_ore::test]
870 fn test_missing_blob_error_tags_expired_lease() {
871 let expired_live_read = IntCounter::new("expired_live_read", "test").expect("valid");
872 let error = |diagnosis| missing_blob_error("key", diagnosis, &expired_live_read);
873 let reader_id = LeasedReaderId::new();
874
875 let expired = error(MissingBlobDiagnosis::LeaseExpired {
876 reader_id: reader_id.clone(),
877 });
878 assert!(expired.is::<ReaderLeaseExpired>(), "{expired:#}");
879 assert_eq!(expired_live_read.get(), 1);
880
881 let not_lost = [
882 MissingBlobDiagnosis::ShardTombstoned {
883 reader_id: reader_id.clone(),
884 },
885 MissingBlobDiagnosis::LeasePresent {
886 reader_id: reader_id.clone(),
887 lease_state: String::new(),
888 },
889 MissingBlobDiagnosis::Unknown { reader_id },
890 ];
891 for diagnosis in not_lost {
892 let error = error(diagnosis);
893 assert!(!error.is::<ReaderLeaseExpired>(), "{error:#}");
894 }
895 assert_eq!(expired_live_read.get(), 1);
896 }
897
898 #[mz_ore::test]
899 fn test_lease_manager() {
900 let lease = Lease::new(SeqNo::minimum());
901 let mut manager = LeaseManager::new();
902 for t in 0u64..10 {
903 manager.push_at(t, lease.clone());
904 }
905 assert_eq!(lease.count(), 11);
906 manager.advance_to(AntichainRef::new(&[5]));
907 assert_eq!(lease.count(), 6);
908 manager.advance_to(AntichainRef::new(&[3]));
909 assert_eq!(lease.count(), 6);
910 manager.advance_to(AntichainRef::new(&[9]));
911 assert_eq!(lease.count(), 2);
912 manager.advance_to(AntichainRef::new(&[10]));
913 assert_eq!(lease.count(), 1);
914 }
915
916 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
924 #[cfg_attr(miri, ignore)] async fn test_shard_source_implicit_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 until = Antichain::new();
940
941 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
942 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
943 let transformer = move |_, descs, _| (descs, vec![]);
944 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
945 outer,
946 scope,
947 "test_source",
948 move || std::future::ready(persist_client.clone()),
949 shard_id,
950 None, SnapshotMode::Include,
952 until,
953 Some(transformer),
954 Arc::new(
955 <std::string::String as mz_persist_types::Codec>::Schema::default(),
956 ),
957 Arc::new(
958 <std::string::String as mz_persist_types::Codec>::Schema::default(),
959 ),
960 FilterResult::keep_all,
961 false.then_some(|| unreachable!()),
962 async {},
963 ErrorHandler::Halt("test"),
964 );
965 (stream.leave(outer), tokens)
966 });
967
968 let probe = ProbeHandle::new();
969 let _stream = stream.probe_with(&probe);
970
971 (probe, token)
972 });
973
974 while probe.less_than(&expected_frontier) {
975 worker.step();
976 }
977
978 let mut probe_frontier = Antichain::new();
979 probe.with_frontier(|f| probe_frontier.extend(f.iter().cloned()));
980
981 probe_frontier
982 });
983
984 assert_eq!(res, Antichain::from_elem(expected_frontier));
985 }
986
987 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
994 #[cfg_attr(miri, ignore)] async fn test_shard_source_explicit_initial_as_of() {
996 let persist_client = PersistClient::new_for_tests().await;
997
998 let expected_frontier = 42;
999 let shard_id = ShardId::new();
1000
1001 initialize_shard(
1002 &persist_client,
1003 shard_id,
1004 Antichain::from_elem(expected_frontier),
1005 )
1006 .await;
1007
1008 let res = timely::execute::execute_directly(move |worker| {
1009 let as_of = Antichain::from_elem(expected_frontier);
1010 let until = Antichain::new();
1011
1012 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1013 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1014 let transformer = move |_, descs, _| (descs, vec![]);
1015 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1016 outer,
1017 scope,
1018 "test_source",
1019 move || std::future::ready(persist_client.clone()),
1020 shard_id,
1021 Some(as_of), SnapshotMode::Include,
1023 until,
1024 Some(transformer),
1025 Arc::new(
1026 <std::string::String as mz_persist_types::Codec>::Schema::default(),
1027 ),
1028 Arc::new(
1029 <std::string::String as mz_persist_types::Codec>::Schema::default(),
1030 ),
1031 FilterResult::keep_all,
1032 false.then_some(|| unreachable!()),
1033 async {},
1034 ErrorHandler::Halt("test"),
1035 );
1036 (stream.leave(outer), tokens)
1037 });
1038
1039 let probe = ProbeHandle::new();
1040 let _stream = stream.probe_with(&probe);
1041
1042 (probe, token)
1043 });
1044
1045 while probe.less_than(&expected_frontier) {
1046 worker.step();
1047 }
1048
1049 let mut probe_frontier = Antichain::new();
1050 probe.with_frontier(|f| probe_frontier.extend(f.iter().cloned()));
1051
1052 probe_frontier
1053 });
1054
1055 assert_eq!(res, Antichain::from_elem(expected_frontier));
1056 }
1057
1058 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1074 #[cfg_attr(miri, ignore)] async fn test_shard_source_hydration_frontier_coalesce() {
1076 const N_BATCHES: u64 = 64;
1077
1078 async fn run(coalesce_bytes: usize) -> Option<u64> {
1083 let mut cache = PersistClientCache::new_no_metrics();
1084 cache.cfg.compaction_enabled = false;
1087 cache
1088 .cfg
1089 .set_config(&SOURCE_HYDRATION_FRONTIER_COALESCE_BYTES, coalesce_bytes);
1090 let persist_client = cache
1091 .open(PersistLocation::new_in_mem())
1092 .await
1093 .expect("in-mem location is valid");
1094 let shard_id = ShardId::new();
1095
1096 let mut write = persist_client
1097 .open_writer::<String, String, u64, u64>(
1098 shard_id,
1099 Arc::new(StringSchema),
1100 Arc::new(StringSchema),
1101 Diagnostics::for_tests(),
1102 )
1103 .await
1104 .expect("invalid usage");
1105
1106 for t in 0..N_BATCHES {
1109 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1110 write.expect_compare_and_append(&[row], t, t + 1).await;
1111 }
1112
1113 timely::execute::execute_directly(move |worker| {
1114 let (probe, receiver, _token) = worker.dataflow::<u64, _, _>(|outer| {
1115 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1116 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1117 outer,
1118 scope,
1119 "test_source",
1120 move || std::future::ready(persist_client.clone()),
1121 shard_id,
1122 Some(Antichain::from_elem(0)),
1123 SnapshotMode::Include,
1124 Antichain::from_elem(N_BATCHES),
1127 Some(move |_, descs, _| (descs, vec![])),
1128 Arc::new(StringSchema),
1129 Arc::new(StringSchema),
1130 FilterResult::keep_all,
1131 false.then_some(|| unreachable!()),
1132 async {},
1133 ErrorHandler::Halt("test"),
1134 );
1135 (stream.leave(outer), tokens)
1136 });
1137 let probe = ProbeHandle::new();
1138 let receiver = stream.probe_with(&probe).capture();
1142 (probe, receiver, token)
1143 });
1144
1145 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1148 while !probe.with_frontier(|f| f.is_empty()) {
1149 assert!(Instant::now() < deadline, "timed out hydrating shard");
1150 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1151 }
1152
1153 let mut max_msg_time: Option<u64> = None;
1158 while let Ok(event) = receiver.try_recv() {
1159 if let CaptureEvent::Messages(time, _) = event {
1160 max_msg_time = max_msg_time.max(Some(time));
1161 }
1162 }
1163 max_msg_time
1164 })
1165 }
1166
1167 assert_eq!(run(0).await, Some(N_BATCHES - 1));
1175 assert_eq!(run(1 << 30).await, Some(N_BATCHES - 1));
1176 }
1177
1178 #[mz_ore::test]
1183 fn test_frontier_coalesce_decision() {
1184 fn forwards(coalesce_target: u64, n: u64, bytes_per_batch: u64) -> usize {
1185 let replay_upper = n;
1186 let mut current = 0u64;
1187 let mut coalesced = 0u64;
1188 let mut forwarded = 0usize;
1189 for _ in 0..n {
1190 current += 1;
1191 coalesced += bytes_per_batch;
1192 let caught_up = replay_upper <= current;
1193 let coalesce = coalesce_target > 0 && !caught_up && coalesced < coalesce_target;
1195 if !coalesce {
1196 forwarded += 1;
1197 coalesced = 0;
1198 }
1199 }
1200 forwarded
1201 }
1202
1203 assert_eq!(forwards(0, 64, 10), 64);
1205 assert_eq!(forwards(1 << 30, 64, 10), 1);
1207 let partial = forwards(100, 64, 10);
1210 assert!(
1211 partial > 1 && partial < 64,
1212 "expected partial coalescing, got {partial}"
1213 );
1214 }
1215
1216 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1220 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetches_data() {
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 = [
1235 (("k1".to_owned(), "v1".to_owned()), 0u64, 1u64),
1236 (("k2".to_owned(), "v2".to_owned()), 1u64, 1u64),
1237 ];
1238 write.expect_compare_and_append(&data[..], 0, 5).await;
1239
1240 let expected_frontier = 5;
1241 let (blob_count, frontier) = timely::execute::execute_directly(move |worker| {
1242 let as_of = Antichain::from_elem(0);
1243 let until = Antichain::new();
1244
1245 let (capture, probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1246 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1247 let transformer = move |_, descs, _| (descs, vec![]);
1248 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1249 outer,
1250 scope,
1251 "test_source",
1252 move || std::future::ready(persist_client.clone()),
1253 shard_id,
1254 Some(as_of),
1255 SnapshotMode::Include,
1256 until,
1257 Some(transformer),
1258 Arc::new(StringSchema),
1259 Arc::new(StringSchema),
1260 FilterResult::keep_all,
1261 false.then_some(|| unreachable!()),
1262 async {},
1263 ErrorHandler::Halt("test"),
1264 );
1265 (stream.leave(outer), tokens)
1266 });
1267
1268 let probe = ProbeHandle::new();
1269 let stream = stream.probe_with(&probe);
1270 (stream.capture(), probe, token)
1271 });
1272
1273 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1274 while probe.less_than(&expected_frontier) {
1275 assert!(
1276 Instant::now() < deadline,
1277 "timed out waiting for output frontier {expected_frontier}"
1278 );
1279 worker.step();
1280 }
1281 drop(token);
1282
1283 let mut blob_count = 0;
1284 while let Ok(event) = capture.try_recv() {
1285 if let CaptureEvent::Messages(_, msgs) = event {
1286 blob_count += msgs.len();
1287 }
1288 }
1289 let mut frontier = Antichain::new();
1290 probe.with_frontier(|f| frontier.extend(f.iter().cloned()));
1291 (blob_count, frontier)
1292 });
1293
1294 assert!(blob_count >= 1, "expected at least one fetched blob");
1295 assert_eq!(frontier, Antichain::from_elem(expected_frontier));
1296 }
1297
1298 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1302 #[cfg_attr(miri, ignore)] async fn test_shard_source_shutdown_mid_stream() {
1304 let persist_client = PersistClient::new_for_tests().await;
1305 let shard_id = ShardId::new();
1306
1307 let mut write = persist_client
1308 .open_writer::<String, String, u64, u64>(
1309 shard_id,
1310 Arc::new(StringSchema),
1311 Arc::new(StringSchema),
1312 Diagnostics::for_tests(),
1313 )
1314 .await
1315 .expect("invalid usage");
1316 let data = [(("k1".to_owned(), "v1".to_owned()), 0u64, 1u64)];
1317 write.expect_compare_and_append(&data[..], 0, 5).await;
1318
1319 timely::execute::execute_directly(move |worker| {
1320 let as_of = Antichain::from_elem(0);
1321 let until = Antichain::new();
1324
1325 let (probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1326 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1327 let transformer = move |_, descs, _| (descs, vec![]);
1328 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1329 outer,
1330 scope,
1331 "test_source",
1332 move || std::future::ready(persist_client.clone()),
1333 shard_id,
1334 Some(as_of),
1335 SnapshotMode::Include,
1336 until,
1337 Some(transformer),
1338 Arc::new(StringSchema),
1339 Arc::new(StringSchema),
1340 FilterResult::keep_all,
1341 false.then_some(|| unreachable!()),
1342 async {},
1343 ErrorHandler::Halt("test"),
1344 );
1345 (stream.leave(outer), tokens)
1346 });
1347
1348 let probe = ProbeHandle::new();
1349 let _stream = stream.probe_with(&probe);
1350 (probe, token)
1351 });
1352
1353 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1356 while probe.less_than(&1) {
1357 assert!(Instant::now() < deadline, "timed out waiting for progress");
1358 worker.step();
1359 }
1360
1361 drop(token);
1365 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1366 loop {
1367 assert!(Instant::now() < deadline, "timed out waiting for shutdown");
1368 worker.step();
1369 if probe.with_frontier(|f| f.is_empty()) {
1370 break;
1371 }
1372 }
1373 });
1374 }
1375
1376 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1380 #[cfg_attr(miri, ignore)] async fn test_shard_source_error_freeze() {
1382 let persist_client = PersistClient::new_for_tests().await;
1383 let shard_id = ShardId::new();
1384
1385 let mut write = persist_client
1389 .open_writer::<String, String, u64, u64>(
1390 shard_id,
1391 Arc::new(StringSchema),
1392 Arc::new(StringSchema),
1393 Diagnostics::for_tests(),
1394 )
1395 .await
1396 .expect("invalid usage");
1397 let data = [(("k1".to_owned(), "v1".to_owned()), 0u64, 1u64)];
1398 write.expect_compare_and_append(&data[..], 0, 5).await;
1399 initialize_shard(&persist_client, shard_id, Antichain::from_elem(3)).await;
1400
1401 let (errored, frontier) = timely::execute::execute_directly(move |worker| {
1402 let as_of = Antichain::from_elem(1);
1403 let until = Antichain::new();
1404
1405 let errored = Rc::new(std::cell::Cell::new(false));
1406 let error_handler = ErrorHandler::signal({
1407 let errored = Rc::clone(&errored);
1408 move |_err| errored.set(true)
1409 });
1410
1411 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1412 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1413 let transformer = move |_, descs, _| (descs, vec![]);
1414 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1415 outer,
1416 scope,
1417 "test_source",
1418 move || std::future::ready(persist_client.clone()),
1419 shard_id,
1420 Some(as_of),
1421 SnapshotMode::Include,
1422 until,
1423 Some(transformer),
1424 Arc::new(StringSchema),
1425 Arc::new(StringSchema),
1426 FilterResult::keep_all,
1427 false.then_some(|| unreachable!()),
1428 async {},
1429 error_handler,
1430 );
1431 (stream.leave(outer), tokens)
1432 });
1433
1434 let probe = ProbeHandle::new();
1435 let _stream = stream.probe_with(&probe);
1436 (probe, token)
1437 });
1438
1439 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1440 while !errored.get() {
1441 assert!(Instant::now() < deadline, "timed out waiting for error");
1442 worker.step();
1443 }
1444 for _ in 0..100 {
1446 worker.step();
1447 }
1448
1449 let mut frontier = Antichain::new();
1450 probe.with_frontier(|f| frontier.extend(f.iter().cloned()));
1451 (errored.get(), frontier)
1452 });
1453
1454 assert!(errored);
1455 assert_eq!(frontier, Antichain::from_elem(1));
1456 }
1457
1458 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1470 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_error_freeze() {
1472 let mut cache = PersistClientCache::new_no_metrics();
1475 cache.cfg.compaction_enabled = false;
1476 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1477 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1478 let persist_client = cache
1479 .open(PersistLocation::new_in_mem())
1480 .await
1481 .expect("in-mem location is valid");
1482 let shard_id = ShardId::new();
1483 let blob = Arc::clone(&persist_client.blob);
1486
1487 let mut write = persist_client
1488 .open_writer::<String, String, u64, u64>(
1489 shard_id,
1490 Arc::new(StringSchema),
1491 Arc::new(StringSchema),
1492 Diagnostics::for_tests(),
1493 )
1494 .await
1495 .expect("invalid usage");
1496
1497 async fn batch_keys(blob: &dyn Blob) -> std::collections::BTreeSet<String> {
1499 let mut keys = std::collections::BTreeSet::new();
1500 blob.list_keys_and_metadata("", &mut |meta| {
1501 if let Ok((_, PartialBlobKey::Batch(..))) = BlobKey::parse_ids(meta.key) {
1502 keys.insert(meta.key.to_owned());
1503 }
1504 })
1505 .await
1506 .expect("list keys");
1507 keys
1508 }
1509
1510 let row = |t: u64| ((format!("k{t}"), format!("v{t}")), t, 1u64);
1511 let before = batch_keys(blob.as_ref()).await;
1512 write.expect_compare_and_append(&[row(0)], 0, 1).await;
1513 let after = batch_keys(blob.as_ref()).await;
1514 write.expect_compare_and_append(&[row(1)], 1, 2).await;
1515 write.expect_compare_and_append(&[row(2)], 2, 3).await;
1516
1517 let missing: Vec<_> = after.difference(&before).cloned().collect();
1520 assert!(!missing.is_empty(), "first batch wrote no blob part");
1521 for key in &missing {
1522 blob.delete(key).await.expect("delete");
1523 }
1524
1525 let frontier = timely::execute::execute_directly(move |worker| {
1526 let errored = Rc::new(std::cell::Cell::new(false));
1527 let error_handler = ErrorHandler::signal({
1528 let errored = Rc::clone(&errored);
1529 move |err: anyhow::Error| {
1530 assert!(!err.is::<ReaderLeaseExpired>(), "{err:#}");
1532 errored.set(true)
1533 }
1534 });
1535
1536 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1537 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1538 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1539 outer,
1540 scope,
1541 "test_source",
1542 move || std::future::ready(persist_client.clone()),
1543 shard_id,
1544 Some(Antichain::from_elem(0)),
1545 SnapshotMode::Include,
1546 Antichain::new(),
1547 Some(move |_, descs, _| (descs, vec![])),
1548 Arc::new(StringSchema),
1549 Arc::new(StringSchema),
1550 FilterResult::keep_all,
1551 false.then_some(|| unreachable!()),
1552 async {},
1553 error_handler,
1554 );
1555 (stream.leave(outer), tokens)
1556 });
1557 let probe = ProbeHandle::new();
1558 stream.probe_with(&probe);
1559 (probe, token)
1560 });
1561
1562 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1565 while !errored.get() {
1566 assert!(
1567 Instant::now() < deadline,
1568 "timed out waiting for fetch error"
1569 );
1570 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1571 }
1572 let mut last = probe.with_frontier(|f| f.to_owned());
1573 let mut stable = 0;
1574 while stable < 100 {
1575 assert!(Instant::now() < deadline, "timed out waiting for quiesce");
1576 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1577 let now = probe.with_frontier(|f| f.to_owned());
1578 if now == last {
1579 stable += 1;
1580 } else {
1581 stable = 0;
1582 last = now;
1583 }
1584 }
1585 last
1586 });
1587
1588 assert_eq!(frontier, Antichain::from_elem(0));
1590 }
1591
1592 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1597 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_concurrent() {
1599 const N_BATCHES: u64 = 16;
1600
1601 let mut cache = PersistClientCache::new_no_metrics();
1602 cache.cfg.compaction_enabled = false;
1603 cache.cfg.set_config(&SOURCE_FETCH_CONCURRENCY, 8);
1604 let persist_client = cache
1605 .open(PersistLocation::new_in_mem())
1606 .await
1607 .expect("in-mem location is valid");
1608 let shard_id = ShardId::new();
1609
1610 let mut write = persist_client
1611 .open_writer::<String, String, u64, u64>(
1612 shard_id,
1613 Arc::new(StringSchema),
1614 Arc::new(StringSchema),
1615 Diagnostics::for_tests(),
1616 )
1617 .await
1618 .expect("invalid usage");
1619 for t in 0..N_BATCHES {
1620 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1621 write.expect_compare_and_append(&[row], t, t + 1).await;
1622 }
1623
1624 let (blob_count, max_time) = timely::execute::execute_directly(move |worker| {
1625 let (capture, probe, token) = worker.dataflow::<u64, _, _>(|outer| {
1626 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1627 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1628 outer,
1629 scope,
1630 "test_source",
1631 move || std::future::ready(persist_client.clone()),
1632 shard_id,
1633 Some(Antichain::from_elem(0)),
1634 SnapshotMode::Include,
1635 Antichain::from_elem(N_BATCHES),
1636 Some(move |_, descs, _| (descs, vec![])),
1637 Arc::new(StringSchema),
1638 Arc::new(StringSchema),
1639 FilterResult::keep_all,
1640 false.then_some(|| unreachable!()),
1641 async {},
1642 ErrorHandler::Halt("test"),
1643 );
1644 (stream.leave(outer), tokens)
1645 });
1646 let probe = ProbeHandle::new();
1647 let stream = stream.probe_with(&probe);
1648 (stream.capture(), probe, token)
1649 });
1650
1651 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1655 while !probe.with_frontier(|f| f.is_empty()) {
1656 assert!(
1657 Instant::now() < deadline,
1658 "timed out waiting for completion"
1659 );
1660 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1661 }
1662 drop(token);
1663
1664 let mut blob_count = 0;
1665 let mut max_time: Option<u64> = None;
1666 while let Ok(event) = capture.try_recv() {
1667 if let CaptureEvent::Messages(time, msgs) = event {
1668 blob_count += msgs.len();
1669 max_time = max_time.max(Some(time));
1670 }
1671 }
1672 (blob_count, max_time)
1673 });
1674
1675 assert!(blob_count >= 1, "expected at least one fetched blob");
1676 assert_eq!(max_time, Some(N_BATCHES - 1));
1678 }
1679
1680 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
1687 #[cfg_attr(miri, ignore)] async fn test_shard_source_fetch_concurrent_error_freeze() {
1689 const N_BATCHES: u64 = 12;
1690 const MISSING_TS: u64 = 5;
1691
1692 let mut cache = PersistClientCache::new_no_metrics();
1693 cache.cfg.compaction_enabled = false;
1694 cache.cfg.set_config(&INLINE_WRITES_SINGLE_MAX_BYTES, 0);
1695 cache.cfg.set_config(&INLINE_WRITES_TOTAL_MAX_BYTES, 0);
1696 cache.cfg.set_config(&SOURCE_FETCH_CONCURRENCY, 8);
1697 let persist_client = cache
1698 .open(PersistLocation::new_in_mem())
1699 .await
1700 .expect("in-mem location is valid");
1701 let shard_id = ShardId::new();
1702 let blob = Arc::clone(&persist_client.blob);
1703
1704 let mut write = persist_client
1705 .open_writer::<String, String, u64, u64>(
1706 shard_id,
1707 Arc::new(StringSchema),
1708 Arc::new(StringSchema),
1709 Diagnostics::for_tests(),
1710 )
1711 .await
1712 .expect("invalid usage");
1713
1714 async fn batch_keys(blob: &dyn Blob) -> std::collections::BTreeSet<String> {
1715 let mut keys = std::collections::BTreeSet::new();
1716 blob.list_keys_and_metadata("", &mut |meta| {
1717 if let Ok((_, PartialBlobKey::Batch(..))) = BlobKey::parse_ids(meta.key) {
1718 keys.insert(meta.key.to_owned());
1719 }
1720 })
1721 .await
1722 .expect("list keys");
1723 keys
1724 }
1725
1726 let mut missing = Vec::new();
1729 for t in 0..N_BATCHES {
1730 let before = batch_keys(blob.as_ref()).await;
1731 let row = ((format!("k{t}"), format!("v{t}")), t, 1u64);
1732 write.expect_compare_and_append(&[row], t, t + 1).await;
1733 if t == MISSING_TS {
1734 let after = batch_keys(blob.as_ref()).await;
1735 missing = after.difference(&before).cloned().collect();
1736 }
1737 }
1738 assert!(!missing.is_empty(), "missing batch wrote no blob part");
1739 for key in &missing {
1740 blob.delete(key).await.expect("delete");
1741 }
1742
1743 let frontier = timely::execute::execute_directly(move |worker| {
1744 let errored = Rc::new(std::cell::Cell::new(false));
1745 let error_handler = ErrorHandler::signal({
1746 let errored = Rc::clone(&errored);
1747 move |_err| errored.set(true)
1748 });
1749
1750 let (probe, _token) = worker.dataflow::<u64, _, _>(|outer| {
1751 let (stream, token) = outer.scoped::<u64, _, _>("hybrid", |scope| {
1752 let (stream, tokens) = shard_source::<String, String, u64, u64, _, _, _>(
1753 outer,
1754 scope,
1755 "test_source",
1756 move || std::future::ready(persist_client.clone()),
1757 shard_id,
1758 Some(Antichain::from_elem(0)),
1759 SnapshotMode::Include,
1760 Antichain::from_elem(N_BATCHES),
1761 Some(move |_, descs, _| (descs, vec![])),
1762 Arc::new(StringSchema),
1763 Arc::new(StringSchema),
1764 FilterResult::keep_all,
1765 false.then_some(|| unreachable!()),
1766 async {},
1767 error_handler,
1768 );
1769 (stream.leave(outer), tokens)
1770 });
1771 let probe = ProbeHandle::new();
1772 stream.probe_with(&probe);
1773 (probe, token)
1774 });
1775
1776 let deadline = Instant::now() + std::time::Duration::from_secs(60);
1777 while !errored.get() {
1778 assert!(
1779 Instant::now() < deadline,
1780 "timed out waiting for fetch error"
1781 );
1782 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1783 }
1784 let mut last = probe.with_frontier(|f| f.to_owned());
1785 let mut stable = 0;
1786 while stable < 100 {
1787 assert!(Instant::now() < deadline, "timed out waiting for quiesce");
1788 worker.step_or_park(Some(std::time::Duration::from_millis(1)));
1789 let now = probe.with_frontier(|f| f.to_owned());
1790 if now == last {
1791 stable += 1;
1792 } else {
1793 stable = 0;
1794 last = now;
1795 }
1796 }
1797 last
1798 });
1799
1800 assert_eq!(frontier, Antichain::from_elem(MISSING_TS));
1803 }
1804
1805 async fn initialize_shard(
1806 persist_client: &PersistClient,
1807 shard_id: ShardId,
1808 since: Antichain<u64>,
1809 ) {
1810 let mut read_handle = persist_client
1811 .open_leased_reader::<String, String, u64, u64>(
1812 shard_id,
1813 Arc::new(<std::string::String as mz_persist_types::Codec>::Schema::default()),
1814 Arc::new(<std::string::String as mz_persist_types::Codec>::Schema::default()),
1815 Diagnostics::for_tests(),
1816 true,
1817 )
1818 .await
1819 .expect("invalid usage");
1820
1821 read_handle.downgrade_since(&since).await;
1822 }
1823}