Skip to main content

mito2/region/
opener.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Region opener.
16
17use std::any::TypeId;
18use std::collections::HashMap;
19use std::sync::atomic::{AtomicI64, AtomicU64};
20use std::sync::{Arc, LazyLock};
21use std::time::Instant;
22
23use arrow_schema::extension::ExtensionType;
24use common_telemetry::{debug, error, info, warn};
25use common_wal::options::WalOptions;
26use datatypes::extension::json::{Json2ExtensionType, JsonMetadata};
27use futures::StreamExt;
28use futures::future::BoxFuture;
29use log_store::kafka::log_store::KafkaLogStore;
30use log_store::noop::log_store::NoopLogStore;
31use log_store::raft_engine::log_store::RaftEngineLogStore;
32use object_store::ObjectStore;
33use object_store::manager::ObjectStoreManagerRef;
34use object_store::util::{is_object_storage, normalize_dir};
35use parquet::file::metadata::PageIndexPolicy;
36use snafu::{OptionExt, ResultExt, ensure};
37use store_api::codec::PrimaryKeyEncoding;
38use store_api::logstore::LogStore;
39use store_api::logstore::provider::Provider;
40use store_api::metadata::{
41    ColumnMetadata, RegionMetadata, RegionMetadataBuilder, RegionMetadataRef,
42};
43use store_api::region_engine::RegionRole;
44use store_api::region_request::{PathType, RegionRequirements};
45use store_api::storage::{ColumnId, RegionId};
46use tokio::sync::Semaphore;
47
48use crate::access_layer::AccessLayer;
49use crate::cache::file_cache::{FileCache, FileType, IndexKey};
50use crate::cache::{CacheManagerRef, SstMetaPreparation, prepare_sst_meta};
51use crate::config::MitoConfig;
52use crate::engine::region_hook::RegionHookRef;
53use crate::error;
54use crate::error::{
55    DataTypeMismatchSnafu, EmptyRegionDirSnafu, InvalidMetadataSnafu, InvalidRegionOptionsSnafu,
56    ObjectStoreNotFoundSnafu, RegionCorruptedSnafu, Result, StaleLogEntrySnafu,
57};
58use crate::lease::NodeLease;
59use crate::manifest::action::RegionManifest;
60use crate::manifest::manager::{RegionManifestManager, RegionManifestOptions};
61use crate::memtable::bulk::part::BulkPart;
62use crate::memtable::time_partition::TimePartitions;
63use crate::memtable::{MemtableBuilderProvider, ensure_json2_uses_flat_format};
64use crate::metrics::{CACHE_FILL_DOWNLOADED_FILES, CACHE_FILL_PENDING_FILES};
65use crate::read::series_candidate::is_sparse_metric_metadata;
66use crate::region::options::RegionOptions;
67use crate::region::version::{Version, VersionBuilder, VersionControl, VersionControlRef};
68use crate::region::{
69    ManifestContext, ManifestStats, MitoRegion, MitoRegionRef, RegionLeaderState, RegionRoleState,
70    RegionStats,
71};
72use crate::region_write_ctx::{RegionWriteCtx, WriteSource};
73use crate::request::OptionOutputTx;
74use crate::schedule::scheduler::SchedulerRef;
75use crate::series_index::{IndexFilePurger, load_version_control};
76use crate::sst::FormatType;
77use crate::sst::file::{FileHandle, RegionFileId, RegionIndexId};
78use crate::sst::file_purger::{FilePurgerRef, create_file_purger};
79use crate::sst::file_ref::FileReferenceManagerRef;
80use crate::sst::index::intermediate::IntermediateManager;
81use crate::sst::index::puffin_manager::PuffinManagerFactory;
82use crate::sst::location::{self, region_dir_from_table_dir};
83use crate::sst::parquet::metadata::{MetadataLoader, extract_primary_key_range};
84use crate::sst::parquet::reader::MetadataCacheMetrics;
85use crate::sst::range_index::RangeIndexDeleter;
86use crate::time_provider::TimeProviderRef;
87use crate::wal::entry_reader::WalEntryReader;
88use crate::wal::{EntryId, Wal};
89
90const PARQUET_META_PRELOAD_CONCURRENCY: usize = 8;
91
92static PARQUET_META_PRELOAD_SEMAPHORE: LazyLock<Semaphore> =
93    LazyLock::new(|| Semaphore::new(PARQUET_META_PRELOAD_CONCURRENCY));
94
95fn initial_pruned_entry_id(wal_options: &WalOptions) -> EntryId {
96    match wal_options {
97        WalOptions::Kafka(options) => options.initial_pruned_entry_id.unwrap_or(0),
98        WalOptions::RaftEngine | WalOptions::Noop | WalOptions::ObjectStore(_) => 0,
99    }
100}
101
102fn maybe_upgrade_json2_layout(metadata: RegionMetadataRef) -> Result<RegionMetadataRef> {
103    let mut upgrades = Vec::new();
104    for (index, column) in metadata.column_metadatas.iter().enumerate() {
105        if !column.column_schema.data_type.is_json2() {
106            continue;
107        }
108        let Some(extension) = column
109            .column_schema
110            .extension_type::<Json2ExtensionType>()
111            .context(DataTypeMismatchSnafu)?
112        else {
113            continue;
114        };
115        if extension.metadata().is_version_2() {
116            continue;
117        }
118        upgrades.push((index, extension.metadata().json_settings().clone()));
119    }
120
121    if upgrades.is_empty() {
122        return Ok(metadata);
123    }
124
125    let mut upgraded = metadata.as_ref().clone();
126    for (index, settings) in upgrades {
127        let extension = Json2ExtensionType::new(Arc::new(JsonMetadata::new(settings)));
128        upgraded.column_metadatas[index]
129            .column_schema
130            .with_extension_type(&extension);
131    }
132
133    let builder = RegionMetadataBuilder::from_existing(upgraded);
134    Ok(Arc::new(builder.build().context(InvalidMetadataSnafu)?))
135}
136
137/// A fetcher to retrieve partition expr for a region.
138///
139/// Compatibility: older regions didn't persist `partition_expr` in engine metadata,
140/// while newer ones do. On open, we backfill it via this fetcher and persist it
141/// to the manifest so future opens don't need refetching.
142#[async_trait::async_trait]
143pub trait PartitionExprFetcher {
144    async fn fetch_expr(&self, region_id: RegionId) -> Option<String>;
145}
146
147pub type PartitionExprFetcherRef = Arc<dyn PartitionExprFetcher + Send + Sync>;
148
149/// Builder to create a new [MitoRegion] or open an existing one.
150pub(crate) struct RegionOpener {
151    region_id: RegionId,
152    metadata_builder: Option<RegionMetadataBuilder>,
153    memtable_builder_provider: MemtableBuilderProvider,
154    object_store_manager: ObjectStoreManagerRef,
155    table_dir: String,
156    path_type: PathType,
157    purge_scheduler: SchedulerRef,
158    options: Option<RegionOptions>,
159    cache_manager: Option<CacheManagerRef>,
160    skip_wal_replay: bool,
161    node_lease: Arc<NodeLease>,
162    puffin_manager_factory: PuffinManagerFactory,
163    intermediate_manager: IntermediateManager,
164    time_provider: TimeProviderRef,
165    stats: ManifestStats,
166    wal_entry_reader: Option<Box<dyn WalEntryReader>>,
167    replay_checkpoint: Option<u64>,
168    file_ref_manager: FileReferenceManagerRef,
169    partition_expr_fetcher: PartitionExprFetcherRef,
170    hook: Option<RegionHookRef>,
171    series_index_store: Option<ObjectStore>,
172    series_index_purger: Option<IndexFilePurger>,
173}
174
175impl RegionOpener {
176    /// Returns a new opener.
177    // TODO(LFC): Reduce the number of arguments.
178    #[allow(clippy::too_many_arguments)]
179    pub(crate) fn new(
180        region_id: RegionId,
181        table_dir: &str,
182        path_type: PathType,
183        node_lease: Arc<NodeLease>,
184        memtable_builder_provider: MemtableBuilderProvider,
185        object_store_manager: ObjectStoreManagerRef,
186        purge_scheduler: SchedulerRef,
187        puffin_manager_factory: PuffinManagerFactory,
188        intermediate_manager: IntermediateManager,
189        time_provider: TimeProviderRef,
190        file_ref_manager: FileReferenceManagerRef,
191        partition_expr_fetcher: PartitionExprFetcherRef,
192    ) -> RegionOpener {
193        RegionOpener {
194            region_id,
195            metadata_builder: None,
196            memtable_builder_provider,
197            object_store_manager,
198            table_dir: normalize_dir(table_dir),
199            path_type,
200            purge_scheduler,
201            options: None,
202            cache_manager: None,
203            skip_wal_replay: false,
204            node_lease,
205            puffin_manager_factory,
206            intermediate_manager,
207            time_provider,
208            stats: ManifestStats::with_region_metrics(region_id),
209            wal_entry_reader: None,
210            replay_checkpoint: None,
211            file_ref_manager,
212            partition_expr_fetcher,
213            hook: None,
214            series_index_store: None,
215            series_index_purger: None,
216        }
217    }
218
219    /// Sets the store for companion range indexes.
220    pub(crate) fn series_index_store(mut self, store: Option<ObjectStore>) -> Self {
221        self.series_index_store = store;
222        self
223    }
224
225    /// Sets the purger shared with the worker's series-index maintenance task.
226    pub(crate) fn series_index_purger(mut self, purger: Option<IndexFilePurger>) -> Self {
227        self.series_index_purger = purger;
228        self
229    }
230
231    /// Sets the region hook for observing manifest mutations.
232    pub(crate) fn hook(mut self, hook: Option<RegionHookRef>) -> Self {
233        self.hook = hook;
234        self
235    }
236
237    /// Sets metadata builder of the region to create.
238    pub(crate) fn metadata_builder(mut self, builder: RegionMetadataBuilder) -> Self {
239        self.metadata_builder = Some(builder);
240        self
241    }
242
243    /// Computes the region directory from table_dir and region_id.
244    fn region_dir(&self) -> String {
245        region_dir_from_table_dir(&self.table_dir, self.region_id, self.path_type)
246    }
247
248    /// Builds the region metadata.
249    ///
250    /// # Panics
251    /// - Panics if `options` is not set.
252    /// - Panics if `metadata_builder` is not set.
253    fn build_metadata(&mut self) -> Result<RegionMetadata> {
254        let options = self.options.as_ref().unwrap();
255        let mut metadata_builder = self.metadata_builder.take().unwrap();
256        metadata_builder.primary_key_encoding(options.primary_key_encoding());
257        metadata_builder.build().context(InvalidMetadataSnafu)
258    }
259
260    /// Parses and sets options for the region.
261    pub(crate) fn parse_options(self, options: HashMap<String, String>) -> Result<Self> {
262        let region_id = self.region_id;
263        let options = self
264            .memtable_builder_provider
265            .parse_options(region_id, &options)?;
266        self.options(options)
267    }
268
269    /// Sets the replay checkpoint for the region.
270    pub(crate) fn replay_checkpoint(mut self, replay_checkpoint: Option<u64>) -> Self {
271        self.replay_checkpoint = replay_checkpoint;
272        self
273    }
274
275    /// If a [WalEntryReader] is set, the [RegionOpener] will use [WalEntryReader] instead of
276    /// constructing a new one from scratch.
277    pub(crate) fn wal_entry_reader(
278        mut self,
279        wal_entry_reader: Option<Box<dyn WalEntryReader>>,
280    ) -> Self {
281        self.wal_entry_reader = wal_entry_reader;
282        self
283    }
284
285    /// Sets options for the region.
286    pub(crate) fn options(mut self, options: RegionOptions) -> Result<Self> {
287        options.validate()?;
288        self.options = Some(options);
289        Ok(self)
290    }
291
292    /// Ensures the current region request satisfies its requirements.
293    pub(crate) fn ensure_region_requirements(
294        &self,
295        requirements: RegionRequirements,
296    ) -> Result<()> {
297        if !requirements.object_storage {
298            return Ok(());
299        }
300
301        let options = self.options.as_ref().context(InvalidRegionOptionsSnafu {
302            reason: "missing region options before requirement check".to_string(),
303        })?;
304        let object_store = get_object_store(&options.storage, &self.object_store_manager)?;
305
306        ensure!(
307            supports_open_region_object_storage_requirement(&object_store),
308            error::RegionRequirementSnafu {
309                region_id: self.region_id,
310                requirement: "object storage",
311                reason: "region data must be accessible from another datanode",
312            }
313        );
314
315        Ok(())
316    }
317
318    /// Sets the cache manager for the region.
319    pub(crate) fn cache(mut self, cache_manager: Option<CacheManagerRef>) -> Self {
320        self.cache_manager = cache_manager;
321        self
322    }
323
324    /// Sets the `skip_wal_replay`.
325    pub(crate) fn skip_wal_replay(mut self, skip: bool) -> Self {
326        self.skip_wal_replay = skip;
327        self
328    }
329
330    /// Writes region manifest and creates a new region if it does not exist.
331    /// Opens the region if it already exists.
332    ///
333    /// # Panics
334    /// - Panics if `metadata_builder` is not set.
335    /// - Panics if `options` is not set.
336    pub(crate) async fn create_or_open<S: LogStore>(
337        mut self,
338        config: &MitoConfig,
339        wal: &Wal<S>,
340    ) -> Result<MitoRegionRef> {
341        let region_id = self.region_id;
342        let region_dir = self.region_dir();
343        let metadata = self.build_metadata()?;
344        // Only an absent region may be created; an open error must not replace its manifest.
345        match self.maybe_open(config, wal).await? {
346            Some(region) => {
347                let recovered = region.metadata();
348                // Checks the schema of the region.
349                let expect = &metadata;
350                check_recovered_region(
351                    &recovered,
352                    expect.region_id,
353                    &expect.column_metadatas,
354                    &expect.primary_key,
355                )?;
356                // To keep consistency with Create behavior, set the opened Region to RegionRole::Leader.
357                region.set_role(RegionRole::Leader);
358
359                return Ok(region);
360            }
361            None => {
362                debug!(
363                    "No data under directory {}, region_id: {}",
364                    region_dir, self.region_id
365                );
366            }
367        }
368        // Safety: must be set before calling this method.
369        let mut options = self.options.take().unwrap();
370        let object_store = get_object_store(&options.storage, &self.object_store_manager)?;
371        let provider = self.provider::<S>(&options.wal_options)?;
372        let metadata = Arc::new(metadata);
373        // Sets the sst_format based on options or flat_format flag
374        let sst_format = if let Some(format) = options.sst_format {
375            format
376        } else if config.default_flat_format {
377            options.sst_format = Some(FormatType::Flat);
378            FormatType::Flat
379        } else {
380            // Default to PrimaryKeyParquet for newly created regions
381            options.sst_format = Some(FormatType::PrimaryKey);
382            FormatType::PrimaryKey
383        };
384        ensure_json2_uses_flat_format(&metadata, &options)?;
385        options.validate_column_compression(&metadata)?;
386        // The region's WAL history starts in this log store, so reopening
387        // the region on it passes the history check without any entry. It is
388        // recorded before the manifest exists: a log store that gave up its
389        // history meanwhile fails the create before it leaves a manifest.
390        wal.start_region_history(region_id, &provider)?;
391        // Create a manifest manager for this region and writes regions to the manifest file.
392        let mut region_manifest_options =
393            RegionManifestOptions::new(config, &region_dir, &object_store)
394                .with_conditional_publication(
395                    options.requires_conditional_manifest_publication(self.node_lease.is_enabled()),
396                );
397        // Set manifest cache if available
398        region_manifest_options.manifest_cache = self
399            .cache_manager
400            .as_ref()
401            .and_then(|cm| cm.write_cache())
402            .and_then(|wc| wc.manifest_cache());
403        // For remote WAL, we need to set flushed_entry_id to current topic's latest entry id.
404        // Kafka WAL allocation also carries the topic's pruned entry id as a create-time hint.
405        let flushed_entry_id = provider
406            .initial_flushed_entry_id::<S>(wal.store())
407            .max(initial_pruned_entry_id(&options.wal_options));
408        let manifest_manager = RegionManifestManager::new(
409            metadata.clone(),
410            flushed_entry_id,
411            region_manifest_options,
412            sst_format,
413            options.append_mode,
414            &self.stats,
415        )
416        .await?;
417
418        let memtable_builder = self.memtable_builder_provider.builder_for_options(&options);
419        let part_duration = options.compaction.time_window();
420        // Initial memtable id is 0.
421        let mutable =
422            TimePartitions::new(metadata.clone(), memtable_builder.clone(), 0, part_duration);
423
424        debug!(
425            "Create region {} with options: {:?}, default_flat_format: {}",
426            region_id, options, config.default_flat_format
427        );
428
429        let version = VersionBuilder::new(metadata, mutable)
430            .options(options)
431            .flushed_entry_id(flushed_entry_id)
432            .build();
433        let version_control = Arc::new(VersionControl::new(version));
434        let access_layer = Arc::new(AccessLayer::new(
435            self.table_dir.clone(),
436            self.path_type,
437            object_store,
438            self.puffin_manager_factory,
439            self.intermediate_manager,
440        ));
441        let now = self.time_provider.current_time_millis();
442
443        Ok(Arc::new(MitoRegion {
444            region_id,
445            compaction_state: Default::default(),
446            version_control,
447            series_index_version_control: Default::default(),
448            series_index_store: self.series_index_store.clone(),
449            access_layer: access_layer.clone(),
450            // Region is writable after it is created.
451            manifest_ctx: Arc::new(ManifestContext::new(
452                manifest_manager,
453                RegionRoleState::Leader(RegionLeaderState::Writable),
454                self.hook.clone(),
455                self.node_lease.clone(),
456            )),
457            file_purger: create_file_purger(
458                config.gc.enable,
459                self.path_type,
460                self.purge_scheduler,
461                access_layer,
462                self.cache_manager,
463                self.file_ref_manager.clone(),
464                self.series_index_store
465                    .map(|store| RangeIndexDeleter::new(store, region_id)),
466            ),
467            provider,
468            last_flush_millis: AtomicI64::new(now),
469            last_schedule_compaction_millis: AtomicI64::new(now),
470            time_provider: self.time_provider.clone(),
471            topic_latest_entry_id: AtomicU64::new(flushed_entry_id),
472            region_stats: RegionStats::new(),
473            stats: self.stats,
474        }))
475    }
476
477    /// Opens an existing region in read only mode.
478    ///
479    /// Returns error if the region doesn't exist.
480    pub(crate) async fn open<S: LogStore>(
481        mut self,
482        config: &MitoConfig,
483        wal: &Wal<S>,
484    ) -> Result<MitoRegionRef> {
485        let region_id = self.region_id;
486        let region_dir = self.region_dir();
487        let region = self
488            .maybe_open(config, wal)
489            .await?
490            .with_context(|| EmptyRegionDirSnafu {
491                region_id,
492                region_dir: &region_dir,
493            })?;
494
495        ensure!(
496            region.region_id == self.region_id,
497            RegionCorruptedSnafu {
498                region_id: self.region_id,
499                reason: format!(
500                    "recovered region has different region id {}",
501                    region.region_id
502                ),
503            }
504        );
505
506        Ok(region)
507    }
508
509    fn provider<S: LogStore>(&self, wal_options: &WalOptions) -> Result<Provider> {
510        provider_from_wal_options::<S>(self.region_id, wal_options)
511    }
512
513    /// Tries to open the region and returns `None` if the region directory is empty.
514    /// A region it replays must find its WAL history in the log store, see
515    /// [`check_wal_history`].
516    async fn maybe_open<S: LogStore>(
517        &mut self,
518        config: &MitoConfig,
519        wal: &Wal<S>,
520    ) -> Result<Option<MitoRegionRef>> {
521        let now = Instant::now();
522        let mut region_options = self.options.as_ref().unwrap().clone();
523        let object_storage = get_object_store(&region_options.storage, &self.object_store_manager)?;
524        let mut region_manifest_options =
525            RegionManifestOptions::new(config, &self.region_dir(), &object_storage)
526                .with_conditional_publication(
527                    region_options
528                        .requires_conditional_manifest_publication(self.node_lease.is_enabled()),
529                );
530        // Set manifest cache if available
531        region_manifest_options.manifest_cache = self
532            .cache_manager
533            .as_ref()
534            .and_then(|cm| cm.write_cache())
535            .and_then(|wc| wc.manifest_cache());
536        let Some(manifest_manager) =
537            RegionManifestManager::open(region_manifest_options, &self.stats).await?
538        else {
539            return Ok(None);
540        };
541
542        // Backfill `partition_expr` if missing. Use the backfilled metadata in-memory during this open.
543        let manifest = manifest_manager.manifest();
544        let metadata = if manifest.metadata.partition_expr.is_none()
545            && let Some(expr_json) = self.partition_expr_fetcher.fetch_expr(self.region_id).await
546        {
547            let metadata = manifest.metadata.as_ref().clone();
548            let mut builder = RegionMetadataBuilder::from_existing(metadata);
549            builder.partition_expr_json(Some(expr_json));
550            Arc::new(builder.build().context(InvalidMetadataSnafu)?)
551        } else {
552            manifest.metadata.clone()
553        };
554        let metadata = maybe_upgrade_json2_layout(metadata)?;
555        // Updates the region options with the manifest.
556        sanitize_region_options(&manifest, &mut region_options);
557        ensure_json2_uses_flat_format(&metadata, &region_options)?;
558        region_options.validate_column_compression(&metadata)?;
559
560        let region_id = self.region_id;
561        let provider = self.provider::<S>(&region_options.wal_options)?;
562        let wal_entry_reader = self
563            .wal_entry_reader
564            .take()
565            .unwrap_or_else(|| wal.wal_entry_reader(&provider, region_id));
566        let on_region_opened = wal.on_region_opened();
567        let object_store = get_object_store(&region_options.storage, &self.object_store_manager)?;
568
569        debug!(
570            "Open region {} at {} with options: {:?}",
571            region_id, self.table_dir, self.options
572        );
573
574        let access_layer = Arc::new(AccessLayer::new(
575            self.table_dir.clone(),
576            self.path_type,
577            object_store,
578            self.puffin_manager_factory.clone(),
579            self.intermediate_manager.clone(),
580        ));
581        let file_purger = create_file_purger(
582            config.gc.enable,
583            self.path_type,
584            self.purge_scheduler.clone(),
585            access_layer.clone(),
586            self.cache_manager.clone(),
587            self.file_ref_manager.clone(),
588            self.series_index_store
589                .clone()
590                .map(|store| RangeIndexDeleter::new(store, region_id)),
591        );
592        // We should sanitize the region options before creating a new memtable.
593        let memtable_builder = self
594            .memtable_builder_provider
595            .builder_for_options(&region_options);
596        // Use compaction time window in the manifest if region doesn't provide
597        // the time window option.
598        let part_duration = region_options
599            .compaction
600            .time_window()
601            .or(manifest.compaction_time_window);
602        // Initial memtable id is 0.
603        let mutable =
604            TimePartitions::new(metadata.clone(), memtable_builder.clone(), 0, part_duration);
605
606        // Updates region options by manifest before creating version.
607        let version_builder = version_builder_from_manifest(
608            &manifest,
609            metadata,
610            file_purger.clone(),
611            mutable,
612            region_options,
613        );
614        let version = version_builder.build();
615        let flushed_entry_id = version.flushed_entry_id;
616        let version_control = Arc::new(VersionControl::new(version));
617
618        let replay_from_entry_id = self
619            .replay_checkpoint
620            .unwrap_or_default()
621            .max(flushed_entry_id);
622        let topic_latest_entry_id = if !self.skip_wal_replay {
623            check_wal_history(wal, &provider, &version_control.current().version).await?;
624            info!(
625                "Start replaying memtable at replay_from_entry_id: {} for region {}, manifest version: {}, flushed entry id: {}, elapsed: {:?}",
626                replay_from_entry_id,
627                region_id,
628                manifest.manifest_version,
629                flushed_entry_id,
630                now.elapsed()
631            );
632            replay_memtable(
633                &provider,
634                wal_entry_reader,
635                region_id,
636                replay_from_entry_id,
637                &version_control,
638                config.allow_stale_entries,
639                on_region_opened,
640            )
641            .await?;
642            // For remote WAL, we need to set topic_latest_entry_id to current topic's latest entry id.
643            // Only set after the WAL replay is completed.
644
645            if provider.is_remote_wal() && version_control.current().version.memtables.is_empty() {
646                wal.store()
647                    .latest_entry_id(&provider)
648                    .unwrap_or(replay_from_entry_id)
649            } else if provider.is_remote_wal() {
650                replay_from_entry_id
651            } else {
652                0
653            }
654        } else {
655            info!(
656                "Skip the WAL replay for region: {}, manifest version: {}, flushed_entry_id: {}, elapsed: {:?}",
657                region_id,
658                manifest.manifest_version,
659                flushed_entry_id,
660                now.elapsed()
661            );
662
663            if provider.is_remote_wal() {
664                replay_from_entry_id
665            } else {
666                0
667            }
668        };
669
670        if let Some(committed_in_manifest) = manifest.committed_sequence {
671            let committed_after_replay = version_control.committed_sequence();
672            if committed_in_manifest > committed_after_replay {
673                info!(
674                    "Overriding committed sequence, region: {}, flushed_sequence: {}, committed_sequence: {} -> {}",
675                    self.region_id,
676                    version_control.current().version.flushed_sequence,
677                    version_control.committed_sequence(),
678                    committed_in_manifest
679                );
680                version_control.set_committed_sequence(committed_in_manifest);
681            }
682        }
683
684        let now = self.time_provider.current_time_millis();
685
686        let series_index_version_control =
687            match (&self.series_index_store, &self.series_index_purger) {
688                (Some(store), Some(purger))
689                    if is_sparse_metric_metadata(&version_control.current().version.metadata) =>
690                {
691                    load_version_control(store, self.region_id, purger).await
692                }
693                _ => Default::default(),
694            };
695
696        let region = MitoRegion {
697            region_id: self.region_id,
698            compaction_state: Default::default(),
699            version_control: version_control.clone(),
700            series_index_version_control,
701            series_index_store: self.series_index_store.clone(),
702            access_layer: access_layer.clone(),
703            // Region is always opened in read only mode.
704            manifest_ctx: Arc::new(ManifestContext::new(
705                manifest_manager,
706                RegionRoleState::Follower,
707                self.hook.clone(),
708                self.node_lease.clone(),
709            )),
710            file_purger,
711            provider: provider.clone(),
712            last_flush_millis: AtomicI64::new(now),
713            last_schedule_compaction_millis: AtomicI64::new(now),
714            time_provider: self.time_provider.clone(),
715            topic_latest_entry_id: AtomicU64::new(topic_latest_entry_id),
716            region_stats: RegionStats::new(),
717            stats: self.stats.clone(),
718        };
719
720        let region = Arc::new(region);
721
722        maybe_load_cache(&region, config, &self.cache_manager);
723        maybe_preload_parquet_meta_cache(&region, config, &self.cache_manager);
724
725        Ok(Some(region))
726    }
727}
728
729pub(crate) fn provider_from_wal_options<S: LogStore>(
730    region_id: RegionId,
731    wal_options: &WalOptions,
732) -> Result<Provider> {
733    match wal_options {
734        WalOptions::RaftEngine => {
735            ensure!(
736                TypeId::of::<RaftEngineLogStore>() == TypeId::of::<S>()
737                    || TypeId::of::<NoopLogStore>() == TypeId::of::<S>(),
738                error::IncompatibleWalProviderChangeSnafu {
739                    global: "`kafka`",
740                    region: "`raft_engine`",
741                }
742            );
743            Ok(Provider::raft_engine_provider(region_id.as_u64()))
744        }
745        WalOptions::Kafka(options) => {
746            ensure!(
747                TypeId::of::<KafkaLogStore>() == TypeId::of::<S>()
748                    || TypeId::of::<NoopLogStore>() == TypeId::of::<S>(),
749                error::IncompatibleWalProviderChangeSnafu {
750                    global: "`raft_engine`",
751                    region: "`kafka`",
752                }
753            );
754            Ok(Provider::kafka_provider(options.topic.clone()))
755        }
756        WalOptions::ObjectStore(options) => {
757            ensure!(
758                TypeId::of::<RaftEngineLogStore>() != TypeId::of::<S>(),
759                error::IncompatibleWalProviderChangeSnafu {
760                    global: "`raft_engine`",
761                    region: "`object_store`",
762                }
763            );
764            ensure!(
765                TypeId::of::<KafkaLogStore>() != TypeId::of::<S>(),
766                error::IncompatibleWalProviderChangeSnafu {
767                    global: "`kafka`",
768                    region: "`object_store`",
769                }
770            );
771            Ok(Provider::object_store_provider(
772                region_id,
773                options.prefix.clone(),
774            ))
775        }
776        WalOptions::Noop => Ok(Provider::noop_provider()),
777    }
778}
779
780#[cfg(not(feature = "test-shared-fs-region-migration"))]
781fn supports_open_region_object_storage_requirement(object_store: &ObjectStore) -> bool {
782    is_object_storage(object_store)
783}
784
785#[cfg(feature = "test-shared-fs-region-migration")]
786fn supports_open_region_object_storage_requirement(object_store: &ObjectStore) -> bool {
787    // Integration tests can configure multiple datanodes to share the same
788    // temporary home dir. That makes file storage accessible to all test
789    // datanodes, but production file storage still does not satisfy this
790    // requirement.
791    is_object_storage(object_store)
792        || object_store.info().scheme() == object_store::services::FS_SCHEME
793}
794
795/// Creates a version builder from a region manifest.
796pub(crate) fn version_builder_from_manifest(
797    manifest: &RegionManifest,
798    metadata: RegionMetadataRef,
799    file_purger: FilePurgerRef,
800    mutable: TimePartitions,
801    region_options: RegionOptions,
802) -> VersionBuilder {
803    VersionBuilder::new(metadata, mutable)
804        .add_files(file_purger, manifest.files.values().cloned())
805        .flushed_entry_id(manifest.flushed_entry_id)
806        .flushed_sequence(manifest.flushed_sequence)
807        .truncated_entry_id(manifest.truncated_entry_id)
808        .compaction_time_window(manifest.compaction_time_window)
809        .options(region_options)
810}
811
812/// Updates region options by persistent options.
813pub(crate) fn sanitize_region_options(manifest: &RegionManifest, options: &mut RegionOptions) {
814    if let Some(encoding) = options.primary_key_encoding
815        && encoding != manifest.metadata.primary_key_encoding
816    {
817        common_telemetry::warn!(
818            "Overriding primary key encoding from {:?} to {:?} for region {}",
819            encoding,
820            manifest.metadata.primary_key_encoding,
821            manifest.metadata.region_id,
822        );
823    }
824    options.primary_key_encoding = Some(manifest.metadata.primary_key_encoding);
825    if manifest.metadata.primary_key_encoding == PrimaryKeyEncoding::Sparse {
826        options.sst_format = Some(FormatType::Flat);
827    }
828    // Caller options take precedence over the manifest, except JSON2 always requires flat output.
829    let format = options.sst_format.unwrap_or(manifest.sst_format);
830    // Older versions allowed JSON2 with primary-key output; recover without rewriting its history.
831    if format != FormatType::Flat
832        && manifest
833            .metadata
834            .column_metadatas
835            .iter()
836            .any(|column| column.column_schema.data_type.is_json2())
837    {
838        warn!(
839            "Overriding SST format from {:?} to flat for existing JSON2 region {}",
840            format, manifest.metadata.region_id,
841        );
842        options.sst_format = Some(FormatType::Flat);
843    } else {
844        if format != manifest.sst_format {
845            warn!(
846                "Overriding SST format from {:?} (manifest) to {:?} (options) for region {}",
847                manifest.sst_format, format, manifest.metadata.region_id,
848            );
849        }
850        options.sst_format = Some(format);
851    }
852    if let Some(manifest_append_mode) = manifest.append_mode
853        && options.append_mode != manifest_append_mode
854    {
855        common_telemetry::warn!(
856            "Overriding append_mode from {} to {} for region {}",
857            options.append_mode,
858            manifest_append_mode,
859            manifest.metadata.region_id,
860        );
861        options.append_mode = manifest_append_mode;
862    }
863    if options.append_mode && options.merge_mode.take().is_some() {
864        common_telemetry::warn!(
865            "Ignoring merge_mode because append_mode is enabled for region {}",
866            manifest.metadata.region_id,
867        );
868    }
869}
870
871/// Sanitizes open request options before parsing.
872pub(crate) fn sanitize_open_request_options(options: &mut HashMap<String, String>) {
873    let append_mode_enabled = options
874        .get("append_mode")
875        .is_some_and(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "1"));
876
877    if append_mode_enabled && options.remove("merge_mode").is_some() {
878        common_telemetry::warn!(
879            "Ignoring merge_mode in open request options because append_mode is enabled"
880        );
881    }
882}
883
884/// Returns an object store corresponding to `name`. If `name` is `None`, this method returns the default object store.
885pub fn get_object_store(
886    name: &Option<String>,
887    object_store_manager: &ObjectStoreManagerRef,
888) -> Result<object_store::ObjectStore> {
889    if let Some(name) = name {
890        Ok(object_store_manager
891            .find(name)
892            .with_context(|| ObjectStoreNotFoundSnafu {
893                object_store: name.clone(),
894            })?
895            .clone())
896    } else {
897        Ok(object_store_manager.default_object_store().clone())
898    }
899}
900
901/// Checks whether the recovered region has the same schema as region to create.
902pub(crate) fn check_recovered_region(
903    recovered: &RegionMetadata,
904    region_id: RegionId,
905    column_metadatas: &[ColumnMetadata],
906    primary_key: &[ColumnId],
907) -> Result<()> {
908    if recovered.region_id != region_id {
909        error!(
910            "Recovered region {}, expect region {}",
911            recovered.region_id, region_id
912        );
913        return RegionCorruptedSnafu {
914            region_id,
915            reason: format!(
916                "recovered metadata has different region id {}",
917                recovered.region_id
918            ),
919        }
920        .fail();
921    }
922    if recovered.column_metadatas != column_metadatas {
923        error!(
924            "Unexpected schema in recovered region {}, recovered: {:?}, expect: {:?}",
925            recovered.region_id, recovered.column_metadatas, column_metadatas
926        );
927
928        return RegionCorruptedSnafu {
929            region_id,
930            reason: "recovered metadata has different schema",
931        }
932        .fail();
933    }
934    if recovered.primary_key != primary_key {
935        error!(
936            "Unexpected primary key in recovered region {}, recovered: {:?}, expect: {:?}",
937            recovered.region_id, recovered.primary_key, primary_key
938        );
939
940        return RegionCorruptedSnafu {
941            region_id,
942            reason: "recovered metadata has different primary key",
943        }
944        .fail();
945    }
946
947    Ok(())
948}
949
950/// Checks that the log store holds the WAL history of a region that writes
951/// to the WAL through the largest replay watermark `version` persisted,
952/// before the region is replayed. A region that skips the WAL keeps no
953/// history there.
954pub(crate) async fn check_wal_history<S: LogStore>(
955    wal: &Wal<S>,
956    provider: &Provider,
957    version: &Version,
958) -> Result<()> {
959    if version.options.skip_wal {
960        return Ok(());
961    }
962    let watermark = version
963        .flushed_entry_id
964        .max(version.truncated_entry_id.unwrap_or_default());
965    wal.check_region_history(version.metadata.region_id, provider, watermark)
966        .await
967}
968
969/// Replays the mutations from WAL and inserts mutations to memtable of given region.
970pub(crate) async fn replay_memtable<F>(
971    provider: &Provider,
972    mut wal_entry_reader: Box<dyn WalEntryReader>,
973    region_id: RegionId,
974    flushed_entry_id: EntryId,
975    version_control: &VersionControlRef,
976    allow_stale_entries: bool,
977    on_region_opened: F,
978) -> Result<EntryId>
979where
980    F: FnOnce(RegionId, EntryId, &Provider) -> BoxFuture<Result<()>> + Send,
981{
982    let now = Instant::now();
983    let mut rows_replayed = 0;
984    // Last entry id should start from flushed entry id since there might be no
985    // data in the WAL.
986    let mut last_entry_id = flushed_entry_id;
987    let replay_from_entry_id = flushed_entry_id + 1;
988    let region_metadata = version_control.current().version.metadata.clone();
989
990    let mut wal_stream = wal_entry_reader.read(provider, replay_from_entry_id)?;
991    while let Some(res) = wal_stream.next().await {
992        let (entry_id, entry) = res?;
993        if entry_id <= flushed_entry_id {
994            warn!(
995                "Stale WAL entries read during replay, region id: {}, flushed entry id: {}, entry id read: {}",
996                region_id, flushed_entry_id, entry_id
997            );
998            ensure!(
999                allow_stale_entries,
1000                StaleLogEntrySnafu {
1001                    region_id,
1002                    flushed_entry_id,
1003                    unexpected_entry_id: entry_id,
1004                }
1005            );
1006        }
1007        last_entry_id = last_entry_id.max(entry_id);
1008
1009        let mut region_write_ctx = RegionWriteCtx::new(
1010            region_id,
1011            version_control,
1012            provider.clone(),
1013            // For WAL replay, we don't need to track the write bytes rate.
1014            None,
1015            WriteSource::WalReplay,
1016        );
1017        for mutation in entry.mutations {
1018            rows_replayed += mutation
1019                .rows
1020                .as_ref()
1021                .map(|rows| rows.rows.len())
1022                .unwrap_or(0);
1023            region_write_ctx.push_mutation(
1024                mutation.op_type,
1025                mutation.rows,
1026                mutation.write_hint,
1027                OptionOutputTx::none(),
1028                // We should respect the sequence in WAL during replay.
1029                Some(mutation.sequence),
1030                false,
1031            );
1032        }
1033
1034        for bulk_entry in entry.bulk_entries {
1035            let mut part = BulkPart::try_from(bulk_entry)?;
1036            // The entry may miss columns added by a concurrent alter if it was
1037            // written by a writer with a stale schema (older versions kept the
1038            // stale raw data when filling missing columns). Fills missing
1039            // columns like the write path, otherwise the memtable rejects the
1040            // batch. Skips sparse batches as they don't carry tag columns by
1041            // design.
1042            part.fill_missing_columns(&region_metadata)?;
1043            rows_replayed += part.num_rows();
1044            // During replay, we should adopt the sequence from WAL.
1045            let bulk_sequence_from_wal = part.sequence;
1046            ensure!(
1047                region_write_ctx.push_bulk(
1048                    OptionOutputTx::none(),
1049                    part,
1050                    Some(bulk_sequence_from_wal),
1051                    false
1052                ),
1053                RegionCorruptedSnafu {
1054                    region_id,
1055                    reason: "unable to replay memtable with bulk entries",
1056                }
1057            );
1058        }
1059
1060        // set next_entry_id and write to memtable.
1061        region_write_ctx.set_next_entry_id(last_entry_id + 1);
1062        // Publish the replayed sequences only after all rows (including bulk
1063        // parts) are installed, matching the write path ordering.
1064        region_write_ctx.write_memtables().await;
1065    }
1066
1067    // TODO(weny): We need to update `flushed_entry_id` in the region manifest
1068    // to avoid reading potentially incomplete entries in the future.
1069    (on_region_opened)(region_id, flushed_entry_id, provider).await?;
1070
1071    let series_count = version_control.current().series_count();
1072    info!(
1073        "Replay WAL for region: {}, provider: {:?}, rows recovered: {}, replay from entry id: {}, last entry id: {}, total timeseries replayed: {}, elapsed: {:?}",
1074        region_id,
1075        provider,
1076        rows_replayed,
1077        replay_from_entry_id,
1078        last_entry_id,
1079        series_count,
1080        now.elapsed()
1081    );
1082    Ok(last_entry_id)
1083}
1084
1085/// A task to load and fill the region file cache.
1086pub(crate) struct RegionLoadCacheTask {
1087    region: MitoRegionRef,
1088}
1089
1090impl RegionLoadCacheTask {
1091    pub(crate) fn new(region: MitoRegionRef) -> Self {
1092        Self { region }
1093    }
1094
1095    /// Fills the file cache with index files from the region.
1096    pub(crate) async fn fill_cache(&self, file_cache: &FileCache) {
1097        let region_id = self.region.region_id;
1098        let table_dir = self.region.access_layer.table_dir();
1099        let path_type = self.region.access_layer.path_type();
1100        let object_store = self.region.access_layer.object_store();
1101        let version_control = &self.region.version_control;
1102
1103        // Collects IndexKeys, file sizes, and max timestamps for files that need to be downloaded
1104        let mut files_to_download = Vec::new();
1105        let mut files_already_cached = 0;
1106
1107        {
1108            let version = version_control.current().version;
1109            for level in version.ssts.levels() {
1110                for file_handle in level.files.values() {
1111                    let file_meta = file_handle.meta_ref();
1112                    if file_meta.exists_index() {
1113                        let puffin_key = IndexKey::new(
1114                            file_meta.region_id,
1115                            file_meta.file_id,
1116                            FileType::Puffin(file_meta.index_version),
1117                        );
1118
1119                        if !file_cache.contains_key(&puffin_key) {
1120                            files_to_download.push((
1121                                puffin_key,
1122                                file_meta.index_file_size,
1123                                file_meta.time_range.1, // max timestamp
1124                            ));
1125                        } else {
1126                            files_already_cached += 1;
1127                        }
1128                    }
1129                }
1130            }
1131            // Releases the Version after the scope to avoid holding the memtables and file handles
1132            // for a long time.
1133        }
1134
1135        // Sorts files by max timestamp in descending order to loads latest files first
1136        files_to_download.sort_by_key(|b| std::cmp::Reverse(b.2));
1137
1138        let total_files = files_to_download.len() as i64;
1139
1140        info!(
1141            "Starting background index cache preload for region {}, total_files_to_download: {}, files_already_cached: {}",
1142            region_id, total_files, files_already_cached
1143        );
1144
1145        CACHE_FILL_PENDING_FILES.add(total_files);
1146
1147        let mut files_downloaded = 0;
1148        let mut files_skipped = 0;
1149
1150        for (puffin_key, file_size, max_timestamp) in files_to_download {
1151            let current_size = file_cache.puffin_cache_size();
1152            let capacity = file_cache.puffin_cache_capacity();
1153            let region_state = self.region.state();
1154            if !can_load_cache(region_state) {
1155                info!(
1156                    "Stopping index cache by state: {:?}, region: {}, current_size: {}, capacity: {}",
1157                    region_state, region_id, current_size, capacity
1158                );
1159                break;
1160            }
1161
1162            // Checks if adding this file would exceed capacity
1163            if current_size + file_size > capacity {
1164                info!(
1165                    "Stopping index cache preload due to capacity limit, region: {}, file_id: {}, current_size: {}, file_size: {}, capacity: {}, file_timestamp: {:?}",
1166                    region_id, puffin_key.file_id, current_size, file_size, capacity, max_timestamp
1167                );
1168                files_skipped = (total_files - files_downloaded) as usize;
1169                CACHE_FILL_PENDING_FILES.sub(total_files - files_downloaded);
1170                break;
1171            }
1172
1173            let index_version = if let FileType::Puffin(version) = puffin_key.file_type {
1174                version
1175            } else {
1176                unreachable!("`files_to_download` should only contains Puffin files");
1177            };
1178            let index_id = RegionIndexId::new(
1179                RegionFileId::new(puffin_key.region_id, puffin_key.file_id),
1180                index_version,
1181            );
1182
1183            let index_remote_path = location::index_file_path(table_dir, index_id, path_type);
1184
1185            match file_cache
1186                .download(puffin_key, &index_remote_path, object_store, file_size)
1187                .await
1188            {
1189                Ok(_) => {
1190                    debug!(
1191                        "Downloaded index file to write cache, region: {}, file_id: {}",
1192                        region_id, puffin_key.file_id
1193                    );
1194                    files_downloaded += 1;
1195                    CACHE_FILL_DOWNLOADED_FILES.inc_by(1);
1196                    CACHE_FILL_PENDING_FILES.dec();
1197                }
1198                Err(e) => {
1199                    warn!(
1200                        e; "Failed to download index file to write cache, region: {}, file_id: {}",
1201                        region_id, puffin_key.file_id
1202                    );
1203                    CACHE_FILL_PENDING_FILES.dec();
1204                }
1205            }
1206        }
1207
1208        info!(
1209            "Completed background cache fill task for region {}, total_files: {}, files_downloaded: {}, files_already_cached: {}, files_skipped: {}",
1210            region_id, total_files, files_downloaded, files_already_cached, files_skipped
1211        );
1212    }
1213}
1214
1215/// Loads all index (Puffin) files from the version into the write cache.
1216fn maybe_load_cache(
1217    region: &MitoRegionRef,
1218    config: &MitoConfig,
1219    cache_manager: &Option<CacheManagerRef>,
1220) {
1221    let Some(cache_manager) = cache_manager else {
1222        return;
1223    };
1224    let Some(write_cache) = cache_manager.write_cache() else {
1225        return;
1226    };
1227
1228    let preload_enabled = config.preload_index_cache;
1229    if !preload_enabled {
1230        return;
1231    }
1232
1233    let task = RegionLoadCacheTask::new(region.clone());
1234    write_cache.load_region_cache(task);
1235}
1236
1237/// Preloads Parquet metadata into the in-memory SST meta cache on region open.
1238///
1239/// This improves the latency of the first query after server start by avoiding large Parquet
1240/// metadata reads on demand.
1241///
1242/// The preload is best-effort:
1243/// - Always tries to warm from the local write cache (file cache) first.
1244/// - If the region storage backend is local filesystem (`Scheme::Fs`), it may also load metadata
1245///   directly from the local store.
1246/// - It will not fetch metadata from remote object stores (S3/GCS/OSS/...).
1247#[allow(clippy::too_many_arguments)]
1248async fn preload_parquet_meta_cache_for_files(
1249    region_id: RegionId,
1250    cache_manager: CacheManagerRef,
1251    sst_meta_cache_capacity: u64,
1252    table_dir: String,
1253    path_type: PathType,
1254    object_store: object_store::ObjectStore,
1255    region_metadata: RegionMetadataRef,
1256    mut files: Vec<FileHandle>,
1257) -> usize {
1258    if !cache_manager.sst_meta_cache_enabled()
1259        || sst_meta_cache_capacity == 0
1260        || cache_manager.sst_meta_cache_is_full()
1261    {
1262        return 0;
1263    }
1264
1265    let allow_direct_load = object_store.info().scheme() == object_store::services::FS_SCHEME;
1266
1267    // Sort by time range so we can prefer preloading newer files first.
1268    files.sort_by_key(|b| std::cmp::Reverse(b.meta_ref().time_range.1));
1269
1270    let mut loaded = 0usize;
1271    for file_handle in files {
1272        // Stop when the protected authoritative tier is full.
1273        if cache_manager.sst_meta_cache_is_full() {
1274            break;
1275        }
1276
1277        let file_id = file_handle.file_id();
1278        let mut cache_metrics = MetadataCacheMetrics::default();
1279        if let Some(metadata) = cache_manager
1280            .get_compact_sst_meta_data(file_id, PageIndexPolicy::Optional)
1281            .await
1282        {
1283            if file_handle.raw_primary_key_range().is_none()
1284                && let Some(primary_key_range) = extract_primary_key_range(
1285                    metadata.parquet_metadata().as_ref(),
1286                    &region_metadata,
1287                )
1288            {
1289                file_handle.set_primary_key_range(primary_key_range);
1290            }
1291            continue;
1292        }
1293
1294        if let Some(write_cache) = cache_manager.write_cache() {
1295            let key = IndexKey::new(file_id.region_id(), file_id.file_id(), FileType::Parquet);
1296            if let Some(metadata) = write_cache
1297                .file_cache()
1298                .get_sst_meta_data(
1299                    key,
1300                    &mut cache_metrics,
1301                    PageIndexPolicy::Optional,
1302                    &common_runtime::global_runtime(),
1303                )
1304                .await
1305            {
1306                let decoded = metadata.decoded();
1307                if file_handle.raw_primary_key_range().is_none()
1308                    && let Some(primary_key_range) = extract_primary_key_range(
1309                        decoded.parquet_metadata().as_ref(),
1310                        &region_metadata,
1311                    )
1312                {
1313                    file_handle.set_primary_key_range(primary_key_range);
1314                }
1315                match metadata {
1316                    SstMetaPreparation::Prepared(metadata) => {
1317                        cache_manager.put_compact_sst_meta(file_id, metadata.compact);
1318                        loaded += 1;
1319                    }
1320                    SstMetaPreparation::DecodedOnly { encoding_error, .. } => warn!(
1321                        encoding_error;
1322                        "Failed to encode file-cached SST metadata during preload, region: {}, file: {}",
1323                        region_id,
1324                        file_id.file_id()
1325                    ),
1326                }
1327                continue;
1328            }
1329        }
1330
1331        if !allow_direct_load {
1332            continue;
1333        }
1334
1335        let file_size = file_handle.meta_ref().file_size;
1336        let file_path = file_handle.file_path(&table_dir, path_type);
1337        let mut loader = MetadataLoader::new(object_store.clone(), &file_path, file_size);
1338        loader.with_page_index_policy(PageIndexPolicy::Optional);
1339        match loader.load(&mut cache_metrics).await {
1340            Ok(metadata) => {
1341                if let Some(primary_key_range) =
1342                    extract_primary_key_range(&metadata, &region_metadata)
1343                {
1344                    file_handle.set_primary_key_range(primary_key_range);
1345                }
1346                match prepare_sst_meta(
1347                    &file_path,
1348                    metadata,
1349                    // The SST can predate schema changes, so decode its embedded region metadata
1350                    // instead of substituting the region's current schema.
1351                    None,
1352                    PageIndexPolicy::Optional,
1353                    &common_runtime::global_runtime(),
1354                )
1355                .await
1356                {
1357                    Ok(SstMetaPreparation::Prepared(metadata)) => {
1358                        // Preload the compact canonical entry only. Query traffic promotes decoded
1359                        // entries into the separate acceleration tier according to actual demand.
1360                        cache_manager.put_compact_sst_meta(file_id, metadata.compact);
1361                        loaded += 1;
1362                    }
1363                    Ok(SstMetaPreparation::DecodedOnly { encoding_error, .. }) => warn!(
1364                        encoding_error;
1365                        "Failed to encode preloaded parquet metadata, region: {}, file: {}",
1366                        region_id,
1367                        file_path
1368                    ),
1369                    Err(err) => {
1370                        warn!(
1371                            err; "Failed to prepare preloaded parquet metadata, region: {}, file: {}",
1372                            region_id, file_path
1373                        );
1374                    }
1375                }
1376            }
1377            Err(err) => {
1378                // Preloading is best-effort. Failure shouldn't affect region open.
1379                warn!(
1380                    err; "Failed to preload parquet metadata from local store, region: {}, file: {}",
1381                    region_id, file_path
1382                );
1383            }
1384        }
1385    }
1386
1387    loaded
1388}
1389
1390fn maybe_preload_parquet_meta_cache(
1391    region: &MitoRegionRef,
1392    config: &MitoConfig,
1393    cache_manager: &Option<CacheManagerRef>,
1394) {
1395    let Some(cache_manager) = cache_manager else {
1396        return;
1397    };
1398    if !cache_manager.sst_meta_cache_enabled() {
1399        return;
1400    }
1401
1402    // Skip if SST meta cache is disabled.
1403    if config.sst_meta_cache_size.as_bytes() == 0 {
1404        return;
1405    }
1406    if !config.preload_index_cache {
1407        return;
1408    }
1409
1410    let region = region.clone();
1411    let cache_manager = cache_manager.clone();
1412    let sst_meta_cache_capacity = config.sst_meta_cache_size.as_bytes();
1413
1414    tokio::spawn(async move {
1415        // Safety: semaphore must exist.
1416        let _permit = PARQUET_META_PRELOAD_SEMAPHORE.acquire().await.unwrap();
1417
1418        let region_id = region.region_id;
1419        let table_dir = region.access_layer.table_dir().to_string();
1420        let path_type = region.access_layer.path_type();
1421        let object_store = region.access_layer.object_store().clone();
1422        let region_metadata = region.version_control.current().version.metadata.clone();
1423
1424        // Collect SST files. Do not hold the version longer than needed.
1425        let mut files = Vec::new();
1426        {
1427            let version = region.version_control.current().version;
1428            for level in version.ssts.levels() {
1429                for file_handle in level.files.values() {
1430                    files.push(file_handle.clone());
1431                }
1432            }
1433        }
1434        let preloading_start = Instant::now();
1435        let loaded = preload_parquet_meta_cache_for_files(
1436            region_id,
1437            cache_manager,
1438            sst_meta_cache_capacity,
1439            table_dir,
1440            path_type,
1441            object_store,
1442            region_metadata,
1443            files,
1444        )
1445        .await;
1446        let preloading_cost = preloading_start.elapsed();
1447
1448        if loaded > 0 {
1449            info!(
1450                "Preloaded parquet metadata for region {}, loaded_files: {}, elapsed_ms: {}",
1451                region_id,
1452                loaded,
1453                preloading_cost.as_millis()
1454            );
1455        }
1456    });
1457}
1458
1459fn can_load_cache(state: RegionRoleState) -> bool {
1460    match state {
1461        RegionRoleState::Leader(RegionLeaderState::Writable)
1462        | RegionRoleState::Leader(RegionLeaderState::Staging)
1463        | RegionRoleState::Leader(RegionLeaderState::Altering)
1464        | RegionRoleState::Leader(RegionLeaderState::EnteringStaging)
1465        | RegionRoleState::Leader(RegionLeaderState::Editing)
1466        | RegionRoleState::Follower => true,
1467        // The region will be closed soon if it is downgrading.
1468        RegionRoleState::Leader(RegionLeaderState::Downgrading)
1469        | RegionRoleState::Leader(RegionLeaderState::Dropping)
1470        | RegionRoleState::Leader(RegionLeaderState::Truncating) => false,
1471    }
1472}
1473
1474#[cfg(test)]
1475mod tests {
1476    use std::collections::HashMap;
1477    use std::sync::Arc;
1478    use store_api::codec::PrimaryKeyEncoding;
1479
1480    use arrow_schema::extension::ExtensionType;
1481    use common_base::readable_size::ReadableSize;
1482    use common_error::ext::WhateverResult;
1483    use common_test_util::temp_dir::create_temp_dir;
1484    use common_time::Timestamp;
1485    use common_wal::options::{KafkaWalOptions, ObjectStoreWalOptions, WalOptions};
1486    use datatypes::arrow::array::{ArrayRef, BinaryArray, Int64Array};
1487    use datatypes::arrow::record_batch::RecordBatch;
1488    use datatypes::extension::json::{Json2ExtensionType, JsonMetadata};
1489    use datatypes::json::JsonSettings;
1490    use datatypes::prelude::ConcreteDataType;
1491    use datatypes::schema::ColumnSchema;
1492    use datatypes::types::json_type::{JsonNativeType, JsonObjectType};
1493    use log_store::kafka::log_store::KafkaLogStore;
1494    use log_store::noop::log_store::NoopLogStore;
1495    use log_store::raft_engine::log_store::RaftEngineLogStore;
1496    use object_store::ObjectStore;
1497    use object_store::services::{Fs, Memory, S3};
1498    use parquet::arrow::ArrowWriter;
1499    use parquet::file::metadata::{KeyValue, PageIndexPolicy};
1500    use parquet::file::properties::WriterProperties;
1501    use store_api::logstore::provider::Provider;
1502    use store_api::metadata::RegionMetadataBuilder;
1503    use store_api::region_request::PathType;
1504    use store_api::storage::{FileId, RegionId};
1505
1506    use super::{
1507        initial_pruned_entry_id, maybe_upgrade_json2_layout, preload_parquet_meta_cache_for_files,
1508        provider_from_wal_options, sanitize_region_options,
1509        supports_open_region_object_storage_requirement,
1510    };
1511    use crate::cache::CacheManager;
1512    use crate::cache::file_cache::{FileType, IndexKey};
1513    use crate::error;
1514    use crate::manifest::action::{RegionManifest, RemovedFilesRecord};
1515    use crate::region::options::RegionOptions;
1516    use crate::sst::FormatType;
1517    use crate::sst::file::{FileHandle, FileMeta};
1518    use crate::sst::file_purger::NoopFilePurger;
1519    use crate::sst::parquet::PARQUET_METADATA_KEY;
1520    use crate::test_util::TestEnv;
1521    use crate::test_util::sst_util::sst_region_metadata;
1522
1523    fn build_test_manifest(sst_format: FormatType) -> RegionManifest {
1524        RegionManifest {
1525            metadata: Arc::new(sst_region_metadata()),
1526            files: HashMap::new(),
1527            removed_files: RemovedFilesRecord::default(),
1528            flushed_entry_id: 0,
1529            flushed_sequence: 0,
1530            committed_sequence: None,
1531            manifest_version: 0,
1532            truncated_entry_id: None,
1533            compaction_time_window: None,
1534            sst_format,
1535            append_mode: None,
1536        }
1537    }
1538
1539    fn build_fs_object_store() -> ObjectStore {
1540        ObjectStore::new(Fs::default().root("/tmp")).unwrap()
1541    }
1542
1543    #[test]
1544    fn test_provider_from_object_store_wal_options() {
1545        let region_id = RegionId::new(1, 2);
1546        let wal_options = WalOptions::ObjectStore(ObjectStoreWalOptions::new("wal".to_string()));
1547
1548        let provider = provider_from_wal_options::<NoopLogStore>(region_id, &wal_options).unwrap();
1549        assert_eq!(
1550            Provider::object_store_provider(region_id, "wal".to_string()),
1551            provider
1552        );
1553
1554        let err =
1555            provider_from_wal_options::<RaftEngineLogStore>(region_id, &wal_options).unwrap_err();
1556        assert!(matches!(
1557            err,
1558            error::Error::IncompatibleWalProviderChange { .. }
1559        ));
1560        let err = provider_from_wal_options::<KafkaLogStore>(region_id, &wal_options).unwrap_err();
1561        assert!(matches!(
1562            err,
1563            error::Error::IncompatibleWalProviderChange { .. }
1564        ));
1565    }
1566
1567    #[test]
1568    fn test_initial_pruned_entry_id() {
1569        assert_eq!(0, initial_pruned_entry_id(&WalOptions::RaftEngine));
1570        assert_eq!(0, initial_pruned_entry_id(&WalOptions::Noop));
1571        assert_eq!(
1572            0,
1573            initial_pruned_entry_id(&WalOptions::ObjectStore(ObjectStoreWalOptions::new(
1574                "wal".to_string()
1575            )))
1576        );
1577        assert_eq!(
1578            0,
1579            initial_pruned_entry_id(&WalOptions::Kafka(KafkaWalOptions::new(
1580                "test_topic".to_string()
1581            )))
1582        );
1583        assert_eq!(
1584            42,
1585            initial_pruned_entry_id(&WalOptions::Kafka(KafkaWalOptions {
1586                topic: "test_topic".to_string(),
1587                initial_pruned_entry_id: Some(42),
1588            }))
1589        );
1590    }
1591
1592    #[test]
1593    fn test_upgrade_json2_layout() -> WhateverResult<()> {
1594        let settings = JsonSettings::try_new(vec![], Some(3))?;
1595        let extension = Json2ExtensionType::new(Arc::new(JsonMetadata::new_v1(settings.clone())));
1596        let mut column = ColumnSchema::new(
1597            "field_0",
1598            ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::new())),
1599            true,
1600        );
1601        column.with_extension_type(&extension);
1602
1603        let mut metadata = sst_region_metadata();
1604        metadata.column_metadatas[2].column_schema = column;
1605        let builder = RegionMetadataBuilder::from_existing(metadata);
1606        let metadata = Arc::new(builder.build()?);
1607
1608        let upgraded = maybe_upgrade_json2_layout(metadata)?;
1609        let column = &upgraded.column_metadatas[2].column_schema;
1610        let extension = column.extension_type::<Json2ExtensionType>()?.unwrap();
1611        assert!(extension.metadata().is_version_2());
1612        assert_eq!(&settings, extension.metadata().json_settings());
1613
1614        let arrow_schema = upgraded.schema.arrow_schema();
1615        let field = arrow_schema.field_with_name("field_0").unwrap();
1616        let extension = field.try_extension_type::<Json2ExtensionType>().unwrap();
1617        assert!(extension.metadata().is_version_2());
1618
1619        let unchanged = maybe_upgrade_json2_layout(upgraded.clone())?;
1620        assert!(Arc::ptr_eq(&upgraded, &unchanged));
1621        Ok(())
1622    }
1623
1624    #[test]
1625    #[cfg(not(feature = "test-shared-fs-region-migration"))]
1626    fn test_open_requirement_rejects_fs_object_store() {
1627        let object_store = build_fs_object_store();
1628
1629        assert!(!supports_open_region_object_storage_requirement(
1630            &object_store
1631        ));
1632    }
1633
1634    #[test]
1635    #[cfg(feature = "test-shared-fs-region-migration")]
1636    fn test_open_requirement_accepts_shared_fs_object_store_for_tests() {
1637        let object_store = build_fs_object_store();
1638
1639        assert!(supports_open_region_object_storage_requirement(
1640            &object_store
1641        ));
1642    }
1643
1644    #[test]
1645    fn test_open_requirement_accepts_s3_object_store() {
1646        let object_store = ObjectStore::new(
1647            S3::default()
1648                .bucket("test-bucket")
1649                .region("us-east-1")
1650                .disable_ec2_metadata(),
1651        )
1652        .unwrap();
1653
1654        assert!(supports_open_region_object_storage_requirement(
1655            &object_store
1656        ));
1657    }
1658
1659    #[test]
1660    fn test_sparse_reopen_uses_persisted_encoding_and_flat_sst() {
1661        for supplied in [None, Some(PrimaryKeyEncoding::Dense)] {
1662            let mut manifest = build_test_manifest(FormatType::PrimaryKey);
1663            Arc::make_mut(&mut manifest.metadata).primary_key_encoding = PrimaryKeyEncoding::Sparse;
1664            let mut options = RegionOptions {
1665                primary_key_encoding: supplied,
1666                sst_format: Some(FormatType::PrimaryKey),
1667                ..Default::default()
1668            };
1669            sanitize_region_options(&manifest, &mut options);
1670            assert_eq!(options.primary_key_encoding(), PrimaryKeyEncoding::Sparse);
1671            assert_eq!(options.sst_format, Some(FormatType::Flat));
1672        }
1673    }
1674
1675    #[test]
1676    fn test_sanitize_region_options_options_format_wins() {
1677        // Manifest persisted PrimaryKey, but the re-parsed options now request Flat
1678        // (e.g., bulk memtable). The options' value must win.
1679        let manifest = build_test_manifest(FormatType::PrimaryKey);
1680        let mut options = RegionOptions {
1681            sst_format: Some(FormatType::Flat),
1682            ..Default::default()
1683        };
1684        sanitize_region_options(&manifest, &mut options);
1685        assert_eq!(options.sst_format, Some(FormatType::Flat));
1686    }
1687
1688    #[test]
1689    fn test_sanitize_region_options_fills_from_manifest_when_unset() {
1690        // When the caller didn't specify sst_format, fall back to the manifest value
1691        // so downstream code always sees a concrete format.
1692        let manifest = build_test_manifest(FormatType::Flat);
1693        let mut options = RegionOptions {
1694            sst_format: None,
1695            ..Default::default()
1696        };
1697        sanitize_region_options(&manifest, &mut options);
1698        assert_eq!(options.sst_format, Some(FormatType::Flat));
1699    }
1700
1701    #[rstest::rstest]
1702    fn test_sanitize_region_options_recovers_json2(
1703        #[values(FormatType::PrimaryKey, FormatType::Flat)] persisted: FormatType,
1704        #[values(None, Some(FormatType::PrimaryKey), Some(FormatType::Flat))] requested: Option<
1705            FormatType,
1706        >,
1707    ) {
1708        let mut manifest = build_test_manifest(persisted);
1709        let mut metadata = manifest.metadata.as_ref().clone();
1710        metadata.column_metadatas[2].column_schema.data_type =
1711            ConcreteDataType::json2(JsonNativeType::Object(JsonObjectType::new()));
1712        manifest.metadata = Arc::new(
1713            RegionMetadataBuilder::from_existing(metadata)
1714                .build()
1715                .unwrap(),
1716        );
1717        let mut options = RegionOptions {
1718            sst_format: requested,
1719            ..Default::default()
1720        };
1721        sanitize_region_options(&manifest, &mut options);
1722        assert_eq!(Some(FormatType::Flat), options.sst_format);
1723        assert_eq!(persisted, manifest.sst_format);
1724    }
1725
1726    fn sst_parquet_bytes(batch: &RecordBatch) -> Vec<u8> {
1727        let key_value_meta = KeyValue::new(
1728            PARQUET_METADATA_KEY.to_string(),
1729            sst_region_metadata().to_json().unwrap(),
1730        );
1731        let props = WriterProperties::builder()
1732            .set_key_value_metadata(Some(vec![key_value_meta]))
1733            .build();
1734
1735        let mut parquet_bytes = Vec::new();
1736        let mut writer =
1737            ArrowWriter::try_new(&mut parquet_bytes, batch.schema(), Some(props)).unwrap();
1738        writer.write(batch).unwrap();
1739        writer.close().unwrap();
1740
1741        parquet_bytes
1742    }
1743
1744    #[tokio::test]
1745    async fn test_preload_parquet_meta_cache_uses_file_cache() {
1746        let env = TestEnv::new().await;
1747
1748        let local_store = ObjectStore::new(Memory::default()).unwrap();
1749        let write_cache = env
1750            .create_write_cache(local_store, ReadableSize::mb(1024))
1751            .await;
1752        let cache_manager = Arc::new(
1753            CacheManager::builder()
1754                .sst_meta_cache_size(1024 * 1024)
1755                .write_cache(Some(write_cache.clone()))
1756                .build(),
1757        );
1758
1759        let region_id = RegionId::new(1, 1);
1760        let file_id = FileId::random();
1761
1762        let col = Arc::new(Int64Array::from_iter_values([1, 2, 3])) as ArrayRef;
1763        let keys =
1764            ["a", "b", "c"].map(|tag| crate::test_util::sst_util::new_primary_key(&[tag, ""]));
1765        let primary_key = Arc::new(BinaryArray::from_iter_values(&keys)) as ArrayRef;
1766        let batch = RecordBatch::try_from_iter([
1767            ("col", col),
1768            (
1769                store_api::storage::consts::PRIMARY_KEY_COLUMN_NAME,
1770                primary_key,
1771            ),
1772        ])
1773        .unwrap();
1774        let parquet_bytes = sst_parquet_bytes(&batch);
1775        let file_size = parquet_bytes.len() as u64;
1776
1777        let file_meta = FileMeta {
1778            region_id,
1779            file_id,
1780            time_range: (Timestamp::new_millisecond(0), Timestamp::new_millisecond(1)),
1781            level: 0,
1782            file_size,
1783            max_row_group_uncompressed_size: 0,
1784            available_indexes: Default::default(),
1785            indexes: vec![],
1786            index_file_size: 0,
1787            index_version: 0,
1788            num_rows: 3,
1789            num_row_groups: 1,
1790            sequence: None,
1791            partition_expr: None,
1792            num_series: 0,
1793            ..Default::default()
1794        };
1795        let file_handle = FileHandle::new(file_meta, Arc::new(NoopFilePurger));
1796
1797        let table_dir = "test_table";
1798        let path_type = PathType::Bare;
1799        let remote_path = file_handle.file_path(table_dir, path_type);
1800
1801        let source_store = ObjectStore::new(Memory::default()).unwrap();
1802        source_store
1803            .write(&remote_path, parquet_bytes)
1804            .await
1805            .unwrap();
1806
1807        // Put the parquet file into the write cache, so file cache contains metadata.
1808        let index_key = IndexKey::new(region_id, file_id, FileType::Parquet);
1809        write_cache
1810            .file_cache()
1811            .download(index_key, &remote_path, &source_store, file_size)
1812            .await
1813            .unwrap();
1814
1815        let region_file_id = file_handle.file_id();
1816        assert!(
1817            cache_manager
1818                .get_parquet_meta_data_from_mem_cache(region_file_id)
1819                .await
1820                .is_none()
1821        );
1822
1823        let loaded = preload_parquet_meta_cache_for_files(
1824            region_id,
1825            cache_manager.clone(),
1826            1024 * 1024,
1827            table_dir.to_string(),
1828            path_type,
1829            source_store.clone(),
1830            Arc::new(sst_region_metadata()),
1831            vec![file_handle.clone()],
1832        )
1833        .await;
1834
1835        // Should warm the in-memory cache from the local file cache.
1836        assert_eq!(loaded, 1);
1837        assert!(
1838            cache_manager
1839                .get_compact_sst_meta_data(
1840                    region_file_id,
1841                    parquet::file::metadata::PageIndexPolicy::Optional,
1842                )
1843                .await
1844                .is_some()
1845        );
1846        assert!(file_handle.raw_primary_key_range().is_some());
1847    }
1848
1849    #[tokio::test]
1850    async fn test_preload_parquet_meta_cache_skips_files_not_in_file_cache() {
1851        let cache_manager = Arc::new(
1852            CacheManager::builder()
1853                .sst_meta_cache_size(1024 * 1024)
1854                .build(),
1855        );
1856
1857        let region_id = RegionId::new(1, 1);
1858        let file_id = FileId::random();
1859
1860        // Without a local file cache entry, preloading should skip the file.
1861        let file_meta = FileMeta {
1862            region_id,
1863            file_id,
1864            time_range: (Timestamp::new_millisecond(0), Timestamp::new_millisecond(1)),
1865            level: 0,
1866            file_size: 0,
1867            max_row_group_uncompressed_size: 0,
1868            available_indexes: Default::default(),
1869            indexes: vec![],
1870            index_file_size: 0,
1871            index_version: 0,
1872            num_rows: 3,
1873            num_row_groups: 1,
1874            sequence: None,
1875            partition_expr: None,
1876            num_series: 0,
1877            ..Default::default()
1878        };
1879        let file_handle = FileHandle::new(file_meta, Arc::new(NoopFilePurger));
1880
1881        let table_dir = "test_table";
1882        let path_type = PathType::Bare;
1883        let remote_path = file_handle.file_path(table_dir, path_type);
1884
1885        // Even if the remote object store has the file, we should not preload from it.
1886        let object_store = ObjectStore::new(Memory::default()).unwrap();
1887        object_store
1888            .write(&remote_path, b"noop".as_slice())
1889            .await
1890            .unwrap();
1891
1892        let region_file_id = file_handle.file_id();
1893        assert!(
1894            cache_manager
1895                .get_parquet_meta_data_from_mem_cache(region_file_id)
1896                .await
1897                .is_none()
1898        );
1899
1900        let loaded = preload_parquet_meta_cache_for_files(
1901            region_id,
1902            cache_manager.clone(),
1903            1024 * 1024,
1904            table_dir.to_string(),
1905            path_type,
1906            object_store,
1907            Arc::new(sst_region_metadata()),
1908            vec![file_handle],
1909        )
1910        .await;
1911
1912        assert_eq!(loaded, 0);
1913        assert!(
1914            cache_manager
1915                .get_parquet_meta_data_from_mem_cache(region_file_id)
1916                .await
1917                .is_none()
1918        );
1919    }
1920
1921    #[tokio::test]
1922    async fn test_preload_parquet_meta_cache_loads_from_local_fs() {
1923        let cache_manager = Arc::new(
1924            CacheManager::builder()
1925                .sst_meta_cache_size(1024 * 1024)
1926                .build(),
1927        );
1928
1929        let region_id = RegionId::new(1, 1);
1930        let file_id = FileId::random();
1931
1932        let col = Arc::new(Int64Array::from_iter_values([1, 2, 3])) as ArrayRef;
1933        let batch = RecordBatch::try_from_iter([("col", col)]).unwrap();
1934        let parquet_bytes = sst_parquet_bytes(&batch);
1935
1936        // file_size is 0 when it's missing/defaulted in manifests; MetadataLoader::load will stat
1937        // the local filesystem to retrieve it.
1938        let file_meta = FileMeta {
1939            region_id,
1940            file_id,
1941            time_range: (Timestamp::new_millisecond(0), Timestamp::new_millisecond(1)),
1942            level: 0,
1943            file_size: 0,
1944            max_row_group_uncompressed_size: 0,
1945            available_indexes: Default::default(),
1946            indexes: vec![],
1947            index_file_size: 0,
1948            index_version: 0,
1949            num_rows: 3,
1950            num_row_groups: 1,
1951            sequence: None,
1952            partition_expr: None,
1953            num_series: 0,
1954            ..Default::default()
1955        };
1956        let file_handle = FileHandle::new(file_meta, Arc::new(NoopFilePurger));
1957
1958        let table_dir = "test_table";
1959        let path_type = PathType::Bare;
1960        let file_path = file_handle.file_path(table_dir, path_type);
1961
1962        let root = create_temp_dir("parquet-meta-preload");
1963        let object_store =
1964            ObjectStore::new(Fs::default().root(root.path().to_str().unwrap())).unwrap();
1965        object_store.write(&file_path, parquet_bytes).await.unwrap();
1966
1967        let region_file_id = file_handle.file_id();
1968        assert!(
1969            cache_manager
1970                .get_parquet_meta_data_from_mem_cache(region_file_id)
1971                .await
1972                .is_none()
1973        );
1974
1975        let loaded = preload_parquet_meta_cache_for_files(
1976            region_id,
1977            cache_manager.clone(),
1978            1024 * 1024,
1979            table_dir.to_string(),
1980            path_type,
1981            object_store,
1982            Arc::new(sst_region_metadata()),
1983            vec![file_handle],
1984        )
1985        .await;
1986
1987        assert_eq!(loaded, 1);
1988        assert!(
1989            cache_manager
1990                .get_compact_sst_meta_data(region_file_id, PageIndexPolicy::Optional)
1991                .await
1992                .is_some()
1993        );
1994    }
1995}