1pub(crate) mod bloom_filter;
16mod column;
17#[cfg(test)]
18mod column_test;
19pub(crate) mod fulltext_index;
20mod indexer;
21pub mod intermediate;
22pub(crate) mod inverted_index;
23mod primary_key;
24pub mod puffin_manager;
25#[cfg(test)]
26mod sparse_test;
27mod statistics;
28pub(crate) mod store;
29
30use std::cmp::Ordering;
31use std::collections::{HashMap, HashSet};
32use std::num::NonZeroUsize;
33use std::ops::ControlFlow;
34use std::sync::Arc;
35
36use arrow_schema::extension::ExtensionType;
37use bloom_filter::creator::BloomFilterIndexer;
38use bytes::Bytes;
39use common_telemetry::{debug, error, info, warn};
40use datatypes::arrow::record_batch::RecordBatch;
41use datatypes::extension::json::Json2ExtensionType;
42use index::target::IndexTarget;
43use mito_codec::row_converter::DensePrimaryKeyCodec;
44use object_store::ObjectStore;
45use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
46use puffin_manager::SstPuffinManager;
47use smallvec::SmallVec;
48use snafu::ResultExt;
49use statistics::{ByteCount, RowCount};
50use store_api::codec::PrimaryKeyEncoding;
51use store_api::metadata::RegionMetadataRef;
52use store_api::storage::{ColumnId, FileId, RegionId};
53use strum::IntoStaticStr;
54use tokio::sync::mpsc::Sender;
55
56use crate::access_layer::{AccessLayerRef, FilePathProvider, OperationType, RegionFilePathFactory};
57use crate::batch_size::{estimate_batch_size, largest_row_group_statistic};
58use crate::cache::file_cache::{FileCacheRef, FileType, IndexKey};
59use crate::cache::write_cache::{UploadOptions, UploadTracker, WriteCacheRef};
60use crate::cache::{CacheManagerRef, CacheStrategy};
61use crate::config::{BloomFilterConfig, FulltextIndexConfig, InvertedIndexConfig};
62use crate::error::{
63 BuildIndexAsyncSnafu, Error, JoinSnafu, RegionClosedSnafu, RegionDroppedSnafu,
64 RegionTruncatedSnafu, Result,
65};
66use crate::metrics::{
67 INDEX_ARTIFACT_CLEANUP_FAILURE_TOTAL, INDEX_CREATE_MEMORY_USAGE, INDEX_PUBLICATION_STALE_TOTAL,
68};
69use crate::read::Batch;
70use crate::region::options::IndexOptions;
71use crate::region::version::VersionControlRef;
72use crate::region::{
73 IndexBuildSource, IndexPublication, IndexPublicationStale, ManifestContextRef,
74};
75use crate::request::{
76 BackgroundNotify, BuildIndexRequest, IndexBuildFinished, IndexBuildStopped, WorkerRequest,
77 WorkerRequestWithTime,
78};
79use crate::schedule::scheduler::{Job, SchedulerRef};
80use crate::sst::file::{
81 ColumnIndexMetadata, FileHandle, IndexType, IndexTypes, RegionFileId, RegionIndexId,
82};
83use crate::sst::file_purger::FilePurgerRef;
84use crate::sst::index::fulltext_index::FULLTEXT_BLOOM_VERSION;
85use crate::sst::index::fulltext_index::creator::FulltextIndexer;
86use crate::sst::index::intermediate::IntermediateManager;
87use crate::sst::index::inverted_index::creator::InvertedIndexer;
88use crate::sst::parquet::SstInfo;
89use crate::worker::WorkerListener;
90
91pub(crate) const TYPE_INVERTED_INDEX: &str = "inverted_index";
92pub(crate) const TYPE_FULLTEXT_INDEX: &str = "fulltext_index";
93pub(crate) const TYPE_BLOOM_FILTER_INDEX: &str = "bloom_filter_index";
94
95pub(crate) async fn cleanup_stale_index_caches(
101 index_id: RegionIndexId,
102 access_layer: &AccessLayerRef,
103 cache_manager: Option<&CacheManagerRef>,
104 write_cache: Option<&WriteCacheRef>,
105) {
106 if let Some(cache_manager) = cache_manager {
107 CacheStrategy::EnableAll(cache_manager.clone())
108 .evict_puffin_cache(index_id)
109 .await;
110 }
111 if let Some(write_cache) = write_cache {
112 write_cache
113 .remove(IndexKey::new(
114 index_id.region_id(),
115 index_id.file_id(),
116 FileType::Puffin(index_id.version),
117 ))
118 .await;
119 }
120 if let Err(e) = access_layer
121 .puffin_manager_factory()
122 .purge_stager(index_id)
123 .await
124 {
125 INDEX_ARTIFACT_CLEANUP_FAILURE_TOTAL.inc();
126 warn!(
127 e;
128 "Failed to purge stale index artifact from stager, region: {}, file_id: {}, index_version: {}",
129 index_id.region_id(),
130 index_id.file_id(),
131 index_id.version
132 );
133 }
134}
135
136pub(crate) fn trigger_index_background_download(
138 file_cache: Option<&FileCacheRef>,
139 file_id: &RegionIndexId,
140 file_size_hint: Option<u64>,
141 path_factory: &RegionFilePathFactory,
142 object_store: &ObjectStore,
143) {
144 if let (Some(file_cache), Some(file_size)) = (file_cache, file_size_hint) {
145 let index_key = IndexKey::new(
146 file_id.region_id(),
147 file_id.file_id(),
148 FileType::Puffin(file_id.version),
149 );
150 let remote_path = path_factory.build_index_file_path(file_id.file_id);
151 file_cache.maybe_download_background(
152 index_key,
153 remote_path,
154 object_store.clone(),
155 file_size,
156 );
157 }
158}
159
160#[derive(Debug, Clone, Default)]
162pub struct IndexOutput {
163 pub file_size: u64,
165 pub version: u64,
167 pub inverted_index: InvertedIndexOutput,
169 pub fulltext_index: FulltextIndexOutput,
171 pub fulltext_bloom_columns: Vec<ColumnId>,
173 pub bloom_filter: BloomFilterOutput,
175}
176
177impl IndexOutput {
178 pub fn build_available_indexes(&self) -> SmallVec<[IndexType; 4]> {
179 let mut indexes = SmallVec::new();
180 if self.inverted_index.is_available() {
181 indexes.push(IndexType::InvertedIndex);
182 }
183 if self.fulltext_index.is_available() {
184 indexes.push(IndexType::FulltextIndex);
185 }
186 if self.bloom_filter.is_available() {
187 indexes.push(IndexType::BloomFilterIndex);
188 }
189 indexes
190 }
191
192 pub fn build_indexes(&self) -> Vec<ColumnIndexMetadata> {
193 let mut map: HashMap<ColumnId, IndexTypes> = HashMap::new();
194
195 if self.inverted_index.is_available() {
196 for &col in &self.inverted_index.columns {
197 map.entry(col).or_default().push(IndexType::InvertedIndex);
198 }
199 }
200 if self.fulltext_index.is_available() {
201 for &col in &self.fulltext_index.columns {
202 map.entry(col).or_default().push(IndexType::FulltextIndex);
203 }
204 }
205 if self.bloom_filter.is_available() {
206 for &col in &self.bloom_filter.columns {
207 map.entry(col)
208 .or_default()
209 .push(IndexType::BloomFilterIndex);
210 }
211 }
212
213 map.into_iter()
214 .map(|(column_id, created_indexes)| ColumnIndexMetadata {
215 column_id,
216 fulltext_bloom_version: if self.fulltext_index.is_available()
217 && self.fulltext_bloom_columns.contains(&column_id)
218 {
219 FULLTEXT_BLOOM_VERSION
220 } else {
221 0
222 },
223 created_indexes,
224 })
225 .collect::<Vec<_>>()
226 }
227}
228
229#[derive(Debug, Clone, Default)]
231pub struct IndexBaseOutput {
232 pub index_size: ByteCount,
234 pub row_count: RowCount,
236 pub columns: Vec<ColumnId>,
238}
239
240impl IndexBaseOutput {
241 pub fn is_available(&self) -> bool {
242 self.index_size > 0
243 }
244}
245
246pub type InvertedIndexOutput = IndexBaseOutput;
248pub type FulltextIndexOutput = IndexBaseOutput;
250pub type BloomFilterOutput = IndexBaseOutput;
252
253#[derive(Default)]
255pub struct Indexer {
256 file_id: FileId,
257 region_id: RegionId,
259 physical_region_id: RegionId,
261 index_version: u64,
262 puffin_manager: Option<SstPuffinManager>,
263 write_cache_enabled: bool,
264 inverted_indexer: Option<InvertedIndexer>,
265 last_mem_inverted_index: usize,
266 fulltext_indexer: Option<FulltextIndexer>,
267 last_mem_fulltext_index: usize,
268 bloom_filter_indexer: Option<BloomFilterIndexer>,
269 last_mem_bloom_filter: usize,
270 dense_pk_decoder: Option<DensePrimaryKeyCodec>,
272 intermediate_manager: Option<IntermediateManager>,
273}
274
275impl Indexer {
276 pub(crate) fn is_empty(&self) -> bool {
278 self.inverted_indexer.is_none()
279 && self.fulltext_indexer.is_none()
280 && self.bloom_filter_indexer.is_none()
281 }
282
283 #[cfg(feature = "testing")]
285 pub fn for_bench(
286 metadata: &RegionMetadataRef,
287 inverted_indexer: Option<InvertedIndexer>,
288 bloom_filter_indexer: Option<BloomFilterIndexer>,
289 ) -> Self {
290 let mut indexer = Self {
291 region_id: metadata.region_id,
292 inverted_indexer,
293 bloom_filter_indexer,
294 ..Default::default()
295 };
296 indexer.prepare_dense_pk_decoder(metadata);
297 indexer
298 }
299
300 fn prepare_dense_pk_decoder(&mut self, metadata: &RegionMetadataRef) {
303 if metadata.primary_key_encoding != PrimaryKeyEncoding::Dense
304 || metadata.primary_key.is_empty()
305 {
306 return;
307 }
308 let requested: HashSet<_> = self
309 .inverted_indexer
310 .iter()
311 .flat_map(|indexer| indexer.indexed_targets())
312 .chain(
313 self.bloom_filter_indexer
314 .iter()
315 .flat_map(|indexer| indexer.indexed_targets()),
316 )
317 .filter_map(|target| match target {
318 IndexTarget::ColumnId(id) => Some(*id),
319 IndexTarget::JsonPath(_) => None,
320 })
321 .collect();
322 if metadata.primary_key.iter().all(|id| requested.contains(id)) {
323 self.dense_pk_decoder = Some(DensePrimaryKeyCodec::new(metadata));
324 }
325 }
326
327 fn prepare_primary_key(&self, batch: &mut Batch) -> Result<()> {
329 if let Some(codec) = &self.dense_pk_decoder {
330 batch.ensure_dense_pk_decoded(codec)?;
331 }
332 Ok(())
333 }
334
335 pub async fn update(&mut self, batch: &mut Batch) {
337 self.do_update(batch).await;
338
339 self.flush_mem_metrics();
340 }
341
342 pub async fn update_flat(&mut self, batch: &RecordBatch) {
344 self.do_update_flat(batch).await;
345
346 self.flush_mem_metrics();
347 }
348
349 pub async fn finish(&mut self) -> IndexOutput {
351 let output = self.do_finish().await;
352
353 self.flush_mem_metrics();
354 output
355 }
356
357 pub async fn abort(&mut self) {
359 self.do_abort().await;
360
361 self.flush_mem_metrics();
362 }
363
364 fn flush_mem_metrics(&mut self) {
365 let inverted_mem = self
366 .inverted_indexer
367 .as_ref()
368 .map_or(0, |creator| creator.memory_usage());
369 INDEX_CREATE_MEMORY_USAGE
370 .with_label_values(&[TYPE_INVERTED_INDEX])
371 .add(inverted_mem as i64 - self.last_mem_inverted_index as i64);
372 self.last_mem_inverted_index = inverted_mem;
373
374 let fulltext_mem = self
375 .fulltext_indexer
376 .as_ref()
377 .map_or(0, |creator| creator.memory_usage());
378 INDEX_CREATE_MEMORY_USAGE
379 .with_label_values(&[TYPE_FULLTEXT_INDEX])
380 .add(fulltext_mem as i64 - self.last_mem_fulltext_index as i64);
381 self.last_mem_fulltext_index = fulltext_mem;
382
383 let bloom_filter_mem = self
384 .bloom_filter_indexer
385 .as_ref()
386 .map_or(0, |creator| creator.memory_usage());
387 INDEX_CREATE_MEMORY_USAGE
388 .with_label_values(&[TYPE_BLOOM_FILTER_INDEX])
389 .add(bloom_filter_mem as i64 - self.last_mem_bloom_filter as i64);
390 self.last_mem_bloom_filter = bloom_filter_mem;
391 }
392}
393
394#[async_trait::async_trait]
395pub trait IndexerBuilder {
396 async fn build(
398 &self,
399 region_file_id: RegionFileId,
400 index_version: u64,
401 row_group_size: Option<usize>,
402 ) -> Indexer;
403}
404#[derive(Clone)]
405pub(crate) struct IndexerBuilderImpl {
406 pub(crate) build_type: IndexBuildType,
407 pub(crate) metadata: RegionMetadataRef,
408 pub(crate) puffin_manager: SstPuffinManager,
409 pub(crate) write_cache_enabled: bool,
410 pub(crate) intermediate_manager: IntermediateManager,
411 pub(crate) index_options: IndexOptions,
412 pub(crate) inverted_index_config: InvertedIndexConfig,
413 pub(crate) fulltext_index_config: FulltextIndexConfig,
414 pub(crate) bloom_filter_index_config: BloomFilterConfig,
415}
416
417#[async_trait::async_trait]
418impl IndexerBuilder for IndexerBuilderImpl {
419 async fn build(
421 &self,
422 region_file_id: RegionFileId,
423 index_version: u64,
424 row_group_size: Option<usize>,
425 ) -> Indexer {
426 let mut indexer = Indexer {
427 file_id: region_file_id.file_id(),
428 region_id: self.metadata.region_id,
429 physical_region_id: region_file_id.region_id(),
430 index_version,
431 write_cache_enabled: self.write_cache_enabled,
432 ..Default::default()
433 };
434
435 indexer.inverted_indexer =
436 self.build_inverted_indexer(region_file_id.file_id(), row_group_size);
437 indexer.fulltext_indexer = self.build_fulltext_indexer(region_file_id.file_id()).await;
438 indexer.bloom_filter_indexer = self.build_bloom_filter_indexer(region_file_id.file_id());
439 indexer.prepare_dense_pk_decoder(&self.metadata);
440 indexer.intermediate_manager = Some(self.intermediate_manager.clone());
441
442 if indexer.is_empty() {
443 indexer.abort().await;
444 return Indexer::default();
445 }
446
447 indexer.puffin_manager = Some(self.puffin_manager.clone());
448 indexer
449 }
450}
451
452pub(crate) async fn build_index_from_parquet_bytes(
460 builder: &IndexerBuilderImpl,
461 region_file_id: RegionFileId,
462 data: &Bytes,
463) -> IndexOutput {
464 let reader_builder = match ParquetRecordBatchReaderBuilder::try_new(data.clone()) {
465 Ok(reader_builder) => reader_builder,
466 Err(e) => {
467 warn!(e; "Failed to read encoded SST {:?} for index creation", region_file_id);
468 return IndexOutput::default();
469 }
470 };
471 let row_group_size = reader_builder
472 .metadata()
473 .row_groups()
474 .first()
475 .map(|row_group| row_group.num_rows() as usize)
476 .filter(|size| *size > 0);
477 let mut indexer = builder.build(region_file_id, 0, row_group_size).await;
478 if indexer.is_empty() {
479 return IndexOutput::default();
480 }
481 let batch_size = estimate_batch_size(largest_row_group_statistic(reader_builder.metadata()));
483
484 let mut reader = match reader_builder.with_batch_size(batch_size).build() {
487 Ok(reader) => reader,
488 Err(e) => {
489 warn!(e; "Failed to read encoded SST {:?} for index creation", region_file_id);
490 indexer.abort().await;
491 return IndexOutput::default();
492 }
493 };
494 loop {
495 let decoded = common_runtime::spawn_blocking_global(move || {
498 let batch = reader.next();
499 (reader, batch)
500 })
501 .await;
502 let batch = match decoded {
503 Ok((next_reader, batch)) => {
504 reader = next_reader;
505 batch
506 }
507 Err(e) => {
508 warn!(e; "Failed to decode encoded SST {:?} for index creation", region_file_id);
509 indexer.abort().await;
510 return IndexOutput::default();
511 }
512 };
513 match batch {
514 Some(Ok(batch)) => indexer.update_flat(&batch).await,
515 Some(Err(e)) => {
516 warn!(e; "Failed to decode encoded SST {:?} for index creation", region_file_id);
517 indexer.abort().await;
518 return IndexOutput::default();
519 }
520 None => break,
521 }
522 }
523 indexer.finish().await
524}
525
526impl IndexerBuilderImpl {
527 fn build_inverted_indexer(
528 &self,
529 file_id: FileId,
530 row_group_size: Option<usize>,
531 ) -> Option<InvertedIndexer> {
532 let create = match self.build_type {
533 IndexBuildType::Flush => self.inverted_index_config.create_on_flush.auto(),
534 IndexBuildType::Compact => self.inverted_index_config.create_on_compaction.auto(),
535 _ => true,
536 };
537
538 if !create {
539 debug!(
540 "Skip creating inverted index due to config, region_id: {}, file_id: {}",
541 self.metadata.region_id, file_id,
542 );
543 return None;
544 }
545
546 let indexed_column_ids = self.metadata.inverted_indexed_column_ids(
547 self.index_options.inverted_index.ignore_column_ids.iter(),
548 );
549 let mut index_targets: Vec<_> = indexed_column_ids
550 .into_iter()
551 .map(IndexTarget::ColumnId)
552 .collect();
553 index_targets.extend(self.collect_json_inverted_index_targets());
554
555 if index_targets.is_empty() {
556 debug!(
557 "No columns to be indexed, skip creating inverted index, region_id: {}, file_id: {}",
558 self.metadata.region_id, file_id,
559 );
560 return None;
561 }
562
563 let Some(mut segment_row_count) =
564 NonZeroUsize::new(self.index_options.inverted_index.segment_row_count)
565 else {
566 warn!(
567 "Segment row count is 0, skip creating index, region_id: {}, file_id: {}",
568 self.metadata.region_id, file_id,
569 );
570 return None;
571 };
572
573 if let Some(row_group_size) = row_group_size.and_then(NonZeroUsize::new)
575 && row_group_size.get() % segment_row_count.get() != 0
576 {
577 segment_row_count = row_group_size;
578 }
579
580 let indexer = InvertedIndexer::new(
581 file_id,
582 &self.metadata,
583 self.intermediate_manager.clone(),
584 self.inverted_index_config.mem_threshold_on_create(),
585 segment_row_count,
586 index_targets,
587 );
588
589 Some(indexer)
590 }
591
592 fn collect_json_inverted_index_targets(&self) -> Vec<IndexTarget> {
597 if !matches!(
599 self.build_type,
600 IndexBuildType::Flush | IndexBuildType::Compact
601 ) {
602 return Vec::new();
603 }
604
605 let mut index_targets = Vec::new();
606 for col in &self.metadata.column_metadatas {
607 if !col.column_schema.data_type.is_json2() {
608 continue;
609 }
610 let Ok(Some(extension)) = col.column_schema.extension_type::<Json2ExtensionType>()
611 else {
612 continue;
613 };
614 for hint in extension.metadata().json_settings().type_hints() {
615 if !hint.inverted_index {
616 continue;
617 }
618 if !(hint.data_type.is_numeric()
620 || hint.data_type.is_boolean()
621 || hint.data_type.is_string())
622 {
623 continue;
624 }
625 match IndexTarget::new_json_path(col.column_id, hint.path.clone()) {
626 Ok(target) => index_targets.push(target),
627 Err(err) => warn!(err; "Skipping invalid JSON inverted index target"),
628 }
629 }
630 }
631
632 index_targets
633 }
634
635 async fn build_fulltext_indexer(&self, file_id: FileId) -> Option<FulltextIndexer> {
636 let create = match self.build_type {
637 IndexBuildType::Flush => self.fulltext_index_config.create_on_flush.auto(),
638 IndexBuildType::Compact => self.fulltext_index_config.create_on_compaction.auto(),
639 _ => true,
640 };
641
642 if !create {
643 debug!(
644 "Skip creating full-text index due to config, region_id: {}, file_id: {}",
645 self.metadata.region_id, file_id,
646 );
647 return None;
648 }
649
650 let mem_limit = self.fulltext_index_config.mem_threshold_on_create();
651 let creator = FulltextIndexer::new(
652 &self.metadata.region_id,
653 &file_id,
654 &self.intermediate_manager,
655 &self.metadata,
656 self.fulltext_index_config.compress,
657 mem_limit,
658 self.build_type.clone(),
659 )
660 .await;
661
662 let err = match creator {
663 Ok(creator) => {
664 if creator.is_none() {
665 debug!(
666 "Skip creating full-text index due to no columns require indexing, region_id: {}, file_id: {}",
667 self.metadata.region_id, file_id,
668 );
669 }
670 return creator;
671 }
672 Err(err) => err,
673 };
674
675 if cfg!(any(test, feature = "test")) {
676 panic!(
677 "Failed to create full-text indexer, region_id: {}, file_id: {}, err: {:?}",
678 self.metadata.region_id, file_id, err
679 );
680 } else {
681 warn!(
682 err; "Failed to create full-text indexer, region_id: {}, file_id: {}",
683 self.metadata.region_id, file_id,
684 );
685 }
686
687 None
688 }
689
690 fn build_bloom_filter_indexer(&self, file_id: FileId) -> Option<BloomFilterIndexer> {
691 let create = match self.build_type {
692 IndexBuildType::Flush => self.bloom_filter_index_config.create_on_flush.auto(),
693 IndexBuildType::Compact => self.bloom_filter_index_config.create_on_compaction.auto(),
694 _ => true,
695 };
696
697 if !create {
698 debug!(
699 "Skip creating bloom filter due to config, region_id: {}, file_id: {}",
700 self.metadata.region_id, file_id,
701 );
702 return None;
703 }
704
705 let mem_limit = self.bloom_filter_index_config.mem_threshold_on_create();
706 let indexer = BloomFilterIndexer::new(
707 file_id,
708 &self.metadata,
709 self.intermediate_manager.clone(),
710 mem_limit,
711 self.build_type.clone(),
712 );
713
714 let err = match indexer {
715 Ok(indexer) => {
716 if indexer.is_none() {
717 debug!(
718 "Skip creating bloom filter due to no columns require indexing, region_id: {}, file_id: {}",
719 self.metadata.region_id, file_id,
720 );
721 }
722 return indexer;
723 }
724 Err(err) => err,
725 };
726
727 if cfg!(any(test, feature = "test")) {
728 panic!(
729 "Failed to create bloom filter, region_id: {}, file_id: {}, err: {:?}",
730 self.metadata.region_id, file_id, err
731 );
732 } else {
733 warn!(
734 err; "Failed to create bloom filter, region_id: {}, file_id: {}",
735 self.metadata.region_id, file_id,
736 );
737 }
738
739 None
740 }
741}
742
743#[derive(Debug, Clone, IntoStaticStr, PartialEq)]
745pub enum IndexBuildType {
746 SchemaChange,
748 Flush,
750 Compact,
752 Manual,
754}
755
756impl IndexBuildType {
757 fn as_str(&self) -> &'static str {
758 self.into()
759 }
760
761 fn priority(&self) -> u8 {
763 match self {
764 IndexBuildType::Manual => 3,
765 IndexBuildType::SchemaChange => 2,
766 IndexBuildType::Flush => 1,
767 IndexBuildType::Compact => 0,
768 }
769 }
770}
771
772impl From<OperationType> for IndexBuildType {
773 fn from(op_type: OperationType) -> Self {
774 match op_type {
775 OperationType::Flush => IndexBuildType::Flush,
776 OperationType::Compact => IndexBuildType::Compact,
777 }
778 }
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Hash)]
783pub enum IndexBuildOutcome {
784 Finished,
785 Aborted(String),
786}
787
788enum IndexBuildReport {
790 Done(IndexBuildOutcome),
792 Deferred,
795}
796
797pub type ResultMpscSender = Sender<Result<IndexBuildOutcome>>;
799
800#[derive(Clone)]
801pub struct IndexBuildTask {
802 pub region_id: RegionId,
804 pub file: FileHandle,
806 pub(crate) target_region_metadata: RegionMetadataRef,
814 pub(crate) source: IndexBuildSource,
816 pub(crate) index_options: IndexOptions,
818 pub reason: IndexBuildType,
819 pub access_layer: AccessLayerRef,
820 pub(crate) listener: WorkerListener,
821 pub(crate) manifest_ctx: ManifestContextRef,
822 pub write_cache: Option<WriteCacheRef>,
823 pub cache_manager: Option<CacheManagerRef>,
824 pub file_purger: FilePurgerRef,
825 pub indexer_builder: Arc<dyn IndexerBuilder + Send + Sync>,
828 pub(crate) request_sender: Sender<WorkerRequestWithTime>,
830 pub(crate) result_senders: Vec<ResultMpscSender>,
833}
834
835impl std::fmt::Debug for IndexBuildTask {
836 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
837 f.debug_struct("IndexBuildTask")
838 .field("region_id", &self.region_id)
839 .field("origin_region_id", &self.source.file_meta.region_id)
840 .field("file_id", &self.source.file_meta.file_id)
841 .field("schema_version", &self.source.schema_version)
842 .field("reason", &self.reason)
843 .finish()
844 }
845}
846
847impl IndexBuildTask {
848 pub async fn on_success(&self, outcome: IndexBuildOutcome) {
850 for sender in &self.result_senders {
851 let _ = sender.send(Ok(outcome.clone())).await;
852 }
853 }
854
855 pub async fn on_failure(&self, err: Arc<Error>) {
857 for sender in &self.result_senders {
858 let _ = sender
859 .send(Err(err.clone()).context(BuildIndexAsyncSnafu {
860 region_id: self.region_id,
861 }))
862 .await;
863 }
864 }
865
866 fn into_index_build_job(mut self, version_control: VersionControlRef) -> Job {
867 Box::pin(async move {
868 self.do_index_build(version_control).await;
869 })
870 }
871
872 async fn do_index_build(&mut self, version_control: VersionControlRef) {
873 self.listener
874 .on_index_build_begin(self.region_file_id())
875 .await;
876 let result = if self.reason == IndexBuildType::Compact {
877 let mut task = self.clone();
878 match common_runtime::spawn_compact(
880 async move { task.index_build(version_control).await },
881 )
882 .await
883 {
884 Ok(result) => result,
885 Err(err) => Err(err).context(JoinSnafu),
886 }
887 } else {
888 self.index_build(version_control).await
889 };
890 match result {
891 Ok(IndexBuildReport::Done(outcome)) => {
892 if let IndexBuildOutcome::Aborted(reason) = &outcome {
893 debug!("Index build aborted: {}", reason);
894 }
895 self.on_success(outcome).await
896 }
897 Ok(IndexBuildReport::Deferred) => {}
898 Err(e) => {
899 warn!(
900 e; "Index build task failed, region: {}, file_id: {}",
901 self.region_id, self.source.file_meta.file_id,
902 );
903 self.on_failure(e.into()).await
904 }
905 }
906 self.notify_worker(BackgroundNotify::IndexBuildStopped(IndexBuildStopped {
907 file_id: self.source.file_meta.file_id,
908 }))
909 .await;
910 }
911
912 fn region_file_id(&self) -> RegionFileId {
913 RegionFileId::new(
914 self.source.file_meta.region_id,
915 self.source.file_meta.file_id,
916 )
917 }
918
919 async fn notify_worker(&self, notify: BackgroundNotify) {
920 let worker_request = WorkerRequest::Background {
921 region_id: self.region_id,
922 notify,
923 };
924 let _ = self
925 .request_sender
926 .send(WorkerRequestWithTime::new(worker_request))
927 .await;
928 }
929
930 fn aborted(&self, reason: &str) -> IndexBuildReport {
934 IndexBuildReport::Done(IndexBuildOutcome::Aborted(format!(
935 "{reason}, region: {}, file_id: {}",
936 self.region_id, self.source.file_meta.file_id
937 )))
938 }
939
940 async fn abort_and_notify(
941 &self,
942 region_file_id: RegionFileId,
943 reason: &str,
944 ) -> IndexBuildReport {
945 self.listener.on_index_build_abort(region_file_id).await;
946 self.aborted(reason)
947 }
948
949 async fn check_sst_file_exists(&self, version_control: &VersionControlRef) -> bool {
951 let file_id = self.source.file_meta.file_id;
952 let level = self.source.file_meta.level;
953 let version = version_control.current().version;
955
956 let Some(level_files) = version.ssts.levels().get(level as usize) else {
957 warn!(
958 "File id {} not found in level {} for index build, region: {}",
959 file_id, level, self.region_id
960 );
961 return false;
962 };
963
964 match level_files.files.get(&file_id) {
965 Some(handle) if !handle.is_deleted() && !handle.compacting() => {
966 true
970 }
971 _ => {
972 warn!(
973 "File id {} not found in region version for index build, region: {}",
974 file_id, self.region_id
975 );
976 false
977 }
978 }
979 }
980
981 async fn index_build(
982 &mut self,
983 version_control: VersionControlRef,
984 ) -> Result<IndexBuildReport> {
985 let region_file_id = self.region_file_id();
988 if let ControlFlow::Break(report) =
989 self.prepare_source(region_file_id, &version_control).await
990 {
991 return Ok(report);
992 }
993 let new_index_version = self
994 .source
995 .file_meta
996 .index_version()
997 .map_or(0, |version| version + 1);
998 let (mut indexer, index_output) =
999 match self.create_index(region_file_id, new_index_version).await? {
1000 ControlFlow::Continue(created) => created,
1001 ControlFlow::Break(report) => return Ok(report),
1002 };
1003
1004 if !self.check_sst_file_exists(&version_control).await {
1006 indexer.abort().await;
1008 return Ok(self
1009 .abort_and_notify(region_file_id, "SST file not found during index build")
1010 .await);
1011 }
1012
1013 self.publish(region_file_id, index_output, new_index_version)
1014 .await
1015 }
1016
1017 async fn prepare_source(
1020 &mut self,
1021 region_file_id: RegionFileId,
1022 version_control: &VersionControlRef,
1023 ) -> ControlFlow<IndexBuildReport> {
1024 if !self.check_sst_file_exists(version_control).await {
1026 return ControlFlow::Break(
1027 self.abort_and_notify(region_file_id, "SST file not found during index build")
1028 .await,
1029 );
1030 }
1031
1032 let current_file_meta = self
1035 .manifest_ctx
1036 .manifest()
1037 .await
1038 .files
1039 .get(&self.source.file_meta.file_id)
1040 .cloned();
1041 let Some(current_file_meta) = current_file_meta else {
1042 return ControlFlow::Break(
1043 self.abort_and_notify(
1044 region_file_id,
1045 "SST file not found in manifest during index build",
1046 )
1047 .await,
1048 );
1049 };
1050 if self.reason == IndexBuildType::Manual
1051 && current_file_meta.is_index_consistent_with_region(
1052 &self.target_region_metadata.column_metadatas,
1053 &self.index_options,
1054 )
1055 {
1056 return ControlFlow::Break(
1057 self.abort_and_notify(region_file_id, "Index is already up to date")
1058 .await,
1059 );
1060 }
1061 self.source.file_meta = current_file_meta;
1062 ControlFlow::Continue(())
1063 }
1064
1065 async fn create_index(
1067 &self,
1068 region_file_id: RegionFileId,
1069 new_index_version: u64,
1070 ) -> Result<ControlFlow<IndexBuildReport, (Indexer, IndexOutput)>> {
1071 let mut parquet_reader = self
1072 .access_layer
1073 .read_sst(self.file.clone()) .expected_metadata(Some(self.target_region_metadata.clone()))
1075 .build()
1076 .await?;
1077
1078 let row_group_size = parquet_reader.as_ref().and_then(|reader| {
1079 reader
1080 .parquet_metadata()
1081 .row_groups()
1082 .first()
1083 .map(|row_group| row_group.num_rows() as usize)
1084 .filter(|size| *size > 0)
1085 });
1086
1087 let mut indexer = self
1088 .indexer_builder
1089 .build(region_file_id, new_index_version, row_group_size)
1090 .await;
1091 if indexer.is_empty() {
1094 return Ok(ControlFlow::Break(self.aborted("No index to build")));
1095 }
1096
1097 if let Some(mut parquet_reader) = parquet_reader.take() {
1098 loop {
1100 match parquet_reader.next_record_batch().await {
1101 Ok(Some(batch)) => {
1102 indexer.update_flat(&batch).await;
1103 }
1104 Ok(None) => break,
1105 Err(e) => {
1106 indexer.abort().await;
1107 return Err(e);
1108 }
1109 }
1110 }
1111 }
1112 let index_output = indexer.finish().await;
1113
1114 if index_output.file_size == 0 {
1115 return Ok(ControlFlow::Break(self.aborted("No index was created")));
1117 }
1118 Ok(ControlFlow::Continue((indexer, index_output)))
1119 }
1120
1121 async fn publish(
1123 &self,
1124 region_file_id: RegionFileId,
1125 index_output: IndexOutput,
1126 new_index_version: u64,
1127 ) -> Result<IndexBuildReport> {
1128 self.maybe_upload_index_file(
1130 index_output.clone(),
1131 region_file_id.file_id(),
1132 new_index_version,
1133 )
1134 .await?;
1135
1136 self.listener
1137 .on_index_build_before_manifest_commit(region_file_id)
1138 .await;
1139
1140 match self
1143 .update_manifest(index_output, new_index_version)
1144 .await?
1145 {
1146 IndexPublication::Committed {
1147 manifest_version,
1148 file_meta,
1149 } => {
1150 self.listener
1151 .on_index_build_manifest_committed(region_file_id)
1152 .await;
1153 self.notify_worker(BackgroundNotify::IndexBuildFinished(IndexBuildFinished {
1154 manifest_version,
1155 file_meta,
1156 result_senders: self.result_senders.clone(),
1157 }))
1158 .await;
1159 Ok(IndexBuildReport::Deferred)
1160 }
1161 IndexPublication::Stale(stale) => Ok(self
1162 .handle_stale_publication(region_file_id, stale, new_index_version)
1163 .await),
1164 }
1165 }
1166
1167 async fn handle_stale_publication(
1168 &self,
1169 region_file_id: RegionFileId,
1170 stale: IndexPublicationStale,
1171 new_index_version: u64,
1172 ) -> IndexBuildReport {
1173 INDEX_PUBLICATION_STALE_TOTAL
1174 .with_label_values(&["manifest_commit"])
1175 .inc();
1176 cleanup_stale_index_caches(
1180 RegionIndexId::new(region_file_id, new_index_version),
1181 &self.access_layer,
1182 self.cache_manager.as_ref(),
1183 self.write_cache.as_ref(),
1184 )
1185 .await;
1186 if stale != IndexPublicationStale::SchemaChanged {
1187 return self
1188 .abort_and_notify(
1189 region_file_id,
1190 "Index build source changed before publication",
1191 )
1192 .await;
1193 }
1194
1195 self.listener.on_index_build_abort(region_file_id).await;
1196 self.notify_worker(BackgroundNotify::IndexBuildRetry(BuildIndexRequest {
1201 region_id: self.region_id,
1202 build_type: IndexBuildType::SchemaChange,
1203 file_metas: vec![self.source.file_meta.clone()],
1204 result_senders: self.result_senders.clone(),
1205 }))
1206 .await;
1207 IndexBuildReport::Deferred
1208 }
1209
1210 async fn maybe_upload_index_file(
1211 &self,
1212 output: IndexOutput,
1213 index_file_id: FileId,
1214 index_version: u64,
1215 ) -> Result<()> {
1216 if let Some(write_cache) = &self.write_cache {
1217 let file_id = self.source.file_meta.file_id;
1218 let region_id = self.source.file_meta.region_id;
1219 let remote_store = self.access_layer.object_store();
1220 let mut upload_tracker = UploadTracker::new(region_id);
1221 let mut err = None;
1222 let puffin_key =
1223 IndexKey::new(region_id, index_file_id, FileType::Puffin(output.version));
1224 let index_id = RegionIndexId::new(RegionFileId::new(region_id, file_id), index_version);
1225 let puffin_path = RegionFilePathFactory::new(
1226 self.access_layer.table_dir().to_string(),
1227 self.access_layer.path_type(),
1228 )
1229 .build_index_file_path_with_version(index_id);
1230 if let Err(e) = write_cache
1231 .upload(
1234 puffin_key,
1235 &puffin_path,
1236 remote_store,
1237 UploadOptions {
1238 op_type: OperationType::Compact,
1239 write_buffer_size: crate::sst::DEFAULT_WRITE_BUFFER_SIZE,
1240 },
1241 )
1242 .await
1243 {
1244 err = Some(e);
1245 }
1246 upload_tracker.push_uploaded_file(puffin_path);
1247 if let Some(err) = err {
1248 upload_tracker
1250 .clean(
1251 &[SstInfo {
1252 file_id,
1253 index_metadata: output,
1254 ..Default::default()
1255 }],
1256 &write_cache.file_cache(),
1257 remote_store,
1258 )
1259 .await;
1260 return Err(err);
1261 }
1262 } else {
1263 debug!("write cache is not available, skip uploading index file");
1264 }
1265 Ok(())
1266 }
1267
1268 async fn update_manifest(
1269 &self,
1270 output: IndexOutput,
1271 new_index_version: u64,
1272 ) -> Result<IndexPublication> {
1273 let mut updated = self.source.file_meta.clone();
1274 updated.available_indexes = output.build_available_indexes();
1275 updated.indexes = output.build_indexes();
1276 updated.index_file_size = output.file_size;
1277 updated.index_version = new_index_version;
1278 let publication = self
1279 .manifest_ctx
1280 .update_manifest_for_index(&self.source, updated)
1281 .await?;
1282 if let IndexPublication::Committed {
1283 manifest_version, ..
1284 } = &publication
1285 {
1286 info!(
1287 "Successfully update manifest version to {}, region: {}, reason: {}",
1288 manifest_version,
1289 self.region_id,
1290 self.reason.as_str()
1291 );
1292 }
1293 Ok(publication)
1294 }
1295}
1296
1297impl PartialEq for IndexBuildTask {
1298 fn eq(&self, other: &Self) -> bool {
1299 self.reason.priority() == other.reason.priority()
1300 }
1301}
1302
1303impl Eq for IndexBuildTask {}
1304
1305impl PartialOrd for IndexBuildTask {
1306 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1307 Some(self.cmp(other))
1308 }
1309}
1310
1311impl Ord for IndexBuildTask {
1312 fn cmp(&self, other: &Self) -> Ordering {
1313 self.reason.priority().cmp(&other.reason.priority())
1314 }
1315}
1316
1317#[derive(Clone)]
1318struct PendingIndexBuild {
1319 task: IndexBuildTask,
1320 version_control: VersionControlRef,
1321}
1322
1323impl PartialEq for PendingIndexBuild {
1324 fn eq(&self, other: &Self) -> bool {
1325 self.task == other.task
1326 }
1327}
1328
1329impl Eq for PendingIndexBuild {}
1330
1331impl PartialOrd for PendingIndexBuild {
1332 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1333 Some(self.cmp(other))
1334 }
1335}
1336
1337impl Ord for PendingIndexBuild {
1338 fn cmp(&self, other: &Self) -> Ordering {
1339 self.task.cmp(&other.task)
1340 }
1341}
1342
1343impl PendingIndexBuild {
1344 fn supersedes(&self, other: &Self) -> bool {
1347 match self
1348 .task
1349 .source
1350 .schema_version
1351 .cmp(&other.task.source.schema_version)
1352 {
1353 Ordering::Greater => true,
1354 Ordering::Less => false,
1355 Ordering::Equal => match self
1356 .task
1357 .source
1358 .file_meta
1359 .index_version()
1360 .cmp(&other.task.source.file_meta.index_version())
1361 {
1362 Ordering::Greater => true,
1363 Ordering::Less => false,
1364 Ordering::Equal => self.task.reason.priority() > other.task.reason.priority(),
1365 },
1366 }
1367 }
1368}
1369
1370#[derive(Default)]
1371struct PendingIndexBuilds {
1372 tasks: HashMap<FileId, PendingIndexBuild>,
1373}
1374
1375impl PendingIndexBuilds {
1376 fn insert(&mut self, mut pending: PendingIndexBuild) -> Option<IndexBuildTask> {
1381 let file_id = pending.task.source.file_meta.file_id;
1382 match self.tasks.entry(file_id) {
1383 std::collections::hash_map::Entry::Vacant(entry) => {
1384 entry.insert(pending);
1385 None
1386 }
1387 std::collections::hash_map::Entry::Occupied(mut entry) => {
1388 let survivor = entry.get_mut();
1389 if pending.supersedes(survivor) {
1390 std::mem::swap(survivor, &mut pending);
1391 }
1392 let senders = std::mem::take(&mut pending.task.result_senders);
1393 survivor.task.result_senders.extend(senders);
1394 Some(pending.task)
1395 }
1396 }
1397 }
1398
1399 fn highest_ready(&self, building_files: &HashSet<FileId>) -> Option<PendingIndexBuild> {
1400 self.tasks
1401 .values()
1402 .filter(|pending| !building_files.contains(&pending.task.source.file_meta.file_id))
1403 .max()
1404 .cloned()
1405 }
1406
1407 fn remove(&mut self, file_id: FileId) {
1408 self.tasks.remove(&file_id);
1409 }
1410
1411 fn drain(&mut self) -> impl Iterator<Item = PendingIndexBuild> {
1412 std::mem::take(&mut self.tasks).into_values()
1413 }
1414
1415 #[cfg(test)]
1416 fn len(&self) -> usize {
1417 self.tasks.len()
1418 }
1419
1420 fn is_empty(&self) -> bool {
1421 self.tasks.is_empty()
1422 }
1423}
1424
1425struct IndexBuildStatus {
1427 building_files: HashSet<FileId>,
1428 pending_tasks: PendingIndexBuilds,
1430 retiring: bool,
1433}
1434
1435impl IndexBuildStatus {
1436 fn new() -> Self {
1437 IndexBuildStatus {
1438 building_files: HashSet::new(),
1439 pending_tasks: PendingIndexBuilds::default(),
1440 retiring: false,
1441 }
1442 }
1443
1444 async fn fail_pending(&mut self, err: Arc<Error>) {
1445 for pending in self.pending_tasks.drain() {
1446 pending.task.on_failure(err.clone()).await;
1447 }
1448 }
1449
1450 async fn retire(&mut self, err: Arc<Error>) {
1451 self.retiring = !self.building_files.is_empty();
1452 self.fail_pending(err).await;
1453 }
1454}
1455
1456pub struct IndexBuildScheduler {
1457 scheduler: SchedulerRef,
1459 region_status: HashMap<RegionId, IndexBuildStatus>,
1461 files_limit: usize,
1463}
1464
1465impl IndexBuildScheduler {
1467 pub fn new(scheduler: SchedulerRef, files_limit: usize) -> Self {
1468 IndexBuildScheduler {
1469 scheduler,
1470 region_status: HashMap::new(),
1471 files_limit,
1472 }
1473 }
1474
1475 pub(crate) async fn schedule_build(
1476 &mut self,
1477 version_control: &VersionControlRef,
1478 task: IndexBuildTask,
1479 ) {
1480 let status = self
1481 .region_status
1482 .entry(task.region_id)
1483 .or_insert_with(IndexBuildStatus::new);
1484
1485 let file_id = task.source.file_meta.file_id;
1486 let region_file_id = RegionFileId::new(
1487 task.source.file_meta.region_id,
1488 task.source.file_meta.file_id,
1489 );
1490 let can_wait_for_active = status.retiring
1493 || matches!(
1494 task.reason,
1495 IndexBuildType::SchemaChange | IndexBuildType::Manual
1496 );
1497 let (rejected, coalesced) =
1498 if status.building_files.contains(&file_id) && !can_wait_for_active {
1499 (Some(task), None)
1500 } else {
1501 let pending = PendingIndexBuild {
1502 task,
1503 version_control: version_control.clone(),
1504 };
1505 (None, status.pending_tasks.insert(pending))
1506 };
1507 let should_schedule = !status.retiring;
1508
1509 if let Some(rejected) = rejected {
1510 debug!(
1511 "Rejecting index build because region file {:?} is already being built",
1512 region_file_id
1513 );
1514 rejected
1515 .on_success(IndexBuildOutcome::Aborted(format!(
1516 "Index is already being built for region file {:?}",
1517 region_file_id
1518 )))
1519 .await;
1520 rejected.listener.on_index_build_abort(region_file_id).await;
1521 }
1522
1523 if let Some(coalesced) = coalesced {
1524 debug!(
1525 "Coalescing redundant index build task for region file {:?}",
1526 region_file_id
1527 );
1528 coalesced
1529 .on_success(IndexBuildOutcome::Aborted(format!(
1530 "Index build was coalesced for region file {:?}",
1531 region_file_id
1532 )))
1533 .await;
1534 coalesced
1535 .listener
1536 .on_index_build_abort(region_file_id)
1537 .await;
1538 }
1539
1540 if should_schedule {
1541 self.schedule_next_build_batch();
1542 }
1543 }
1544
1545 fn schedule_next_build_batch(&mut self) {
1547 let mut building_count = self
1548 .region_status
1549 .values()
1550 .map(|status| status.building_files.len())
1551 .sum::<usize>();
1552
1553 while building_count < self.files_limit {
1554 let Some(pending) = self.find_next_task() else {
1555 break;
1556 };
1557
1558 let task = pending.task;
1559 let region_id = task.region_id;
1560 let file_id = task.source.file_meta.file_id;
1561 let job = task.clone().into_index_build_job(pending.version_control);
1562 match self.scheduler.schedule(job) {
1563 Ok(()) => {
1564 if let Some(status) = self.region_status.get_mut(®ion_id) {
1565 status.pending_tasks.remove(file_id);
1566 status.building_files.insert(file_id);
1567 building_count += 1;
1568 } else {
1569 error!(
1570 "Region status not found when scheduling index build task, region: {}",
1571 region_id
1572 );
1573 }
1574 }
1575 Err(err) => {
1576 error!(
1577 err;
1578 "Failed to schedule index build job, region: {}, file_id: {}",
1579 region_id,
1580 file_id
1581 );
1582 if let Some(status) = self.region_status.get_mut(®ion_id) {
1583 status.pending_tasks.remove(file_id);
1584 }
1585 common_runtime::spawn_global(async move {
1586 task.on_failure(Arc::new(err)).await;
1587 });
1588 }
1589 }
1590 }
1591
1592 self.region_status.retain(|_, status| {
1593 !status.building_files.is_empty() || !status.pending_tasks.is_empty()
1594 });
1595 }
1596
1597 fn find_next_task(&self) -> Option<PendingIndexBuild> {
1599 self.region_status
1600 .values()
1601 .filter(|status| !status.retiring)
1602 .filter_map(|status| status.pending_tasks.highest_ready(&status.building_files))
1603 .max()
1604 }
1605
1606 pub(crate) fn on_task_stopped(&mut self, region_id: RegionId, file_id: FileId) {
1607 if let Some(status) = self.region_status.get_mut(®ion_id) {
1608 if !status.building_files.remove(&file_id) {
1609 debug!(
1610 "Index build task is not tracked as building, region: {}, file: {}",
1611 region_id, file_id
1612 );
1613 return;
1614 }
1615 if status.building_files.is_empty() && status.pending_tasks.is_empty() {
1616 self.region_status.remove(®ion_id);
1618 } else if status.building_files.is_empty() {
1619 status.retiring = false;
1621 }
1622 }
1623
1624 self.schedule_next_build_batch();
1625 }
1626
1627 pub(crate) async fn on_region_dropped(&mut self, region_id: RegionId) {
1629 self.retire_region(
1630 region_id,
1631 Arc::new(RegionDroppedSnafu { region_id }.build()),
1632 )
1633 .await;
1634 }
1635
1636 pub(crate) async fn on_region_closed(&mut self, region_id: RegionId) {
1638 self.retire_region(region_id, Arc::new(RegionClosedSnafu { region_id }.build()))
1639 .await;
1640 }
1641
1642 pub(crate) async fn on_region_truncated(&mut self, region_id: RegionId) {
1644 self.retire_region(
1645 region_id,
1646 Arc::new(RegionTruncatedSnafu { region_id }.build()),
1647 )
1648 .await;
1649 }
1650
1651 async fn retire_region(&mut self, region_id: RegionId, err: Arc<Error>) {
1652 let Some(status) = self.region_status.get_mut(®ion_id) else {
1653 return;
1654 };
1655 status.retire(err).await;
1656 if status.building_files.is_empty() {
1657 self.region_status.remove(®ion_id);
1658 }
1659 }
1660}
1661
1662#[cfg(test)]
1663mod tests {
1664 use std::sync::Arc;
1665
1666 use api::v1::SemanticType;
1667 use common_base::readable_size::ReadableSize;
1668 use datafusion_common::HashMap;
1669 use datatypes::data_type::ConcreteDataType;
1670 use datatypes::schema::{
1671 ColumnSchema, FulltextOptions, SkippingIndexOptions, SkippingIndexType,
1672 };
1673 use datatypes::value::Value;
1674 use index::inverted_index::format::reader::InvertedIndexReader;
1675 use object_store::ObjectStore;
1676 use object_store::services::Memory;
1677 use partition::expr::col;
1678 use puffin::puffin_manager::{PuffinManager, PuffinReader};
1679 use puffin_manager::PuffinManagerFactory;
1680 use smallvec::smallvec;
1681 use store_api::metadata::{ColumnMetadata, RegionMetadataBuilder};
1682 use tokio::sync::mpsc;
1683
1684 use super::*;
1685 use crate::access_layer::{FilePathProvider, Metrics, SstWriteRequest, WriteType};
1686 use crate::cache::write_cache::WriteCache;
1687 use crate::config::{FulltextIndexConfig, IndexBuildMode, MitoConfig, Mode};
1688 use crate::manifest::action::{RegionEdit, RegionMetaAction, RegionMetaActionList};
1689 use crate::memtable::time_partition::TimePartitions;
1690 use crate::region::RegionLeaderState;
1691 use crate::region::version::{VersionBuilder, VersionControl};
1692 use crate::schedule::scheduler::{LocalScheduler, Scheduler};
1693 use crate::sst::file::{FileMeta, RegionFileId};
1694 use crate::sst::file_purger::NoopFilePurger;
1695 use crate::sst::location;
1696 use crate::sst::parquet::WriteOptions;
1697 use crate::test_util::memtable_util::EmptyMemtableBuilder;
1698 use crate::test_util::scheduler_util::{SchedulerEnv, VecScheduler};
1699 use crate::test_util::sst_util::{
1700 new_flat_source_from_record_batches, new_record_batch_by_range, sst_region_metadata,
1701 };
1702
1703 struct MetaConfig {
1704 with_inverted: bool,
1705 with_fulltext: bool,
1706 with_skipping_bloom: bool,
1707 }
1708
1709 async fn recv_index_build_finished(
1711 rx: &mut mpsc::Receiver<WorkerRequestWithTime>,
1712 ) -> IndexBuildFinished {
1713 match rx.recv().await.unwrap().request {
1714 WorkerRequest::Background {
1715 notify: BackgroundNotify::IndexBuildFinished(finished),
1716 ..
1717 } => finished,
1718 other => panic!("Expect index build finished, got: {other:?}"),
1719 }
1720 }
1721
1722 async fn seed_manifest_file(manifest_ctx: &ManifestContextRef, file_meta: &FileMeta) {
1723 manifest_ctx
1724 .update_manifest(
1725 RegionLeaderState::Writable,
1726 RegionMetaActionList::with_action(RegionMetaAction::Edit(RegionEdit {
1727 files_to_add: vec![file_meta.clone()],
1728 files_to_remove: Vec::new(),
1729 timestamp_ms: None,
1730 flushed_sequence: None,
1731 flushed_entry_id: None,
1732 committed_sequence: None,
1733 compaction_time_window: None,
1734 })),
1735 false,
1736 )
1737 .await
1738 .unwrap();
1739 }
1740
1741 fn mock_region_metadata(
1742 MetaConfig {
1743 with_inverted,
1744 with_fulltext,
1745 with_skipping_bloom,
1746 }: MetaConfig,
1747 ) -> RegionMetadataRef {
1748 let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 2));
1749 let mut column_schema = ColumnSchema::new("a", ConcreteDataType::int64_datatype(), false);
1750 if with_inverted {
1751 column_schema = column_schema.with_inverted_index(true);
1752 }
1753 builder
1754 .push_column_metadata(ColumnMetadata {
1755 column_schema,
1756 semantic_type: SemanticType::Field,
1757 column_id: 1,
1758 })
1759 .push_column_metadata(ColumnMetadata {
1760 column_schema: ColumnSchema::new("b", ConcreteDataType::float64_datatype(), false),
1761 semantic_type: SemanticType::Field,
1762 column_id: 2,
1763 })
1764 .push_column_metadata(ColumnMetadata {
1765 column_schema: ColumnSchema::new(
1766 "c",
1767 ConcreteDataType::timestamp_millisecond_datatype(),
1768 false,
1769 ),
1770 semantic_type: SemanticType::Timestamp,
1771 column_id: 3,
1772 });
1773
1774 if with_fulltext {
1775 let column_schema =
1776 ColumnSchema::new("text", ConcreteDataType::string_datatype(), true)
1777 .with_fulltext_options(FulltextOptions {
1778 enable: true,
1779 ..Default::default()
1780 })
1781 .unwrap();
1782
1783 let column = ColumnMetadata {
1784 column_schema,
1785 semantic_type: SemanticType::Field,
1786 column_id: 4,
1787 };
1788
1789 builder.push_column_metadata(column);
1790 }
1791
1792 if with_skipping_bloom {
1793 let column_schema =
1794 ColumnSchema::new("bloom", ConcreteDataType::string_datatype(), false)
1795 .with_skipping_options(SkippingIndexOptions::new_unchecked(
1796 42,
1797 0.01,
1798 SkippingIndexType::BloomFilter,
1799 ))
1800 .unwrap();
1801
1802 let column = ColumnMetadata {
1803 column_schema,
1804 semantic_type: SemanticType::Field,
1805 column_id: 5,
1806 };
1807
1808 builder.push_column_metadata(column);
1809 }
1810
1811 Arc::new(builder.build().unwrap())
1812 }
1813
1814 fn mock_object_store() -> ObjectStore {
1815 ObjectStore::new(Memory::default()).unwrap()
1816 }
1817
1818 async fn mock_intm_mgr(path: impl AsRef<str>) -> IntermediateManager {
1819 IntermediateManager::init_fs(path).await.unwrap()
1820 }
1821
1822 fn random_region_file_id() -> RegionFileId {
1823 RegionFileId::new(RegionId::new(1, 2), FileId::random())
1824 }
1825
1826 struct NoopPathProvider;
1827
1828 impl FilePathProvider for NoopPathProvider {
1829 fn build_index_file_path(&self, _file_id: RegionFileId) -> String {
1830 unreachable!()
1831 }
1832
1833 fn build_index_file_path_with_version(&self, _index_id: RegionIndexId) -> String {
1834 unreachable!()
1835 }
1836
1837 fn build_sst_file_path(&self, _file_id: RegionFileId) -> String {
1838 unreachable!()
1839 }
1840 }
1841
1842 async fn mock_sst_file(
1843 metadata: RegionMetadataRef,
1844 env: &SchedulerEnv,
1845 build_mode: IndexBuildMode,
1846 ) -> SstInfo {
1847 let source = new_flat_source_from_record_batches(vec![
1848 new_record_batch_by_range(&["a", "d"], 0, 60),
1849 new_record_batch_by_range(&["b", "f"], 0, 40),
1850 new_record_batch_by_range(&["b", "h"], 100, 200),
1851 ]);
1852 let mut index_config = MitoConfig::default().index;
1853 index_config.build_mode = build_mode;
1854 let write_request = SstWriteRequest {
1855 op_type: OperationType::Flush,
1856 metadata: metadata.clone(),
1857 source,
1858 max_sequence: None,
1859 sst_write_format: Default::default(),
1860 cache_manager: Default::default(),
1861 preserve_row_sequence: false,
1862 index_options: IndexOptions::default(),
1863 index_config,
1864 inverted_index_config: Default::default(),
1865 fulltext_index_config: Default::default(),
1866 bloom_filter_index_config: Default::default(),
1867 };
1868 let mut metrics = Metrics::new(WriteType::Flush);
1869 env.access_layer
1870 .write_sst(write_request, &WriteOptions::default(), &mut metrics)
1871 .await
1872 .unwrap()
1873 .remove(0)
1874 }
1875
1876 async fn mock_version_control(
1877 metadata: RegionMetadataRef,
1878 file_purger: FilePurgerRef,
1879 files: HashMap<FileId, FileMeta>,
1880 ) -> VersionControlRef {
1881 let mutable = TimePartitions::new(
1882 metadata.clone(),
1883 Arc::new(EmptyMemtableBuilder::default()),
1884 0,
1885 None,
1886 );
1887 let version_builder = VersionBuilder::new(metadata, mutable)
1888 .add_files(file_purger, files.values().cloned())
1889 .build();
1890 Arc::new(VersionControl::new(version_builder))
1891 }
1892
1893 async fn mock_indexer_builder(
1894 metadata: RegionMetadataRef,
1895 env: &SchedulerEnv,
1896 ) -> Arc<dyn IndexerBuilder + Send + Sync> {
1897 let (dir, factory) = PuffinManagerFactory::new_for_test_async("mock_indexer_builder").await;
1898 let intm_manager = mock_intm_mgr(dir.path().to_string_lossy()).await;
1899 let puffin_manager = factory.build(
1900 env.access_layer.object_store().clone(),
1901 RegionFilePathFactory::new(
1902 env.access_layer.table_dir().to_string(),
1903 env.access_layer.path_type(),
1904 ),
1905 );
1906 Arc::new(IndexerBuilderImpl {
1907 build_type: IndexBuildType::Flush,
1908 metadata,
1909 puffin_manager,
1910 write_cache_enabled: false,
1911 intermediate_manager: intm_manager,
1912 index_options: IndexOptions::default(),
1913 inverted_index_config: InvertedIndexConfig::default(),
1914 fulltext_index_config: FulltextIndexConfig::default(),
1915 bloom_filter_index_config: BloomFilterConfig::default(),
1916 })
1917 }
1918
1919 #[tokio::test]
1920 async fn test_build_indexer_basic() {
1921 let (dir, factory) =
1922 PuffinManagerFactory::new_for_test_async("test_build_indexer_basic_").await;
1923 let intm_manager = mock_intm_mgr(dir.path().to_string_lossy()).await;
1924
1925 let metadata = mock_region_metadata(MetaConfig {
1926 with_inverted: true,
1927 with_fulltext: true,
1928 with_skipping_bloom: true,
1929 });
1930 let indexer = IndexerBuilderImpl {
1931 build_type: IndexBuildType::Flush,
1932 metadata,
1933 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
1934 write_cache_enabled: false,
1935 intermediate_manager: intm_manager,
1936 index_options: IndexOptions::default(),
1937 inverted_index_config: InvertedIndexConfig::default(),
1938 fulltext_index_config: FulltextIndexConfig::default(),
1939 bloom_filter_index_config: BloomFilterConfig::default(),
1940 }
1941 .build(random_region_file_id(), 0, Some(1024))
1942 .await;
1943
1944 assert!(indexer.inverted_indexer.is_some());
1945 assert!(indexer.fulltext_indexer.is_some());
1946 assert!(indexer.bloom_filter_indexer.is_some());
1947 }
1948
1949 #[tokio::test]
1950 async fn test_build_indexer_disable_create() {
1951 let (dir, factory) =
1952 PuffinManagerFactory::new_for_test_async("test_build_indexer_disable_create_").await;
1953 let intm_manager = mock_intm_mgr(dir.path().to_string_lossy()).await;
1954
1955 let metadata = mock_region_metadata(MetaConfig {
1956 with_inverted: true,
1957 with_fulltext: true,
1958 with_skipping_bloom: true,
1959 });
1960 let indexer = IndexerBuilderImpl {
1961 build_type: IndexBuildType::Flush,
1962 metadata: metadata.clone(),
1963 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
1964 write_cache_enabled: false,
1965 intermediate_manager: intm_manager.clone(),
1966 index_options: IndexOptions::default(),
1967 inverted_index_config: InvertedIndexConfig {
1968 create_on_flush: Mode::Disable,
1969 ..Default::default()
1970 },
1971 fulltext_index_config: FulltextIndexConfig::default(),
1972 bloom_filter_index_config: BloomFilterConfig::default(),
1973 }
1974 .build(random_region_file_id(), 0, Some(1024))
1975 .await;
1976
1977 assert!(indexer.inverted_indexer.is_none());
1978 assert!(indexer.fulltext_indexer.is_some());
1979 assert!(indexer.bloom_filter_indexer.is_some());
1980
1981 let indexer = IndexerBuilderImpl {
1982 build_type: IndexBuildType::Compact,
1983 metadata: metadata.clone(),
1984 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
1985 write_cache_enabled: false,
1986 intermediate_manager: intm_manager.clone(),
1987 index_options: IndexOptions::default(),
1988 inverted_index_config: InvertedIndexConfig::default(),
1989 fulltext_index_config: FulltextIndexConfig {
1990 create_on_compaction: Mode::Disable,
1991 ..Default::default()
1992 },
1993 bloom_filter_index_config: BloomFilterConfig::default(),
1994 }
1995 .build(random_region_file_id(), 0, Some(1024))
1996 .await;
1997
1998 assert!(indexer.inverted_indexer.is_some());
1999 assert!(indexer.fulltext_indexer.is_none());
2000 assert!(indexer.bloom_filter_indexer.is_some());
2001
2002 let indexer = IndexerBuilderImpl {
2003 build_type: IndexBuildType::Compact,
2004 metadata,
2005 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
2006 write_cache_enabled: false,
2007 intermediate_manager: intm_manager,
2008 index_options: IndexOptions::default(),
2009 inverted_index_config: InvertedIndexConfig::default(),
2010 fulltext_index_config: FulltextIndexConfig::default(),
2011 bloom_filter_index_config: BloomFilterConfig {
2012 create_on_compaction: Mode::Disable,
2013 ..Default::default()
2014 },
2015 }
2016 .build(random_region_file_id(), 0, Some(1024))
2017 .await;
2018
2019 assert!(indexer.inverted_indexer.is_some());
2020 assert!(indexer.fulltext_indexer.is_some());
2021 assert!(indexer.bloom_filter_indexer.is_none());
2022 }
2023
2024 #[tokio::test]
2025 async fn test_build_indexer_no_required() {
2026 let (dir, factory) =
2027 PuffinManagerFactory::new_for_test_async("test_build_indexer_no_required_").await;
2028 let intm_manager = mock_intm_mgr(dir.path().to_string_lossy()).await;
2029
2030 let metadata = mock_region_metadata(MetaConfig {
2031 with_inverted: false,
2032 with_fulltext: true,
2033 with_skipping_bloom: true,
2034 });
2035 let indexer = IndexerBuilderImpl {
2036 build_type: IndexBuildType::Flush,
2037 metadata: metadata.clone(),
2038 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
2039 write_cache_enabled: false,
2040 intermediate_manager: intm_manager.clone(),
2041 index_options: IndexOptions::default(),
2042 inverted_index_config: InvertedIndexConfig::default(),
2043 fulltext_index_config: FulltextIndexConfig::default(),
2044 bloom_filter_index_config: BloomFilterConfig::default(),
2045 }
2046 .build(random_region_file_id(), 0, Some(1024))
2047 .await;
2048
2049 assert!(indexer.inverted_indexer.is_none());
2050 assert!(indexer.fulltext_indexer.is_some());
2051 assert!(indexer.bloom_filter_indexer.is_some());
2052
2053 let metadata = mock_region_metadata(MetaConfig {
2054 with_inverted: true,
2055 with_fulltext: false,
2056 with_skipping_bloom: true,
2057 });
2058 let indexer = IndexerBuilderImpl {
2059 build_type: IndexBuildType::Flush,
2060 metadata: metadata.clone(),
2061 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
2062 write_cache_enabled: false,
2063 intermediate_manager: intm_manager.clone(),
2064 index_options: IndexOptions::default(),
2065 inverted_index_config: InvertedIndexConfig::default(),
2066 fulltext_index_config: FulltextIndexConfig::default(),
2067 bloom_filter_index_config: BloomFilterConfig::default(),
2068 }
2069 .build(random_region_file_id(), 0, Some(1024))
2070 .await;
2071
2072 assert!(indexer.inverted_indexer.is_some());
2073 assert!(indexer.fulltext_indexer.is_none());
2074 assert!(indexer.bloom_filter_indexer.is_some());
2075
2076 let metadata = mock_region_metadata(MetaConfig {
2077 with_inverted: true,
2078 with_fulltext: true,
2079 with_skipping_bloom: false,
2080 });
2081 let indexer = IndexerBuilderImpl {
2082 build_type: IndexBuildType::Flush,
2083 metadata: metadata.clone(),
2084 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
2085 write_cache_enabled: false,
2086 intermediate_manager: intm_manager,
2087 index_options: IndexOptions::default(),
2088 inverted_index_config: InvertedIndexConfig::default(),
2089 fulltext_index_config: FulltextIndexConfig::default(),
2090 bloom_filter_index_config: BloomFilterConfig::default(),
2091 }
2092 .build(random_region_file_id(), 0, Some(1024))
2093 .await;
2094
2095 assert!(indexer.inverted_indexer.is_some());
2096 assert!(indexer.fulltext_indexer.is_some());
2097 assert!(indexer.bloom_filter_indexer.is_none());
2098 }
2099
2100 #[tokio::test]
2101 async fn test_build_indexer_zero_row_group_hint() {
2102 let (dir, factory) =
2103 PuffinManagerFactory::new_for_test_async("test_build_indexer_zero_row_group_").await;
2104 let intm_manager = mock_intm_mgr(dir.path().to_string_lossy()).await;
2105
2106 let metadata = mock_region_metadata(MetaConfig {
2107 with_inverted: true,
2108 with_fulltext: true,
2109 with_skipping_bloom: true,
2110 });
2111 let indexer = IndexerBuilderImpl {
2112 build_type: IndexBuildType::Flush,
2113 metadata,
2114 puffin_manager: factory.build(mock_object_store(), NoopPathProvider),
2115 write_cache_enabled: false,
2116 intermediate_manager: intm_manager,
2117 index_options: IndexOptions::default(),
2118 inverted_index_config: InvertedIndexConfig::default(),
2119 fulltext_index_config: FulltextIndexConfig::default(),
2120 bloom_filter_index_config: BloomFilterConfig::default(),
2121 }
2122 .build(random_region_file_id(), 0, Some(0))
2123 .await;
2124
2125 assert!(indexer.inverted_indexer.is_some());
2126 }
2127
2128 #[tokio::test]
2129 async fn test_index_build_task_sst_not_exist() {
2130 let env = SchedulerEnv::new().await;
2131 let (tx, _rx) = mpsc::channel(4);
2132 let (result_tx, mut result_rx) = mpsc::channel::<Result<IndexBuildOutcome>>(4);
2133 let mut scheduler = env.mock_index_build_scheduler(4);
2134 let metadata = Arc::new(sst_region_metadata());
2135 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
2136 let file_purger = Arc::new(NoopFilePurger {});
2137 let files = HashMap::new();
2138 let version_control =
2139 mock_version_control(metadata.clone(), file_purger.clone(), files).await;
2140 let region_id = metadata.region_id;
2141 let indexer_builder = mock_indexer_builder(metadata, &env).await;
2142
2143 let file_meta = FileMeta {
2144 region_id,
2145 file_id: FileId::random(),
2146 file_size: 100,
2147 ..Default::default()
2148 };
2149
2150 let file = FileHandle::new(file_meta.clone(), file_purger.clone());
2151
2152 let task = IndexBuildTask {
2154 region_id,
2155 file,
2156 target_region_metadata: version_control.current().version.metadata.clone(),
2157 source: IndexBuildSource::new(
2158 file_meta,
2159 version_control.current().version.metadata.schema_version,
2160 ),
2161 reason: IndexBuildType::Flush,
2162 access_layer: env.access_layer.clone(),
2163 listener: WorkerListener::default(),
2164 manifest_ctx,
2165 write_cache: None,
2166 cache_manager: None,
2167 file_purger,
2168 indexer_builder,
2169 request_sender: tx,
2170 index_options: IndexOptions::default(),
2171 result_senders: vec![result_tx],
2172 };
2173
2174 scheduler.schedule_build(&version_control, task).await;
2176 match result_rx.recv().await.unwrap() {
2177 Ok(outcome) => {
2178 if outcome == IndexBuildOutcome::Finished {
2179 panic!("Expect aborted result due to missing SST file")
2180 }
2181 }
2182 _ => panic!("Expect aborted result due to missing SST file"),
2183 }
2184 }
2185
2186 #[tokio::test]
2187 async fn test_index_build_task_foreign_file_uses_target_metadata() {
2188 let env = SchedulerEnv::new().await;
2189 let mut scheduler = env.mock_index_build_scheduler(4);
2190 let source_metadata = Arc::new(sst_region_metadata());
2191 let mut target_metadata = (*source_metadata).clone();
2192 target_metadata.region_id = RegionId::new(1, 3);
2193 let mut target_builder = RegionMetadataBuilder::new(target_metadata.region_id);
2194 for mut column_metadata in target_metadata.column_metadatas.clone() {
2195 if column_metadata.column_id == 2 {
2196 column_metadata.column_schema =
2197 column_metadata.column_schema.with_inverted_index(true);
2198 }
2199 target_builder.push_column_metadata(column_metadata);
2200 }
2201 let partition_expr = col("field_0")
2202 .gt_eq(Value::UInt64(100))
2203 .and(col("field_0").lt(Value::UInt64(200)));
2204 target_builder
2205 .primary_key(target_metadata.primary_key.clone())
2206 .partition_expr_json(Some(partition_expr.as_json_str().unwrap()))
2207 .bump_version();
2208 let target_metadata = Arc::new(target_builder.build().unwrap());
2209 let manifest_ctx = env.mock_manifest_context(target_metadata.clone()).await;
2210 let region_id = target_metadata.region_id;
2211 let file_purger = Arc::new(NoopFilePurger {});
2212 let sst_info = mock_sst_file(source_metadata.clone(), &env, IndexBuildMode::Async).await;
2213 let file_meta = FileMeta {
2214 region_id: source_metadata.region_id,
2215 file_id: sst_info.file_id,
2216 file_size: sst_info.file_size,
2217 max_row_group_uncompressed_size: sst_info.max_row_group_uncompressed_size,
2218 available_indexes: smallvec![IndexType::InvertedIndex],
2219 index_file_size: 0,
2221 index_version: 0,
2222 num_rows: sst_info.num_rows as u64,
2223 num_row_groups: sst_info.num_row_groups,
2224 ..Default::default()
2225 };
2226 seed_manifest_file(&manifest_ctx, &file_meta).await;
2227 let files = HashMap::from([(file_meta.file_id, file_meta.clone())]);
2228 let version_control =
2229 mock_version_control(target_metadata.clone(), file_purger.clone(), files).await;
2230 let indexer_builder = mock_indexer_builder(target_metadata.clone(), &env).await;
2231
2232 let file = FileHandle::new(file_meta.clone(), file_purger.clone());
2233
2234 let (tx, mut rx) = mpsc::channel(4);
2236 let (result_tx, _result_rx) = mpsc::channel::<Result<IndexBuildOutcome>>(4);
2237 let task = IndexBuildTask {
2238 region_id,
2239 file,
2240 target_region_metadata: version_control.current().version.metadata.clone(),
2241 source: IndexBuildSource::new(
2242 file_meta.clone(),
2243 version_control.current().version.metadata.schema_version,
2244 ),
2245 reason: IndexBuildType::Flush,
2246 access_layer: env.access_layer.clone(),
2247 listener: WorkerListener::default(),
2248 manifest_ctx,
2249 write_cache: None,
2250 cache_manager: None,
2251 file_purger,
2252 indexer_builder,
2253 request_sender: tx,
2254 index_options: IndexOptions::default(),
2255 result_senders: vec![result_tx],
2256 };
2257
2258 scheduler.schedule_build(&version_control, task).await;
2259
2260 let worker_req = rx.recv().await.unwrap().request;
2262 match worker_req {
2263 WorkerRequest::Background {
2264 region_id: req_region_id,
2265 notify: BackgroundNotify::IndexBuildFinished(finished),
2266 } => {
2267 assert_eq!(req_region_id, region_id);
2268 let updated_meta = &finished.file_meta;
2269
2270 assert!(!updated_meta.available_indexes.is_empty());
2272 assert!(updated_meta.index_file_size > 0);
2273 assert_eq!(updated_meta.file_id, file_meta.file_id);
2274 assert_eq!(updated_meta.index_version, 1);
2275 let field_0_index = updated_meta
2276 .indexes
2277 .iter()
2278 .find(|index| index.column_id == 2)
2279 .expect("field_0 should have an inverted index");
2280 assert_eq!(
2281 field_0_index.created_indexes.as_slice(),
2282 [IndexType::InvertedIndex]
2283 );
2284 }
2285 _ => panic!("Unexpected worker request: {:?}", worker_req),
2286 }
2287
2288 let puffin_reader = env
2289 .access_layer
2290 .build_puffin_manager()
2291 .reader(&RegionIndexId::new(
2292 RegionFileId::new(source_metadata.region_id, file_meta.file_id),
2293 1,
2294 ))
2295 .await
2296 .unwrap();
2297 let blob = puffin_reader
2298 .blob(inverted_index::INDEX_BLOB_TYPE)
2299 .await
2300 .unwrap();
2301 let blob_reader = blob.reader().await.unwrap();
2302 let index_metadata =
2303 index::inverted_index::format::reader::InvertedIndexBlobReader::new(blob_reader)
2304 .metadata(None)
2305 .await
2306 .unwrap();
2307 assert!(index_metadata.metas.contains_key("2"));
2308 assert_eq!(index_metadata.total_row_count, 100);
2309 }
2310
2311 async fn schedule_index_build_task_with_mode(build_mode: IndexBuildMode) {
2312 let env = SchedulerEnv::new().await;
2313 let mut scheduler = env.mock_index_build_scheduler(4);
2314 let metadata = Arc::new(sst_region_metadata());
2315 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
2316 let file_purger = Arc::new(NoopFilePurger {});
2317 let region_id = metadata.region_id;
2318 let sst_info = mock_sst_file(metadata.clone(), &env, build_mode.clone()).await;
2319 let file_meta = FileMeta {
2320 region_id,
2321 file_id: sst_info.file_id,
2322 file_size: sst_info.file_size,
2323 max_row_group_uncompressed_size: sst_info.max_row_group_uncompressed_size,
2324 index_file_size: sst_info.index_metadata.file_size,
2325 num_rows: sst_info.num_rows as u64,
2326 num_row_groups: sst_info.num_row_groups,
2327 ..Default::default()
2328 };
2329 seed_manifest_file(&manifest_ctx, &file_meta).await;
2330 let files = HashMap::from([(file_meta.file_id, file_meta.clone())]);
2331 let version_control =
2332 mock_version_control(metadata.clone(), file_purger.clone(), files).await;
2333 let indexer_builder = mock_indexer_builder(metadata.clone(), &env).await;
2334
2335 let file = FileHandle::new(file_meta.clone(), file_purger.clone());
2336
2337 let (tx, mut rx) = mpsc::channel(4);
2339 let (result_tx, _result_rx) = mpsc::channel::<Result<IndexBuildOutcome>>(4);
2340 let task = IndexBuildTask {
2341 region_id,
2342 file,
2343 target_region_metadata: version_control.current().version.metadata.clone(),
2344 source: IndexBuildSource::new(
2345 file_meta.clone(),
2346 version_control.current().version.metadata.schema_version,
2347 ),
2348 reason: IndexBuildType::Flush,
2349 access_layer: env.access_layer.clone(),
2350 listener: WorkerListener::default(),
2351 manifest_ctx,
2352 write_cache: None,
2353 cache_manager: None,
2354 file_purger,
2355 indexer_builder,
2356 request_sender: tx,
2357 index_options: IndexOptions::default(),
2358 result_senders: vec![result_tx],
2359 };
2360
2361 scheduler.schedule_build(&version_control, task).await;
2362
2363 let puffin_path = location::index_file_path(
2364 env.access_layer.table_dir(),
2365 RegionIndexId::new(RegionFileId::new(region_id, file_meta.file_id), 0),
2366 env.access_layer.path_type(),
2367 );
2368
2369 if build_mode == IndexBuildMode::Async {
2370 assert!(
2372 !env.access_layer
2373 .object_store()
2374 .exists(&puffin_path)
2375 .await
2376 .unwrap()
2377 );
2378 } else {
2379 assert!(
2381 env.access_layer
2382 .object_store()
2383 .exists(&puffin_path)
2384 .await
2385 .unwrap()
2386 );
2387 }
2388
2389 recv_index_build_finished(&mut rx).await;
2391
2392 assert!(
2394 env.access_layer
2395 .object_store()
2396 .exists(&puffin_path)
2397 .await
2398 .unwrap()
2399 );
2400 }
2401
2402 #[tokio::test]
2403 async fn test_index_build_task_build_mode() {
2404 schedule_index_build_task_with_mode(IndexBuildMode::Async).await;
2405 schedule_index_build_task_with_mode(IndexBuildMode::Sync).await;
2406 }
2407
2408 #[tokio::test]
2409 async fn test_index_build_task_no_index() {
2410 let env = SchedulerEnv::new().await;
2411 let mut scheduler = env.mock_index_build_scheduler(4);
2412 let mut metadata = sst_region_metadata();
2413 metadata.column_metadatas.iter_mut().for_each(|col| {
2415 col.column_schema.set_inverted_index(false);
2416 let _ = col.column_schema.unset_skipping_options();
2417 });
2418 let region_id = metadata.region_id;
2419 let metadata = Arc::new(metadata);
2420 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
2421 let file_purger = Arc::new(NoopFilePurger {});
2422 let sst_info = mock_sst_file(metadata.clone(), &env, IndexBuildMode::Async).await;
2423 let file_meta = FileMeta {
2424 region_id,
2425 file_id: sst_info.file_id,
2426 file_size: sst_info.file_size,
2427 max_row_group_uncompressed_size: sst_info.max_row_group_uncompressed_size,
2428 index_file_size: sst_info.index_metadata.file_size,
2429 num_rows: sst_info.num_rows as u64,
2430 num_row_groups: sst_info.num_row_groups,
2431 ..Default::default()
2432 };
2433 seed_manifest_file(&manifest_ctx, &file_meta).await;
2434 let files = HashMap::from([(file_meta.file_id, file_meta.clone())]);
2435 let version_control =
2436 mock_version_control(metadata.clone(), file_purger.clone(), files).await;
2437 let indexer_builder = mock_indexer_builder(metadata.clone(), &env).await;
2438
2439 let file = FileHandle::new(file_meta.clone(), file_purger.clone());
2440
2441 let (tx, mut rx) = mpsc::channel(4);
2443 let (result_tx, mut result_rx) = mpsc::channel::<Result<IndexBuildOutcome>>(4);
2444 let task = IndexBuildTask {
2445 region_id,
2446 file,
2447 target_region_metadata: version_control.current().version.metadata.clone(),
2448 source: IndexBuildSource::new(
2449 file_meta.clone(),
2450 version_control.current().version.metadata.schema_version,
2451 ),
2452 reason: IndexBuildType::Flush,
2453 access_layer: env.access_layer.clone(),
2454 listener: WorkerListener::default(),
2455 manifest_ctx,
2456 write_cache: None,
2457 cache_manager: None,
2458 file_purger,
2459 indexer_builder,
2460 request_sender: tx,
2461 index_options: IndexOptions::default(),
2462 result_senders: vec![result_tx],
2463 };
2464
2465 scheduler.schedule_build(&version_control, task).await;
2466
2467 assert!(matches!(
2469 result_rx.recv().await.unwrap(),
2470 Ok(IndexBuildOutcome::Aborted(reason)) if reason.contains("No index to build")
2471 ));
2472
2473 assert!(matches!(
2475 rx.recv().await.unwrap().request,
2476 WorkerRequest::Background {
2477 notify: BackgroundNotify::IndexBuildStopped(_),
2478 ..
2479 }
2480 ));
2481 }
2482
2483 #[tokio::test]
2484 async fn test_index_build_task_with_write_cache() {
2485 let env = SchedulerEnv::new().await;
2486 let mut scheduler = env.mock_index_build_scheduler(4);
2487 let metadata = Arc::new(sst_region_metadata());
2488 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
2489 let file_purger = Arc::new(NoopFilePurger {});
2490 let region_id = metadata.region_id;
2491
2492 let (dir, factory) = PuffinManagerFactory::new_for_test_async("test_write_cache").await;
2493 let intm_manager = mock_intm_mgr(dir.path().to_string_lossy()).await;
2494
2495 let write_cache = Arc::new(
2497 WriteCache::new_fs(
2498 dir.path().to_str().unwrap(),
2499 ReadableSize::mb(10),
2500 None,
2501 None,
2502 true, factory,
2504 intm_manager,
2505 ReadableSize::mb(10),
2506 )
2507 .await
2508 .unwrap(),
2509 );
2510 let indexer_builder = Arc::new(IndexerBuilderImpl {
2512 build_type: IndexBuildType::Flush,
2513 metadata: metadata.clone(),
2514 puffin_manager: write_cache.build_puffin_manager().clone(),
2515 write_cache_enabled: true,
2516 intermediate_manager: write_cache.intermediate_manager().clone(),
2517 index_options: IndexOptions::default(),
2518 inverted_index_config: InvertedIndexConfig::default(),
2519 fulltext_index_config: FulltextIndexConfig::default(),
2520 bloom_filter_index_config: BloomFilterConfig::default(),
2521 });
2522
2523 let sst_info = mock_sst_file(metadata.clone(), &env, IndexBuildMode::Async).await;
2524 let file_meta = FileMeta {
2525 region_id,
2526 file_id: sst_info.file_id,
2527 file_size: sst_info.file_size,
2528 index_file_size: sst_info.index_metadata.file_size,
2529 num_rows: sst_info.num_rows as u64,
2530 num_row_groups: sst_info.num_row_groups,
2531 ..Default::default()
2532 };
2533 seed_manifest_file(&manifest_ctx, &file_meta).await;
2534 let files = HashMap::from([(file_meta.file_id, file_meta.clone())]);
2535 let version_control =
2536 mock_version_control(metadata.clone(), file_purger.clone(), files).await;
2537
2538 let file = FileHandle::new(file_meta.clone(), file_purger.clone());
2539
2540 let (tx, mut rx) = mpsc::channel(4);
2542 let (result_tx, _result_rx) = mpsc::channel::<Result<IndexBuildOutcome>>(4);
2543 let task = IndexBuildTask {
2544 region_id,
2545 file,
2546 target_region_metadata: version_control.current().version.metadata.clone(),
2547 source: IndexBuildSource::new(
2548 file_meta.clone(),
2549 version_control.current().version.metadata.schema_version,
2550 ),
2551 reason: IndexBuildType::Flush,
2552 access_layer: env.access_layer.clone(),
2553 listener: WorkerListener::default(),
2554 manifest_ctx,
2555 write_cache: Some(write_cache.clone()),
2556 cache_manager: None,
2557 file_purger,
2558 indexer_builder,
2559 request_sender: tx,
2560 index_options: IndexOptions::default(),
2561 result_senders: vec![result_tx],
2562 };
2563
2564 scheduler.schedule_build(&version_control, task).await;
2565
2566 recv_index_build_finished(&mut rx).await;
2567
2568 let index_key = IndexKey::new(
2570 region_id,
2571 file_meta.file_id,
2572 FileType::Puffin(sst_info.index_metadata.version),
2573 );
2574 assert!(write_cache.file_cache().contains_key(&index_key));
2575 }
2576
2577 async fn create_mock_task_for_schedule(
2578 env: &SchedulerEnv,
2579 file_id: FileId,
2580 region_id: RegionId,
2581 reason: IndexBuildType,
2582 ) -> IndexBuildTask {
2583 create_mock_task_for_schedule_with_result(env, file_id, region_id, reason)
2584 .await
2585 .0
2586 }
2587
2588 async fn create_mock_task_for_schedule_with_result(
2591 env: &SchedulerEnv,
2592 file_id: FileId,
2593 region_id: RegionId,
2594 reason: IndexBuildType,
2595 ) -> (IndexBuildTask, mpsc::Receiver<Result<IndexBuildOutcome>>) {
2596 let metadata = Arc::new(sst_region_metadata());
2597 let schema_version = metadata.schema_version;
2598 let manifest_ctx = env.mock_manifest_context(metadata.clone()).await;
2599 let file_purger = Arc::new(NoopFilePurger {});
2600 let indexer_builder = mock_indexer_builder(metadata.clone(), env).await;
2601 let (tx, _rx) = mpsc::channel(4);
2602 let (result_tx, result_rx) = mpsc::channel::<Result<IndexBuildOutcome>>(4);
2603
2604 let file_meta = FileMeta {
2605 region_id,
2606 file_id,
2607 file_size: 100,
2608 ..Default::default()
2609 };
2610
2611 let file = FileHandle::new(file_meta.clone(), file_purger.clone());
2612
2613 let task = IndexBuildTask {
2614 region_id,
2615 file,
2616 target_region_metadata: metadata,
2617 source: IndexBuildSource::new(file_meta, schema_version),
2618 reason,
2619 access_layer: env.access_layer.clone(),
2620 listener: WorkerListener::default(),
2621 manifest_ctx,
2622 write_cache: None,
2623 cache_manager: None,
2624 file_purger,
2625 indexer_builder,
2626 request_sender: tx,
2627 index_options: IndexOptions::default(),
2628 result_senders: vec![result_tx],
2629 };
2630 (task, result_rx)
2631 }
2632
2633 #[tokio::test]
2634 async fn test_scheduler_coalesces_latest_schema_generation_per_sst() {
2635 let job_scheduler = Arc::new(VecScheduler::default());
2636 let env = SchedulerEnv::new().await.scheduler(job_scheduler.clone());
2637 let mut scheduler = env.mock_index_build_scheduler(2);
2638 let metadata = Arc::new(sst_region_metadata());
2639 let region_id = metadata.region_id;
2640 let file_id = FileId::random();
2641 let file_purger = Arc::new(NoopFilePurger {});
2642 let files = HashMap::from([(
2643 file_id,
2644 FileMeta {
2645 region_id,
2646 file_id,
2647 file_size: 100,
2648 ..Default::default()
2649 },
2650 )]);
2651 let version_control = mock_version_control(metadata, file_purger, files).await;
2652
2653 let (active, _active_rx) = create_mock_task_for_schedule_with_result(
2654 &env,
2655 file_id,
2656 region_id,
2657 IndexBuildType::Manual,
2658 )
2659 .await;
2660 scheduler.schedule_build(&version_control, active).await;
2661 assert_eq!(job_scheduler.num_jobs(), 1);
2662
2663 let (mut older, mut older_rx) = create_mock_task_for_schedule_with_result(
2664 &env,
2665 file_id,
2666 region_id,
2667 IndexBuildType::SchemaChange,
2668 )
2669 .await;
2670 older.source.schema_version = 1;
2671 scheduler.schedule_build(&version_control, older).await;
2672
2673 let (mut latest, mut latest_rx) = create_mock_task_for_schedule_with_result(
2674 &env,
2675 file_id,
2676 region_id,
2677 IndexBuildType::SchemaChange,
2678 )
2679 .await;
2680 latest.source.schema_version = 2;
2681 scheduler.schedule_build(&version_control, latest).await;
2682
2683 for rx in [&mut older_rx, &mut latest_rx] {
2686 assert!(matches!(
2687 rx.try_recv(),
2688 Err(mpsc::error::TryRecvError::Empty)
2689 ));
2690 }
2691
2692 let status = &scheduler.region_status[®ion_id];
2693 assert_eq!(status.building_files.len(), 1);
2694 assert_eq!(status.pending_tasks.len(), 1);
2695 let survivor = &status.pending_tasks.tasks[&file_id].task;
2696 assert_eq!(survivor.source.schema_version, 2);
2697 assert_eq!(survivor.result_senders.len(), 2);
2698 assert_eq!(job_scheduler.num_jobs(), 1);
2699
2700 scheduler.on_task_stopped(region_id, file_id);
2701 let status = &scheduler.region_status[®ion_id];
2702 assert_eq!(status.building_files.len(), 1);
2703 assert!(status.pending_tasks.is_empty());
2704 assert_eq!(job_scheduler.num_jobs(), 2);
2705 }
2706
2707 #[tokio::test]
2708 async fn test_scheduler_completes_sender_when_job_is_rejected() {
2709 let job_scheduler = Arc::new(LocalScheduler::new(1));
2710 job_scheduler.stop(false).await.unwrap();
2711 let env = SchedulerEnv::new().await.scheduler(job_scheduler);
2712 let mut scheduler = env.mock_index_build_scheduler(1);
2713 let metadata = Arc::new(sst_region_metadata());
2714 let region_id = metadata.region_id;
2715 let file_id = FileId::random();
2716 let file_purger = Arc::new(NoopFilePurger {});
2717 let files = HashMap::from([(
2718 file_id,
2719 FileMeta {
2720 region_id,
2721 file_id,
2722 file_size: 100,
2723 ..Default::default()
2724 },
2725 )]);
2726 let version_control = mock_version_control(metadata, file_purger, files).await;
2727 let (task, mut result_rx) = create_mock_task_for_schedule_with_result(
2728 &env,
2729 file_id,
2730 region_id,
2731 IndexBuildType::Flush,
2732 )
2733 .await;
2734
2735 scheduler.schedule_build(&version_control, task).await;
2736
2737 let result = tokio::time::timeout(std::time::Duration::from_secs(5), result_rx.recv())
2738 .await
2739 .expect("scheduler rejection did not complete the result sender")
2740 .expect("result channel closed without a result");
2741 assert!(result.is_err());
2742 assert!(!scheduler.region_status.contains_key(®ion_id));
2743 }
2744
2745 #[tokio::test]
2746 async fn test_scheduler_comprehensive() {
2747 let env = SchedulerEnv::new().await;
2748 let mut scheduler = env.mock_index_build_scheduler(2);
2749 let metadata = Arc::new(sst_region_metadata());
2750 let region_id = metadata.region_id;
2751 let file_purger = Arc::new(NoopFilePurger {});
2752
2753 let file_id1 = FileId::random();
2755 let file_id2 = FileId::random();
2756 let file_id3 = FileId::random();
2757 let file_id4 = FileId::random();
2758 let file_id5 = FileId::random();
2759
2760 let mut files = HashMap::new();
2761 for file_id in [file_id1, file_id2, file_id3, file_id4, file_id5] {
2762 files.insert(
2763 file_id,
2764 FileMeta {
2765 region_id,
2766 file_id,
2767 file_size: 100,
2768 ..Default::default()
2769 },
2770 );
2771 }
2772
2773 let version_control = mock_version_control(metadata, file_purger, files).await;
2774
2775 let task1 =
2777 create_mock_task_for_schedule(&env, file_id1, region_id, IndexBuildType::Flush).await;
2778 scheduler.schedule_build(&version_control, task1).await;
2779 assert!(scheduler.region_status.contains_key(®ion_id));
2780 let status = scheduler.region_status.get(®ion_id).unwrap();
2781 assert_eq!(status.building_files.len(), 1);
2782 assert!(status.building_files.contains(&file_id1));
2783
2784 let task1_dup =
2786 create_mock_task_for_schedule(&env, file_id1, region_id, IndexBuildType::Flush).await;
2787 scheduler.schedule_build(&version_control, task1_dup).await;
2788 let status = scheduler.region_status.get(®ion_id).unwrap();
2789 assert_eq!(status.building_files.len(), 1); let task2 =
2793 create_mock_task_for_schedule(&env, file_id2, region_id, IndexBuildType::Flush).await;
2794 scheduler.schedule_build(&version_control, task2).await;
2795 let status = scheduler.region_status.get(®ion_id).unwrap();
2796 assert_eq!(status.building_files.len(), 2); assert_eq!(status.pending_tasks.len(), 0);
2798
2799 let task3 =
2802 create_mock_task_for_schedule(&env, file_id3, region_id, IndexBuildType::Compact).await;
2803 let task4 =
2804 create_mock_task_for_schedule(&env, file_id4, region_id, IndexBuildType::SchemaChange)
2805 .await;
2806 let task5 =
2807 create_mock_task_for_schedule(&env, file_id5, region_id, IndexBuildType::Manual).await;
2808
2809 scheduler.schedule_build(&version_control, task3).await;
2810 scheduler.schedule_build(&version_control, task4).await;
2811 scheduler.schedule_build(&version_control, task5).await;
2812
2813 let status = scheduler.region_status.get(®ion_id).unwrap();
2814 assert_eq!(status.building_files.len(), 2); assert_eq!(status.pending_tasks.len(), 3); scheduler.on_task_stopped(region_id, file_id1);
2819 let status = scheduler.region_status.get(®ion_id).unwrap();
2820 assert!(!status.building_files.contains(&file_id1));
2821 assert_eq!(status.building_files.len(), 2); assert_eq!(status.pending_tasks.len(), 2); assert!(status.building_files.contains(&file_id5));
2825
2826 scheduler.on_task_stopped(region_id, file_id2);
2828 let status = scheduler.region_status.get(®ion_id).unwrap();
2829 assert_eq!(status.building_files.len(), 2);
2830 assert_eq!(status.pending_tasks.len(), 1); assert!(status.building_files.contains(&file_id4)); scheduler.on_task_stopped(region_id, file_id5);
2835 scheduler.on_task_stopped(region_id, file_id4);
2836
2837 let status = scheduler.region_status.get(®ion_id).unwrap();
2838 assert_eq!(status.building_files.len(), 1); assert_eq!(status.pending_tasks.len(), 0);
2840 assert!(status.building_files.contains(&file_id3));
2841
2842 scheduler.on_task_stopped(region_id, file_id3);
2843
2844 assert!(!scheduler.region_status.contains_key(®ion_id));
2846 }
2847
2848 async fn setup_scheduler_with_pending_tasks(
2852 env: &SchedulerEnv,
2853 ) -> (
2854 IndexBuildScheduler,
2855 mpsc::Receiver<Result<IndexBuildOutcome>>,
2856 mpsc::Receiver<Result<IndexBuildOutcome>>,
2857 VersionControlRef,
2858 RegionId,
2859 FileId, ) {
2861 let metadata = Arc::new(sst_region_metadata());
2862 let region_id = metadata.region_id;
2863 let file_purger = Arc::new(NoopFilePurger {});
2864
2865 let file_id1 = FileId::random();
2866 let file_id2 = FileId::random();
2867 let file_id3 = FileId::random();
2868
2869 let files = HashMap::from([
2870 (
2871 file_id1,
2872 FileMeta {
2873 region_id,
2874 file_id: file_id1,
2875 file_size: 100,
2876 ..Default::default()
2877 },
2878 ),
2879 (
2880 file_id2,
2881 FileMeta {
2882 region_id,
2883 file_id: file_id2,
2884 file_size: 100,
2885 ..Default::default()
2886 },
2887 ),
2888 (
2889 file_id3,
2890 FileMeta {
2891 region_id,
2892 file_id: file_id3,
2893 file_size: 100,
2894 ..Default::default()
2895 },
2896 ),
2897 ]);
2898 let version_control =
2899 mock_version_control(metadata.clone(), file_purger.clone(), files).await;
2900
2901 let mut scheduler = env.mock_index_build_scheduler(1);
2902
2903 let (task1, _rx1) = create_mock_task_for_schedule_with_result(
2907 env,
2908 file_id1,
2909 region_id,
2910 IndexBuildType::Flush,
2911 )
2912 .await;
2913 let (task2, rx2) = create_mock_task_for_schedule_with_result(
2914 env,
2915 file_id2,
2916 region_id,
2917 IndexBuildType::Flush,
2918 )
2919 .await;
2920 let (task3, rx3) = create_mock_task_for_schedule_with_result(
2921 env,
2922 file_id3,
2923 region_id,
2924 IndexBuildType::Flush,
2925 )
2926 .await;
2927
2928 scheduler.schedule_build(&version_control, task1).await;
2929 scheduler.schedule_build(&version_control, task2).await;
2930 scheduler.schedule_build(&version_control, task3).await;
2931
2932 assert!(scheduler.region_status.contains_key(®ion_id));
2934 let status = scheduler.region_status.get(®ion_id).unwrap();
2935 assert_eq!(status.building_files.len(), 1);
2936 assert_eq!(status.pending_tasks.len(), 2);
2937
2938 (scheduler, rx2, rx3, version_control, region_id, file_id1)
2939 }
2940
2941 fn assert_lifecycle_error(
2946 err: crate::error::Error,
2947 expected_source: &str,
2948 lifecycle_name: &str,
2949 ) {
2950 let crate::error::Error::BuildIndexAsync { source, .. } = err else {
2951 panic!("[{lifecycle_name}] Expected BuildIndexAsync outer error, got: {err:?}");
2952 };
2953 let actual = match &*source {
2954 crate::error::Error::RegionDropped { .. } => "dropped",
2955 crate::error::Error::RegionClosed { .. } => "closed",
2956 crate::error::Error::RegionTruncated { .. } => "truncated",
2957 other => panic!(
2958 "[{lifecycle_name}] Expected lifecycle source variant (RegionDropped/RegionClosed/RegionTruncated), got: {other:?}"
2959 ),
2960 };
2961 assert_eq!(
2962 actual, expected_source,
2963 "[{lifecycle_name}] Source variant mismatch"
2964 );
2965 }
2966
2967 async fn recv_lifecycle_error(
2970 rx: &mut mpsc::Receiver<Result<IndexBuildOutcome>>,
2971 expected_source: &str,
2972 lifecycle_name: &str,
2973 ) {
2974 let result = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
2975 .await
2976 .unwrap_or_else(|_| {
2977 panic!(
2978 "[{lifecycle_name}] Timeout (5s) waiting for lifecycle error from pending task"
2979 )
2980 });
2981 let result = result.unwrap_or_else(|| {
2982 panic!("[{lifecycle_name}] Channel closed without receiving lifecycle error")
2983 });
2984 let err = result.unwrap_err();
2985 assert_lifecycle_error(err, expected_source, lifecycle_name);
2986 }
2987
2988 #[tokio::test]
2989 async fn test_scheduler_lifecycle_cleanup() {
2990 let env = SchedulerEnv::new().await;
2991
2992 {
2994 let (mut scheduler, mut rx2, mut rx3, _version_control, region_id, building_file_id) =
2995 setup_scheduler_with_pending_tasks(&env).await;
2996
2997 scheduler.on_region_dropped(region_id).await;
2998
2999 let status = scheduler.region_status.get(®ion_id).unwrap();
3000 assert!(status.retiring);
3001 assert_eq!(status.building_files.len(), 1);
3002 assert!(status.pending_tasks.is_empty());
3003
3004 recv_lifecycle_error(&mut rx2, "dropped", "on_region_dropped").await;
3006 recv_lifecycle_error(&mut rx3, "dropped", "on_region_dropped").await;
3007
3008 scheduler.on_task_stopped(region_id, building_file_id);
3010 assert!(!scheduler.region_status.contains_key(®ion_id));
3011 }
3012
3013 {
3015 let (mut scheduler, mut rx2, mut rx3, version_control, region_id, building_file_id) =
3016 setup_scheduler_with_pending_tasks(&env).await;
3017
3018 scheduler.on_region_closed(region_id).await;
3019
3020 let status = scheduler.region_status.get(®ion_id).unwrap();
3021 assert!(status.retiring);
3022 assert_eq!(status.building_files.len(), 1);
3023 assert!(status.pending_tasks.is_empty());
3024
3025 recv_lifecycle_error(&mut rx2, "closed", "on_region_closed").await;
3026 recv_lifecycle_error(&mut rx3, "closed", "on_region_closed").await;
3027
3028 let (reopened_task, _reopened_rx) = create_mock_task_for_schedule_with_result(
3031 &env,
3032 building_file_id,
3033 region_id,
3034 IndexBuildType::Manual,
3035 )
3036 .await;
3037 scheduler
3038 .schedule_build(&version_control, reopened_task)
3039 .await;
3040 let status = scheduler.region_status.get(®ion_id).unwrap();
3041 assert!(status.retiring);
3042 assert_eq!(status.building_files.len(), 1);
3043 assert_eq!(status.pending_tasks.len(), 1);
3044
3045 scheduler.on_task_stopped(region_id, building_file_id);
3046 let status = scheduler.region_status.get(®ion_id).unwrap();
3047 assert!(!status.retiring);
3048 assert_eq!(status.building_files.len(), 1);
3049 assert!(status.pending_tasks.is_empty());
3050
3051 scheduler.on_task_stopped(region_id, building_file_id);
3052 assert!(!scheduler.region_status.contains_key(®ion_id));
3053 }
3054
3055 {
3057 let (mut scheduler, mut rx2, mut rx3, _version_control, region_id, building_file_id) =
3058 setup_scheduler_with_pending_tasks(&env).await;
3059
3060 scheduler.on_region_truncated(region_id).await;
3061
3062 let status = scheduler.region_status.get(®ion_id).unwrap();
3063 assert!(status.retiring);
3064 assert_eq!(status.building_files.len(), 1);
3065 assert!(status.pending_tasks.is_empty());
3066
3067 recv_lifecycle_error(&mut rx2, "truncated", "on_region_truncated").await;
3068 recv_lifecycle_error(&mut rx3, "truncated", "on_region_truncated").await;
3069
3070 scheduler.on_task_stopped(region_id, building_file_id);
3071 assert!(!scheduler.region_status.contains_key(®ion_id));
3072 }
3073 }
3074}