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