1pub(crate) mod backend;
4#[cfg(feature = "datafusion")]
5pub mod datafusion;
6pub(crate) mod deletion_vector;
7pub(crate) mod metrics;
8mod options;
9pub(crate) mod partition_target;
10pub(crate) mod planning;
11pub(crate) mod predicate;
12#[allow(dead_code)]
13pub(crate) mod scheduling;
14pub(crate) mod transform;
15
16#[doc(hidden)]
17pub use metrics::ParquetRangePlanningDiagnosticSnapshot;
18pub use metrics::{DeltaScanMetrics, DeltaScanMetricsSnapshot};
19#[doc(hidden)]
20pub use options::ParquetRangeReadPolicy;
21pub use options::{
22 DeltaScanExecutionOptions, DeltaSnapshotSelection, DeltaStorageOptions, ParquetReaderBackend,
23};
24pub use predicate::{DeltaComparison, DeltaPredicate, DeltaScalar};
25
26use std::{
27 fmt,
28 pin::Pin,
29 sync::Arc,
30 task::{Context, Poll},
31};
32
33use arrow::{datatypes::SchemaRef, record_batch::RecordBatch};
34use futures_util::Stream;
35use snafu::ResultExt;
36
37use self::{
38 planning::{
39 DeltaScanPartitionTargetOptions, DeltaScanPlan, build_physical_row_predicate, plan_scan,
40 },
41 predicate::{evaluate_predicate, referenced_columns, validate_predicate},
42 scheduling::{
43 DeltaScanScheduler, FileAdmissionDecision, FileAdmissionPolicy, FileBatchStream,
44 FileExecutor, OrderedPartitionStream,
45 },
46};
47
48use crate::{
49 DeltaProtocol, DeltaReaderError,
50 delta::{
51 kernel::{DeltaKernelPredicate, kernel_pruning_predicate, kernel_row_predicate},
52 protocol::validate_protocol,
53 snapshot::{
54 ArrowTableSnapshot, KernelTableSnapshot, load_delta_table_snapshot,
55 load_kernel_table_snapshot,
56 },
57 },
58 error::{DataFileReadSnafu, InvalidConfigurationSnafu, ScanPlanningSnafu, SnapshotLoadSnafu},
59};
60
61const TRACING_TARGET: &str = "delta_arrow_reader";
62const DELTA_LOG_SCAN_METADATA_SOURCE: &str = "delta_log";
63const QUERY_PLANNING_CACHE_SCAN_METADATA_SOURCE: &str = "query_planning_cache";
64
65#[non_exhaustive]
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub enum WarmupMode {
72 #[default]
74 None,
75 QueryPlanning,
77}
78
79#[must_use = "table builder settings do nothing unless the builder is loaded"]
114pub struct DeltaTableBuilder {
115 table_location: String,
116 storage_options: DeltaStorageOptions,
117 snapshot_selection: DeltaSnapshotSelection,
118 execution_options: DeltaScanExecutionOptions,
119 warmup: WarmupMode,
120}
121
122impl DeltaTableBuilder {
123 pub fn new(table_location: impl Into<String>) -> Self {
125 Self {
126 table_location: table_location.into(),
127 storage_options: DeltaStorageOptions::new(),
128 snapshot_selection: DeltaSnapshotSelection::Latest,
129 execution_options: DeltaScanExecutionOptions::new(),
130 warmup: WarmupMode::None,
131 }
132 }
133
134 pub fn with_storage_options(mut self, storage_options: DeltaStorageOptions) -> Self {
136 self.storage_options = storage_options;
137 self
138 }
139
140 pub const fn with_snapshot_selection(
142 mut self,
143 snapshot_selection: DeltaSnapshotSelection,
144 ) -> Self {
145 self.snapshot_selection = snapshot_selection;
146 self
147 }
148
149 pub const fn with_execution_options(
151 mut self,
152 execution_options: DeltaScanExecutionOptions,
153 ) -> Self {
154 self.execution_options = execution_options;
155 self
156 }
157
158 pub const fn with_warmup(mut self, warmup: WarmupMode) -> Self {
163 self.warmup = warmup;
164 self
165 }
166
167 pub async fn load_table(self) -> Result<DeltaTable, DeltaReaderError> {
183 let snapshot = load_delta_table_snapshot(
184 self.table_location,
185 self.storage_options,
186 self.snapshot_selection,
187 )
188 .await?;
189 let snapshot = if self.warmup == WarmupMode::None {
190 snapshot
191 } else {
192 validate_protocol(snapshot.protocol())?;
193 materialize_eager_scan_metadata(snapshot).await?
194 };
195 Ok(DeltaTable::new(snapshot, self.execution_options))
196 }
197
198 pub async fn load_snapshot(self) -> Result<DeltaTableSnapshot, DeltaReaderError> {
209 if self.warmup != WarmupMode::None {
210 return InvalidConfigurationSnafu {
211 reason: "snapshot_load_does_not_support_table_warmup",
212 }
213 .fail();
214 }
215 let snapshot = load_kernel_table_snapshot(
216 self.table_location,
217 self.storage_options,
218 self.snapshot_selection,
219 )
220 .await?;
221 Ok(DeltaTableSnapshot::new(snapshot, self.execution_options))
222 }
223}
224
225async fn materialize_eager_scan_metadata(
226 snapshot: ArrowTableSnapshot,
227) -> Result<ArrowTableSnapshot, DeltaReaderError> {
228 tokio::task::spawn_blocking(move || snapshot.materialize_eager_scan_metadata())
229 .await
230 .boxed()
231 .context(ScanPlanningSnafu {
232 reason: "query_planning_warmup_task_failed",
233 })?
234}
235
236impl fmt::Debug for DeltaTableBuilder {
237 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238 formatter
239 .debug_struct("DeltaTableBuilder")
240 .field("table_location", &"<redacted>")
241 .field("storage_options", &"<redacted>")
242 .field("snapshot_selection", &self.snapshot_selection)
243 .field("execution_options", &self.execution_options)
244 .field("warmup", &self.warmup)
245 .finish()
246 }
247}
248
249pub struct DeltaTableSnapshot {
251 snapshot: KernelTableSnapshot,
252 execution_options: DeltaScanExecutionOptions,
253}
254
255impl DeltaTableSnapshot {
256 fn new(snapshot: KernelTableSnapshot, execution_options: DeltaScanExecutionOptions) -> Self {
257 Self {
258 snapshot,
259 execution_options,
260 }
261 }
262
263 pub fn version(&self) -> u64 {
265 self.snapshot.version()
266 }
267
268 pub fn protocol(&self) -> &DeltaProtocol {
270 self.snapshot.protocol()
271 }
272
273 pub fn table_url(&self) -> &str {
277 self.snapshot.table_url()
278 }
279
280 pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
282 validate_protocol(self.protocol())
283 }
284
285 pub fn into_table(self) -> Result<DeltaTable, DeltaReaderError> {
287 Ok(DeltaTable::new(
288 self.snapshot.into_arrow_snapshot()?,
289 self.execution_options,
290 ))
291 }
292}
293
294impl fmt::Debug for DeltaTableSnapshot {
295 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296 formatter
297 .debug_struct("DeltaTableSnapshot")
298 .field("version", &self.version())
299 .finish_non_exhaustive()
300 }
301}
302
303#[derive(Clone)]
305pub struct DeltaTable {
306 snapshot: Arc<ArrowTableSnapshot>,
307 execution_options: DeltaScanExecutionOptions,
308}
309
310impl DeltaTable {
311 fn new(snapshot: ArrowTableSnapshot, execution_options: DeltaScanExecutionOptions) -> Self {
312 Self {
313 snapshot: Arc::new(snapshot),
314 execution_options,
315 }
316 }
317
318 pub fn version(&self) -> u64 {
320 self.snapshot.version()
321 }
322
323 pub fn schema(&self) -> SchemaRef {
325 self.snapshot.schema()
326 }
327
328 pub fn protocol(&self) -> &DeltaProtocol {
330 self.snapshot.protocol()
331 }
332
333 pub fn table_url(&self) -> &str {
337 self.snapshot.table_url()
338 }
339
340 #[allow(dead_code)]
341 pub(crate) fn partition_columns(&self) -> &[String] {
342 self.snapshot.partition_columns()
343 }
344
345 #[allow(dead_code)]
346 pub(crate) fn snapshot(&self) -> &ArrowTableSnapshot {
347 self.snapshot.as_ref()
348 }
349
350 pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
352 validate_protocol(self.protocol())
353 }
354
355 pub async fn refresh(&self) -> Result<Self, DeltaReaderError> {
366 let snapshot = Arc::clone(&self.snapshot);
367 let snapshot = tokio::task::spawn_blocking(move || snapshot.refresh())
368 .await
369 .boxed()
370 .context(SnapshotLoadSnafu {
371 reason: "snapshot_refresh_task_failed",
372 })
373 .and_then(|result| result)?;
374 Ok(Self::new(snapshot, self.execution_options))
375 }
376
377 pub fn scan(&self) -> DeltaScanBuilder<'_> {
379 DeltaScanBuilder {
380 table: self,
381 projection: None,
382 predicate: None,
383 limit: None,
384 target_partitions: None,
385 execution_options: self.execution_options,
386 }
387 }
388}
389
390impl fmt::Debug for DeltaTable {
391 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
392 formatter
393 .debug_struct("DeltaTable")
394 .field("version", &self.version())
395 .finish_non_exhaustive()
396 }
397}
398
399#[must_use = "scan builder settings do nothing unless the scan is built"]
401pub struct DeltaScanBuilder<'table> {
402 table: &'table DeltaTable,
403 projection: Option<Vec<String>>,
404 predicate: Option<DeltaPredicate>,
405 limit: Option<usize>,
406 target_partitions: Option<usize>,
407 execution_options: DeltaScanExecutionOptions,
408}
409
410impl<'table> DeltaScanBuilder<'table> {
411 pub fn with_projection(
413 mut self,
414 logical_columns: impl IntoIterator<Item = impl Into<String>>,
415 ) -> Self {
416 self.projection = Some(logical_columns.into_iter().map(Into::into).collect());
417 self
418 }
419
420 pub fn with_predicate(mut self, predicate: DeltaPredicate) -> Self {
422 self.predicate = Some(predicate);
423 self
424 }
425
426 pub const fn with_limit(mut self, limit: usize) -> Self {
428 self.limit = Some(limit);
429 self
430 }
431
432 pub fn with_target_partitions(
434 mut self,
435 target_partitions: usize,
436 ) -> Result<Self, DeltaReaderError> {
437 if target_partitions == 0 {
438 return InvalidConfigurationSnafu {
439 reason: "scan_partition_target_must_be_positive",
440 }
441 .fail();
442 }
443 self.target_partitions = Some(target_partitions);
444 Ok(self)
445 }
446
447 pub const fn with_execution_options(
449 mut self,
450 execution_options: DeltaScanExecutionOptions,
451 ) -> Self {
452 self.execution_options = execution_options;
453 self
454 }
455
456 pub async fn build(self) -> Result<DeltaScan, DeltaReaderError> {
458 self.table.validate_protocol()?;
459 if let Some(predicate) = self.predicate.as_ref() {
460 validate_predicate(predicate, self.table.schema().as_ref())?;
461 }
462
463 let snapshot_version = self.table.version();
464 let backend = self.execution_options.parquet_backend();
465 let scan_metadata_source = if self.table.snapshot.eager_scan_metadata().is_some() {
466 QUERY_PLANNING_CACHE_SCAN_METADATA_SOURCE
467 } else {
468 DELTA_LOG_SCAN_METADATA_SOURCE
469 };
470 trace_planning_started(snapshot_version, backend, scan_metadata_source);
471 let snapshot = Arc::clone(&self.table.snapshot);
472 let projection = self.projection;
473 let predicate = self.predicate;
474 let hidden_columns = predicate
475 .as_ref()
476 .map(referenced_columns)
477 .unwrap_or_default();
478 let row_predicate = predicate
479 .as_ref()
480 .filter(|_| backend == ParquetReaderBackend::Direct)
481 .and_then(|predicate| kernel_row_predicate(predicate, snapshot.partition_columns()));
482 let kernel_predicate = predicate.as_ref().and_then(kernel_pruning_predicate);
483 let include_stats = kernel_predicate.is_some();
484 let execution_options = self.execution_options;
485 let target_partitions = self.target_partitions;
486 let result = tokio::task::spawn_blocking(move || {
487 let same_predicate = row_predicate.is_some() && row_predicate == kernel_predicate;
489 let plan = plan_scan(
490 snapshot.as_ref(),
491 projection.as_deref(),
492 &hidden_columns,
493 kernel_predicate,
494 include_stats,
495 execution_options,
496 DeltaScanPartitionTargetOptions {
497 explicit_target_partitions: target_partitions,
498 datafusion_target_partitions: None,
499 },
500 )?;
501 let physical_row_predicate = if plan.partitions.is_empty() {
502 None
503 } else if same_predicate {
504 plan.physical_predicate.clone()
505 } else {
506 build_physical_row_predicate(
507 snapshot.as_ref(),
508 projection.as_deref(),
509 &hidden_columns,
510 row_predicate,
511 )?
512 };
513 Ok((plan, physical_row_predicate))
514 })
515 .await
516 .boxed()
517 .context(ScanPlanningSnafu {
518 reason: "scan_planning_task_failed",
519 })
520 .and_then(|result| result);
521
522 match result {
523 Ok((plan, physical_row_predicate)) => {
524 trace_planning_completed(
525 snapshot_version,
526 backend,
527 plan.partitions.len(),
528 scan_metadata_source,
529 );
530 Ok(DeltaScan {
531 plan: Arc::new(plan),
532 predicate,
533 limit: self.limit,
534 physical_row_predicate,
535 })
536 }
537 Err(error) => {
538 trace_planning_failed(snapshot_version, backend, scan_metadata_source, &error);
539 Err(error)
540 }
541 }
542 }
543}
544
545#[must_use = "scans do nothing unless converted into a stream"]
566pub struct DeltaScan {
567 plan: Arc<DeltaScanPlan>,
568 predicate: Option<DeltaPredicate>,
569 limit: Option<usize>,
570 physical_row_predicate: Option<DeltaKernelPredicate>,
571}
572
573impl DeltaScan {
574 pub fn schema(&self) -> SchemaRef {
576 Arc::clone(&self.plan.projected_schema)
577 }
578
579 pub fn partition_count(&self) -> usize {
581 self.plan.partitions.len()
582 }
583
584 pub fn into_stream(self) -> DeltaBatchStream {
588 let metrics = self.plan.metrics.clone();
589 let schema = Arc::clone(&self.plan.projected_schema);
590 let partition_count = self.plan.partitions.len();
591 let snapshot_version = self.plan.snapshot_version;
592 let backend = self.plan.execution_options.parquet_backend();
593 let projection = (self.plan.logical_schema.as_ref() != schema.as_ref())
594 .then(|| (0..schema.fields().len()).collect::<Vec<_>>());
595 let partitions = if self.limit == Some(0) {
596 OrderedPartitionStream::default()
597 } else {
598 let scheduler = DeltaScanScheduler::new(Arc::clone(&self.plan));
599 let admission: FileAdmissionPolicy<_> = Arc::new(|_| Ok(FileAdmissionDecision::Admit));
600 let executor = match backend {
601 ParquetReaderBackend::Direct => {
602 direct_parquet_executor(&self.plan, None, self.physical_row_predicate)
603 }
604 ParquetReaderBackend::DeltaKernel => delta_kernel_executor(&self.plan),
605 };
606 scheduler.partition_streams(admission, executor)
607 };
608
609 DeltaBatchStream {
610 schema,
611 metrics,
612 partitions,
613 predicate: self.predicate,
614 projection,
615 remaining: self.limit,
616 snapshot_version,
617 backend,
618 partition_count,
619 started: false,
620 done: false,
621 }
622 }
623}
624
625#[must_use = "streams do nothing unless polled"]
638pub struct DeltaBatchStream {
639 schema: SchemaRef,
640 metrics: DeltaScanMetrics,
641 partitions: OrderedPartitionStream,
642 predicate: Option<DeltaPredicate>,
643 projection: Option<Vec<usize>>,
644 remaining: Option<usize>,
645 snapshot_version: u64,
646 backend: ParquetReaderBackend,
647 partition_count: usize,
648 started: bool,
649 done: bool,
650}
651
652impl DeltaBatchStream {
653 pub fn schema(&self) -> SchemaRef {
655 Arc::clone(&self.schema)
656 }
657
658 pub fn metrics(&self) -> DeltaScanMetrics {
660 self.metrics.clone()
661 }
662
663 fn start(&mut self) {
664 if self.started {
665 return;
666 }
667 self.started = true;
668 trace_execution_started(self.snapshot_version, self.backend, self.partition_count);
669 }
670
671 fn complete(&mut self) {
672 if self.done {
673 return;
674 }
675 self.partitions.clear();
676 self.done = true;
677 trace_execution_completed(self.snapshot_version, self.backend, self.partition_count);
678 }
679
680 fn fail(&mut self, error: &DeltaReaderError) {
681 self.partitions.clear();
682 self.done = true;
683 trace_execution_failed(
684 self.snapshot_version,
685 self.backend,
686 self.partition_count,
687 error,
688 );
689 }
690
691 fn finalize_batch(&self, mut batch: RecordBatch) -> Result<RecordBatch, DeltaReaderError> {
692 if let Some(predicate) = self.predicate.as_ref() {
693 batch = evaluate_predicate(&batch, predicate)?;
694 }
695 if let Some(projection) = self.projection.as_ref() {
696 batch = batch
697 .project(projection)
698 .boxed()
699 .context(DataFileReadSnafu {
700 reason: "direct_projection_failed",
701 })?;
702 }
703 Ok(batch)
704 }
705}
706
707impl Stream for DeltaBatchStream {
708 type Item = Result<RecordBatch, DeltaReaderError>;
709
710 fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
711 let this = self.get_mut();
712 if this.done {
713 return Poll::Ready(None);
714 }
715 this.start();
716
717 match Pin::new(&mut this.partitions).poll_next(context) {
718 Poll::Ready(Some(Ok(batch))) => {
719 let mut batch = match this.finalize_batch(batch) {
720 Ok(batch) => batch,
721 Err(error) => {
722 this.fail(&error);
723 return Poll::Ready(Some(Err(error)));
724 }
725 };
726 if let Some(remaining) = this.remaining.as_mut() {
727 if batch.num_rows() >= *remaining {
728 batch = batch.slice(0, *remaining);
729 *remaining = 0;
730 this.complete();
731 } else {
732 *remaining -= batch.num_rows();
733 }
734 }
735 Poll::Ready(Some(Ok(batch)))
736 }
737 Poll::Ready(Some(Err(error))) => {
738 this.fail(&error);
739 Poll::Ready(Some(Err(error)))
740 }
741 Poll::Ready(None) => {
742 this.complete();
743 Poll::Ready(None)
744 }
745 Poll::Pending => Poll::Pending,
746 }
747 }
748}
749
750impl Drop for DeltaBatchStream {
751 fn drop(&mut self) {
752 if self.done {
753 return;
754 }
755 self.partitions.clear();
756 self.done = true;
757 trace_execution_dropped(self.snapshot_version, self.backend, self.partition_count);
758 }
759}
760
761pub(crate) fn direct_parquet_executor(
762 plan: &Arc<DeltaScanPlan>,
763 output_batch_size_rows: Option<usize>,
764 row_predicate: Option<crate::delta::kernel::DeltaKernelPredicate>,
765) -> FileExecutor<planning::DeltaScanFileTask, FileBatchStream> {
766 backend::direct_parquet::direct_parquet_file_executor(
767 plan,
768 output_batch_size_rows,
769 row_predicate,
770 Arc::default(),
771 None,
772 )
773}
774
775pub(crate) fn delta_kernel_executor(
776 plan: &Arc<DeltaScanPlan>,
777) -> FileExecutor<planning::DeltaScanFileTask, FileBatchStream> {
778 backend::kernel_reader::delta_kernel_file_executor(plan)
779}
780
781fn trace_planning_started(
782 snapshot_version: u64,
783 backend: ParquetReaderBackend,
784 scan_metadata_source: &'static str,
785) {
786 tracing::debug!(
787 target: TRACING_TARGET,
788 event = "scan_planning.started",
789 snapshot_version,
790 backend = ?backend,
791 partition_count = tracing::field::Empty,
792 scan_metadata_source,
793 outcome = "started"
794 );
795}
796
797fn trace_planning_completed(
798 snapshot_version: u64,
799 backend: ParquetReaderBackend,
800 partition_count: usize,
801 scan_metadata_source: &'static str,
802) {
803 tracing::debug!(
804 target: TRACING_TARGET,
805 event = "scan_planning.completed",
806 snapshot_version,
807 backend = ?backend,
808 partition_count,
809 scan_metadata_source,
810 outcome = "completed"
811 );
812}
813
814fn trace_planning_failed(
815 snapshot_version: u64,
816 backend: ParquetReaderBackend,
817 scan_metadata_source: &'static str,
818 error: &DeltaReaderError,
819) {
820 tracing::debug!(
821 target: TRACING_TARGET,
822 event = "scan_planning.failed",
823 snapshot_version,
824 backend = ?backend,
825 partition_count = tracing::field::Empty,
826 scan_metadata_source,
827 outcome = "failed",
828 error_code = error.code(),
829 error_phase = error.phase().as_str()
830 );
831}
832
833fn trace_execution_started(
834 snapshot_version: u64,
835 backend: ParquetReaderBackend,
836 partition_count: usize,
837) {
838 tracing::debug!(
839 target: TRACING_TARGET,
840 event = "scan_execution.started",
841 snapshot_version,
842 backend = ?backend,
843 partition_count,
844 outcome = "started"
845 );
846}
847
848fn trace_execution_completed(
849 snapshot_version: u64,
850 backend: ParquetReaderBackend,
851 partition_count: usize,
852) {
853 tracing::debug!(
854 target: TRACING_TARGET,
855 event = "scan_execution.completed",
856 snapshot_version,
857 backend = ?backend,
858 partition_count,
859 outcome = "completed"
860 );
861}
862
863fn trace_execution_failed(
864 snapshot_version: u64,
865 backend: ParquetReaderBackend,
866 partition_count: usize,
867 error: &DeltaReaderError,
868) {
869 tracing::debug!(
870 target: TRACING_TARGET,
871 event = "scan_execution.failed",
872 snapshot_version,
873 backend = ?backend,
874 partition_count,
875 outcome = "failed",
876 error_code = error.code(),
877 error_phase = error.phase().as_str()
878 );
879}
880
881fn trace_execution_dropped(
882 snapshot_version: u64,
883 backend: ParquetReaderBackend,
884 partition_count: usize,
885) {
886 tracing::debug!(
887 target: TRACING_TARGET,
888 event = "scan_execution.dropped",
889 snapshot_version,
890 backend = ?backend,
891 partition_count,
892 outcome = "dropped"
893 );
894}
895
896#[cfg(test)]
897mod tests {
898 use std::{
899 collections::{BTreeMap, VecDeque},
900 fmt, fs,
901 future::pending,
902 path::{Path, PathBuf},
903 sync::{Arc, Mutex, Once},
904 time::{Duration, SystemTime, UNIX_EPOCH},
905 };
906
907 use arrow::{
908 array::Int32Array,
909 datatypes::{DataType, Field, Schema, SchemaRef},
910 record_batch::RecordBatch,
911 };
912 use futures_util::{FutureExt, StreamExt, stream};
913 use tokio::{sync::Notify, time::timeout};
914 use tracing::{
915 Event, Level, Metadata, Subscriber,
916 field::{Field as TracingField, Visit},
917 span::{Attributes, Id, Record},
918 subscriber::{Interest, with_default},
919 };
920
921 use super::{
922 DeltaBatchStream, DeltaTable, trace_execution_completed, trace_execution_dropped,
923 trace_execution_failed, trace_execution_started, trace_planning_completed,
924 trace_planning_failed, trace_planning_started,
925 };
926 use crate::{
927 DeltaScanExecutionOptions, DeltaScanMetrics, DeltaSnapshotSelection, DeltaStorageOptions,
928 ParquetReaderBackend,
929 delta::snapshot::load_delta_table_snapshot_blocking,
930 error::InvalidConfigurationSnafu,
931 reader::{
932 metrics::DeltaScanMetricsConfig,
933 scheduling::{
934 FileAdmissionDecision, FileAdmissionPolicy, FileBatchStream, FileExecutor,
935 FileReadPermit, OrderedPartitionStream, PartitionStream, ScanCancellation,
936 ScanReadLimiter,
937 },
938 },
939 };
940
941 static TRACING_TEST_LOCK: Mutex<()> = Mutex::new(());
942 static TRACING_TEST_GLOBAL_SUBSCRIBER: Once = Once::new();
943
944 #[derive(Clone, Default)]
945 struct EventFields(Arc<Mutex<Vec<BTreeMap<String, String>>>>);
946
947 impl Subscriber for EventFields {
948 fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
949 if metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG {
950 Interest::always()
951 } else {
952 Interest::sometimes()
953 }
954 }
955
956 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
957 metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG
958 }
959
960 fn new_span(&self, _attributes: &Attributes<'_>) -> Id {
961 Id::from_u64(1)
962 }
963
964 fn record(&self, _span: &Id, _values: &Record<'_>) {}
965
966 fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
967
968 fn event(&self, event: &Event<'_>) {
969 let metadata = event.metadata();
970 assert_eq!(metadata.target(), "delta_arrow_reader");
971 let mut fields = metadata
972 .fields()
973 .iter()
974 .map(|field| (field.name().to_owned(), "<empty>".to_owned()))
975 .collect();
976 event.record(&mut FieldVisitor(&mut fields));
977 self.0.lock().expect("event lock").push(fields);
978 }
979
980 fn enter(&self, _span: &Id) {}
981
982 fn exit(&self, _span: &Id) {}
983 }
984
985 struct FieldVisitor<'fields>(&'fields mut BTreeMap<String, String>);
986
987 impl Visit for FieldVisitor<'_> {
988 fn record_debug(&mut self, field: &TracingField, value: &dyn fmt::Debug) {
989 self.0.insert(field.name().to_owned(), format!("{value:?}"));
990 }
991
992 fn record_str(&mut self, field: &TracingField, value: &str) {
993 self.0.insert(field.name().to_owned(), value.to_owned());
994 }
995
996 fn record_u64(&mut self, field: &TracingField, value: u64) {
997 self.0.insert(field.name().to_owned(), value.to_string());
998 }
999
1000 fn record_i64(&mut self, field: &TracingField, value: i64) {
1001 self.0.insert(field.name().to_owned(), value.to_string());
1002 }
1003
1004 fn record_u128(&mut self, field: &TracingField, value: u128) {
1005 self.0.insert(field.name().to_owned(), value.to_string());
1006 }
1007 }
1008
1009 struct DeltaLogTable(PathBuf);
1010
1011 impl DeltaLogTable {
1012 fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
1013 let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
1014 let path = Path::new("target")
1015 .join("delta-arrow-reader-tracing-tests")
1016 .join(format!("{}-{name}-{nanos}", std::process::id()));
1017 fs::create_dir_all(path.join("_delta_log"))?;
1018 fs::write(
1019 path.join("_delta_log/00000000000000000000.json"),
1020 r#"{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}
1021{"metaData":{"id":"tracing-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}
1022{"add":{"path":"secret-planning-object.parquet","partitionValues":{},"size":10,"modificationTime":1587968586000,"dataChange":true}}
1023"#,
1024 )?;
1025 Ok(Self(path))
1026 }
1027 }
1028
1029 impl Drop for DeltaLogTable {
1030 fn drop(&mut self) {
1031 let _ = fs::remove_dir_all(&self.0);
1032 }
1033 }
1034
1035 fn capture_events<T>(run: impl FnOnce() -> T) -> (T, Vec<BTreeMap<String, String>>) {
1036 let _lock = TRACING_TEST_LOCK
1037 .lock()
1038 .unwrap_or_else(std::sync::PoisonError::into_inner);
1039 let events = Arc::new(Mutex::new(Vec::new()));
1040 let subscriber = EventFields(Arc::clone(&events));
1041 TRACING_TEST_GLOBAL_SUBSCRIBER.call_once(|| {
1042 let _ = tracing::subscriber::set_global_default(EventFields::default());
1043 });
1044 let result = with_default(subscriber, || {
1045 tracing::callsite::rebuild_interest_cache();
1046 run()
1047 });
1048 tracing::callsite::rebuild_interest_cache();
1049 let captured = events
1050 .lock()
1051 .map(|events| events.clone())
1052 .unwrap_or_default();
1053 (result, captured)
1054 }
1055
1056 struct ControlledMerge {
1057 stream: DeltaBatchStream,
1058 limiter: Arc<ScanReadLimiter>,
1059 cancellation: ScanCancellation,
1060 metrics: DeltaScanMetrics,
1061 first_partition_gate: Arc<Notify>,
1062 }
1063
1064 fn schema() -> SchemaRef {
1065 Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]))
1066 }
1067
1068 fn batch(id: i32) -> RecordBatch {
1069 RecordBatch::try_new(schema(), vec![Arc::new(Int32Array::from(vec![id]))])
1070 .expect("valid test batch")
1071 }
1072
1073 fn batch_id(batch: &RecordBatch) -> i32 {
1074 batch
1075 .column(0)
1076 .as_any()
1077 .downcast_ref::<Int32Array>()
1078 .expect("Int32 id")
1079 .value(0)
1080 }
1081
1082 fn execution_options() -> Result<DeltaScanExecutionOptions, crate::DeltaReaderError> {
1083 DeltaScanExecutionOptions::new()
1084 .with_prefetch_files_per_partition(0)
1085 .with_max_concurrent_file_reads_per_partition(1)?
1086 .with_max_concurrent_file_reads_per_scan(Some(2))?
1087 .with_output_buffer_batches_per_partition(1)
1088 }
1089
1090 fn metrics() -> DeltaScanMetrics {
1091 DeltaScanMetrics::new(DeltaScanMetricsConfig {
1092 snapshot_version: 7,
1093 parquet_backend: ParquetReaderBackend::Direct,
1094 scan_partitions_planned: 2,
1095 files_planned: 2,
1096 add_actions_excluded_during_planning: Some(0),
1097 estimated_input_rows: Some(4),
1098 estimated_input_bytes: Some(4),
1099 })
1100 }
1101
1102 fn file_stream(permit: FileReadPermit, batches: Vec<RecordBatch>) -> FileBatchStream {
1103 Box::pin(stream::unfold(
1104 (VecDeque::from(batches), permit),
1105 |(mut batches, permit)| async move {
1106 batches
1107 .pop_front()
1108 .map(|batch| (Ok(batch), (batches, permit)))
1109 },
1110 ))
1111 }
1112
1113 fn gated_file_stream(
1114 permit: FileReadPermit,
1115 batches: Vec<RecordBatch>,
1116 gate: Arc<Notify>,
1117 ) -> FileBatchStream {
1118 Box::pin(stream::unfold(
1119 (false, VecDeque::from(batches), permit, gate),
1120 |(wait, mut batches, permit, gate)| async move {
1121 let batch = batches.pop_front()?;
1122 if wait {
1123 gate.notified().await;
1124 }
1125 Some((Ok(batch), (true, batches, permit, gate)))
1126 },
1127 ))
1128 }
1129
1130 fn direct_stream(
1131 partitions: VecDeque<PartitionStream>,
1132 metrics: DeltaScanMetrics,
1133 scan_capacity: usize,
1134 ) -> DeltaBatchStream {
1135 DeltaBatchStream {
1136 schema: schema(),
1137 metrics,
1138 partitions: OrderedPartitionStream::new(partitions, scan_capacity),
1139 predicate: None,
1140 projection: None,
1141 remaining: None,
1142 snapshot_version: 7,
1143 backend: ParquetReaderBackend::Direct,
1144 partition_count: 2,
1145 started: false,
1146 done: false,
1147 }
1148 }
1149
1150 fn controlled_merge() -> Result<ControlledMerge, Box<dyn std::error::Error>> {
1151 let options = execution_options()?;
1152 let limiter = ScanReadLimiter::new(options, 2, 2);
1153 let cancellation = ScanCancellation::new();
1154 let metrics = metrics();
1155 let first_partition_gate = Arc::new(Notify::new());
1156 let executor: FileExecutor<i32, FileBatchStream> = {
1157 let gate = Arc::clone(&first_partition_gate);
1158 Arc::new(move |task, permit, _| {
1159 let gate = Arc::clone(&gate);
1160 async move {
1161 let batches = vec![batch(task), batch(task * 2)];
1162 Ok(if task == 1 {
1163 gated_file_stream(permit, batches, gate)
1164 } else {
1165 file_stream(permit, batches)
1166 })
1167 }
1168 .boxed()
1169 })
1170 };
1171 let admission: FileAdmissionPolicy<i32> =
1172 Arc::new(|_: &i32| Ok(FileAdmissionDecision::Admit));
1173 let first = PartitionStream::new(
1174 vec![1],
1175 limiter.partition(0)?,
1176 options,
1177 admission.clone(),
1178 Arc::clone(&executor),
1179 metrics.clone(),
1180 cancellation.clone(),
1181 );
1182 let second = PartitionStream::new(
1183 vec![10],
1184 limiter.partition(1)?,
1185 options,
1186 admission,
1187 executor,
1188 metrics.clone(),
1189 cancellation.clone(),
1190 );
1191
1192 Ok(ControlledMerge {
1193 stream: direct_stream(VecDeque::from([first, second]), metrics.clone(), 2),
1194 limiter,
1195 cancellation,
1196 metrics,
1197 first_partition_gate,
1198 })
1199 }
1200
1201 async fn wait_for_batches(metrics: &DeltaScanMetrics, expected: u64) {
1202 timeout(Duration::from_secs(5), async {
1203 while metrics.snapshot().scheduler_batches_emitted < expected {
1204 tokio::task::yield_now().await;
1205 }
1206 })
1207 .await
1208 .expect("batch production reached expected bound");
1209 }
1210
1211 #[test]
1212 fn lifecycle_tracing_has_only_bounded_fields() {
1213 let error = InvalidConfigurationSnafu { reason: "test" }.build();
1214
1215 let (_, events) = capture_events(|| {
1216 trace_planning_started(7, ParquetReaderBackend::Direct, "query_planning_cache");
1217 trace_planning_completed(7, ParquetReaderBackend::Direct, 2, "query_planning_cache");
1218 trace_planning_failed(
1219 7,
1220 ParquetReaderBackend::Direct,
1221 "query_planning_cache",
1222 &error,
1223 );
1224 trace_execution_started(7, ParquetReaderBackend::Direct, 2);
1225 trace_execution_completed(7, ParquetReaderBackend::Direct, 2);
1226 trace_execution_failed(7, ParquetReaderBackend::Direct, 2, &error);
1227 trace_execution_dropped(7, ParquetReaderBackend::Direct, 2);
1228 });
1229
1230 assert_eq!(events.len(), 7);
1231 let allowed = [
1232 "backend",
1233 "error_phase",
1234 "error_code",
1235 "event",
1236 "outcome",
1237 "partition_count",
1238 "scan_metadata_source",
1239 "snapshot_version",
1240 ];
1241 for fields in events.iter() {
1242 assert!(fields.keys().all(|field| allowed.contains(&field.as_str())));
1243 assert!(fields.contains_key("event"));
1244 assert!(fields.contains_key("snapshot_version"));
1245 assert!(fields.contains_key("backend"));
1246 assert!(fields.contains_key("partition_count"));
1247 assert!(fields.contains_key("outcome"));
1248 if fields
1249 .get("event")
1250 .is_some_and(|event| event.starts_with("scan_planning."))
1251 {
1252 assert_eq!(
1253 fields.get("scan_metadata_source").map(String::as_str),
1254 Some("query_planning_cache")
1255 );
1256 }
1257 }
1258 }
1259
1260 #[test]
1261 fn planning_tracing_reports_the_table_metadata_source_on_success_and_failure()
1262 -> Result<(), Box<dyn std::error::Error>> {
1263 const OBJECT_KEY: &str = "secret-planning-object.parquet";
1264 const STORAGE_VALUE: &str = "secret-planning-storage-value";
1265 let fixture = DeltaLogTable::new("planning-source")?;
1266 let mut storage_options = DeltaStorageOptions::new();
1267 storage_options.insert("secret-option".to_owned(), STORAGE_VALUE.to_owned());
1268 let snapshot = load_delta_table_snapshot_blocking(
1269 &fixture.0.to_string_lossy(),
1270 &storage_options,
1271 DeltaSnapshotSelection::Latest,
1272 )?;
1273 let eager_snapshot = snapshot.clone().materialize_eager_scan_metadata()?;
1274 let lazy = DeltaTable::new(snapshot, DeltaScanExecutionOptions::new());
1275 let eager = DeltaTable::new(eager_snapshot, DeltaScanExecutionOptions::new());
1276 let runtime = tokio::runtime::Builder::new_current_thread()
1277 .enable_all()
1278 .build()?;
1279
1280 let (eager_result, eager_events) =
1281 capture_events(|| runtime.block_on(eager.scan().build()));
1282 let _ = eager_result?;
1283 assert_eq!(eager_events.len(), 2);
1284 assert_eq!(
1285 eager_events[0].get("event").map(String::as_str),
1286 Some("scan_planning.started")
1287 );
1288 assert_eq!(
1289 eager_events[1].get("event").map(String::as_str),
1290 Some("scan_planning.completed")
1291 );
1292 assert!(eager_events.iter().all(|event| {
1293 event.get("scan_metadata_source").map(String::as_str) == Some("query_planning_cache")
1294 }));
1295
1296 let (failed_result, failed_events) =
1297 capture_events(|| runtime.block_on(eager.scan().with_projection(["missing"]).build()));
1298 assert!(failed_result.is_err());
1299 assert_eq!(failed_events.len(), 2);
1300 assert_eq!(
1301 failed_events[0].get("event").map(String::as_str),
1302 Some("scan_planning.started")
1303 );
1304 assert_eq!(
1305 failed_events[1].get("event").map(String::as_str),
1306 Some("scan_planning.failed")
1307 );
1308 assert!(failed_events.iter().all(|event| {
1309 event.get("scan_metadata_source").map(String::as_str) == Some("query_planning_cache")
1310 }));
1311
1312 let (lazy_result, lazy_events) = capture_events(|| runtime.block_on(lazy.scan().build()));
1313 let _ = lazy_result?;
1314 assert_eq!(lazy_events.len(), 2);
1315 assert!(lazy_events.iter().all(|event| {
1316 event.get("scan_metadata_source").map(String::as_str) == Some("delta_log")
1317 }));
1318
1319 let captured = format!("{eager_events:?}{failed_events:?}{lazy_events:?}");
1320 assert!(!captured.contains(&fixture.0.to_string_lossy().into_owned()));
1321 assert!(!captured.contains(OBJECT_KEY));
1322 assert!(!captured.contains(STORAGE_VALUE));
1323 Ok(())
1324 }
1325
1326 #[tokio::test]
1327 async fn merged_stream_stops_without_admitting_waiting_partitions()
1328 -> Result<(), Box<dyn std::error::Error>> {
1329 for stop in ["limit", "drop", "error"] {
1330 let options = execution_options()?.with_max_concurrent_file_reads_per_scan(Some(1))?;
1331 let limiter = ScanReadLimiter::new(options, 3, 3);
1332 let cancellation = ScanCancellation::new();
1333 let metrics = metrics();
1334 let executor: FileExecutor<i32, FileBatchStream> = Arc::new(move |task, permit, _| {
1335 async move {
1336 Ok(if stop == "error" {
1337 Box::pin(stream::once(async move {
1338 let _permit = permit;
1339 Err(InvalidConfigurationSnafu {
1340 reason: "controlled_partition_failure",
1341 }
1342 .build())
1343 })) as FileBatchStream
1344 } else {
1345 gated_file_stream(permit, vec![batch(task); 3], Arc::new(Notify::new()))
1346 })
1347 }
1348 .boxed()
1349 });
1350 let partitions = (0..3)
1351 .map(|index| {
1352 Ok(PartitionStream::new(
1353 vec![index as i32; 2],
1354 limiter.partition(index)?,
1355 options,
1356 Arc::new(|_| Ok(FileAdmissionDecision::Admit)),
1357 Arc::clone(&executor),
1358 metrics.clone(),
1359 cancellation.clone(),
1360 ))
1361 })
1362 .collect::<Result<VecDeque<_>, crate::DeltaReaderError>>()?;
1363 let mut stream = direct_stream(partitions, metrics.clone(), 1);
1364 if stop == "limit" {
1365 stream.remaining = Some(1);
1366 }
1367 let first = timeout(Duration::from_secs(5), stream.next())
1368 .await?
1369 .ok_or("missing result")?;
1370 if stop == "error" {
1371 assert_eq!(
1372 first.expect_err("controlled failure").code(),
1373 "invalid_configuration"
1374 );
1375 } else {
1376 assert_eq!(batch_id(&first?), 0);
1377 }
1378 if stop != "drop" {
1379 assert!(stream.next().await.is_none());
1380 }
1381 drop(stream);
1382 timeout(Duration::from_secs(5), async {
1383 while limiter.active_file_reads() != 0 {
1384 tokio::task::yield_now().await;
1385 }
1386 })
1387 .await?;
1388 assert!(cancellation.is_cancelled());
1389 assert_eq!(metrics.snapshot().file_tasks_started, 1);
1390 assert_eq!(metrics.snapshot().scan_partitions_started, 1);
1391 }
1392 Ok(())
1393 }
1394
1395 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1396 async fn merged_stream_progresses_with_small_scan_read_limits()
1397 -> Result<(), Box<dyn std::error::Error>> {
1398 for backend in [
1399 ParquetReaderBackend::Direct,
1400 ParquetReaderBackend::DeltaKernel,
1401 ] {
1402 for cap in [1, 2, 4, 8] {
1403 for prefetch in [0, 2, usize::MAX] {
1404 let options = DeltaScanExecutionOptions::new()
1405 .with_parquet_backend(backend)
1406 .with_max_concurrent_file_reads_per_scan(Some(cap))?
1407 .with_prefetch_files_per_partition(prefetch);
1408 let limiter = ScanReadLimiter::new(options, 5, 5);
1409 let cancellation = ScanCancellation::new();
1410 let metrics = metrics();
1411 let executor: FileExecutor<i32, FileBatchStream> =
1412 Arc::new(|task, permit, _| {
1413 async move {
1414 Ok(file_stream(permit, vec![batch(task); 3]))
1417 }
1418 .boxed()
1419 });
1420 let partitions = (0..5)
1421 .map(|partition| {
1422 Ok(PartitionStream::new(
1423 (partition * 4..partition * 4 + 4)
1424 .map(|id| id as i32)
1425 .collect(),
1426 limiter.partition(partition)?,
1427 options,
1428 Arc::new(|_| Ok(FileAdmissionDecision::Admit)),
1429 Arc::clone(&executor),
1430 metrics.clone(),
1431 cancellation.clone(),
1432 ))
1433 })
1434 .collect::<Result<VecDeque<_>, crate::DeltaReaderError>>()?;
1435 let mut stream = direct_stream(partitions, metrics.clone(), cap);
1436 let ids = timeout(Duration::from_secs(2), async {
1437 let mut ids = Vec::new();
1438 while let Some(batch) = stream.next().await {
1439 ids.push(batch_id(&batch?));
1440 assert!(limiter.active_file_reads() <= cap);
1441 tokio::task::yield_now().await;
1442 }
1443 Ok::<_, crate::DeltaReaderError>(ids)
1444 })
1445 .await
1446 .unwrap_or_else(|_| {
1447 panic!("stalled: {backend:?}, cap={cap}, prefetch={prefetch}")
1448 })?;
1449 assert_eq!(ids, (0..20).flat_map(|id| [id; 3]).collect::<Vec<_>>());
1450 assert_eq!(limiter.active_file_reads(), 0);
1451 assert_eq!(metrics.snapshot().file_tasks_completed, 20);
1452 assert_eq!(metrics.snapshot().scan_partitions_completed, 5);
1453 }
1454 }
1455 }
1456 Ok(())
1457 }
1458
1459 #[tokio::test]
1460 async fn merged_stream_is_ordered_and_bounds_later_partition_queues()
1461 -> Result<(), Box<dyn std::error::Error>> {
1462 let ControlledMerge {
1463 mut stream,
1464 limiter,
1465 metrics,
1466 first_partition_gate,
1467 ..
1468 } = controlled_merge()?;
1469
1470 let first = stream.next().await.ok_or("first batch missing")??;
1471 assert_eq!(batch_id(&first), 1);
1472 wait_for_batches(&metrics, 2).await;
1473 for _ in 0..32 {
1474 tokio::task::yield_now().await;
1475 }
1476 assert_eq!(metrics.snapshot().scheduler_batches_emitted, 2);
1477 assert_eq!(limiter.active_file_reads(), 2);
1478
1479 first_partition_gate.notify_one();
1480 let mut ids = vec![batch_id(
1481 &stream.next().await.ok_or("second batch missing")??,
1482 )];
1483 while let Some(batch) = stream.next().await {
1484 ids.push(batch_id(&batch?));
1485 }
1486 assert_eq!(ids, [2, 10, 20]);
1487 assert_eq!(metrics.snapshot().scheduler_batches_emitted, 4);
1488 assert_eq!(metrics.snapshot().scan_partitions_completed, 2);
1489 assert_eq!(limiter.active_file_reads(), 0);
1490 Ok(())
1491 }
1492
1493 #[tokio::test]
1494 async fn merged_stream_drop_cancels_blocked_partitions_and_releases_permits()
1495 -> Result<(), Box<dyn std::error::Error>> {
1496 let ControlledMerge {
1497 mut stream,
1498 limiter,
1499 cancellation,
1500 metrics,
1501 ..
1502 } = controlled_merge()?;
1503
1504 let first = stream.next().await.ok_or("first batch missing")??;
1505 assert_eq!(batch_id(&first), 1);
1506 wait_for_batches(&metrics, 2).await;
1507 assert_eq!(limiter.active_file_reads(), 2);
1508 drop(stream);
1509
1510 assert!(cancellation.is_cancelled());
1511 timeout(Duration::from_secs(5), async {
1512 while limiter.active_file_reads() != 0 {
1513 tokio::task::yield_now().await;
1514 }
1515 })
1516 .await?;
1517 assert_eq!(metrics.snapshot().scheduler_batches_emitted, 2);
1518 assert_eq!(metrics.snapshot().scan_partitions_completed, 0);
1519 Ok(())
1520 }
1521
1522 #[tokio::test]
1523 async fn merged_stream_forwards_one_concurrent_error_and_releases_permits()
1524 -> Result<(), Box<dyn std::error::Error>> {
1525 let options = execution_options()?;
1526 let limiter = ScanReadLimiter::new(options, 3, 3);
1527 let cancellation = ScanCancellation::new();
1528 let metrics = metrics();
1529 let executor: FileExecutor<i32, FileBatchStream> = Arc::new(|task, permit, _| {
1530 async move {
1531 Ok(if task == 1 {
1532 Box::pin(stream::once(async move {
1533 let _permit = permit;
1534 pending::<Result<RecordBatch, crate::DeltaReaderError>>().await
1535 })) as FileBatchStream
1536 } else {
1537 Box::pin(stream::once(async move {
1538 let _permit = permit;
1539 Err(InvalidConfigurationSnafu {
1540 reason: "controlled_partition_failure",
1541 }
1542 .build())
1543 })) as FileBatchStream
1544 })
1545 }
1546 .boxed()
1547 });
1548 let admission = Arc::new(|_: &i32| Ok(FileAdmissionDecision::Admit));
1549 let first = PartitionStream::new(
1550 vec![1],
1551 limiter.partition(0)?,
1552 options,
1553 admission.clone(),
1554 Arc::clone(&executor),
1555 metrics.clone(),
1556 cancellation.clone(),
1557 );
1558 let second = PartitionStream::new(
1559 vec![2],
1560 limiter.partition(1)?,
1561 options,
1562 admission.clone(),
1563 Arc::clone(&executor),
1564 metrics.clone(),
1565 cancellation.clone(),
1566 );
1567 let third = PartitionStream::new(
1568 vec![3],
1569 limiter.partition(2)?,
1570 options,
1571 admission,
1572 executor,
1573 metrics.clone(),
1574 cancellation.clone(),
1575 );
1576 let mut stream = direct_stream(VecDeque::from([first, second, third]), metrics.clone(), 2);
1577
1578 let error = timeout(Duration::from_secs(5), stream.next())
1579 .await?
1580 .ok_or("error item missing")?
1581 .expect_err("controlled partition must fail");
1582 assert_eq!(error.code(), "invalid_configuration");
1583 assert!(stream.next().await.is_none());
1584 assert!(cancellation.is_cancelled());
1585 assert_eq!(metrics.snapshot().file_tasks_started, 2);
1586 timeout(Duration::from_secs(5), async {
1587 while limiter.active_file_reads() != 0 {
1588 tokio::task::yield_now().await;
1589 }
1590 })
1591 .await?;
1592 Ok(())
1593 }
1594}