1use std::cmp::Reverse;
18use std::collections::HashSet;
19use std::sync::Arc;
20use std::time::Instant;
21
22use async_stream::try_stream;
23use datafusion::execution::memory_pool::{MemoryConsumer, MemoryPool};
24use datafusion::physical_expr::{LexOrdering, PhysicalSortExpr};
25use datafusion::physical_plan::expressions::Column;
26use datafusion::physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricBuilder};
27use datafusion::physical_plan::sorts::streaming_merge::StreamingMergeBuilder;
28use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
29use datafusion_common::DataFusionError;
30use datatypes::arrow::array::{Array, BinaryArray, BinaryBuilder};
31use datatypes::arrow::compute::SortOptions;
32use datatypes::arrow::datatypes::{DataType, Field, Schema, SchemaRef};
33use datatypes::arrow::record_batch::RecordBatch;
34use datatypes::prelude::ConcreteDataType;
35use futures::{StreamExt, TryStreamExt};
36use mito_codec::row_converter::{PrimaryKeyFilter, SparsePrimaryKeyCodec};
37use snafu::{OptionExt, ResultExt, ensure};
38use store_api::codec::PrimaryKeyEncoding;
39use store_api::region_engine::PartitionRange;
40use store_api::storage::consts::{PRIMARY_KEY_COLUMN_NAME, ReservedColumnId};
41use tokio::sync::Semaphore;
42
43use crate::error::{
44 InvalidRequestSnafu, JoinSnafu, MergeCandidateSeriesSnafu, NewRecordBatchSnafu, Result,
45 UnexpectedSnafu,
46};
47use crate::read::BoxedRecordBatchStream;
48use crate::read::pruner::{PartitionPruner, Pruner};
49use crate::read::range::RowGroupIndex;
50use crate::read::range_cache::{CachedRangeReader, build_candidate_range_cache_key};
51use crate::read::scan_region::StreamContext;
52use crate::read::scan_util::{PartitionMetrics, new_filter_metrics, scan_flat_mem_ranges};
53use crate::series_index::{
54 METRIC_SERIES_ID_BATCH_SIZE, MetricSeriesId, MetricSeriesIdStream, SeriesIndexFileHandle,
55 SeriesIndexReadContext, SeriesIndexSearcher,
56};
57use crate::sst::parquet::DEFAULT_READ_BATCH_SIZE;
58use crate::sst::parquet::format::PrimaryKeyArray;
59use crate::sst::parquet::prefilter::{
60 CachedPrimaryKeyFilter, build_primary_key_filter, prefilter_flat_batch_by_primary_key,
61};
62use crate::sst::parquet::reader::ReaderMetrics;
63use crate::sst::parquet::row_group::ParquetFetchMetrics;
64
65pub(crate) struct SeriesCandidateScanner {
67 stream_ctx: Arc<StreamContext>,
68 partitions: Vec<Vec<PartitionRange>>,
69 partition_pruner: Arc<PartitionPruner>,
70 candidate_pruner: Arc<PartitionPruner>,
71 coverage: Arc<SeriesIndexCoverage>,
72 range_semaphore: Arc<Semaphore>,
73 memory_pool: Arc<dyn MemoryPool>,
74 metrics_set: ExecutionPlanMetricsSet,
75 part_metrics: PartitionMetrics,
76}
77
78impl SeriesCandidateScanner {
79 pub(crate) fn try_new(
88 stream_ctx: Arc<StreamContext>,
89 partitions: Vec<Vec<PartitionRange>>,
90 pruner: Arc<Pruner>,
91 range_semaphore: Arc<Semaphore>,
92 memory_pool: Arc<dyn MemoryPool>,
93 metrics_set: ExecutionPlanMetricsSet,
94 part_metrics: PartitionMetrics,
95 ) -> Result<Self> {
96 validate_metric_metadata(&stream_ctx)?;
97 #[cfg(feature = "enterprise")]
98 ensure!(
99 stream_ctx.input.extension_ranges().is_empty(),
100 InvalidRequestSnafu {
101 region_id: stream_ctx.input.region_metadata().region_id,
102 reason: "candidate-series scan does not support extension ranges; use the legacy series-scan path",
103 }
104 );
105 ensure!(
106 !pruner.predicate_prefilter_enabled(),
107 UnexpectedSnafu {
108 reason: format!(
109 "candidate-series scan for region {} requires a pruner without predicate prefiltering",
110 stream_ctx.input.region_metadata().region_id
111 ),
112 }
113 );
114 let all_ranges = partitions.iter().flatten().copied().collect::<Vec<_>>();
115 pruner.add_partition_ranges(&all_ranges);
116 let coverage = Arc::new(SeriesIndexCoverage::new(&stream_ctx, &all_ranges));
117 let partition_pruner = Arc::new(PartitionPruner::new(pruner.clone(), &all_ranges));
118 let candidate_pruner = if coverage.covered_files.is_empty() {
119 partition_pruner.clone()
120 } else {
121 Arc::new(
122 PartitionPruner::new(pruner, &all_ranges).excluding_files(&coverage.covered_files),
123 )
124 };
125 Ok(Self {
126 stream_ctx,
127 partitions,
128 partition_pruner,
129 candidate_pruner,
130 coverage,
131 range_semaphore,
132 memory_pool,
133 metrics_set,
134 part_metrics,
135 })
136 }
137
138 pub(crate) async fn build_stream(&self) -> Result<MetricSeriesIdStream> {
140 let all_ranges = self
141 .partitions
142 .iter()
143 .flatten()
144 .copied()
145 .collect::<Vec<_>>();
146 let range_builder = SeriesCandidateRangeBuilder {
147 stream_ctx: self.stream_ctx.clone(),
148 partition_pruner: self.candidate_pruner.clone(),
149 coverage: self.coverage.clone(),
150 range_semaphore: self.range_semaphore.clone(),
151 memory_pool: self.memory_pool.clone(),
152 metrics_set: self.metrics_set.clone(),
153 part_metrics: self.part_metrics.clone(),
154 };
155 let mut tasks = Vec::with_capacity(all_ranges.len());
156 for (range_idx, part_range) in all_ranges.into_iter().enumerate() {
157 let range_builder = range_builder.clone();
158 tasks.push(common_runtime::spawn_query(async move {
159 range_builder
160 .build_range_stream(part_range, range_idx)
161 .await
162 }));
163 }
164
165 let mut range_streams = Vec::with_capacity(tasks.len());
166 for task in tasks {
167 range_streams.push(task.await.context(JoinSnafu)??);
168 }
169
170 if let Some(context) = &self.stream_ctx.input.series_index {
171 MetricBuilder::new(&self.metrics_set)
172 .counter("candidate_index_files", self.partitions.len())
173 .add(self.coverage.indexes.len());
174 MetricBuilder::new(&self.metrics_set)
175 .counter("candidate_index_covered_ssts", self.partitions.len())
176 .add(self.coverage.covered_files.len());
177 for index in &self.coverage.indexes {
178 range_streams.push(index_primary_key_stream(
179 self.stream_ctx.clone(),
180 context.clone(),
181 index.clone(),
182 self.range_semaphore.clone(),
183 ));
184 }
185 }
186
187 let merged = merge_primary_key_streams(
190 range_streams,
191 self.memory_pool.clone(),
192 &self.metrics_set,
193 self.partitions.len(),
194 "SeriesCandidateScanner::final_merge",
195 )?;
196 decode_metric_series(merged, self.stream_ctx.input.region_metadata().clone())
197 }
198
199 pub(crate) fn partition_pruner(&self) -> Arc<PartitionPruner> {
201 self.partition_pruner.clone()
202 }
203}
204
205#[derive(Default)]
207struct SeriesIndexCoverage {
208 indexes: Vec<SeriesIndexFileHandle>,
209 covered_files: HashSet<usize>,
211}
212
213impl SeriesIndexCoverage {
214 fn new(stream_ctx: &StreamContext, ranges: &[PartitionRange]) -> Self {
215 let Some(context) = &stream_ctx.input.series_index else {
216 return Self::default();
217 };
218 let mut uncovered: HashSet<_> = ranges
219 .iter()
220 .flat_map(|range| {
221 stream_ctx.ranges[range.identifier]
222 .row_group_indices
223 .iter()
224 .filter(|index| stream_ctx.is_file_range_index(**index))
225 .map(|index| index.index - stream_ctx.input.num_memtables())
226 })
227 .collect();
228 let region_id = stream_ctx.input.region_metadata().region_id;
229 let mut candidates: Vec<_> = context
230 .version
231 .series_indexes
232 .values()
233 .map(|index| {
234 let files: HashSet<_> = uncovered
235 .iter()
236 .copied()
237 .filter(|file_index| {
238 index
239 .entry()
240 .covers_file(stream_ctx.input.files[*file_index].meta_ref(), region_id)
241 })
242 .collect();
243 (index, files)
244 })
245 .collect();
246 let mut coverage = Self::default();
247 while let Some((position, count)) = candidates
248 .iter()
249 .enumerate()
250 .map(|(position, (index, files))| {
251 (
252 position,
253 files.intersection(&uncovered).count(),
254 index.entry(),
255 )
256 })
257 .max_by_key(|(_, count, entry)| {
258 (
259 *count,
260 entry.max_file_sequence,
261 Reverse(entry.index_uuid.as_bytes()),
262 )
263 })
264 .map(|(position, count, _)| (position, count))
265 {
266 if count == 0 {
267 break;
268 }
269 let (index, files) = candidates.swap_remove(position);
270 for file in files {
271 if uncovered.remove(&file) {
272 coverage.covered_files.insert(file);
273 }
274 }
275 coverage.indexes.push(index.clone());
276 }
277 coverage
278 }
279
280 fn covers_source(&self, stream_ctx: &StreamContext, index: RowGroupIndex) -> bool {
281 stream_ctx.is_file_range_index(index)
282 && self
283 .covered_files
284 .contains(&(index.index - stream_ctx.input.num_memtables()))
285 }
286}
287
288fn index_primary_key_stream(
290 stream_ctx: Arc<StreamContext>,
291 context: SeriesIndexReadContext,
292 index: SeriesIndexFileHandle,
293 semaphore: Arc<Semaphore>,
294) -> BoxedRecordBatchStream {
295 Box::pin(try_stream! {
296 let metadata = stream_ctx.input.region_metadata();
297 let codec = SparsePrimaryKeyCodec::new(metadata);
298 let mut series = {
299 let _permit = semaphore.acquire().await.map_err(|error| UnexpectedSnafu {
300 reason: format!("failed to acquire candidate index permit: {error}"),
301 }.build())?;
302 SeriesIndexSearcher::try_new(
303 metadata.clone(),
304 context.store.clone(),
305 index,
306 stream_ctx.input.predicate_group().predicate(),
307 stream_ctx.input.time_range,
308 ).await?.search()?
309 };
310 loop {
311 let batch = {
312 let _permit = semaphore.acquire().await.map_err(|error| UnexpectedSnafu {
313 reason: format!("failed to acquire candidate index permit: {error}"),
314 }.build())?;
315 series.try_next().await?
316 };
317 let Some(batch) = batch else { break };
318 let mut builder = BinaryBuilder::new();
319 let mut key = Vec::new();
320 for series in batch {
321 key.clear();
322 codec.encode_internal(series.table_id, series.tsid, &mut key)
323 .context(crate::error::EncodeSnafu)?;
324 builder.append_value(&key);
325 }
326 yield RecordBatch::try_new(primary_key_schema(), vec![Arc::new(builder.finish())])
327 .context(NewRecordBatchSnafu)?;
328 }
329 })
330}
331
332#[derive(Clone)]
333struct SeriesCandidateRangeBuilder {
334 stream_ctx: Arc<StreamContext>,
335 coverage: Arc<SeriesIndexCoverage>,
336 partition_pruner: Arc<PartitionPruner>,
337 range_semaphore: Arc<Semaphore>,
338 memory_pool: Arc<dyn MemoryPool>,
339 metrics_set: ExecutionPlanMetricsSet,
340 part_metrics: PartitionMetrics,
341}
342
343impl SeriesCandidateRangeBuilder {
344 async fn build_range_stream(
345 &self,
346 part_range: PartitionRange,
347 merge_partition: usize,
348 ) -> Result<BoxedRecordBatchStream> {
349 let range_meta = &self.stream_ctx.ranges[part_range.identifier];
350 let replaced_sources = range_meta
353 .row_group_indices
354 .iter()
355 .any(|index| self.coverage.covers_source(&self.stream_ctx, *index));
356 let cache_key = if replaced_sources {
357 None
358 } else {
359 build_candidate_range_cache_key(&self.stream_ctx, &part_range, &self.partition_pruner)
360 };
361 let builder = self.clone();
362 let (stream, _) = CachedRangeReader {
363 stream_ctx: &self.stream_ctx,
364 part_range: &part_range,
365 key: cache_key,
366 metrics: &self.part_metrics,
367 }
368 .build_with_memory_pool(&self.memory_pool, move || async move {
369 let stream = builder
370 .build_uncached_range_stream(part_range, merge_partition)
371 .await?;
372 Ok((stream, DEFAULT_READ_BATCH_SIZE))
373 })
374 .await?;
375 Ok(stream)
376 }
377
378 async fn build_uncached_range_stream(
379 &self,
380 part_range: PartitionRange,
381 merge_partition: usize,
382 ) -> Result<BoxedRecordBatchStream> {
383 let _permit = self.range_semaphore.acquire().await.map_err(|error| {
386 UnexpectedSnafu {
387 reason: format!("failed to acquire candidate range permit: {error}"),
388 }
389 .build()
390 })?;
391 let range_meta = &self.stream_ctx.ranges[part_range.identifier];
392 let mut sources = Vec::with_capacity(range_meta.row_group_indices.len());
393 for index in &range_meta.row_group_indices {
394 let source = self.build_source(*index, range_meta.time_range).await?;
395 if let Some(source) = source {
396 sources.push(source);
397 }
398 }
399
400 let sources = self.stream_ctx.input.create_parallel_flat_sources(
401 sources,
402 self.range_semaphore.clone(),
403 2,
404 )?;
405 let stream = merge_primary_key_streams(
406 sources,
407 self.memory_pool.clone(),
408 &self.metrics_set,
409 merge_partition,
410 "SeriesCandidateScanner::range_merge",
411 )?;
412
413 Ok(stream)
414 }
415
416 async fn build_source(
417 &self,
418 index: RowGroupIndex,
419 time_range: crate::sst::file::FileTimeRange,
420 ) -> Result<Option<BoxedRecordBatchStream>> {
421 let metadata = self.stream_ctx.input.region_metadata().clone();
422 if self.stream_ctx.is_mem_range_index(index) {
423 let raw = scan_flat_mem_ranges(
424 self.stream_ctx.clone(),
425 self.part_metrics.clone(),
426 index,
427 time_range,
428 );
429 let filter = build_primary_key_filter(
430 &metadata,
431 None,
432 self.stream_ctx.input.predicate_group().predicate(),
433 );
434 return Ok(Some(candidate_primary_key_stream(Box::pin(raw), filter)));
435 }
436
437 if self.stream_ctx.is_file_range_index(index) {
438 if self.coverage.covers_source(&self.stream_ctx, index) {
439 return Ok(None);
443 }
444 let file = self.stream_ctx.input.file_from_index(index);
445 let predicate = self.stream_ctx.input.predicate_for_file(file);
446 if self
447 .partition_pruner
448 .try_skip_manifest_pruned_file_range(index, &self.part_metrics)
449 {
450 return Ok(None);
451 }
452 let mut reader_metrics = ReaderMetrics {
453 filter_metrics: new_filter_metrics(self.part_metrics.explain_verbose()),
454 ..Default::default()
455 };
456 let ranges = self
457 .partition_pruner
458 .build_file_ranges(index, &self.part_metrics, &mut reader_metrics)
459 .await?;
460 self.part_metrics.inc_num_file_ranges(ranges.len());
461 self.part_metrics
462 .merge_reader_metrics(&reader_metrics, None);
463
464 let filter = ranges.first().and_then(|range| {
467 build_primary_key_filter(
468 range.region_metadata(),
469 Some(metadata.as_ref()),
470 predicate.as_ref(),
471 )
472 });
473 let part_metrics = self.part_metrics.clone();
474 let raw = Box::pin(try_stream! {
475 let fetch_metrics = part_metrics
476 .explain_verbose()
477 .then(|| Arc::new(ParquetFetchMetrics::default()));
478 let mut reader_metrics = ReaderMetrics {
479 fetch_metrics: fetch_metrics.clone(),
480 ..Default::default()
481 };
482 for range in ranges {
483 let build_start = Instant::now();
484 let Some(mut reader) = range
485 .primary_key_reader(fetch_metrics.as_deref())
486 .await?
487 else {
488 continue;
489 };
490 reader_metrics.build_cost += build_start.elapsed();
491
492 let scan_start = Instant::now();
493 while let Some(batch) = reader.try_next().await? {
494 reader_metrics.num_record_batches += 1;
495 reader_metrics.num_batches += 1;
496 reader_metrics.num_rows += batch.num_rows();
497 yield batch;
498 }
499 reader_metrics.scan_cost += scan_start.elapsed();
500 }
501 reader_metrics.observe_rows("candidate_series");
502 part_metrics.merge_reader_metrics(&reader_metrics, None);
503 });
504 return Ok(Some(candidate_primary_key_stream(raw, filter)));
505 }
506
507 UnexpectedSnafu {
508 reason: format!(
509 "candidate-series scan received unsupported range index {}",
510 index.index
511 ),
512 }
513 .fail()
514 }
515}
516
517pub(crate) fn is_sparse_metric_metadata(metadata: &store_api::metadata::RegionMetadataRef) -> bool {
518 let valid_prefix = metadata
519 .primary_key
520 .starts_with(&[ReservedColumnId::table_id(), ReservedColumnId::tsid()]);
521 let valid_types = metadata
522 .column_by_id(ReservedColumnId::table_id())
523 .zip(metadata.column_by_id(ReservedColumnId::tsid()))
524 .is_some_and(|(table_id, tsid)| {
525 table_id.column_schema.data_type == ConcreteDataType::uint32_datatype()
526 && tsid.column_schema.data_type == ConcreteDataType::uint64_datatype()
527 });
528
529 metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse && valid_prefix && valid_types
530}
531
532pub(crate) fn validate_metric_metadata(stream_ctx: &StreamContext) -> Result<()> {
533 let metadata = stream_ctx.input.region_metadata();
534 ensure!(
535 is_sparse_metric_metadata(metadata),
536 InvalidRequestSnafu {
537 region_id: metadata.region_id,
538 reason: "candidate-series scan requires sparse (__table_id, __tsid) primary keys",
539 }
540 );
541 Ok(())
542}
543
544fn primary_key_schema() -> SchemaRef {
545 Arc::new(Schema::new(vec![Field::new(
546 PRIMARY_KEY_COLUMN_NAME,
547 DataType::Binary,
548 false,
549 )]))
550}
551
552fn candidate_primary_key_stream(
554 mut input: BoxedRecordBatchStream,
555 mut filter: Option<CachedPrimaryKeyFilter>,
556) -> BoxedRecordBatchStream {
557 Box::pin(try_stream! {
558 let mut last_primary_key = Vec::new();
559 let mut has_last = false;
560 while let Some(batch) = input.try_next().await? {
561 if let Some(batch) = normalize_candidate_batch(
562 batch,
563 filter.as_mut(),
564 &mut last_primary_key,
565 &mut has_last,
566 )? {
567 yield batch;
568 }
569 }
570 })
571}
572
573fn normalize_candidate_batch(
574 mut batch: RecordBatch,
575 filter: Option<&mut CachedPrimaryKeyFilter>,
576 last_primary_key: &mut Vec<u8>,
577 has_last: &mut bool,
578) -> Result<Option<RecordBatch>> {
579 let pk_idx = batch
580 .schema()
581 .column_with_name(PRIMARY_KEY_COLUMN_NAME)
582 .map(|(idx, _)| idx)
583 .context(UnexpectedSnafu {
584 reason: "candidate source does not contain __primary_key",
585 })?;
586 if let Some(filter) = filter {
587 let Some(filtered) = prefilter_flat_batch_by_primary_key(
588 batch,
589 pk_idx,
590 filter as &mut dyn PrimaryKeyFilter,
591 )?
592 else {
593 return Ok(None);
594 };
595 batch = filtered;
596 }
597
598 let pk_column = batch.column(pk_idx);
599 let mut builder = BinaryBuilder::new();
600 if let Some(array) = pk_column.as_any().downcast_ref::<PrimaryKeyArray>() {
601 let values = array
602 .values()
603 .as_any()
604 .downcast_ref::<BinaryArray>()
605 .context(UnexpectedSnafu {
606 reason: "dictionary primary-key values are not binary",
607 })?;
608 for key in array.keys().values() {
609 append_unique_primary_key(
610 values.value(*key as usize),
611 &mut builder,
612 last_primary_key,
613 has_last,
614 );
615 }
616 } else if let Some(array) = pk_column.as_any().downcast_ref::<BinaryArray>() {
617 for value in array.iter().flatten() {
618 append_unique_primary_key(value, &mut builder, last_primary_key, has_last);
619 }
620 } else {
621 return UnexpectedSnafu {
622 reason: format!(
623 "primary-key column is neither binary nor dictionary, got {:?}",
624 pk_column.data_type()
625 ),
626 }
627 .fail();
628 }
629
630 let array = builder.finish();
631 if array.is_empty() {
632 return Ok(None);
633 }
634 let batch = RecordBatch::try_new(primary_key_schema(), vec![Arc::new(array)])
635 .context(NewRecordBatchSnafu)?;
636 Ok(Some(batch))
637}
638
639fn append_unique_primary_key(
640 value: &[u8],
641 builder: &mut BinaryBuilder,
642 last_primary_key: &mut Vec<u8>,
643 has_last: &mut bool,
644) {
645 if !*has_last || last_primary_key != value {
646 builder.append_value(value);
647 last_primary_key.clear();
648 last_primary_key.extend_from_slice(value);
649 *has_last = true;
650 }
651}
652
653fn merge_primary_key_streams(
654 sources: Vec<BoxedRecordBatchStream>,
655 memory_pool: Arc<dyn MemoryPool>,
656 metrics_set: &ExecutionPlanMetricsSet,
657 partition: usize,
658 consumer_name: &'static str,
659) -> Result<BoxedRecordBatchStream> {
660 if sources.is_empty() {
661 return Ok(Box::pin(futures::stream::empty()));
662 }
663 if sources.len() == 1 {
664 return Ok(sources.into_iter().next().unwrap());
665 }
666
667 let schema = primary_key_schema();
668 let df_sources = sources
669 .into_iter()
670 .map(|source| {
671 let stream = source.map_err(|error| DataFusionError::External(Box::new(error)));
672 Box::pin(RecordBatchStreamAdapter::new(schema.clone(), stream)) as _
673 })
674 .collect();
675 let ordering = LexOrdering::new([PhysicalSortExpr {
676 expr: Arc::new(Column::new(PRIMARY_KEY_COLUMN_NAME, 0)),
677 options: SortOptions {
678 descending: false,
679 nulls_first: false,
680 },
681 }])
682 .unwrap();
685 let reservation = MemoryConsumer::new(consumer_name).register(&memory_pool);
686 let mut merged = StreamingMergeBuilder::new()
687 .with_streams(df_sources)
688 .with_schema(schema)
689 .with_expressions(&ordering)
690 .with_metrics(BaselineMetrics::new(metrics_set, partition))
691 .with_batch_size(DEFAULT_READ_BATCH_SIZE)
692 .with_reservation(reservation)
693 .build()
694 .context(MergeCandidateSeriesSnafu)?;
695
696 Ok(Box::pin(try_stream! {
697 while let Some(batch) = merged.next().await {
698 yield batch.context(MergeCandidateSeriesSnafu)?;
699 }
700 }))
701}
702
703fn decode_metric_series(
704 mut input: BoxedRecordBatchStream,
705 metadata: store_api::metadata::RegionMetadataRef,
706) -> Result<MetricSeriesIdStream> {
707 let codec = SparsePrimaryKeyCodec::new(&metadata);
708 Ok(Box::pin(try_stream! {
709 let mut last_series = None;
710 let mut output = Vec::with_capacity(METRIC_SERIES_ID_BATCH_SIZE);
711 while let Some(batch) = input.try_next().await? {
712 let array = batch
713 .column(0)
714 .as_any()
715 .downcast_ref::<BinaryArray>()
716 .context(UnexpectedSnafu {
717 reason: "merged candidate primary key is not binary",
718 })?;
719 for primary_key in array.iter().flatten() {
720 let (table_id, tsid) = codec
721 .decode_ids(primary_key)
722 .context(crate::error::DecodeSnafu)?;
723 let series = MetricSeriesId { table_id, tsid };
724 if last_series == Some(series) {
725 continue;
726 }
727 last_series = Some(series);
728 output.push(series);
729 if output.len() == METRIC_SERIES_ID_BATCH_SIZE {
730 yield std::mem::replace(
731 &mut output,
732 Vec::with_capacity(METRIC_SERIES_ID_BATCH_SIZE),
733 );
734 }
735 }
736 }
737 if !output.is_empty() {
738 yield output;
739 }
740 }))
741}
742
743#[cfg(test)]
744mod tests {
745 use std::num::NonZeroU64;
746 use std::time::Instant;
747
748 use common_time::Timestamp;
749 use datafusion::execution::memory_pool::UnboundedMemoryPool;
750 use datafusion_expr::{col, lit};
751 use datatypes::arrow::array::{
752 ArrayRef, DictionaryArray, TimestampMillisecondArray, UInt8Array, UInt32Array, UInt64Array,
753 };
754 use datatypes::arrow::datatypes::UInt32Type;
755 use futures::TryStreamExt;
756 use store_api::codec::PrimaryKeyEncoding;
757 use store_api::storage::FileId;
758 use table::predicate::Predicate;
759
760 use super::*;
761 use crate::cache::{CacheManager, CacheStrategy};
762 use crate::read::flat_projection::FlatProjectionMapper;
763 use crate::read::pruner::PrunerOptions;
764 use crate::read::scan_region::ScanInput;
765 use crate::read::scan_util::PartitionMetrics;
766 use crate::series_index::{
767 SeriesIndexEntry, SeriesIndexVersion, SeriesIndexWriter, SeriesIndexWriterOptions,
768 series_index_channel, series_index_path,
769 };
770 use crate::sst::file::{FileHandle, FileMeta};
771 use crate::test_util::new_noop_file_purger;
772 use crate::test_util::scheduler_util::SchedulerEnv;
773 use crate::test_util::sst_util::sst_region_metadata_with_encoding;
774
775 #[tokio::test]
776 async fn shared_candidate_ranges_isolate_query_memory_pools() {
777 use datafusion::execution::memory_pool::GreedyMemoryPool;
778 use datatypes::arrow::array::BinaryDictionaryBuilder;
779 use parquet::arrow::ArrowWriter;
780 use parquet::file::metadata::KeyValue;
781 use parquet::file::properties::WriterProperties;
782 use store_api::region_request::PathType;
783
784 use crate::read::scan_region::PredicateGroup;
785 use crate::sst::parquet::PARQUET_METADATA_KEY;
786 use crate::sst::parquet::flat_format::primary_key_column_index;
787 use crate::sst::{FlatSchemaOptions, to_flat_sst_arrow_schema};
788 use crate::test_util::sst_util::{new_record_batch_by_range, new_sparse_primary_key};
789
790 let env = SchedulerEnv::new().await;
791 let metadata = Arc::new(sst_region_metadata_with_encoding(
792 PrimaryKeyEncoding::Sparse,
793 ));
794 let dense = new_record_batch_by_range(&["a", "x"], 0, 2);
795 let mut columns: Vec<ArrayRef> = vec![
796 Arc::new(UInt32Array::from(vec![1, 1])),
797 Arc::new(UInt64Array::from(vec![10, 20])),
798 ];
799 columns.extend_from_slice(dense.columns());
800 let mut keys = BinaryDictionaryBuilder::<UInt32Type>::new();
801 for tsid in [10, 20] {
802 keys.append(new_sparse_primary_key(&["a", "x"], &metadata, 1, tsid))
803 .unwrap();
804 }
805 let pk_index = primary_key_column_index(columns.len());
806 columns[pk_index] = Arc::new(keys.finish());
807 let batch = RecordBatch::try_new(
808 to_flat_sst_arrow_schema(&metadata, &FlatSchemaOptions::default()),
809 columns,
810 )
811 .unwrap();
812 let properties = WriterProperties::builder()
813 .set_key_value_metadata(Some(vec![KeyValue::new(
814 PARQUET_METADATA_KEY.to_string(),
815 metadata.to_json().unwrap(),
816 )]))
817 .build();
818 let mut bytes = Vec::new();
819 let mut writer =
820 ArrowWriter::try_new(&mut bytes, batch.schema(), Some(properties)).unwrap();
821 writer.write(&batch).unwrap();
822 writer.close().unwrap();
823 let mut files = Vec::new();
824 for _ in 0..2 {
825 let file = FileHandle::new(
826 FileMeta {
827 region_id: metadata.region_id,
828 file_id: FileId::random(),
829 time_range: (Timestamp::new_millisecond(0), Timestamp::new_millisecond(1)),
830 file_size: bytes.len() as u64,
831 num_rows: 2,
832 num_row_groups: 1,
833 preserve_row_sequence: true,
834 ..Default::default()
835 },
836 new_noop_file_purger(),
837 );
838 env.access_layer
839 .object_store()
840 .write(&file.file_path("", PathType::Bare), bytes.clone())
841 .await
842 .unwrap();
843 files.push(file);
844 }
845 let strategy = CacheStrategy::EnableAll(Arc::new(
846 CacheManager::builder()
847 .range_result_cache_size(1024 * 1024)
848 .build(),
849 ));
850 let mapper =
851 FlatProjectionMapper::new(&metadata, 0..metadata.column_metadatas.len()).unwrap();
852 let input = ScanInput::builder(env.access_layer.clone(), mapper)
853 .with_files(files)
854 .with_cache(strategy)
855 .with_predicate(
856 PredicateGroup::new(&metadata, &[col("__table_id").eq(lit(1_u32))]).unwrap(),
857 )
858 .build();
859 let context = Arc::new(StreamContext::seq_scan_ctx(input));
860 let ranges = context.partition_ranges();
861 assert_eq!(ranges.len(), 1);
862 assert_eq!(context.ranges[0].row_group_indices.len(), 2);
863 let make_scanner = |pool: Arc<dyn MemoryPool>| {
864 let pruner = Arc::new(Pruner::new_with_options(
865 context.clone(),
866 1,
867 PrunerOptions {
868 retain_builders: true,
869 enable_predicate_prefilter: false,
870 },
871 ));
872 let metrics = ExecutionPlanMetricsSet::new();
873 let partition_metrics = PartitionMetrics::new(
874 metadata.region_id,
875 1,
876 "pool-test",
877 Instant::now(),
878 false,
879 &metrics,
880 );
881 SeriesCandidateScanner::try_new(
882 context.clone(),
883 vec![ranges.clone()],
884 pruner,
885 Arc::new(Semaphore::new(1)),
886 pool,
887 metrics,
888 partition_metrics,
889 )
890 .unwrap()
891 };
892 let low_pool = Arc::new(GreedyMemoryPool::new(0));
893 let high_pool = Arc::new(UnboundedMemoryPool::default());
894 let low = make_scanner(low_pool.clone());
895 let high = make_scanner(high_pool.clone());
896 tokio::time::timeout(std::time::Duration::from_secs(5), async {
897 let low_stream = low.build_stream().await.unwrap();
899 let high_stream = high.build_stream().await.unwrap();
900 let error = low_stream.try_collect::<Vec<_>>().await.unwrap_err();
901 assert!(
902 format!("{error:?}").contains("ResourcesExhausted"),
903 "{error:?}"
904 );
905 let groups = high_stream.try_collect::<Vec<_>>().await.unwrap();
906 assert_eq!(
907 groups.into_iter().flatten().collect::<Vec<_>>(),
908 [10, 20]
909 .map(|tsid| MetricSeriesId { table_id: 1, tsid })
910 .to_vec(),
911 );
912 })
913 .await
914 .expect("candidate scans stalled with different query pools");
915 drop(low);
916 drop(high);
917 assert_eq!(low_pool.reserved(), 0);
918 assert_eq!(high_pool.reserved(), 0);
919 }
920
921 async fn indexed_scanner() -> (SchedulerEnv, SeriesCandidateScanner, Arc<Pruner>) {
923 let env = SchedulerEnv::new().await;
924 let metadata = Arc::new(sst_region_metadata_with_encoding(
925 PrimaryKeyEncoding::Sparse,
926 ));
927 let store = env.access_layer.object_store().clone();
928 let entry = SeriesIndexEntry {
929 file_size: 0,
930 index_uuid: FileId::random(),
931 bucket_start: Timestamp::new_millisecond(0),
932 bucket_end: Timestamp::new_millisecond(20),
933 source_file_ids: Vec::new(),
934 min_file_sequence: 1,
935 max_file_sequence: 2,
936 compaction_window_secs: 1,
937 window_sequences: Default::default(),
938 };
939 let path = series_index_path(metadata.region_id, entry.index_uuid);
940 let codec = SparsePrimaryKeyCodec::new(&metadata);
941 let keys: Vec<_> = (0..1001)
942 .map(|tsid| {
943 let mut key = Vec::new();
944 codec.encode_internal(1, tsid, &mut key).unwrap();
945 key
946 })
947 .collect();
948 let batch = RecordBatch::try_from_iter(vec![
949 (
950 "ts",
951 Arc::new(TimestampMillisecondArray::from(vec![10; keys.len()])) as ArrayRef,
952 ),
953 (
954 "__primary_key",
955 Arc::new(BinaryArray::from_iter_values(&keys)),
956 ),
957 (
958 "__sequence",
959 Arc::new(UInt64Array::from_value(1, keys.len())),
960 ),
961 ("__op_type", Arc::new(UInt8Array::from_value(0, keys.len()))),
962 ])
963 .unwrap();
964 let mut writer = SeriesIndexWriter::try_new(
965 metadata.clone(),
966 store.clone(),
967 &path,
968 SeriesIndexWriterOptions {
969 row_group_size: 500,
970 },
971 None,
972 )
973 .await
974 .unwrap();
975 writer.write(&batch).await.unwrap();
976 writer.finish().await.unwrap();
977 let (purger, _receiver) = series_index_channel(store.clone());
978 let handle = SeriesIndexFileHandle::new(metadata.region_id, entry.clone(), purger);
979 let context = SeriesIndexReadContext {
980 store,
981 version: Arc::new(SeriesIndexVersion {
982 series_indexes: [(entry.index_uuid, handle)].into(),
983 ..Default::default()
984 }),
985 };
986 let files = (0..2)
987 .map(|i| {
988 FileHandle::new(
989 FileMeta {
990 region_id: metadata.region_id,
991 file_id: FileId::random(),
992 time_range: (
993 Timestamp::new_millisecond(i * 10),
994 Timestamp::new_millisecond(i * 10 + 9),
995 ),
996 sequence: NonZeroU64::new(i as u64 + 1),
997 num_row_groups: 1,
998 ..Default::default()
999 },
1000 new_noop_file_purger(),
1001 )
1002 })
1003 .collect();
1004 let mapper =
1005 FlatProjectionMapper::new(&metadata, 0..metadata.column_metadatas.len()).unwrap();
1006 let input = ScanInput::builder(env.access_layer.clone(), mapper)
1007 .with_predicate(
1008 crate::read::scan_region::PredicateGroup::new(
1009 &metadata,
1010 &[col("__table_id").eq(lit(1_u32))],
1011 )
1012 .unwrap(),
1013 )
1014 .with_files(files)
1015 .with_series_index(Some(context))
1016 .with_cache(CacheStrategy::EnableAll(Arc::new(
1017 CacheManager::builder()
1018 .range_result_cache_size(1024 * 1024)
1019 .build(),
1020 )))
1021 .build();
1022 let stream_ctx = Arc::new(StreamContext::seq_scan_ctx(input));
1023 let ranges = stream_ctx.partition_ranges();
1024 assert_eq!(2, ranges.len());
1025 let metrics_set = ExecutionPlanMetricsSet::new();
1026 let part_metrics = PartitionMetrics::new(
1027 metadata.region_id,
1028 2,
1029 "candidate-test",
1030 Instant::now(),
1031 false,
1032 &metrics_set,
1033 );
1034 let pruner = Arc::new(Pruner::new_with_options(
1035 stream_ctx.clone(),
1036 1,
1037 PrunerOptions {
1038 retain_builders: true,
1039 enable_predicate_prefilter: false,
1040 },
1041 ));
1042 let scanner = SeriesCandidateScanner::try_new(
1043 stream_ctx,
1044 ranges.into_iter().map(|range| vec![range]).collect(),
1045 pruner.clone(),
1046 Arc::new(Semaphore::new(1)),
1047 Arc::new(UnboundedMemoryPool::default()),
1048 metrics_set,
1049 part_metrics,
1050 )
1051 .unwrap();
1052 (env, scanner, pruner)
1053 }
1054
1055 #[tokio::test]
1056 async fn index_is_shared_across_ranges_without_caching_partial_candidates() {
1057 let (_env, scanner, pruner) = indexed_scanner().await;
1058 let keys: Vec<_> = scanner
1059 .partitions
1060 .iter()
1061 .flatten()
1062 .map(|range| {
1063 build_candidate_range_cache_key(
1064 &scanner.stream_ctx,
1065 range,
1066 &scanner.candidate_pruner,
1067 )
1068 .unwrap()
1069 })
1070 .collect();
1071 let groups = scanner
1072 .build_stream()
1073 .await
1074 .unwrap()
1075 .try_collect::<Vec<_>>()
1076 .await
1077 .unwrap();
1078 assert_eq!(
1079 (0..1001)
1080 .map(|tsid| MetricSeriesId { table_id: 1, tsid })
1081 .collect::<Vec<_>>(),
1082 groups.into_iter().flatten().collect::<Vec<_>>()
1083 );
1084 for file_index in 0..2 {
1087 assert_eq!(1, pruner.test_remaining_ranges(file_index));
1088 }
1089 assert_eq!(
1090 1,
1091 scanner
1092 .metrics_set
1093 .clone_inner()
1094 .sum_by_name("candidate_index_files")
1095 .unwrap()
1096 .as_usize()
1097 );
1098 assert_eq!(
1099 2,
1100 scanner
1101 .metrics_set
1102 .clone_inner()
1103 .sum_by_name("candidate_index_covered_ssts")
1104 .unwrap()
1105 .as_usize()
1106 );
1107 assert!(!format!("{:?}", scanner.part_metrics).contains("range_cache_miss"));
1110 assert!(!format!("{:?}", scanner.part_metrics).contains("range_cache_hit"));
1111 for key in keys {
1112 assert!(
1113 scanner
1114 .stream_ctx
1115 .input
1116 .cache_strategy
1117 .get_range_result(&key)
1118 .is_none()
1119 );
1120 }
1121 }
1122
1123 #[tokio::test]
1124 async fn index_read_failure_after_output_is_propagated() {
1125 let (_env, scanner, _pruner) = indexed_scanner().await;
1126 let context = scanner.stream_ctx.input.series_index.clone().unwrap();
1127 let index = scanner.coverage.indexes[0].clone();
1128 let path = series_index_path(
1129 scanner.stream_ctx.input.region_metadata().region_id,
1130 index.entry().index_uuid,
1131 );
1132 let mut stream = index_primary_key_stream(
1133 scanner.stream_ctx.clone(),
1134 context.clone(),
1135 index,
1136 scanner.range_semaphore.clone(),
1137 );
1138 assert_eq!(500, stream.try_next().await.unwrap().unwrap().num_rows());
1139 context.store.delete(&path).await.unwrap();
1140 assert!(stream.try_collect::<Vec<_>>().await.is_err());
1141 assert!(
1144 scanner
1145 .build_stream()
1146 .await
1147 .unwrap()
1148 .try_collect::<Vec<_>>()
1149 .await
1150 .is_err()
1151 );
1152 }
1153
1154 #[tokio::test]
1155 async fn candidate_scanner_rejects_predicate_prefilter_pruner() {
1156 let env = SchedulerEnv::new().await;
1157 let metadata = Arc::new(sst_region_metadata_with_encoding(
1158 PrimaryKeyEncoding::Sparse,
1159 ));
1160 let mapper =
1161 FlatProjectionMapper::new(&metadata, 0..metadata.column_metadatas.len()).unwrap();
1162 let stream_ctx = Arc::new(StreamContext::seq_scan_ctx(
1163 ScanInput::builder(env.access_layer.clone(), mapper).build(),
1164 ));
1165 let pruner = Arc::new(Pruner::new(stream_ctx.clone(), 1));
1166 let metrics_set = ExecutionPlanMetricsSet::new();
1167 let part_metrics = PartitionMetrics::new(
1168 metadata.region_id,
1169 0,
1170 "candidate-test",
1171 Instant::now(),
1172 false,
1173 &metrics_set,
1174 );
1175
1176 let error = SeriesCandidateScanner::try_new(
1177 stream_ctx,
1178 Vec::new(),
1179 pruner,
1180 Arc::new(Semaphore::new(1)),
1181 Arc::new(UnboundedMemoryPool::default()),
1182 metrics_set,
1183 part_metrics,
1184 )
1185 .err()
1186 .unwrap();
1187
1188 assert!(matches!(error, crate::error::Error::Unexpected { .. }));
1189 assert!(
1190 error
1191 .to_string()
1192 .contains("requires a pruner without predicate prefiltering")
1193 );
1194 }
1195
1196 fn binary_batch(values: &[&[u8]]) -> RecordBatch {
1197 RecordBatch::try_new(
1198 primary_key_schema(),
1199 vec![Arc::new(BinaryArray::from_iter_values(
1200 values.iter().copied(),
1201 ))],
1202 )
1203 .unwrap()
1204 }
1205
1206 fn dictionary_batch(values: &[&[u8]], keys: &[u32]) -> RecordBatch {
1207 let dict_values: ArrayRef = Arc::new(BinaryArray::from_iter_values(values.iter().copied()));
1208 let dict =
1209 DictionaryArray::<UInt32Type>::try_new(UInt32Array::from(keys.to_vec()), dict_values)
1210 .unwrap();
1211 let schema = Arc::new(Schema::new(vec![Field::new_dictionary(
1212 PRIMARY_KEY_COLUMN_NAME,
1213 DataType::UInt32,
1214 DataType::Binary,
1215 false,
1216 )]));
1217 RecordBatch::try_new(schema, vec![Arc::new(dict)]).unwrap()
1218 }
1219
1220 #[tokio::test]
1221 async fn candidate_stream_normalizes_and_deduplicates_primary_keys() {
1222 let input = Box::pin(futures::stream::iter(vec![
1223 Ok(dictionary_batch(&[b"a", b"b"], &[0, 0, 1])),
1224 Ok(binary_batch(&[b"b", b"c", b"c"])),
1225 ]));
1226 let batches = candidate_primary_key_stream(input, None)
1227 .try_collect::<Vec<_>>()
1228 .await
1229 .unwrap();
1230 let actual = batches
1231 .iter()
1232 .flat_map(|batch| {
1233 batch
1234 .column(0)
1235 .as_any()
1236 .downcast_ref::<BinaryArray>()
1237 .unwrap()
1238 .iter()
1239 .flatten()
1240 })
1241 .collect::<Vec<_>>();
1242 assert_eq!(actual, vec![b"a".as_slice(), b"b", b"c"]);
1243 }
1244
1245 #[tokio::test]
1246 async fn candidate_stream_filters_primary_keys_before_merge() {
1247 let metadata = Arc::new(sst_region_metadata_with_encoding(
1248 PrimaryKeyEncoding::Sparse,
1249 ));
1250 let codec = SparsePrimaryKeyCodec::new(&metadata);
1251 let mut table_1 = Vec::new();
1252 let mut table_2 = Vec::new();
1253 codec.encode_internal(1, 10, &mut table_1).unwrap();
1254 codec.encode_internal(2, 20, &mut table_2).unwrap();
1255
1256 let predicate = Predicate::new(vec![
1257 col(store_api::metric_engine_consts::DATA_SCHEMA_TABLE_ID_COLUMN_NAME).eq(lit(1_u32)),
1258 ]);
1259 let filter = build_primary_key_filter(&metadata, None, Some(&predicate));
1260 let input = Box::pin(futures::stream::iter(vec![Ok(dictionary_batch(
1261 &[table_1.as_slice(), table_2.as_slice()],
1262 &[0, 1],
1263 ))]));
1264 let batches = candidate_primary_key_stream(input, filter)
1265 .try_collect::<Vec<_>>()
1266 .await
1267 .unwrap();
1268
1269 assert_eq!(batches.len(), 1);
1270 let array = batches[0]
1271 .column(0)
1272 .as_any()
1273 .downcast_ref::<BinaryArray>()
1274 .unwrap();
1275 assert_eq!(array.len(), 1);
1276 assert_eq!(array.value(0), table_1);
1277 }
1278
1279 #[tokio::test]
1280 async fn decode_metric_series_yields_groups_of_500() {
1281 let metadata = Arc::new(sst_region_metadata_with_encoding(
1282 PrimaryKeyEncoding::Sparse,
1283 ));
1284 let codec = SparsePrimaryKeyCodec::new(&metadata);
1285 let primary_keys = (0..501_u64)
1286 .map(|tsid| {
1287 let mut primary_key = Vec::new();
1288 codec.encode_internal(1, tsid, &mut primary_key).unwrap();
1289 primary_key
1290 })
1291 .collect::<Vec<_>>();
1292 let batch = binary_batch(&primary_keys.iter().map(Vec::as_slice).collect::<Vec<_>>());
1293 let source = Box::pin(futures::stream::iter(vec![Ok(batch)]));
1294 let metrics = ExecutionPlanMetricsSet::new();
1295 let pool = Arc::new(UnboundedMemoryPool::default());
1296 let merged =
1297 merge_primary_key_streams(vec![source], pool, &metrics, 0, "candidate-test").unwrap();
1298 let groups = decode_metric_series(merged, metadata)
1299 .unwrap()
1300 .try_collect::<Vec<_>>()
1301 .await
1302 .unwrap();
1303
1304 assert_eq!(groups.iter().map(Vec::len).collect::<Vec<_>>(), [500, 1]);
1305 assert_eq!(
1306 groups[0][0],
1307 MetricSeriesId {
1308 table_id: 1,
1309 tsid: 0
1310 }
1311 );
1312 assert_eq!(
1313 groups[1][0],
1314 MetricSeriesId {
1315 table_id: 1,
1316 tsid: 500
1317 }
1318 );
1319 }
1320
1321 #[tokio::test]
1322 async fn merge_primary_keys_globally_sorts_and_deduplicates_series() {
1323 let metadata = Arc::new(sst_region_metadata_with_encoding(
1324 PrimaryKeyEncoding::Sparse,
1325 ));
1326 let codec = SparsePrimaryKeyCodec::new(&metadata);
1327 let encode = |tsid| {
1328 let mut primary_key = Vec::new();
1329 codec.encode_internal(1, tsid, &mut primary_key).unwrap();
1330 primary_key
1331 };
1332 let keys_1 = [encode(1), encode(3)];
1333 let mut alternate_key_for_series_1 = encode(1);
1334 alternate_key_for_series_1.push(0);
1335 let keys_2 = [alternate_key_for_series_1, encode(2)];
1336 let sources = vec![
1337 Box::pin(futures::stream::iter(vec![Ok(binary_batch(
1338 &keys_1.iter().map(Vec::as_slice).collect::<Vec<_>>(),
1339 ))])) as BoxedRecordBatchStream,
1340 Box::pin(futures::stream::iter(vec![Ok(binary_batch(
1341 &keys_2.iter().map(Vec::as_slice).collect::<Vec<_>>(),
1342 ))])),
1343 ];
1344
1345 let merged = merge_primary_key_streams(
1346 sources,
1347 Arc::new(UnboundedMemoryPool::default()),
1348 &ExecutionPlanMetricsSet::new(),
1349 0,
1350 "candidate-merge-test",
1351 )
1352 .unwrap();
1353 let groups = decode_metric_series(merged, metadata)
1354 .unwrap()
1355 .try_collect::<Vec<_>>()
1356 .await
1357 .unwrap();
1358
1359 assert_eq!(
1360 groups,
1361 vec![vec![
1362 MetricSeriesId {
1363 table_id: 1,
1364 tsid: 1,
1365 },
1366 MetricSeriesId {
1367 table_id: 1,
1368 tsid: 2,
1369 },
1370 MetricSeriesId {
1371 table_id: 1,
1372 tsid: 3,
1373 },
1374 ]]
1375 );
1376 }
1377}