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