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 time::Duration,
32};
33
34use arrow::{
35 datatypes::{DataType, SchemaRef},
36 record_batch::RecordBatch,
37};
38use futures_util::Stream;
39use snafu::ResultExt;
40
41use self::{
42 backend::direct_parquet::{
43 NetworkWarmupPlan, ParquetRangeReadEstimator, ParquetRangeReadState,
44 },
45 planning::{
46 DeltaScanPartitionTargetOptions, DeltaScanPlan, build_physical_row_predicate, plan_scan,
47 },
48 predicate::{column_data_type, evaluate_predicate, referenced_columns, validate_predicate},
49 scheduling::{
50 DeltaScanScheduler, FileAdmissionDecision, FileAdmissionPolicy, FileBatchStream,
51 FileExecutor, OrderedPartitionStream,
52 },
53};
54
55use crate::{
56 DeltaProtocol, DeltaReaderError,
57 delta::{
58 kernel::{
59 DeltaKernelEngineContext, DeltaKernelPredicate, kernel_pruning_predicate,
60 kernel_row_predicate,
61 },
62 protocol::validate_protocol,
63 snapshot::{
64 ArrowTableSnapshot, KernelTableSnapshot, load_delta_table_snapshot,
65 load_kernel_table_snapshot,
66 },
67 },
68 error::{DataFileReadSnafu, InvalidConfigurationSnafu, ScanPlanningSnafu, SnapshotLoadSnafu},
69};
70
71const TRACING_TARGET: &str = "delta_arrow_reader";
72const DELTA_LOG_SCAN_METADATA_SOURCE: &str = "delta_log";
73const QUERY_PLANNING_CACHE_SCAN_METADATA_SOURCE: &str = "query_planning_cache";
74
75#[non_exhaustive]
80#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81pub enum WarmupMode {
82 #[default]
88 Automatic,
89 None,
91 QueryPlanning,
93 Network {
101 max_duration: Duration,
103 },
104}
105
106impl WarmupMode {
107 fn for_table(
108 self,
109 context: &DeltaKernelEngineContext,
110 options: DeltaScanExecutionOptions,
111 ) -> Self {
112 if self != Self::Automatic {
113 return self;
114 }
115 if NetworkWarmupPlan::supports_scan(options) && context.supports_partial_reads() {
116 Self::Network {
117 max_duration: Duration::from_secs(5),
118 }
119 } else {
120 Self::None
121 }
122 }
123}
124
125#[must_use = "table builder settings do nothing unless the builder is loaded"]
159pub struct DeltaTableBuilder {
160 table_location: String,
161 storage_options: DeltaStorageOptions,
162 snapshot_selection: DeltaSnapshotSelection,
163 execution_options: DeltaScanExecutionOptions,
164 warmup: WarmupMode,
165}
166
167impl DeltaTableBuilder {
168 pub fn new(table_location: impl Into<String>) -> Self {
170 Self {
171 table_location: table_location.into(),
172 storage_options: DeltaStorageOptions::new(),
173 snapshot_selection: DeltaSnapshotSelection::Latest,
174 execution_options: DeltaScanExecutionOptions::new(),
175 warmup: WarmupMode::Automatic,
176 }
177 }
178
179 pub fn with_storage_options(mut self, storage_options: DeltaStorageOptions) -> Self {
181 self.storage_options = storage_options;
182 self
183 }
184
185 pub const fn with_snapshot_selection(
187 mut self,
188 snapshot_selection: DeltaSnapshotSelection,
189 ) -> Self {
190 self.snapshot_selection = snapshot_selection;
191 self
192 }
193
194 pub const fn with_execution_options(
196 mut self,
197 execution_options: DeltaScanExecutionOptions,
198 ) -> Self {
199 self.execution_options = execution_options;
200 self
201 }
202
203 pub const fn with_warmup(mut self, warmup: WarmupMode) -> Self {
208 self.warmup = warmup;
209 self
210 }
211
212 pub async fn load_table(self) -> Result<DeltaTable, DeltaReaderError> {
234 let snapshot = load_delta_table_snapshot(
235 self.table_location,
236 self.storage_options,
237 self.snapshot_selection,
238 )
239 .await?;
240 let warmup = self
241 .warmup
242 .for_table(snapshot.engine_context(), self.execution_options);
243 let snapshot = if warmup == WarmupMode::None {
244 snapshot
245 } else {
246 validate_protocol(snapshot.protocol())?;
247 materialize_eager_scan_metadata(snapshot).await?
248 };
249 let table = DeltaTable::new(snapshot, self.execution_options);
250 if let WarmupMode::Network { max_duration } = warmup
251 && let Some(plan) = NetworkWarmupPlan::new(
252 table.snapshot.as_ref(),
253 self.execution_options,
254 max_duration,
255 )
256 {
257 match plan
258 .run("initialization", &table.range_read_estimator())
259 .await
260 {
261 Some(estimator) => table.range_read_state.set_estimator(estimator),
262 None => table.range_read_state.defer_network_warmup(plan).await,
263 }
264 }
265 Ok(table)
266 }
267
268 pub async fn load_snapshot(self) -> Result<DeltaTableSnapshot, DeltaReaderError> {
279 if !matches!(self.warmup, WarmupMode::Automatic | WarmupMode::None) {
280 return InvalidConfigurationSnafu {
281 reason: "snapshot_load_does_not_support_table_warmup",
282 }
283 .fail();
284 }
285 let snapshot = load_kernel_table_snapshot(
286 self.table_location,
287 self.storage_options,
288 self.snapshot_selection,
289 )
290 .await?;
291 Ok(DeltaTableSnapshot::new(snapshot, self.execution_options))
292 }
293}
294
295async fn materialize_eager_scan_metadata(
296 snapshot: ArrowTableSnapshot,
297) -> Result<ArrowTableSnapshot, DeltaReaderError> {
298 tokio::task::spawn_blocking(move || snapshot.materialize_eager_scan_metadata())
299 .await
300 .boxed()
301 .context(ScanPlanningSnafu {
302 reason: "query_planning_warmup_task_failed",
303 })?
304}
305
306impl fmt::Debug for DeltaTableBuilder {
307 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308 formatter
309 .debug_struct("DeltaTableBuilder")
310 .field("table_location", &"<redacted>")
311 .field("storage_options", &"<redacted>")
312 .field("snapshot_selection", &self.snapshot_selection)
313 .field("execution_options", &self.execution_options)
314 .field("warmup", &self.warmup)
315 .finish()
316 }
317}
318
319pub struct DeltaTableSnapshot {
321 snapshot: KernelTableSnapshot,
322 execution_options: DeltaScanExecutionOptions,
323}
324
325impl DeltaTableSnapshot {
326 fn new(snapshot: KernelTableSnapshot, execution_options: DeltaScanExecutionOptions) -> Self {
327 Self {
328 snapshot,
329 execution_options,
330 }
331 }
332
333 pub fn version(&self) -> u64 {
335 self.snapshot.version()
336 }
337
338 pub fn protocol(&self) -> &DeltaProtocol {
340 self.snapshot.protocol()
341 }
342
343 pub fn table_url(&self) -> &str {
347 self.snapshot.table_url()
348 }
349
350 pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
352 validate_protocol(self.protocol())
353 }
354
355 pub fn into_table(self) -> Result<DeltaTable, DeltaReaderError> {
357 Ok(DeltaTable::new(
358 self.snapshot.into_arrow_snapshot()?,
359 self.execution_options,
360 ))
361 }
362}
363
364impl fmt::Debug for DeltaTableSnapshot {
365 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
366 formatter
367 .debug_struct("DeltaTableSnapshot")
368 .field("version", &self.version())
369 .finish_non_exhaustive()
370 }
371}
372
373#[derive(Clone)]
375pub struct DeltaTable {
376 snapshot: Arc<ArrowTableSnapshot>,
377 execution_options: DeltaScanExecutionOptions,
378 range_read_state: Arc<ParquetRangeReadState>,
379}
380
381impl DeltaTable {
382 fn new(snapshot: ArrowTableSnapshot, execution_options: DeltaScanExecutionOptions) -> Self {
383 Self {
384 snapshot: Arc::new(snapshot),
385 execution_options,
386 range_read_state: Arc::default(),
387 }
388 }
389
390 pub(crate) fn range_read_estimator(&self) -> Arc<ParquetRangeReadEstimator> {
391 self.range_read_state.estimator()
392 }
393
394 pub(crate) async fn recover_network_warmup(&self, options: DeltaScanExecutionOptions) {
395 self.range_read_state.recover_network_warmup(options).await;
396 }
397
398 #[cfg(feature = "datafusion")]
399 pub(crate) fn network_warmup_pending(&self, options: DeltaScanExecutionOptions) -> bool {
400 self.range_read_state.network_warmup_pending(options)
401 }
402
403 pub fn version(&self) -> u64 {
405 self.snapshot.version()
406 }
407
408 pub fn schema(&self) -> SchemaRef {
410 self.snapshot.schema()
411 }
412
413 pub fn predicate_column_type(&self, column: &str) -> Result<DataType, DeltaReaderError> {
422 column_data_type(self.schema().as_ref(), column).cloned()
423 }
424
425 pub fn protocol(&self) -> &DeltaProtocol {
427 self.snapshot.protocol()
428 }
429
430 pub fn table_url(&self) -> &str {
434 self.snapshot.table_url()
435 }
436
437 #[allow(dead_code)]
438 pub(crate) fn partition_columns(&self) -> &[String] {
439 self.snapshot.partition_columns()
440 }
441
442 #[allow(dead_code)]
443 pub(crate) fn snapshot(&self) -> &ArrowTableSnapshot {
444 self.snapshot.as_ref()
445 }
446
447 pub fn validate_protocol(&self) -> Result<(), DeltaReaderError> {
449 validate_protocol(self.protocol())
450 }
451
452 pub async fn refresh(&self) -> Result<Self, DeltaReaderError> {
463 let snapshot = Arc::clone(&self.snapshot);
464 let range_read_estimator = self.range_read_estimator();
465 let snapshot = tokio::task::spawn_blocking(move || {
466 let _active_read = range_read_estimator.start_read();
468 snapshot.refresh()
469 })
470 .await
471 .boxed()
472 .context(SnapshotLoadSnafu {
473 reason: "snapshot_refresh_task_failed",
474 })
475 .and_then(|result| result)?;
476 Ok(Self {
477 snapshot: Arc::new(snapshot),
478 execution_options: self.execution_options,
479 range_read_state: Arc::clone(&self.range_read_state),
480 })
481 }
482
483 pub fn scan(&self) -> DeltaScanBuilder<'_> {
485 DeltaScanBuilder {
486 table: self,
487 projection: None,
488 predicate: None,
489 limit: None,
490 target_partitions: None,
491 execution_options: self.execution_options,
492 }
493 }
494}
495
496impl fmt::Debug for DeltaTable {
497 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
498 formatter
499 .debug_struct("DeltaTable")
500 .field("version", &self.version())
501 .finish_non_exhaustive()
502 }
503}
504
505#[must_use = "scan builder settings do nothing unless the scan is built"]
507pub struct DeltaScanBuilder<'table> {
508 table: &'table DeltaTable,
509 projection: Option<Vec<String>>,
510 predicate: Option<DeltaPredicate>,
511 limit: Option<usize>,
512 target_partitions: Option<usize>,
513 execution_options: DeltaScanExecutionOptions,
514}
515
516impl<'table> DeltaScanBuilder<'table> {
517 pub fn with_projection(
519 mut self,
520 logical_columns: impl IntoIterator<Item = impl Into<String>>,
521 ) -> Self {
522 self.projection = Some(logical_columns.into_iter().map(Into::into).collect());
523 self
524 }
525
526 pub fn with_predicate(mut self, predicate: DeltaPredicate) -> Self {
528 self.predicate = Some(predicate);
529 self
530 }
531
532 pub const fn with_limit(mut self, limit: usize) -> Self {
534 self.limit = Some(limit);
535 self
536 }
537
538 pub fn with_target_partitions(
540 mut self,
541 target_partitions: usize,
542 ) -> Result<Self, DeltaReaderError> {
543 if target_partitions == 0 {
544 return InvalidConfigurationSnafu {
545 reason: "scan_partition_target_must_be_positive",
546 }
547 .fail();
548 }
549 self.target_partitions = Some(target_partitions);
550 Ok(self)
551 }
552
553 pub const fn with_execution_options(
555 mut self,
556 execution_options: DeltaScanExecutionOptions,
557 ) -> Self {
558 self.execution_options = execution_options;
559 self
560 }
561
562 pub async fn build(self) -> Result<DeltaScan, DeltaReaderError> {
568 self.table.validate_protocol()?;
569 if let Some(predicate) = self.predicate.as_ref() {
570 validate_predicate(predicate, self.table.schema().as_ref())?;
571 }
572
573 let snapshot_version = self.table.version();
574 let backend = self.execution_options.parquet_backend();
575 let scan_metadata_source = if self.table.snapshot.eager_scan_metadata().is_some() {
576 QUERY_PLANNING_CACHE_SCAN_METADATA_SOURCE
577 } else {
578 DELTA_LOG_SCAN_METADATA_SOURCE
579 };
580 trace_planning_started(snapshot_version, backend, scan_metadata_source);
581 let snapshot = Arc::clone(&self.table.snapshot);
582 let projection = self.projection;
583 let predicate = self.predicate;
584 let hidden_columns = predicate
585 .as_ref()
586 .map(referenced_columns)
587 .unwrap_or_default();
588 let row_predicate = predicate
589 .as_ref()
590 .filter(|_| backend == ParquetReaderBackend::Direct)
591 .and_then(|predicate| kernel_row_predicate(predicate, snapshot.partition_columns()));
592 let kernel_predicate = predicate.as_ref().and_then(kernel_pruning_predicate);
593 let include_stats = kernel_predicate.is_some();
594 let execution_options = self.execution_options;
595 let target_partitions = self.target_partitions;
596 let result = tokio::task::spawn_blocking(move || {
597 let same_predicate = row_predicate.is_some() && row_predicate == kernel_predicate;
599 let plan = plan_scan(
600 snapshot.as_ref(),
601 projection.as_deref(),
602 &hidden_columns,
603 kernel_predicate,
604 include_stats,
605 execution_options,
606 DeltaScanPartitionTargetOptions {
607 explicit_target_partitions: target_partitions,
608 datafusion_target_partitions: None,
609 },
610 )?;
611 let physical_row_predicate = if plan.partitions.is_empty() {
612 None
613 } else if same_predicate {
614 plan.physical_predicate.clone()
615 } else {
616 build_physical_row_predicate(
617 snapshot.as_ref(),
618 projection.as_deref(),
619 &hidden_columns,
620 row_predicate,
621 )?
622 };
623 Ok((plan, physical_row_predicate))
624 })
625 .await
626 .boxed()
627 .context(ScanPlanningSnafu {
628 reason: "scan_planning_task_failed",
629 })
630 .and_then(|result| result);
631
632 match result {
633 Ok((plan, physical_row_predicate)) => {
634 if self.limit != Some(0)
635 && physical_row_predicate.is_some()
636 && !plan.partitions.is_empty()
637 {
638 self.table
639 .recover_network_warmup(self.execution_options)
640 .await;
641 }
642 trace_planning_completed(
643 snapshot_version,
644 backend,
645 plan.partitions.len(),
646 scan_metadata_source,
647 );
648 Ok(DeltaScan {
649 plan: Arc::new(plan),
650 predicate,
651 limit: self.limit,
652 physical_row_predicate,
653 range_read_estimator: self.table.range_read_estimator(),
654 })
655 }
656 Err(error) => {
657 trace_planning_failed(snapshot_version, backend, scan_metadata_source, &error);
658 Err(error)
659 }
660 }
661 }
662}
663
664#[must_use = "scans do nothing unless converted into a stream"]
685pub struct DeltaScan {
686 plan: Arc<DeltaScanPlan>,
687 predicate: Option<DeltaPredicate>,
688 limit: Option<usize>,
689 physical_row_predicate: Option<DeltaKernelPredicate>,
690 range_read_estimator: Arc<ParquetRangeReadEstimator>,
691}
692
693impl DeltaScan {
694 pub fn schema(&self) -> SchemaRef {
696 Arc::clone(&self.plan.projected_schema)
697 }
698
699 pub fn partition_count(&self) -> usize {
701 self.plan.partitions.len()
702 }
703
704 pub fn into_stream(self) -> DeltaBatchStream {
709 let metrics = self.plan.metrics.clone();
710 let schema = Arc::clone(&self.plan.projected_schema);
711 let partition_count = self.plan.partitions.len();
712 let snapshot_version = self.plan.snapshot_version;
713 let backend = self.plan.execution_options.parquet_backend();
714 let projection = (self.plan.logical_schema.as_ref() != schema.as_ref())
715 .then(|| (0..schema.fields().len()).collect::<Vec<_>>());
716 let partitions = if self.limit == Some(0) {
717 OrderedPartitionStream::default()
718 } else {
719 let scheduler = DeltaScanScheduler::new(Arc::clone(&self.plan));
720 let admission: FileAdmissionPolicy<_> = Arc::new(|_| Ok(FileAdmissionDecision::Admit));
721 let executor = match backend {
722 ParquetReaderBackend::Direct => {
723 backend::direct_parquet::direct_parquet_file_executor(
724 &self.plan,
725 None,
726 self.physical_row_predicate,
727 self.range_read_estimator,
728 None,
729 )
730 }
731 ParquetReaderBackend::DeltaKernel => {
732 delta_kernel_executor(&self.plan, self.range_read_estimator)
733 }
734 };
735 scheduler.partition_streams(admission, executor)
736 };
737
738 DeltaBatchStream {
739 schema,
740 metrics,
741 partitions,
742 predicate: self.predicate,
743 projection,
744 remaining: self.limit,
745 snapshot_version,
746 backend,
747 partition_count,
748 started: false,
749 done: false,
750 }
751 }
752}
753
754#[must_use = "streams do nothing unless polled"]
767pub struct DeltaBatchStream {
768 schema: SchemaRef,
769 metrics: DeltaScanMetrics,
770 partitions: OrderedPartitionStream,
771 predicate: Option<DeltaPredicate>,
772 projection: Option<Vec<usize>>,
773 remaining: Option<usize>,
774 snapshot_version: u64,
775 backend: ParquetReaderBackend,
776 partition_count: usize,
777 started: bool,
778 done: bool,
779}
780
781impl DeltaBatchStream {
782 pub async fn next_batch(&mut self) -> Result<Option<RecordBatch>, DeltaReaderError> {
788 futures_util::TryStreamExt::try_next(self).await
789 }
790
791 pub fn schema(&self) -> SchemaRef {
793 Arc::clone(&self.schema)
794 }
795
796 pub fn metrics(&self) -> DeltaScanMetrics {
798 self.metrics.clone()
799 }
800
801 fn start(&mut self) {
802 if self.started {
803 return;
804 }
805 self.started = true;
806 trace_execution_started(self.snapshot_version, self.backend, self.partition_count);
807 }
808
809 fn complete(&mut self) {
810 if self.done {
811 return;
812 }
813 self.partitions.clear();
814 self.done = true;
815 trace_execution_completed(self.snapshot_version, self.backend, self.partition_count);
816 }
817
818 fn fail(&mut self, error: &DeltaReaderError) {
819 self.partitions.clear();
820 self.done = true;
821 trace_execution_failed(
822 self.snapshot_version,
823 self.backend,
824 self.partition_count,
825 error,
826 );
827 }
828
829 fn finalize_batch(&self, mut batch: RecordBatch) -> Result<RecordBatch, DeltaReaderError> {
830 if let Some(predicate) = self.predicate.as_ref() {
831 batch = evaluate_predicate(&batch, predicate)?;
832 }
833 if let Some(projection) = self.projection.as_ref() {
834 batch = batch
835 .project(projection)
836 .boxed()
837 .context(DataFileReadSnafu {
838 reason: "direct_projection_failed",
839 })?;
840 }
841 Ok(batch)
842 }
843}
844
845impl Stream for DeltaBatchStream {
846 type Item = Result<RecordBatch, DeltaReaderError>;
847
848 fn poll_next(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
849 let this = self.get_mut();
850 if this.done {
851 return Poll::Ready(None);
852 }
853 this.start();
854
855 match Pin::new(&mut this.partitions).poll_next(context) {
856 Poll::Ready(Some(Ok(batch))) => {
857 let mut batch = match this.finalize_batch(batch) {
858 Ok(batch) => batch,
859 Err(error) => {
860 this.fail(&error);
861 return Poll::Ready(Some(Err(error)));
862 }
863 };
864 if let Some(remaining) = this.remaining.as_mut() {
865 if batch.num_rows() >= *remaining {
866 batch = batch.slice(0, *remaining);
867 *remaining = 0;
868 this.complete();
869 } else {
870 *remaining -= batch.num_rows();
871 }
872 }
873 Poll::Ready(Some(Ok(batch)))
874 }
875 Poll::Ready(Some(Err(error))) => {
876 this.fail(&error);
877 Poll::Ready(Some(Err(error)))
878 }
879 Poll::Ready(None) => {
880 this.complete();
881 Poll::Ready(None)
882 }
883 Poll::Pending => Poll::Pending,
884 }
885 }
886}
887
888impl Drop for DeltaBatchStream {
889 fn drop(&mut self) {
890 if self.done {
891 return;
892 }
893 self.partitions.clear();
894 self.done = true;
895 trace_execution_dropped(self.snapshot_version, self.backend, self.partition_count);
896 }
897}
898
899pub(crate) fn delta_kernel_executor(
900 plan: &Arc<DeltaScanPlan>,
901 range_read_estimator: Arc<ParquetRangeReadEstimator>,
902) -> FileExecutor<planning::DeltaScanFileTask, FileBatchStream> {
903 backend::kernel_reader::delta_kernel_file_executor(plan, range_read_estimator)
904}
905
906fn trace_planning_started(
907 snapshot_version: u64,
908 backend: ParquetReaderBackend,
909 scan_metadata_source: &'static str,
910) {
911 tracing::debug!(
912 target: TRACING_TARGET,
913 event = "scan_planning.started",
914 snapshot_version,
915 backend = ?backend,
916 partition_count = tracing::field::Empty,
917 scan_metadata_source,
918 outcome = "started"
919 );
920}
921
922fn trace_planning_completed(
923 snapshot_version: u64,
924 backend: ParquetReaderBackend,
925 partition_count: usize,
926 scan_metadata_source: &'static str,
927) {
928 tracing::debug!(
929 target: TRACING_TARGET,
930 event = "scan_planning.completed",
931 snapshot_version,
932 backend = ?backend,
933 partition_count,
934 scan_metadata_source,
935 outcome = "completed"
936 );
937}
938
939fn trace_planning_failed(
940 snapshot_version: u64,
941 backend: ParquetReaderBackend,
942 scan_metadata_source: &'static str,
943 error: &DeltaReaderError,
944) {
945 tracing::debug!(
946 target: TRACING_TARGET,
947 event = "scan_planning.failed",
948 snapshot_version,
949 backend = ?backend,
950 partition_count = tracing::field::Empty,
951 scan_metadata_source,
952 outcome = "failed",
953 error_code = error.code(),
954 error_phase = error.phase().as_str()
955 );
956}
957
958fn trace_execution_started(
959 snapshot_version: u64,
960 backend: ParquetReaderBackend,
961 partition_count: usize,
962) {
963 tracing::debug!(
964 target: TRACING_TARGET,
965 event = "scan_execution.started",
966 snapshot_version,
967 backend = ?backend,
968 partition_count,
969 outcome = "started"
970 );
971}
972
973fn trace_execution_completed(
974 snapshot_version: u64,
975 backend: ParquetReaderBackend,
976 partition_count: usize,
977) {
978 tracing::debug!(
979 target: TRACING_TARGET,
980 event = "scan_execution.completed",
981 snapshot_version,
982 backend = ?backend,
983 partition_count,
984 outcome = "completed"
985 );
986}
987
988fn trace_execution_failed(
989 snapshot_version: u64,
990 backend: ParquetReaderBackend,
991 partition_count: usize,
992 error: &DeltaReaderError,
993) {
994 tracing::debug!(
995 target: TRACING_TARGET,
996 event = "scan_execution.failed",
997 snapshot_version,
998 backend = ?backend,
999 partition_count,
1000 outcome = "failed",
1001 error_code = error.code(),
1002 error_phase = error.phase().as_str()
1003 );
1004}
1005
1006fn trace_execution_dropped(
1007 snapshot_version: u64,
1008 backend: ParquetReaderBackend,
1009 partition_count: usize,
1010) {
1011 tracing::debug!(
1012 target: TRACING_TARGET,
1013 event = "scan_execution.dropped",
1014 snapshot_version,
1015 backend = ?backend,
1016 partition_count,
1017 outcome = "dropped"
1018 );
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use std::{
1024 collections::{BTreeMap, VecDeque},
1025 fmt, fs,
1026 future::pending,
1027 path::{Path, PathBuf},
1028 sync::{Arc, Mutex, Once},
1029 time::{Duration, SystemTime, UNIX_EPOCH},
1030 };
1031
1032 use arrow::{
1033 array::Int32Array,
1034 datatypes::{DataType, Field, Schema, SchemaRef},
1035 record_batch::RecordBatch,
1036 };
1037 use futures_util::{FutureExt, StreamExt, stream};
1038 use tokio::{sync::Notify, time::timeout};
1039 use tracing::{
1040 Event, Level, Metadata, Subscriber,
1041 field::{Field as TracingField, Visit},
1042 span::{Attributes, Id, Record},
1043 subscriber::{Interest, with_default},
1044 };
1045
1046 use super::{
1047 DeltaBatchStream, DeltaTable, trace_execution_completed, trace_execution_dropped,
1048 trace_execution_failed, trace_execution_started, trace_planning_completed,
1049 trace_planning_failed, trace_planning_started,
1050 };
1051 use crate::{
1052 DeltaScanExecutionOptions, DeltaScanMetrics, DeltaSnapshotSelection, DeltaStorageOptions,
1053 ParquetReaderBackend,
1054 delta::snapshot::load_delta_table_snapshot_blocking,
1055 error::InvalidConfigurationSnafu,
1056 reader::{
1057 metrics::DeltaScanMetricsConfig,
1058 scheduling::{
1059 FileAdmissionDecision, FileAdmissionPolicy, FileBatchStream, FileExecutor,
1060 FileReadPermit, OrderedPartitionStream, PartitionStream, ScanCancellation,
1061 ScanReadLimiter,
1062 },
1063 },
1064 };
1065
1066 static TRACING_TEST_LOCK: Mutex<()> = Mutex::new(());
1067 static TRACING_TEST_GLOBAL_SUBSCRIBER: Once = Once::new();
1068
1069 #[tokio::test]
1070 async fn automatic_warmup_respects_storage_and_explicit_options()
1071 -> Result<(), Box<dyn std::error::Error>> {
1072 use super::{
1073 DeltaKernelEngineContext, DeltaStorageOptions, DeltaTableBuilder,
1074 ParquetRangeReadPolicy, WarmupMode,
1075 };
1076
1077 let options = DeltaScanExecutionOptions::default();
1078 let automatic = DeltaTableBuilder::new("s3://bucket/table").warmup;
1079 assert_eq!(automatic, WarmupMode::default());
1080 let network = WarmupMode::Network {
1081 max_duration: Duration::from_secs(5),
1082 };
1083 for (location, extra, supported) in [
1084 ("s3://bucket/table", None, true),
1085 ("s3a://bucket/table", None, true),
1086 ("s3://bucket/table", Some(("timeout", "17s")), false),
1087 ("https://example.com/table/", None, false),
1088 ] {
1089 let mut storage = DeltaStorageOptions::from([
1090 ("aws_region".into(), "us-west-2".into()),
1091 ("aws_skip_signature".into(), "true".into()),
1092 ]);
1093 if let Some((key, value)) = extra {
1094 storage.insert(key.into(), value.into());
1095 }
1096 let context = DeltaKernelEngineContext::try_new(url::Url::parse(location)?, &storage)?;
1097 assert_eq!(
1098 automatic.for_table(&context, options),
1099 if supported { network } else { WarmupMode::None },
1100 "{location}, {extra:?}",
1101 );
1102 for disabled in [
1103 options.with_experimental_intra_page_reads(false),
1104 options.with_parquet_backend(ParquetReaderBackend::DeltaKernel),
1105 options.with_parquet_range_read_policy(ParquetRangeReadPolicy::ExactRanges),
1106 ] {
1107 assert_eq!(automatic.for_table(&context, disabled), WarmupMode::None);
1108 }
1109 for explicit in [WarmupMode::None, WarmupMode::QueryPlanning, network] {
1110 assert_eq!(explicit.for_table(&context, options), explicit);
1111 assert_eq!(
1112 explicit.for_table(&context, options.with_experimental_intra_page_reads(false)),
1113 explicit
1114 );
1115 }
1116 }
1117 Ok(())
1118 }
1119
1120 #[derive(Clone, Default)]
1121 struct EventFields(Arc<Mutex<Vec<BTreeMap<String, String>>>>);
1122
1123 impl Subscriber for EventFields {
1124 fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
1125 if metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG {
1126 Interest::always()
1127 } else {
1128 Interest::sometimes()
1129 }
1130 }
1131
1132 fn enabled(&self, metadata: &Metadata<'_>) -> bool {
1133 metadata.target() == "delta_arrow_reader" && *metadata.level() == Level::DEBUG
1134 }
1135
1136 fn new_span(&self, _attributes: &Attributes<'_>) -> Id {
1137 Id::from_u64(1)
1138 }
1139
1140 fn record(&self, _span: &Id, _values: &Record<'_>) {}
1141
1142 fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
1143
1144 fn event(&self, event: &Event<'_>) {
1145 let metadata = event.metadata();
1146 assert_eq!(metadata.target(), "delta_arrow_reader");
1147 let mut fields = metadata
1148 .fields()
1149 .iter()
1150 .map(|field| (field.name().to_owned(), "<empty>".to_owned()))
1151 .collect();
1152 event.record(&mut FieldVisitor(&mut fields));
1153 self.0.lock().expect("event lock").push(fields);
1154 }
1155
1156 fn enter(&self, _span: &Id) {}
1157
1158 fn exit(&self, _span: &Id) {}
1159 }
1160
1161 struct FieldVisitor<'fields>(&'fields mut BTreeMap<String, String>);
1162
1163 impl Visit for FieldVisitor<'_> {
1164 fn record_debug(&mut self, field: &TracingField, value: &dyn fmt::Debug) {
1165 self.0.insert(field.name().to_owned(), format!("{value:?}"));
1166 }
1167
1168 fn record_str(&mut self, field: &TracingField, value: &str) {
1169 self.0.insert(field.name().to_owned(), value.to_owned());
1170 }
1171
1172 fn record_u64(&mut self, field: &TracingField, value: u64) {
1173 self.0.insert(field.name().to_owned(), value.to_string());
1174 }
1175
1176 fn record_i64(&mut self, field: &TracingField, value: i64) {
1177 self.0.insert(field.name().to_owned(), value.to_string());
1178 }
1179
1180 fn record_u128(&mut self, field: &TracingField, value: u128) {
1181 self.0.insert(field.name().to_owned(), value.to_string());
1182 }
1183 }
1184
1185 struct DeltaLogTable(PathBuf);
1186
1187 impl DeltaLogTable {
1188 fn new(name: &str) -> Result<Self, Box<dyn std::error::Error>> {
1189 let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
1190 let path = Path::new("target")
1191 .join("delta-arrow-reader-tracing-tests")
1192 .join(format!("{}-{name}-{nanos}", std::process::id()));
1193 fs::create_dir_all(path.join("_delta_log"))?;
1194 fs::write(
1195 path.join("_delta_log/00000000000000000000.json"),
1196 r#"{"protocol":{"minReaderVersion":1,"minWriterVersion":2}}
1197{"metaData":{"id":"tracing-test","format":{"provider":"parquet","options":{}},"schemaString":"{\"type\":\"struct\",\"fields\":[{\"name\":\"id\",\"type\":\"integer\",\"nullable\":true,\"metadata\":{}}]}","partitionColumns":[],"configuration":{},"createdTime":1587968585495}}
1198{"add":{"path":"secret-planning-object.parquet","partitionValues":{},"size":10,"modificationTime":1587968586000,"dataChange":true}}
1199"#,
1200 )?;
1201 Ok(Self(path))
1202 }
1203 }
1204
1205 impl Drop for DeltaLogTable {
1206 fn drop(&mut self) {
1207 let _ = fs::remove_dir_all(&self.0);
1208 }
1209 }
1210
1211 fn capture_events<T>(run: impl FnOnce() -> T) -> (T, Vec<BTreeMap<String, String>>) {
1212 let _lock = TRACING_TEST_LOCK
1213 .lock()
1214 .unwrap_or_else(std::sync::PoisonError::into_inner);
1215 let events = Arc::new(Mutex::new(Vec::new()));
1216 let subscriber = EventFields(Arc::clone(&events));
1217 TRACING_TEST_GLOBAL_SUBSCRIBER.call_once(|| {
1218 let _ = tracing::subscriber::set_global_default(EventFields::default());
1219 });
1220 let result = with_default(subscriber, || {
1221 tracing::callsite::rebuild_interest_cache();
1222 run()
1223 });
1224 tracing::callsite::rebuild_interest_cache();
1225 let captured = events
1226 .lock()
1227 .map(|events| events.clone())
1228 .unwrap_or_default();
1229 (result, captured)
1230 }
1231
1232 struct ControlledMerge {
1233 stream: DeltaBatchStream,
1234 limiter: Arc<ScanReadLimiter>,
1235 cancellation: ScanCancellation,
1236 metrics: DeltaScanMetrics,
1237 first_partition_gate: Arc<Notify>,
1238 }
1239
1240 fn schema() -> SchemaRef {
1241 Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]))
1242 }
1243
1244 fn batch(id: i32) -> RecordBatch {
1245 RecordBatch::try_new(schema(), vec![Arc::new(Int32Array::from(vec![id]))])
1246 .expect("valid test batch")
1247 }
1248
1249 fn batch_id(batch: &RecordBatch) -> i32 {
1250 batch
1251 .column(0)
1252 .as_any()
1253 .downcast_ref::<Int32Array>()
1254 .expect("Int32 id")
1255 .value(0)
1256 }
1257
1258 fn execution_options() -> Result<DeltaScanExecutionOptions, crate::DeltaReaderError> {
1259 DeltaScanExecutionOptions::new()
1260 .with_prefetch_files_per_partition(0)
1261 .with_max_concurrent_file_reads_per_partition(1)?
1262 .with_max_concurrent_file_reads_per_scan(Some(2))?
1263 .with_output_buffer_batches_per_partition(1)
1264 }
1265
1266 fn metrics() -> DeltaScanMetrics {
1267 DeltaScanMetrics::new(DeltaScanMetricsConfig {
1268 snapshot_version: 7,
1269 parquet_backend: ParquetReaderBackend::Direct,
1270 scan_partitions_planned: 2,
1271 files_planned: 2,
1272 add_actions_excluded_during_planning: Some(0),
1273 estimated_input_rows: Some(4),
1274 estimated_input_bytes: Some(4),
1275 })
1276 }
1277
1278 fn file_stream(permit: FileReadPermit, batches: Vec<RecordBatch>) -> FileBatchStream {
1279 Box::pin(stream::unfold(
1280 (VecDeque::from(batches), permit),
1281 |(mut batches, permit)| async move {
1282 batches
1283 .pop_front()
1284 .map(|batch| (Ok(batch), (batches, permit)))
1285 },
1286 ))
1287 }
1288
1289 fn gated_file_stream(
1290 permit: FileReadPermit,
1291 batches: Vec<RecordBatch>,
1292 gate: Arc<Notify>,
1293 ) -> FileBatchStream {
1294 Box::pin(stream::unfold(
1295 (false, VecDeque::from(batches), permit, gate),
1296 |(wait, mut batches, permit, gate)| async move {
1297 let batch = batches.pop_front()?;
1298 if wait {
1299 gate.notified().await;
1300 }
1301 Some((Ok(batch), (true, batches, permit, gate)))
1302 },
1303 ))
1304 }
1305
1306 fn direct_stream(
1307 partitions: VecDeque<PartitionStream>,
1308 metrics: DeltaScanMetrics,
1309 scan_capacity: usize,
1310 ) -> DeltaBatchStream {
1311 DeltaBatchStream {
1312 schema: schema(),
1313 metrics,
1314 partitions: OrderedPartitionStream::new(partitions, scan_capacity),
1315 predicate: None,
1316 projection: None,
1317 remaining: None,
1318 snapshot_version: 7,
1319 backend: ParquetReaderBackend::Direct,
1320 partition_count: 2,
1321 started: false,
1322 done: false,
1323 }
1324 }
1325
1326 fn controlled_merge() -> Result<ControlledMerge, Box<dyn std::error::Error>> {
1327 let options = execution_options()?;
1328 let limiter = ScanReadLimiter::new(options, 2, 2);
1329 let cancellation = ScanCancellation::new();
1330 let metrics = metrics();
1331 let first_partition_gate = Arc::new(Notify::new());
1332 let executor: FileExecutor<i32, FileBatchStream> = {
1333 let gate = Arc::clone(&first_partition_gate);
1334 Arc::new(move |task, permit, _| {
1335 let gate = Arc::clone(&gate);
1336 async move {
1337 let batches = vec![batch(task), batch(task * 2)];
1338 Ok(if task == 1 {
1339 gated_file_stream(permit, batches, gate)
1340 } else {
1341 file_stream(permit, batches)
1342 })
1343 }
1344 .boxed()
1345 })
1346 };
1347 let admission: FileAdmissionPolicy<i32> =
1348 Arc::new(|_: &i32| Ok(FileAdmissionDecision::Admit));
1349 let first = PartitionStream::new(
1350 vec![1],
1351 limiter.partition(0)?,
1352 options,
1353 admission.clone(),
1354 Arc::clone(&executor),
1355 metrics.clone(),
1356 cancellation.clone(),
1357 );
1358 let second = PartitionStream::new(
1359 vec![10],
1360 limiter.partition(1)?,
1361 options,
1362 admission,
1363 executor,
1364 metrics.clone(),
1365 cancellation.clone(),
1366 );
1367
1368 Ok(ControlledMerge {
1369 stream: direct_stream(VecDeque::from([first, second]), metrics.clone(), 2),
1370 limiter,
1371 cancellation,
1372 metrics,
1373 first_partition_gate,
1374 })
1375 }
1376
1377 async fn wait_for_batches(metrics: &DeltaScanMetrics, expected: u64) {
1378 timeout(Duration::from_secs(5), async {
1379 while metrics.snapshot().scheduler_batches_emitted < expected {
1380 tokio::task::yield_now().await;
1381 }
1382 })
1383 .await
1384 .expect("batch production reached expected bound");
1385 }
1386
1387 #[test]
1388 fn lifecycle_tracing_has_only_bounded_fields() {
1389 let error = InvalidConfigurationSnafu { reason: "test" }.build();
1390
1391 let (_, events) = capture_events(|| {
1392 trace_planning_started(7, ParquetReaderBackend::Direct, "query_planning_cache");
1393 trace_planning_completed(7, ParquetReaderBackend::Direct, 2, "query_planning_cache");
1394 trace_planning_failed(
1395 7,
1396 ParquetReaderBackend::Direct,
1397 "query_planning_cache",
1398 &error,
1399 );
1400 trace_execution_started(7, ParquetReaderBackend::Direct, 2);
1401 trace_execution_completed(7, ParquetReaderBackend::Direct, 2);
1402 trace_execution_failed(7, ParquetReaderBackend::Direct, 2, &error);
1403 trace_execution_dropped(7, ParquetReaderBackend::Direct, 2);
1404 });
1405
1406 assert_eq!(events.len(), 7);
1407 let allowed = [
1408 "backend",
1409 "error_phase",
1410 "error_code",
1411 "event",
1412 "outcome",
1413 "partition_count",
1414 "scan_metadata_source",
1415 "snapshot_version",
1416 ];
1417 for fields in events.iter() {
1418 assert!(fields.keys().all(|field| allowed.contains(&field.as_str())));
1419 assert!(fields.contains_key("event"));
1420 assert!(fields.contains_key("snapshot_version"));
1421 assert!(fields.contains_key("backend"));
1422 assert!(fields.contains_key("partition_count"));
1423 assert!(fields.contains_key("outcome"));
1424 if fields
1425 .get("event")
1426 .is_some_and(|event| event.starts_with("scan_planning."))
1427 {
1428 assert_eq!(
1429 fields.get("scan_metadata_source").map(String::as_str),
1430 Some("query_planning_cache")
1431 );
1432 }
1433 }
1434 }
1435
1436 #[test]
1437 fn planning_tracing_reports_the_table_metadata_source_on_success_and_failure()
1438 -> Result<(), Box<dyn std::error::Error>> {
1439 const OBJECT_KEY: &str = "secret-planning-object.parquet";
1440 const STORAGE_VALUE: &str = "secret-planning-storage-value";
1441 let fixture = DeltaLogTable::new("planning-source")?;
1442 let mut storage_options = DeltaStorageOptions::new();
1443 storage_options.insert("secret-option".to_owned(), STORAGE_VALUE.to_owned());
1444 let snapshot = load_delta_table_snapshot_blocking(
1445 &fixture.0.to_string_lossy(),
1446 &storage_options,
1447 DeltaSnapshotSelection::Latest,
1448 )?;
1449 let eager_snapshot = snapshot.clone().materialize_eager_scan_metadata()?;
1450 let lazy = DeltaTable::new(snapshot, DeltaScanExecutionOptions::new());
1451 let eager = DeltaTable::new(eager_snapshot, DeltaScanExecutionOptions::new());
1452 let runtime = tokio::runtime::Builder::new_current_thread()
1453 .enable_all()
1454 .build()?;
1455
1456 let (eager_result, eager_events) =
1457 capture_events(|| runtime.block_on(eager.scan().build()));
1458 let _ = eager_result?;
1459 assert_eq!(eager_events.len(), 2);
1460 assert_eq!(
1461 eager_events[0].get("event").map(String::as_str),
1462 Some("scan_planning.started")
1463 );
1464 assert_eq!(
1465 eager_events[1].get("event").map(String::as_str),
1466 Some("scan_planning.completed")
1467 );
1468 assert!(eager_events.iter().all(|event| {
1469 event.get("scan_metadata_source").map(String::as_str) == Some("query_planning_cache")
1470 }));
1471
1472 let (failed_result, failed_events) =
1473 capture_events(|| runtime.block_on(eager.scan().with_projection(["missing"]).build()));
1474 assert!(failed_result.is_err());
1475 assert_eq!(failed_events.len(), 2);
1476 assert_eq!(
1477 failed_events[0].get("event").map(String::as_str),
1478 Some("scan_planning.started")
1479 );
1480 assert_eq!(
1481 failed_events[1].get("event").map(String::as_str),
1482 Some("scan_planning.failed")
1483 );
1484 assert!(failed_events.iter().all(|event| {
1485 event.get("scan_metadata_source").map(String::as_str) == Some("query_planning_cache")
1486 }));
1487
1488 let (lazy_result, lazy_events) = capture_events(|| runtime.block_on(lazy.scan().build()));
1489 let _ = lazy_result?;
1490 assert_eq!(lazy_events.len(), 2);
1491 assert!(lazy_events.iter().all(|event| {
1492 event.get("scan_metadata_source").map(String::as_str) == Some("delta_log")
1493 }));
1494
1495 let captured = format!("{eager_events:?}{failed_events:?}{lazy_events:?}");
1496 assert!(!captured.contains(&fixture.0.to_string_lossy().into_owned()));
1497 assert!(!captured.contains(OBJECT_KEY));
1498 assert!(!captured.contains(STORAGE_VALUE));
1499 Ok(())
1500 }
1501
1502 #[tokio::test]
1503 async fn merged_stream_stops_without_admitting_waiting_partitions()
1504 -> Result<(), Box<dyn std::error::Error>> {
1505 for stop in ["limit", "drop", "error"] {
1506 let options = execution_options()?.with_max_concurrent_file_reads_per_scan(Some(1))?;
1507 let limiter = ScanReadLimiter::new(options, 3, 3);
1508 let cancellation = ScanCancellation::new();
1509 let metrics = metrics();
1510 let executor: FileExecutor<i32, FileBatchStream> = Arc::new(move |task, permit, _| {
1511 async move {
1512 Ok(if stop == "error" {
1513 Box::pin(stream::once(async move {
1514 let _permit = permit;
1515 Err(InvalidConfigurationSnafu {
1516 reason: "controlled_partition_failure",
1517 }
1518 .build())
1519 })) as FileBatchStream
1520 } else {
1521 gated_file_stream(permit, vec![batch(task); 3], Arc::new(Notify::new()))
1522 })
1523 }
1524 .boxed()
1525 });
1526 let partitions = (0..3)
1527 .map(|index| {
1528 Ok(PartitionStream::new(
1529 vec![index as i32; 2],
1530 limiter.partition(index)?,
1531 options,
1532 Arc::new(|_| Ok(FileAdmissionDecision::Admit)),
1533 Arc::clone(&executor),
1534 metrics.clone(),
1535 cancellation.clone(),
1536 ))
1537 })
1538 .collect::<Result<VecDeque<_>, crate::DeltaReaderError>>()?;
1539 let mut stream = direct_stream(partitions, metrics.clone(), 1);
1540 if stop == "limit" {
1541 stream.remaining = Some(1);
1542 }
1543 let first = timeout(Duration::from_secs(5), stream.next())
1544 .await?
1545 .ok_or("missing result")?;
1546 if stop == "error" {
1547 assert_eq!(
1548 first.expect_err("controlled failure").code(),
1549 "invalid_configuration"
1550 );
1551 } else {
1552 assert_eq!(batch_id(&first?), 0);
1553 }
1554 if stop != "drop" {
1555 assert!(stream.next().await.is_none());
1556 }
1557 drop(stream);
1558 timeout(Duration::from_secs(5), async {
1559 while limiter.active_file_reads() != 0 {
1560 tokio::task::yield_now().await;
1561 }
1562 })
1563 .await?;
1564 assert!(cancellation.is_cancelled());
1565 assert_eq!(metrics.snapshot().file_tasks_started, 1);
1566 assert_eq!(metrics.snapshot().scan_partitions_started, 1);
1567 }
1568 Ok(())
1569 }
1570
1571 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1572 async fn merged_stream_progresses_with_small_scan_read_limits()
1573 -> Result<(), Box<dyn std::error::Error>> {
1574 for backend in [
1575 ParquetReaderBackend::Direct,
1576 ParquetReaderBackend::DeltaKernel,
1577 ] {
1578 for cap in [1, 2, 4, 8] {
1579 for prefetch in [0, 2, usize::MAX] {
1580 let options = DeltaScanExecutionOptions::new()
1581 .with_parquet_backend(backend)
1582 .with_max_concurrent_file_reads_per_scan(Some(cap))?
1583 .with_prefetch_files_per_partition(prefetch);
1584 let limiter = ScanReadLimiter::new(options, 5, 5);
1585 let cancellation = ScanCancellation::new();
1586 let metrics = metrics();
1587 let executor: FileExecutor<i32, FileBatchStream> =
1588 Arc::new(|task, permit, _| {
1589 async move {
1590 Ok(file_stream(permit, vec![batch(task); 3]))
1593 }
1594 .boxed()
1595 });
1596 let partitions = (0..5)
1597 .map(|partition| {
1598 Ok(PartitionStream::new(
1599 (partition * 4..partition * 4 + 4)
1600 .map(|id| id as i32)
1601 .collect(),
1602 limiter.partition(partition)?,
1603 options,
1604 Arc::new(|_| Ok(FileAdmissionDecision::Admit)),
1605 Arc::clone(&executor),
1606 metrics.clone(),
1607 cancellation.clone(),
1608 ))
1609 })
1610 .collect::<Result<VecDeque<_>, crate::DeltaReaderError>>()?;
1611 let mut stream = direct_stream(partitions, metrics.clone(), cap);
1612 let ids = timeout(Duration::from_secs(2), async {
1613 let mut ids = Vec::new();
1614 while let Some(batch) = stream.next().await {
1615 ids.push(batch_id(&batch?));
1616 assert!(limiter.active_file_reads() <= cap);
1617 tokio::task::yield_now().await;
1618 }
1619 Ok::<_, crate::DeltaReaderError>(ids)
1620 })
1621 .await
1622 .unwrap_or_else(|_| {
1623 panic!("stalled: {backend:?}, cap={cap}, prefetch={prefetch}")
1624 })?;
1625 assert_eq!(ids, (0..20).flat_map(|id| [id; 3]).collect::<Vec<_>>());
1626 assert_eq!(limiter.active_file_reads(), 0);
1627 assert_eq!(metrics.snapshot().file_tasks_completed, 20);
1628 assert_eq!(metrics.snapshot().scan_partitions_completed, 5);
1629 }
1630 }
1631 }
1632 Ok(())
1633 }
1634
1635 #[tokio::test]
1636 async fn merged_stream_is_ordered_and_bounds_later_partition_queues()
1637 -> Result<(), Box<dyn std::error::Error>> {
1638 let ControlledMerge {
1639 mut stream,
1640 limiter,
1641 metrics,
1642 first_partition_gate,
1643 ..
1644 } = controlled_merge()?;
1645
1646 let first = stream.next().await.ok_or("first batch missing")??;
1647 assert_eq!(batch_id(&first), 1);
1648 wait_for_batches(&metrics, 2).await;
1649 for _ in 0..32 {
1650 tokio::task::yield_now().await;
1651 }
1652 assert_eq!(metrics.snapshot().scheduler_batches_emitted, 2);
1653 assert_eq!(limiter.active_file_reads(), 2);
1654
1655 assert!(stream.next_batch().now_or_never().is_none());
1657 first_partition_gate.notify_one();
1658 let mut ids = vec![batch_id(
1659 &stream.next_batch().await?.ok_or("second batch missing")?,
1660 )];
1661 while let Some(batch) = stream.next_batch().await? {
1662 ids.push(batch_id(&batch));
1663 }
1664 assert_eq!(ids, [2, 10, 20]);
1665 assert_eq!(metrics.snapshot().scheduler_batches_emitted, 4);
1666 assert_eq!(metrics.snapshot().scan_partitions_completed, 2);
1667 assert_eq!(limiter.active_file_reads(), 0);
1668 Ok(())
1669 }
1670
1671 #[tokio::test]
1672 async fn merged_stream_drop_cancels_blocked_partitions_and_releases_permits()
1673 -> Result<(), Box<dyn std::error::Error>> {
1674 let ControlledMerge {
1675 mut stream,
1676 limiter,
1677 cancellation,
1678 metrics,
1679 ..
1680 } = controlled_merge()?;
1681
1682 let first = stream.next().await.ok_or("first batch missing")??;
1683 assert_eq!(batch_id(&first), 1);
1684 wait_for_batches(&metrics, 2).await;
1685 assert_eq!(limiter.active_file_reads(), 2);
1686 drop(stream);
1687
1688 assert!(cancellation.is_cancelled());
1689 timeout(Duration::from_secs(5), async {
1690 while limiter.active_file_reads() != 0 {
1691 tokio::task::yield_now().await;
1692 }
1693 })
1694 .await?;
1695 assert_eq!(metrics.snapshot().scheduler_batches_emitted, 2);
1696 assert_eq!(metrics.snapshot().scan_partitions_completed, 0);
1697 Ok(())
1698 }
1699
1700 #[tokio::test]
1701 async fn merged_stream_forwards_one_concurrent_error_and_releases_permits()
1702 -> Result<(), Box<dyn std::error::Error>> {
1703 let options = execution_options()?;
1704 let limiter = ScanReadLimiter::new(options, 3, 3);
1705 let cancellation = ScanCancellation::new();
1706 let metrics = metrics();
1707 let executor: FileExecutor<i32, FileBatchStream> = Arc::new(|task, permit, _| {
1708 async move {
1709 Ok(if task == 1 {
1710 Box::pin(stream::once(async move {
1711 let _permit = permit;
1712 pending::<Result<RecordBatch, crate::DeltaReaderError>>().await
1713 })) as FileBatchStream
1714 } else {
1715 Box::pin(stream::once(async move {
1716 let _permit = permit;
1717 Err(InvalidConfigurationSnafu {
1718 reason: "controlled_partition_failure",
1719 }
1720 .build())
1721 })) as FileBatchStream
1722 })
1723 }
1724 .boxed()
1725 });
1726 let admission = Arc::new(|_: &i32| Ok(FileAdmissionDecision::Admit));
1727 let first = PartitionStream::new(
1728 vec![1],
1729 limiter.partition(0)?,
1730 options,
1731 admission.clone(),
1732 Arc::clone(&executor),
1733 metrics.clone(),
1734 cancellation.clone(),
1735 );
1736 let second = PartitionStream::new(
1737 vec![2],
1738 limiter.partition(1)?,
1739 options,
1740 admission.clone(),
1741 Arc::clone(&executor),
1742 metrics.clone(),
1743 cancellation.clone(),
1744 );
1745 let third = PartitionStream::new(
1746 vec![3],
1747 limiter.partition(2)?,
1748 options,
1749 admission,
1750 executor,
1751 metrics.clone(),
1752 cancellation.clone(),
1753 );
1754 let mut stream = direct_stream(VecDeque::from([first, second, third]), metrics.clone(), 2);
1755
1756 let error = timeout(Duration::from_secs(5), stream.next())
1757 .await?
1758 .ok_or("error item missing")?
1759 .expect_err("controlled partition must fail");
1760 assert_eq!(error.code(), "invalid_configuration");
1761 assert!(stream.next().await.is_none());
1762 assert!(cancellation.is_cancelled());
1763 assert_eq!(metrics.snapshot().file_tasks_started, 2);
1764 timeout(Duration::from_secs(5), async {
1765 while limiter.active_file_reads() != 0 {
1766 tokio::task::yield_now().await;
1767 }
1768 })
1769 .await?;
1770 Ok(())
1771 }
1772}