Skip to main content

mito2/
engine.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//! Mito region engine.
16
17#[cfg(test)]
18mod alter_test;
19#[cfg(test)]
20mod append_mode_test;
21#[cfg(test)]
22mod basic_test;
23#[cfg(test)]
24mod batch_catchup_test;
25#[cfg(test)]
26mod batch_open_test;
27#[cfg(test)]
28mod bulk_insert_validation_test;
29#[cfg(test)]
30mod bump_committed_sequence_test;
31#[cfg(test)]
32mod catchup_test;
33#[cfg(test)]
34mod close_test;
35#[cfg(test)]
36pub(crate) mod compaction_test;
37#[cfg(test)]
38mod create_test;
39#[cfg(test)]
40mod drop_test;
41#[cfg(test)]
42mod edit_region_test;
43#[cfg(test)]
44mod file_ref_test;
45#[cfg(test)]
46mod filter_deleted_test;
47#[cfg(test)]
48mod flush_test;
49#[cfg(test)]
50mod index_build_test;
51#[cfg(test)]
52mod lease_test;
53#[cfg(any(test, feature = "test"))]
54pub mod listener;
55#[cfg(test)]
56mod merge_mode_test;
57#[cfg(test)]
58mod object_store_wal_recovery_test;
59#[cfg(test)]
60mod object_store_wal_test;
61#[cfg(test)]
62mod open_test;
63#[cfg(test)]
64mod parallel_test;
65#[cfg(test)]
66mod projection_test;
67#[cfg(test)]
68mod prune_test;
69pub mod region_hook;
70#[cfg(test)]
71mod row_selector_test;
72#[cfg(test)]
73mod scan_corrupt;
74#[cfg(test)]
75mod scan_test;
76#[cfg(test)]
77mod sequence_test;
78#[cfg(test)]
79mod set_role_state_test;
80#[cfg(test)]
81mod skip_wal_test;
82#[cfg(test)]
83mod staging_test;
84#[cfg(test)]
85mod sync_test;
86#[cfg(test)]
87mod truncate_test;
88
89#[cfg(test)]
90mod copy_region_from_test;
91#[cfg(test)]
92mod remap_manifests_test;
93
94#[cfg(test)]
95mod apply_staging_manifest_test;
96#[cfg(test)]
97mod partition_filter_test;
98mod puffin_index;
99
100use std::any::Any;
101use std::collections::{HashMap, HashSet};
102use std::sync::Arc;
103use std::time::Instant;
104
105use api::region::RegionResponse;
106use async_trait::async_trait;
107use common_base::Plugins;
108use common_error::ext::BoxedError;
109use common_meta::error::UnexpectedSnafu;
110use common_meta::key::SchemaMetadataManagerRef;
111use common_recordbatch::filter::MetadataFilter;
112use common_recordbatch::{QueryMemoryTracker, SendableRecordBatchStream};
113use common_stat::get_total_memory_bytes;
114use common_telemetry::{debug, info, tracing, warn};
115use common_wal::options::WalOptions;
116use datafusion_common::ScalarValue;
117use datafusion_expr::Expr;
118use futures::FutureExt;
119use futures::future::{self, join_all, try_join_all};
120use futures::stream::{self, Stream, StreamExt};
121use object_store::manager::ObjectStoreManagerRef;
122use region_hook::RegionHookRef;
123use snafu::{OptionExt, ResultExt, ensure};
124use store_api::ManifestVersion;
125use store_api::codec::PrimaryKeyEncoding;
126use store_api::logstore::LogStore;
127use store_api::logstore::provider::{KafkaProvider, Provider};
128use store_api::metadata::{ColumnMetadata, RegionMetadataRef};
129use store_api::metric_engine_consts::{
130    MANIFEST_INFO_EXTENSION_KEY, TABLE_COLUMN_METADATA_EXTENSION_KEY,
131};
132use store_api::region_engine::{
133    BatchPutResponse, BatchResponses, MitoCopyRegionFromRequest, MitoCopyRegionFromResponse,
134    RegionEngine, RegionManifestInfo, RegionRole, RegionScannerRef, RegionStatistic,
135    RemapManifestsRequest, RemapManifestsResponse, SetRegionRoleStateResponse,
136    SettableRegionRoleState, SyncRegionFromRequest, SyncRegionFromResponse,
137};
138use store_api::region_info::RegionInfoEntry;
139use store_api::region_request::{
140    AffectedRows, RegionCatchupRequest, RegionOpenRequest, RegionPutRequest, RegionRequest,
141};
142use store_api::sst_entry::{self, ManifestSstEntry, PuffinIndexMetaEntry, StorageSstEntry};
143use store_api::storage::{FileId, FileRefsManifest, RegionId, ScanRequest, SequenceNumber};
144use tokio::sync::{Semaphore, oneshot};
145
146use crate::access_layer::RegionFilePathFactory;
147use crate::cache::{CacheManagerRef, CacheStrategy};
148use crate::config::{MitoConfig, ScanMemoryMode};
149use crate::engine::puffin_index::{IndexEntryContext, collect_index_entries_from_puffin};
150use crate::error::{
151    IncrementalQueryStaleSnafu, InvalidRequestSnafu, JoinSnafu, MitoManifestInfoSnafu, RecvSnafu,
152    RegionNotFoundSnafu, Result, SequenceRangeUnsupportedSnafu, SerdeJsonSnafu,
153    SerializeColumnMetadataSnafu, SnapshotFenceStaleSnafu,
154};
155#[cfg(feature = "enterprise")]
156use crate::extension::BoxedExtensionRangeProviderFactory;
157use crate::gc::GcLimiterRef;
158use crate::manifest::action::RegionEdit;
159use crate::memtable::MemtableStats;
160use crate::metrics::{
161    HANDLE_REQUEST_ELAPSED, SCAN_MEMORY_EXHAUSTED_TOTAL, SCAN_MEMORY_USAGE_BYTES,
162    SCAN_REQUESTS_REJECTED_TOTAL,
163};
164use crate::read::scan_region::{ScanRegion, Scanner, exact_sequence_range};
165use crate::read::stream::ScanBatchStream;
166use crate::region::MitoRegionRef;
167use crate::region::opener::PartitionExprFetcherRef;
168use crate::region::options::parse_wal_options;
169use crate::request::{RegionEditRequest, WorkerRequest};
170use crate::sst::file::{FileMeta, RegionFileId, RegionIndexId};
171use crate::sst::file_ref::FileReferenceManagerRef;
172use crate::sst::index::intermediate::IntermediateManager;
173use crate::sst::index::puffin_manager::PuffinManagerFactory;
174use crate::wal::entry_distributor::{
175    DEFAULT_ENTRY_RECEIVER_BUFFER_SIZE, build_wal_entry_distributor_and_receivers,
176};
177use crate::wal::raw_entry_reader::{LogStoreRawEntryReader, RawEntryReader};
178use crate::worker::WorkerGroup;
179
180pub const MITO_ENGINE_NAME: &str = "mito";
181
182pub struct MitoEngineBuilder<'a, S: LogStore> {
183    data_home: &'a str,
184    config: MitoConfig,
185    log_store: Arc<S>,
186    object_store_manager: ObjectStoreManagerRef,
187    schema_metadata_manager: SchemaMetadataManagerRef,
188    file_ref_manager: FileReferenceManagerRef,
189    partition_expr_fetcher: PartitionExprFetcherRef,
190    plugins: Plugins,
191    #[cfg(feature = "enterprise")]
192    extension_range_provider_factory: Option<BoxedExtensionRangeProviderFactory>,
193}
194
195impl<'a, S: LogStore> MitoEngineBuilder<'a, S> {
196    #[allow(clippy::too_many_arguments)]
197    pub fn new(
198        data_home: &'a str,
199        config: MitoConfig,
200        log_store: Arc<S>,
201        object_store_manager: ObjectStoreManagerRef,
202        schema_metadata_manager: SchemaMetadataManagerRef,
203        file_ref_manager: FileReferenceManagerRef,
204        partition_expr_fetcher: PartitionExprFetcherRef,
205        plugins: Plugins,
206    ) -> Self {
207        Self {
208            data_home,
209            config,
210            log_store,
211            object_store_manager,
212            schema_metadata_manager,
213            file_ref_manager,
214            plugins,
215            partition_expr_fetcher,
216            #[cfg(feature = "enterprise")]
217            extension_range_provider_factory: None,
218        }
219    }
220
221    #[cfg(feature = "enterprise")]
222    #[must_use]
223    pub fn with_extension_range_provider_factory(
224        self,
225        extension_range_provider_factory: Option<BoxedExtensionRangeProviderFactory>,
226    ) -> Self {
227        Self {
228            extension_range_provider_factory,
229            ..self
230        }
231    }
232
233    pub async fn try_build(mut self) -> Result<MitoEngine> {
234        self.config.sanitize(self.data_home)?;
235
236        let config = Arc::new(self.config);
237        // Extract the region hook before `plugins` is moved into the WorkerGroup,
238        // so the engine (and thus the GC worker) can fire `on_region_gc`.
239        let region_hook = self.plugins.get::<RegionHookRef>();
240        let workers = WorkerGroup::start(
241            self.data_home,
242            config.clone(),
243            self.log_store.clone(),
244            self.object_store_manager,
245            self.schema_metadata_manager,
246            self.file_ref_manager,
247            self.partition_expr_fetcher.clone(),
248            self.plugins,
249        )
250        .await?;
251        let wal_raw_entry_reader = Arc::new(LogStoreRawEntryReader::new(self.log_store));
252        let total_memory = get_total_memory_bytes().max(0) as u64;
253        let scan_memory_limit = config.scan_memory_limit.resolve(total_memory) as usize;
254        let scan_memory_tracker = QueryMemoryTracker::builder(scan_memory_limit)
255            .on_update(|usage| {
256                SCAN_MEMORY_USAGE_BYTES.set(usage as i64);
257            })
258            .on_exhausted(|| {
259                SCAN_MEMORY_EXHAUSTED_TOTAL.inc();
260            })
261            .on_reject(|| {
262                SCAN_REQUESTS_REJECTED_TOTAL.inc();
263            })
264            .build();
265
266        let inner = EngineInner {
267            workers,
268            config,
269            wal_raw_entry_reader,
270            scan_memory_tracker,
271            region_hook,
272            #[cfg(feature = "enterprise")]
273            extension_range_provider_factory: None,
274        };
275
276        #[cfg(feature = "enterprise")]
277        let inner =
278            inner.with_extension_range_provider_factory(self.extension_range_provider_factory);
279
280        Ok(MitoEngine {
281            inner: Arc::new(inner),
282        })
283    }
284}
285
286/// Region engine implementation for timeseries data.
287#[derive(Clone)]
288pub struct MitoEngine {
289    inner: Arc<EngineInner>,
290}
291
292impl MitoEngine {
293    /// Returns a new [MitoEngine] with specific `config`, `log_store` and `object_store`.
294    #[allow(clippy::too_many_arguments)]
295    pub async fn new<S: LogStore>(
296        data_home: &str,
297        config: MitoConfig,
298        log_store: Arc<S>,
299        object_store_manager: ObjectStoreManagerRef,
300        schema_metadata_manager: SchemaMetadataManagerRef,
301        file_ref_manager: FileReferenceManagerRef,
302        partition_expr_fetcher: PartitionExprFetcherRef,
303        plugins: Plugins,
304    ) -> Result<MitoEngine> {
305        let builder = MitoEngineBuilder::new(
306            data_home,
307            config,
308            log_store,
309            object_store_manager,
310            schema_metadata_manager,
311            file_ref_manager,
312            partition_expr_fetcher,
313            plugins,
314        );
315        builder.try_build().await
316    }
317
318    pub fn mito_config(&self) -> &MitoConfig {
319        &self.inner.config
320    }
321
322    pub fn cache_manager(&self) -> CacheManagerRef {
323        self.inner.workers.cache_manager()
324    }
325
326    pub fn file_ref_manager(&self) -> FileReferenceManagerRef {
327        self.inner.workers.file_ref_manager()
328    }
329
330    pub fn gc_limiter(&self) -> GcLimiterRef {
331        self.inner.workers.gc_limiter()
332    }
333
334    /// Makes regions acknowledge only writes whose WAL append completes within their lease.
335    pub fn require_write_lease(&self) {
336        self.inner.workers.node_lease().enable();
337    }
338
339    /// Renews the lease of this node from a heartbeat response that carries region leases.
340    pub fn renew_node_lease(&self, deadline: Instant) {
341        self.inner.workers.node_lease().renew(deadline);
342    }
343
344    /// Freezes the clock of the lease checks at `at`.
345    #[cfg(any(test, feature = "test"))]
346    pub fn freeze_lease_clock(&self, at: Instant) {
347        self.inner.workers.node_lease().freeze_clock(at);
348    }
349
350    pub fn object_store_manager(&self) -> &ObjectStoreManagerRef {
351        self.inner.workers.object_store_manager()
352    }
353
354    pub fn puffin_manager_factory(&self) -> &PuffinManagerFactory {
355        self.inner.workers.puffin_manager_factory()
356    }
357
358    pub fn intermediate_manager(&self) -> &IntermediateManager {
359        self.inner.workers.intermediate_manager()
360    }
361
362    pub fn schema_metadata_manager(&self) -> &SchemaMetadataManagerRef {
363        self.inner.workers.schema_metadata_manager()
364    }
365
366    /// Returns the registered region hook (if any), for the GC worker to fire
367    /// [`RegionHook::on_region_gc`].
368    pub fn region_hook(&self) -> Option<RegionHookRef> {
369        self.inner.region_hook.clone()
370    }
371
372    /// Get all tmp ref files for given region ids, excluding files that's already in manifest.
373    pub async fn get_snapshot_of_file_refs(
374        &self,
375        file_handle_regions: impl IntoIterator<Item = RegionId>,
376        related_regions: HashMap<RegionId, HashSet<RegionId>>,
377    ) -> Result<FileRefsManifest> {
378        let file_ref_mgr = self.file_ref_manager();
379
380        let file_handle_regions = file_handle_regions.into_iter().collect::<Vec<_>>();
381        // Convert region IDs to MitoRegionRef objects, ignore regions that do not exist on current datanode
382        // as regions on other datanodes are not managed by this engine.
383        let query_regions: Vec<MitoRegionRef> = file_handle_regions
384            .into_iter()
385            .filter_map(|region_id| self.find_region(region_id))
386            .collect();
387
388        let dst_region_to_src_regions: Vec<(MitoRegionRef, HashSet<RegionId>)> = {
389            let dst2src = related_regions
390                .into_iter()
391                .flat_map(|(src, dsts)| dsts.into_iter().map(move |dst| (dst, src)))
392                .fold(
393                    HashMap::<RegionId, HashSet<RegionId>>::new(),
394                    |mut acc, (k, v)| {
395                        let entry = acc.entry(k).or_default();
396                        entry.insert(v);
397                        acc
398                    },
399                );
400            let mut dst_region_to_src_regions = Vec::with_capacity(dst2src.len());
401            for (dst_region, srcs) in dst2src {
402                let Some(region) = self.find_region(dst_region) else {
403                    return RegionNotFoundSnafu {
404                        region_id: dst_region,
405                    }
406                    .fail();
407                };
408                dst_region_to_src_regions.push((region, srcs));
409            }
410            dst_region_to_src_regions
411        };
412
413        file_ref_mgr
414            .get_snapshot_of_file_refs(query_regions, dst_region_to_src_regions)
415            .await
416    }
417
418    /// Returns true if the specific region exists.
419    pub fn is_region_exists(&self, region_id: RegionId) -> bool {
420        self.inner.workers.is_region_exists(region_id)
421    }
422
423    /// Returns true if the specific region exists.
424    pub fn is_region_opening(&self, region_id: RegionId) -> bool {
425        self.inner.workers.is_region_opening(region_id)
426    }
427
428    /// Returns true if the specific region is catching up.
429    pub fn is_region_catching_up(&self, region_id: RegionId) -> bool {
430        self.inner.workers.is_region_catching_up(region_id)
431    }
432
433    /// Returns the region disk/memory statistic.
434    pub fn get_region_statistic(&self, region_id: RegionId) -> Option<RegionStatistic> {
435        self.find_region(region_id)
436            .map(|region| region.region_statistic())
437    }
438
439    /// Returns primary key encoding of the region.
440    pub fn get_primary_key_encoding(&self, region_id: RegionId) -> Option<PrimaryKeyEncoding> {
441        self.find_region(region_id)
442            .map(|r| r.primary_key_encoding())
443    }
444
445    /// Handle substrait query and return a stream of record batches
446    ///
447    /// Notice that the output stream's ordering is not guranateed. If order
448    /// matter, please use [`scanner`] to build a [`Scanner`] to consume.
449    #[tracing::instrument(skip_all)]
450    pub async fn scan_to_stream(
451        &self,
452        region_id: RegionId,
453        request: ScanRequest,
454    ) -> Result<SendableRecordBatchStream, BoxedError> {
455        self.scanner(region_id, request)
456            .await
457            .map_err(BoxedError::new)?
458            .scan()
459            .await
460    }
461
462    /// Scan [`Batch`]es by [`ScanRequest`].
463    pub async fn scan_batch(
464        &self,
465        region_id: RegionId,
466        request: ScanRequest,
467        filter_deleted: bool,
468    ) -> Result<ScanBatchStream> {
469        let mut scan_region = self.scan_region(region_id, request)?;
470        scan_region.set_filter_deleted(filter_deleted);
471        scan_region.scanner().await?.scan_batch()
472    }
473
474    /// Returns a scanner to scan for `request`.
475    pub(crate) async fn scanner(
476        &self,
477        region_id: RegionId,
478        request: ScanRequest,
479    ) -> Result<Scanner> {
480        self.scan_region(region_id, request)?.scanner().await
481    }
482
483    /// Scans a region.
484    #[tracing::instrument(skip_all, fields(region_id = %region_id))]
485    fn scan_region(&self, region_id: RegionId, request: ScanRequest) -> Result<ScanRegion> {
486        self.inner.scan_region(region_id, request)
487    }
488
489    /// Edit region's metadata by [RegionEdit] directly. Use with care.
490    /// Now we only allow adding files or removing files from region (the [RegionEdit] struct can only contain a non-empty "files_to_add" or "files_to_remove" field).
491    /// Other region editing intention will result in an "invalid request" error.
492    /// Also note that if a region is to be edited directly, we MUST not write data to it thereafter.
493    pub async fn edit_region(&self, region_id: RegionId, edit: RegionEdit) -> Result<()> {
494        let _timer = HANDLE_REQUEST_ELAPSED
495            .with_label_values(&["edit_region"])
496            .start_timer();
497
498        ensure!(
499            is_valid_region_edit(&edit),
500            InvalidRequestSnafu {
501                region_id,
502                reason: "invalid region edit"
503            }
504        );
505
506        let (tx, rx) = oneshot::channel();
507        let request = WorkerRequest::EditRegion(RegionEditRequest::new(region_id, edit, true, tx));
508        self.inner
509            .workers
510            .submit_to_worker(region_id, request)
511            .await?;
512        rx.await.context(RecvSnafu)?
513    }
514
515    /// Handles copy region from request.
516    ///
517    /// This method is only supported for internal use and is not exposed in the trait implementation.
518    pub async fn copy_region_from(
519        &self,
520        region_id: RegionId,
521        request: MitoCopyRegionFromRequest,
522    ) -> Result<MitoCopyRegionFromResponse> {
523        self.inner.copy_region_from(region_id, request).await
524    }
525
526    #[cfg(test)]
527    pub(crate) fn get_region(&self, id: RegionId) -> Option<crate::region::MitoRegionRef> {
528        self.find_region(id)
529    }
530
531    pub fn find_region(&self, region_id: RegionId) -> Option<MitoRegionRef> {
532        self.inner.workers.get_region(region_id)
533    }
534
535    /// Returns all regions.
536    pub fn regions(&self) -> Vec<MitoRegionRef> {
537        self.inner.workers.all_regions().collect()
538    }
539
540    fn encode_manifest_info_to_extensions(
541        region_id: &RegionId,
542        manifest_info: RegionManifestInfo,
543        extensions: &mut HashMap<String, Vec<u8>>,
544    ) -> Result<()> {
545        let region_manifest_info = vec![(*region_id, manifest_info)];
546
547        extensions.insert(
548            MANIFEST_INFO_EXTENSION_KEY.to_string(),
549            RegionManifestInfo::encode_list(&region_manifest_info).context(SerdeJsonSnafu)?,
550        );
551        info!(
552            "Added manifest info: {:?} to extensions, region_id: {:?}",
553            region_manifest_info, region_id
554        );
555        Ok(())
556    }
557
558    fn encode_column_metadatas_to_extensions(
559        region_id: &RegionId,
560        column_metadatas: Vec<ColumnMetadata>,
561        extensions: &mut HashMap<String, Vec<u8>>,
562    ) -> Result<()> {
563        extensions.insert(
564            TABLE_COLUMN_METADATA_EXTENSION_KEY.to_string(),
565            ColumnMetadata::encode_list(&column_metadatas).context(SerializeColumnMetadataSnafu)?,
566        );
567        info!(
568            "Added column metadatas: {:?} to extensions, region_id: {:?}",
569            column_metadatas, region_id
570        );
571        Ok(())
572    }
573
574    /// Find the current version's memtables and SSTs stats by region_id.
575    /// The stats must be collected in one place one time to ensure data consistency.
576    pub fn find_memtable_and_sst_stats(
577        &self,
578        region_id: RegionId,
579    ) -> Result<(Vec<MemtableStats>, Vec<FileMeta>)> {
580        let region = self
581            .find_region(region_id)
582            .context(RegionNotFoundSnafu { region_id })?;
583
584        let version = region.version();
585        let memtable_stats = version
586            .memtables
587            .list_memtables()
588            .iter()
589            .map(|x| x.stats())
590            .collect::<Vec<_>>();
591
592        let sst_stats = version
593            .ssts
594            .levels()
595            .iter()
596            .flat_map(|level| level.files().map(|x| x.meta_ref()))
597            .cloned()
598            .collect::<Vec<_>>();
599        Ok((memtable_stats, sst_stats))
600    }
601
602    /// Lists all SSTs from the manifest of all regions in the engine.
603    pub async fn all_ssts_from_manifest(&self) -> Vec<ManifestSstEntry> {
604        self.scan_ssts_from_manifest(&[], 1024)
605            .flat_map(stream::iter)
606            .collect()
607            .await
608    }
609
610    /// Lazily emits bounded batches of manifest entries, pruning regions before
611    /// opening their manifest snapshots and files before constructing entries.
612    /// Filters are conservative; callers must retain their residual predicates.
613    pub(crate) fn scan_ssts_from_manifest(
614        &self,
615        filters: &[Expr],
616        batch_size: usize,
617    ) -> futures::stream::BoxStream<'static, Vec<ManifestSstEntry>> {
618        self.scan_ssts_from_manifest_projected(filters, batch_size, None)
619    }
620
621    /// Like [`Self::scan_ssts_from_manifest`], but avoids allocating unrequested
622    /// variable-sized fields. The projection must include residual filter columns.
623    pub fn scan_ssts_from_manifest_projected(
624        &self,
625        filters: &[Expr],
626        batch_size: usize,
627        projection: Option<&[usize]>,
628    ) -> futures::stream::BoxStream<'static, Vec<ManifestSstEntry>> {
629        let engine = self.clone();
630        let projection = crate::region::ManifestSstProjection::new(projection);
631        let filters = MetadataFilter::new(filters);
632        Box::pin(async_stream::stream! {
633            let node_id = engine.inner.workers.file_ref_manager().node_id();
634            if !filters.matches(|column| match column {
635                sst_entry::NODE_ID => Some(ScalarValue::UInt64(node_id)),
636                _ => None,
637            }) {
638                return;
639            }
640            let mut batch = Vec::with_capacity(batch_size.max(1));
641            for region in engine.inner.workers.all_regions() {
642                if !manifest_region_matches(region.region_id, node_id, &filters)
643                    || !filters.matches(|column| match column {
644                        sst_entry::TABLE_DIR => Some(ScalarValue::Utf8(Some(region.table_dir().to_string()))),
645                        _ => None,
646                    })
647                {
648                    continue;
649                }
650                let mut entries = region.manifest_sst_stream(filters.clone(), projection);
651                while let Some(mut entry) = entries.next().await {
652                    entry.node_id = node_id;
653                    batch.push(entry);
654                    if batch.len() == batch_size.max(1) {
655                        yield std::mem::replace(&mut batch, Vec::with_capacity(batch_size.max(1)));
656                    }
657                }
658            }
659            if !batch.is_empty() {
660                yield batch;
661            }
662        })
663    }
664
665    /// Lists metadata about all puffin index targets stored in the engine.
666    pub async fn all_index_metas(&self) -> Vec<PuffinIndexMetaEntry> {
667        let node_id = self.inner.workers.file_ref_manager().node_id();
668        let cache_manager = self.inner.workers.cache_manager();
669        let puffin_metadata_cache = cache_manager.puffin_metadata_cache().cloned();
670        let bloom_filter_cache = cache_manager.bloom_filter_index_cache().cloned();
671        let inverted_index_cache = cache_manager.inverted_index_cache().cloned();
672
673        let mut results = Vec::new();
674
675        for region in self.inner.workers.all_regions() {
676            let manifest_entries = region.manifest_sst_entries().await;
677            let access_layer = region.access_layer.clone();
678            let table_dir = access_layer.table_dir().to_string();
679            let path_type = access_layer.path_type();
680            let object_store = access_layer.object_store().clone();
681            let puffin_factory = access_layer.puffin_manager_factory().clone();
682            let path_factory = RegionFilePathFactory::new(table_dir, path_type);
683
684            let entry_futures = manifest_entries.into_iter().map(|entry| {
685                let object_store = object_store.clone();
686                let path_factory = path_factory.clone();
687                let puffin_factory = puffin_factory.clone();
688                let puffin_metadata_cache = puffin_metadata_cache.clone();
689                let bloom_filter_cache = bloom_filter_cache.clone();
690                let inverted_index_cache = inverted_index_cache.clone();
691
692                async move {
693                    let Some(index_file_path) = entry.index_file_path.as_ref() else {
694                        return Vec::new();
695                    };
696
697                    let index_version = entry.index_version;
698                    let file_id = match FileId::parse_str(&entry.file_id) {
699                        Ok(file_id) => file_id,
700                        Err(err) => {
701                            warn!(
702                                err;
703                                "Failed to parse puffin index file id, table_dir: {}, file_id: {}",
704                                entry.table_dir,
705                                entry.file_id
706                            );
707                            return Vec::new();
708                        }
709                    };
710                    // The index file path is derived from the physical file owner. After
711                    // repartition, `entry.region_id` is only the referring region.
712                    let region_index_id = RegionIndexId::new(
713                        RegionFileId::new(entry.origin_region_id, file_id),
714                        index_version,
715                    );
716                    let context = IndexEntryContext {
717                        table_dir: &entry.table_dir,
718                        index_file_path: index_file_path.as_str(),
719                        region_id: entry.region_id,
720                        table_id: entry.table_id,
721                        region_number: entry.region_number,
722                        region_group: entry.region_group,
723                        region_sequence: entry.region_sequence,
724                        file_id: &entry.file_id,
725                        index_file_size: entry.index_file_size,
726                        node_id,
727                    };
728
729                    let manager = puffin_factory
730                        .build(object_store, path_factory)
731                        .with_puffin_metadata_cache(puffin_metadata_cache);
732
733                    collect_index_entries_from_puffin(
734                        manager,
735                        region_index_id,
736                        context,
737                        bloom_filter_cache,
738                        inverted_index_cache,
739                    )
740                    .await
741                }
742            });
743
744            let mut meta_stream = stream::iter(entry_futures).buffer_unordered(8); // Parallelism is 8.
745            while let Some(mut metas) = meta_stream.next().await {
746                results.append(&mut metas);
747            }
748        }
749
750        results
751    }
752
753    /// Lists region info entries of all regions in the engine.
754    pub async fn all_region_infos(&self) -> Vec<RegionInfoEntry> {
755        let node_id = self.inner.workers.file_ref_manager().node_id();
756        self.inner
757            .workers
758            .all_regions()
759            .map(|region| region.region_info_entry(node_id))
760            .collect()
761    }
762
763    /// Lists all SSTs from the storage layer of all regions in the engine.
764    pub fn all_ssts_from_storage(&self) -> impl Stream<Item = Result<StorageSstEntry>> {
765        let node_id = self.inner.workers.file_ref_manager().node_id();
766        let regions = self.inner.workers.all_regions();
767
768        let mut layers_distinct_table_dirs = HashMap::new();
769        for region in regions {
770            let table_dir = region.access_layer.table_dir();
771            if !layers_distinct_table_dirs.contains_key(table_dir) {
772                layers_distinct_table_dirs
773                    .insert(table_dir.to_string(), region.access_layer.clone());
774            }
775        }
776
777        stream::iter(layers_distinct_table_dirs)
778            .map(|(_, access_layer)| access_layer.storage_sst_entries())
779            .flatten()
780            .map(move |entry| {
781                entry.map(move |mut entry| {
782                    entry.node_id = node_id;
783                    entry
784                })
785            })
786    }
787}
788
789/// Check whether the region edit is valid.
790///
791/// Only adding or removing files to region is considered valid now.
792fn is_valid_region_edit(edit: &RegionEdit) -> bool {
793    (!edit.files_to_add.is_empty() || !edit.files_to_remove.is_empty())
794        && matches!(
795            edit,
796            RegionEdit {
797                files_to_add: _,
798                files_to_remove: _,
799                timestamp_ms: _,
800                compaction_time_window: None,
801                flushed_entry_id: None,
802                flushed_sequence: None,
803                ..
804            }
805        )
806}
807
808/// Inner struct of [MitoEngine].
809struct EngineInner {
810    /// Region workers group.
811    workers: WorkerGroup,
812    /// Config of the engine.
813    config: Arc<MitoConfig>,
814    /// The Wal raw entry reader.
815    wal_raw_entry_reader: Arc<dyn RawEntryReader>,
816    /// Memory tracker for table scans.
817    scan_memory_tracker: QueryMemoryTracker,
818    /// The region hook (if any) registered via plugins; exposed for the GC worker
819    /// to fire [`RegionHook::on_region_gc`].
820    region_hook: Option<RegionHookRef>,
821    #[cfg(feature = "enterprise")]
822    extension_range_provider_factory: Option<BoxedExtensionRangeProviderFactory>,
823}
824
825type TopicGroupedRegionOpenRequests = HashMap<String, Vec<(RegionId, RegionOpenRequest)>>;
826
827/// Returns requests([TopicGroupedRegionOpenRequests]) grouped by topic and remaining requests.
828fn prepare_batch_open_requests(
829    requests: Vec<(RegionId, RegionOpenRequest)>,
830) -> Result<(
831    TopicGroupedRegionOpenRequests,
832    Vec<(RegionId, RegionOpenRequest)>,
833)> {
834    let mut topic_to_regions: HashMap<String, Vec<(RegionId, RegionOpenRequest)>> = HashMap::new();
835    let mut remaining_regions: Vec<(RegionId, RegionOpenRequest)> = Vec::new();
836    for (region_id, request) in requests {
837        match parse_wal_options(&request.options).context(SerdeJsonSnafu)? {
838            WalOptions::Kafka(options) => {
839                topic_to_regions
840                    .entry(options.topic)
841                    .or_default()
842                    .push((region_id, request));
843            }
844            WalOptions::RaftEngine | WalOptions::Noop | WalOptions::ObjectStore(_) => {
845                remaining_regions.push((region_id, request));
846            }
847        }
848    }
849
850    Ok((topic_to_regions, remaining_regions))
851}
852
853impl EngineInner {
854    #[cfg(feature = "enterprise")]
855    #[must_use]
856    fn with_extension_range_provider_factory(
857        self,
858        extension_range_provider_factory: Option<BoxedExtensionRangeProviderFactory>,
859    ) -> Self {
860        Self {
861            extension_range_provider_factory,
862            ..self
863        }
864    }
865
866    /// Stop the inner engine.
867    async fn stop(&self) -> Result<()> {
868        self.workers.stop().await
869    }
870
871    fn find_region(&self, region_id: RegionId) -> Result<MitoRegionRef> {
872        self.workers
873            .get_region(region_id)
874            .context(RegionNotFoundSnafu { region_id })
875    }
876
877    /// Get metadata of a region.
878    ///
879    /// Returns error if the region doesn't exist.
880    fn get_metadata(&self, region_id: RegionId) -> Result<RegionMetadataRef> {
881        // Reading a region doesn't need to go through the region worker thread.
882        let region = self.find_region(region_id)?;
883        Ok(region.metadata())
884    }
885
886    async fn open_topic_regions(
887        &self,
888        topic: String,
889        region_requests: Vec<(RegionId, RegionOpenRequest)>,
890    ) -> Result<Vec<(RegionId, Result<AffectedRows>)>> {
891        let now = Instant::now();
892        let region_ids = region_requests
893            .iter()
894            .map(|(region_id, _)| *region_id)
895            .collect::<Vec<_>>();
896        let provider = Provider::kafka_provider(topic);
897        let (distributor, entry_receivers) = build_wal_entry_distributor_and_receivers(
898            provider.clone(),
899            self.wal_raw_entry_reader.clone(),
900            &region_ids,
901            DEFAULT_ENTRY_RECEIVER_BUFFER_SIZE,
902        );
903
904        let mut responses = Vec::with_capacity(region_requests.len());
905        for ((region_id, request), entry_receiver) in
906            region_requests.into_iter().zip(entry_receivers)
907        {
908            let (request, receiver) =
909                WorkerRequest::new_open_region_request(region_id, request, Some(entry_receiver));
910            self.workers.submit_to_worker(region_id, request).await?;
911            responses.push(async move { receiver.await.context(RecvSnafu)? });
912        }
913
914        // Waits for entries distribution.
915        let distribution =
916            common_runtime::spawn_global(async move { distributor.distribute().await });
917        // Waits for worker returns.
918        let responses = join_all(responses).await;
919        distribution.await.context(JoinSnafu)??;
920
921        let num_failure = responses.iter().filter(|r| r.is_err()).count();
922        info!(
923            "Opened {} regions for topic '{}', failures: {}, elapsed: {:?}",
924            region_ids.len() - num_failure,
925            // Safety: provider is kafka provider.
926            provider.as_kafka_provider().unwrap(),
927            num_failure,
928            now.elapsed(),
929        );
930        Ok(region_ids.into_iter().zip(responses).collect())
931    }
932
933    async fn handle_batch_open_requests(
934        &self,
935        parallelism: usize,
936        requests: Vec<(RegionId, RegionOpenRequest)>,
937    ) -> Result<Vec<(RegionId, Result<AffectedRows>)>> {
938        let semaphore = Arc::new(Semaphore::new(parallelism));
939        let (topic_to_region_requests, remaining_region_requests) =
940            prepare_batch_open_requests(requests)?;
941        let mut responses =
942            Vec::with_capacity(topic_to_region_requests.len() + remaining_region_requests.len());
943
944        if !topic_to_region_requests.is_empty() {
945            let mut tasks = Vec::with_capacity(topic_to_region_requests.len());
946            for (topic, region_requests) in topic_to_region_requests {
947                let semaphore_moved = semaphore.clone();
948                tasks.push(async move {
949                    // Safety: semaphore must exist
950                    let _permit = semaphore_moved.acquire().await.unwrap();
951                    self.open_topic_regions(topic, region_requests).await
952                })
953            }
954            let r = try_join_all(tasks).await?;
955            responses.extend(r.into_iter().flatten());
956        }
957
958        if !remaining_region_requests.is_empty() {
959            let mut tasks = Vec::with_capacity(remaining_region_requests.len());
960            let mut region_ids = Vec::with_capacity(remaining_region_requests.len());
961            for (region_id, request) in remaining_region_requests {
962                let semaphore_moved = semaphore.clone();
963                region_ids.push(region_id);
964                tasks.push(async move {
965                    // Safety: semaphore must exist
966                    let _permit = semaphore_moved.acquire().await.unwrap();
967                    let (request, receiver) =
968                        WorkerRequest::new_open_region_request(region_id, request, None);
969
970                    self.workers.submit_to_worker(region_id, request).await?;
971
972                    receiver.await.context(RecvSnafu)?
973                })
974            }
975
976            let results = join_all(tasks).await;
977            responses.extend(region_ids.into_iter().zip(results));
978        }
979
980        Ok(responses)
981    }
982
983    async fn catchup_topic_regions(
984        &self,
985        provider: Provider,
986        region_requests: Vec<(RegionId, RegionCatchupRequest)>,
987    ) -> Result<Vec<(RegionId, Result<AffectedRows>)>> {
988        let now = Instant::now();
989        let region_ids = region_requests
990            .iter()
991            .map(|(region_id, _)| *region_id)
992            .collect::<Vec<_>>();
993        let (distributor, entry_receivers) = build_wal_entry_distributor_and_receivers(
994            provider.clone(),
995            self.wal_raw_entry_reader.clone(),
996            &region_ids,
997            DEFAULT_ENTRY_RECEIVER_BUFFER_SIZE,
998        );
999
1000        let mut responses = Vec::with_capacity(region_requests.len());
1001        for ((region_id, request), entry_receiver) in
1002            region_requests.into_iter().zip(entry_receivers)
1003        {
1004            let (request, receiver) =
1005                WorkerRequest::new_catchup_region_request(region_id, request, Some(entry_receiver));
1006            self.workers.submit_to_worker(region_id, request).await?;
1007            responses.push(async move { receiver.await.context(RecvSnafu)? });
1008        }
1009
1010        // Wait for entries distribution.
1011        let distribution =
1012            common_runtime::spawn_global(async move { distributor.distribute().await });
1013        // Wait for worker returns.
1014        let responses = join_all(responses).await;
1015        distribution.await.context(JoinSnafu)??;
1016
1017        let num_failure = responses.iter().filter(|r| r.is_err()).count();
1018        info!(
1019            "Caught up {} regions for topic '{}', failures: {}, elapsed: {:?}",
1020            region_ids.len() - num_failure,
1021            // Safety: provider is kafka provider.
1022            provider.as_kafka_provider().unwrap(),
1023            num_failure,
1024            now.elapsed(),
1025        );
1026
1027        Ok(region_ids.into_iter().zip(responses).collect())
1028    }
1029
1030    async fn handle_batch_catchup_requests(
1031        &self,
1032        parallelism: usize,
1033        requests: Vec<(RegionId, RegionCatchupRequest)>,
1034    ) -> Result<Vec<(RegionId, Result<AffectedRows>)>> {
1035        let mut responses = Vec::with_capacity(requests.len());
1036        let mut topic_regions: HashMap<Arc<KafkaProvider>, Vec<_>> = HashMap::new();
1037        let mut remaining_region_requests = vec![];
1038
1039        for (region_id, request) in requests {
1040            match self.workers.get_region(region_id) {
1041                Some(region) => match region.provider.as_kafka_provider() {
1042                    Some(provider) => {
1043                        topic_regions
1044                            .entry(provider.clone())
1045                            .or_default()
1046                            .push((region_id, request));
1047                    }
1048                    None => {
1049                        remaining_region_requests.push((region_id, request));
1050                    }
1051                },
1052                None => responses.push((region_id, RegionNotFoundSnafu { region_id }.fail())),
1053            }
1054        }
1055
1056        let semaphore = Arc::new(Semaphore::new(parallelism));
1057
1058        if !topic_regions.is_empty() {
1059            let mut tasks = Vec::with_capacity(topic_regions.len());
1060            for (provider, region_requests) in topic_regions {
1061                let semaphore_moved = semaphore.clone();
1062                tasks.push(async move {
1063                    // Safety: semaphore must exist
1064                    let _permit = semaphore_moved.acquire().await.unwrap();
1065                    self.catchup_topic_regions(Provider::Kafka(provider), region_requests)
1066                        .await
1067                })
1068            }
1069
1070            let r = try_join_all(tasks).await?;
1071            responses.extend(r.into_iter().flatten());
1072        }
1073
1074        if !remaining_region_requests.is_empty() {
1075            let mut tasks = Vec::with_capacity(remaining_region_requests.len());
1076            let mut region_ids = Vec::with_capacity(remaining_region_requests.len());
1077            for (region_id, request) in remaining_region_requests {
1078                let semaphore_moved = semaphore.clone();
1079                region_ids.push(region_id);
1080                tasks.push(async move {
1081                    // Safety: semaphore must exist
1082                    let _permit = semaphore_moved.acquire().await.unwrap();
1083                    let (request, receiver) =
1084                        WorkerRequest::new_catchup_region_request(region_id, request, None);
1085
1086                    self.workers.submit_to_worker(region_id, request).await?;
1087
1088                    receiver.await.context(RecvSnafu)?
1089                })
1090            }
1091
1092            let results = join_all(tasks).await;
1093            responses.extend(region_ids.into_iter().zip(results));
1094        }
1095
1096        Ok(responses)
1097    }
1098
1099    /// Handles [RegionRequest] and return its executed result.
1100    async fn handle_request(
1101        &self,
1102        region_id: RegionId,
1103        request: RegionRequest,
1104    ) -> Result<AffectedRows> {
1105        let region_metadata = self.get_metadata(region_id).ok();
1106        let (request, receiver) =
1107            WorkerRequest::try_from_region_request(region_id, request, region_metadata)?;
1108        self.workers.submit_to_worker(region_id, request).await?;
1109
1110        receiver.await.context(RecvSnafu)?
1111    }
1112
1113    /// Returns the sequence of latest committed data.
1114    fn get_committed_sequence(&self, region_id: RegionId) -> Result<SequenceNumber> {
1115        // Reading a region doesn't need to go through the region worker thread.
1116        self.find_region(region_id)
1117            .map(|r| r.find_committed_sequence())
1118    }
1119
1120    /// Handles the scan `request` and returns a [ScanRegion].
1121    #[tracing::instrument(skip_all, fields(region_id = %region_id))]
1122    fn scan_region(&self, region_id: RegionId, mut request: ScanRequest) -> Result<ScanRegion> {
1123        let query_start = Instant::now();
1124        // Reading a region doesn't need to go through the region worker thread.
1125        let region = self.find_region(region_id)?;
1126        // Pin the index before the data snapshot: compaction and index publication
1127        // could otherwise give us a newer index that omits series still visible
1128        // in the query's older SST snapshot.
1129        let series_index = region.series_index_store.as_ref().map(|store| {
1130            crate::series_index::SeriesIndexReadContext {
1131                store: store.clone(),
1132                version: region.series_index_version(),
1133            }
1134        });
1135        let version_data = region.version_control.current();
1136        let version = version_data.version;
1137
1138        if request.snapshot_on_scan && request.memtable_max_sequence.is_none() {
1139            request.memtable_max_sequence = Some(version_data.committed_sequence);
1140        }
1141
1142        // Select the files and decide the exact capability from the same version
1143        // snapshot. Keep them together so the reader cannot recompute either
1144        // side from a different file set.
1145        let exact_selection = if request.exact_sequence_range {
1146            match exact_sequence_range(&request, &version) {
1147                Ok((files, sequence_range)) => Some((files, sequence_range)),
1148                Err(err) => {
1149                    debug!(
1150                        "Scan region {} exact sequence range denied: min={:?}, max={:?}, denial_reason=foreign_file_missing_barrier",
1151                        region_id, request.memtable_min_sequence, request.memtable_max_sequence,
1152                    );
1153                    return Err(err);
1154                }
1155            }
1156        } else {
1157            None
1158        };
1159        let exact_sequence_range = exact_selection
1160            .as_ref()
1161            .and_then(|(_, sequence_range)| *sequence_range);
1162
1163        // An extension range provider may contribute ranges on a follower
1164        // region, and extension streams are returned without a row-level
1165        // sequence filter (`scan_flat_extension_range`), so exactness cannot
1166        // be proven when one is attached. Treat the capability as missing so
1167        // the request fails closed below instead of emitting out-of-range
1168        // rows. See the injection point in `ScanRegion::scan_input`.
1169        #[cfg(feature = "enterprise")]
1170        let extension_provider_blocks_exact = region.is_follower()
1171            && self.extension_range_provider_factory.is_some()
1172            && exact_sequence_range.is_some();
1173        #[cfg(not(feature = "enterprise"))]
1174        let extension_provider_blocks_exact = false;
1175        let exact_sequence_range = if extension_provider_blocks_exact {
1176            None
1177        } else {
1178            exact_sequence_range
1179        };
1180        let exact_selection = exact_selection.map(|(files, _)| (files, exact_sequence_range));
1181        let exact_denial_reason = if !request.exact_sequence_range {
1182            "not_requested"
1183        } else if request.skip_sst_files {
1184            "sst_files_skipped"
1185        } else if request.memtable_min_sequence.is_none() {
1186            "missing_lower_bound"
1187        } else if request.memtable_max_sequence.is_none() {
1188            "missing_upper_bound"
1189        } else if !version.options.preserve_row_sequence {
1190            "preserve_row_sequence_disabled"
1191        } else if extension_provider_blocks_exact {
1192            "extension_provider"
1193        } else if exact_sequence_range.is_none() {
1194            "selected_file_barrier_not_admitted"
1195        } else {
1196            "none"
1197        };
1198
1199        debug!(
1200            "Scan region {} exact sequence range: requested={}, min={:?}, max={:?}, committed={}, flushed={}, available={}, selected_files={}, denial_reason={}",
1201            region_id,
1202            request.exact_sequence_range,
1203            request.memtable_min_sequence,
1204            request.memtable_max_sequence,
1205            version_data.committed_sequence,
1206            version.flushed_sequence,
1207            exact_sequence_range.is_some(),
1208            exact_selection.as_ref().map_or(0, |(files, _)| files.len()),
1209            exact_denial_reason,
1210        );
1211        validate_sequence_fences(
1212            &request,
1213            &version,
1214            region_id,
1215            exact_sequence_range.is_some(),
1216            extension_provider_blocks_exact,
1217        )?;
1218
1219        // Get cache.
1220        let cache_manager = self.workers.cache_manager();
1221
1222        let scan_region = ScanRegion::new(
1223            version,
1224            region.access_layer.clone(),
1225            request,
1226            CacheStrategy::EnableAll(cache_manager),
1227        )
1228        .with_series_index(series_index)
1229        .with_ignore_range_index(!self.config.experimental_enable_range_index)
1230        .with_query_stat_counters(region.region_stats.query_stat_counters())
1231        .with_max_concurrent_scan_files(self.config.max_concurrent_scan_files)
1232        .with_scan_memory_tracker(self.scan_memory_tracker.clone())
1233        .with_experimental_series_scan_v2(self.config.experimental_series_scan_v2)
1234        .with_ignore_inverted_index(self.config.inverted_index.apply_on_query.disabled())
1235        .with_ignore_fulltext_index(self.config.fulltext_index.apply_on_query.disabled())
1236        .with_ignore_bloom_filter(self.config.bloom_filter_index.apply_on_query.disabled())
1237        .with_start_time(query_start);
1238        let scan_region = if let Some(selection) = exact_selection {
1239            scan_region.with_exact_selection(selection)
1240        } else {
1241            scan_region
1242        };
1243
1244        #[cfg(feature = "enterprise")]
1245        let scan_region = self.maybe_fill_extension_range_provider(scan_region, region);
1246
1247        Ok(scan_region)
1248    }
1249
1250    #[cfg(feature = "enterprise")]
1251    fn maybe_fill_extension_range_provider(
1252        &self,
1253        mut scan_region: ScanRegion,
1254        region: MitoRegionRef,
1255    ) -> ScanRegion {
1256        if region.is_follower()
1257            && let Some(factory) = self.extension_range_provider_factory.as_ref()
1258        {
1259            scan_region
1260                .set_extension_range_provider(factory.create_extension_range_provider(region));
1261        }
1262        scan_region
1263    }
1264
1265    /// Converts the [`RegionRole`].
1266    fn set_region_role(&self, region_id: RegionId, role: RegionRole) -> Result<()> {
1267        let region = self.find_region(region_id)?;
1268        region.set_role(role);
1269        Ok(())
1270    }
1271
1272    /// Sets read-only for a region and ensures no more writes in the region after it returns.
1273    async fn set_region_role_state_gracefully(
1274        &self,
1275        region_id: RegionId,
1276        region_role_state: SettableRegionRoleState,
1277    ) -> Result<SetRegionRoleStateResponse> {
1278        // Notes: It acquires the mutable ownership to ensure no other threads,
1279        // Therefore, we submit it to the worker.
1280        let (request, receiver) =
1281            WorkerRequest::new_set_readonly_gracefully(region_id, region_role_state);
1282        self.workers.submit_to_worker(region_id, request).await?;
1283
1284        receiver.await.context(RecvSnafu)
1285    }
1286
1287    async fn sync_region(
1288        &self,
1289        region_id: RegionId,
1290        manifest_info: RegionManifestInfo,
1291    ) -> Result<(ManifestVersion, bool)> {
1292        ensure!(manifest_info.is_mito(), MitoManifestInfoSnafu);
1293        let manifest_version = manifest_info.data_manifest_version();
1294        let (request, receiver) =
1295            WorkerRequest::new_sync_region_request(region_id, manifest_version);
1296        self.workers.submit_to_worker(region_id, request).await?;
1297
1298        receiver.await.context(RecvSnafu)?
1299    }
1300
1301    async fn remap_manifests(
1302        &self,
1303        request: RemapManifestsRequest,
1304    ) -> Result<RemapManifestsResponse> {
1305        let region_id = request.region_id;
1306        let (request, receiver) = WorkerRequest::try_from_remap_manifests_request(request)?;
1307        self.workers.submit_to_worker(region_id, request).await?;
1308        let manifest_paths = receiver.await.context(RecvSnafu)??;
1309        Ok(RemapManifestsResponse { manifest_paths })
1310    }
1311
1312    async fn copy_region_from(
1313        &self,
1314        region_id: RegionId,
1315        request: MitoCopyRegionFromRequest,
1316    ) -> Result<MitoCopyRegionFromResponse> {
1317        let (request, receiver) =
1318            WorkerRequest::try_from_copy_region_from_request(region_id, request)?;
1319        self.workers.submit_to_worker(region_id, request).await?;
1320        let response = receiver.await.context(RecvSnafu)??;
1321        Ok(response)
1322    }
1323
1324    fn role(&self, region_id: RegionId) -> Option<RegionRole> {
1325        self.workers
1326            .get_region(region_id)
1327            .map(|region| region.region_role())
1328    }
1329}
1330
1331fn validate_sequence_fences(
1332    request: &ScanRequest,
1333    version: &crate::region::version::Version,
1334    region_id: RegionId,
1335    exact_sequence_range_available: bool,
1336    extension_provider_blocks_exact: bool,
1337) -> Result<()> {
1338    if request.exact_sequence_range {
1339        // Exact `sequence_range` mode: when the capability is available the
1340        // requested (C, H] delta is enforced row-level on memtables and every
1341        // SST, so the lower/upper fences can be relaxed. When it is not, we
1342        // must fail with a structured stale/unsupported error so Flow falls
1343        // back instead of silently reading an approximate superset.
1344        if !exact_sequence_range_available {
1345            if let Some(given_seq) = request.memtable_min_sequence {
1346                let min_readable_seq = version.flushed_sequence;
1347                ensure!(
1348                    given_seq >= min_readable_seq,
1349                    IncrementalQueryStaleSnafu {
1350                        region_id,
1351                        given_seq,
1352                        min_readable_seq,
1353                    }
1354                );
1355            }
1356
1357            if let Some(given_seq) = request.memtable_max_sequence {
1358                let min_enforceable_seq = version.flushed_sequence;
1359                ensure!(
1360                    given_seq >= min_enforceable_seq,
1361                    SnapshotFenceStaleSnafu {
1362                        region_id,
1363                        given_seq,
1364                        min_enforceable_seq,
1365                    }
1366                );
1367            }
1368
1369            // Both bounds are enforceable against the flushed frontier, yet the
1370            // region cannot serve an exact row-level delta (preserve option off,
1371            // a file without the preserved-sequence marker, or a follower with
1372            // an extension range provider). Main semantics would silently
1373            // return rows outside (C, H], so fail instead.
1374            return SequenceRangeUnsupportedSnafu {
1375                region_id,
1376                min_seq: request.memtable_min_sequence.unwrap_or_default(),
1377                max_seq: request.memtable_max_sequence.unwrap_or_default(),
1378                reason: sequence_range_unsupported_reason(version, extension_provider_blocks_exact),
1379            }
1380            .fail();
1381        }
1382    } else {
1383        // Non-exact mode (including historical `memtable_only` scans): keep
1384        // main's fences exactly as-is. The preserve option never relaxes them
1385        // because `exact_sequence_range` is the only explicit exact intent.
1386        if let Some(given_seq) = request.memtable_min_sequence {
1387            let min_readable_seq = version.flushed_sequence;
1388            ensure!(
1389                given_seq >= min_readable_seq,
1390                IncrementalQueryStaleSnafu {
1391                    region_id,
1392                    given_seq,
1393                    min_readable_seq,
1394                }
1395            );
1396        }
1397
1398        if let Some(given_seq) = request.memtable_max_sequence
1399            && !request.skip_sst_files
1400        {
1401            // Explicit snapshot fences that include SST reads are enforceable
1402            // only while the requested upper bound is not older than the
1403            // region's flushed frontier. If H has already been flushed into SST,
1404            // mito cannot apply a memtable-only sequence upper bound to that
1405            // SST scan, so fail and let Flow rebind the fenced repair instead
1406            // of reading rows beyond H.
1407            let min_enforceable_seq = version.flushed_sequence;
1408            ensure!(
1409                given_seq >= min_enforceable_seq,
1410                SnapshotFenceStaleSnafu {
1411                    region_id,
1412                    given_seq,
1413                    min_enforceable_seq,
1414                }
1415            );
1416        }
1417    }
1418
1419    Ok(())
1420}
1421
1422fn sequence_range_unsupported_reason(
1423    version: &crate::region::version::Version,
1424    extension_provider_blocks_exact: bool,
1425) -> String {
1426    if extension_provider_blocks_exact {
1427        "region is a follower with an extension range provider, whose streams cannot be filtered by sequence".to_string()
1428    } else if version.options.preserve_row_sequence {
1429        "region has files without preserved per-row sequences".to_string()
1430    } else {
1431        "region does not preserve per-row sequences (preserve_row_sequence is off)".to_string()
1432    }
1433}
1434
1435fn map_batch_responses(responses: Vec<(RegionId, Result<AffectedRows>)>) -> BatchResponses {
1436    responses
1437        .into_iter()
1438        .map(|(region_id, response)| {
1439            (
1440                region_id,
1441                response.map(RegionResponse::new).map_err(BoxedError::new),
1442            )
1443        })
1444        .collect()
1445}
1446
1447#[async_trait]
1448impl RegionEngine for MitoEngine {
1449    fn name(&self) -> &str {
1450        MITO_ENGINE_NAME
1451    }
1452
1453    #[tracing::instrument(skip_all)]
1454    async fn handle_batch_open_requests(
1455        &self,
1456        parallelism: usize,
1457        requests: Vec<(RegionId, RegionOpenRequest)>,
1458    ) -> Result<BatchResponses, BoxedError> {
1459        // TODO(weny): add metrics.
1460        self.inner
1461            .handle_batch_open_requests(parallelism, requests)
1462            .await
1463            .map(map_batch_responses)
1464            .map_err(BoxedError::new)
1465    }
1466
1467    #[tracing::instrument(skip_all)]
1468    async fn handle_batch_catchup_requests(
1469        &self,
1470        parallelism: usize,
1471        requests: Vec<(RegionId, RegionCatchupRequest)>,
1472    ) -> Result<BatchResponses, BoxedError> {
1473        self.inner
1474            .handle_batch_catchup_requests(parallelism, requests)
1475            .await
1476            .map(map_batch_responses)
1477            .map_err(BoxedError::new)
1478    }
1479
1480    /// Sends the puts to regions of the same worker in one message, so that the worker writes
1481    /// them in one batch and one WAL append. The messages are sent on their own task, so every
1482    /// response can be awaited at once.
1483    fn handle_batch_put_requests(
1484        self: Arc<Self>,
1485        requests: Vec<(RegionId, RegionPutRequest)>,
1486    ) -> Vec<BatchPutResponse> {
1487        let mut writes = Vec::with_capacity(requests.len());
1488        let mut responses = Vec::with_capacity(requests.len());
1489        for (region_id, request) in requests {
1490            let request = RegionRequest::Put(request);
1491            let timer = HANDLE_REQUEST_ELAPSED
1492                .with_label_values(&[request.request_type()])
1493                .start_timer();
1494            let region_metadata = self.inner.get_metadata(region_id).ok();
1495            let response =
1496                match WorkerRequest::try_from_region_request(region_id, request, region_metadata) {
1497                    Ok((WorkerRequest::Write(write), receiver)) => {
1498                        writes.push(write);
1499                        async move {
1500                            let _timer = timer;
1501                            receiver
1502                                .await
1503                                .context(RecvSnafu)
1504                                .flatten()
1505                                .map(RegionResponse::new)
1506                                .map_err(BoxedError::new)
1507                        }
1508                        .boxed()
1509                    }
1510                    Ok(_) => unreachable!("a put converts to a write"),
1511                    Err(e) => future::ready(Err(BoxedError::new(e))).boxed(),
1512                };
1513            responses.push((vec![region_id], response));
1514        }
1515        let inner = self.inner.clone();
1516        common_runtime::spawn_ingest(async move { inner.workers.submit_writes(writes).await });
1517        responses
1518    }
1519
1520    #[tracing::instrument(skip_all)]
1521    async fn handle_request(
1522        &self,
1523        region_id: RegionId,
1524        request: RegionRequest,
1525    ) -> Result<RegionResponse, BoxedError> {
1526        let _timer = HANDLE_REQUEST_ELAPSED
1527            .with_label_values(&[request.request_type()])
1528            .start_timer();
1529
1530        let is_alter = matches!(request, RegionRequest::Alter(_));
1531        let is_create = matches!(request, RegionRequest::Create(_));
1532        let mut response = self
1533            .inner
1534            .handle_request(region_id, request)
1535            .await
1536            .map(RegionResponse::new)
1537            .map_err(BoxedError::new)?;
1538
1539        if is_alter {
1540            self.handle_alter_response(region_id, &mut response)
1541                .map_err(BoxedError::new)?;
1542        } else if is_create {
1543            self.handle_create_response(region_id, &mut response)
1544                .map_err(BoxedError::new)?;
1545        }
1546
1547        Ok(response)
1548    }
1549
1550    #[tracing::instrument(skip_all)]
1551    async fn handle_query(
1552        &self,
1553        region_id: RegionId,
1554        request: ScanRequest,
1555    ) -> Result<RegionScannerRef, BoxedError> {
1556        self.scan_region(region_id, request)
1557            .map_err(BoxedError::new)?
1558            .region_scanner()
1559            .await
1560            .map_err(BoxedError::new)
1561    }
1562
1563    fn query_memory_tracker(&self) -> Option<QueryMemoryTracker> {
1564        (self.inner.config.scan_memory_mode == ScanMemoryMode::Extended)
1565            .then(|| self.inner.scan_memory_tracker.clone())
1566    }
1567
1568    async fn get_committed_sequence(
1569        &self,
1570        region_id: RegionId,
1571    ) -> Result<SequenceNumber, BoxedError> {
1572        self.inner
1573            .get_committed_sequence(region_id)
1574            .map_err(BoxedError::new)
1575    }
1576
1577    /// Retrieve region's metadata.
1578    async fn get_metadata(
1579        &self,
1580        region_id: RegionId,
1581    ) -> std::result::Result<RegionMetadataRef, BoxedError> {
1582        self.inner.get_metadata(region_id).map_err(BoxedError::new)
1583    }
1584
1585    /// Stop the engine.
1586    ///
1587    /// Stopping the engine doesn't stop the underlying log store as other components might
1588    /// still use it. (When no other components are referencing the log store, it will
1589    /// automatically shutdown.)
1590    async fn stop(&self) -> std::result::Result<(), BoxedError> {
1591        self.inner.stop().await.map_err(BoxedError::new)
1592    }
1593
1594    fn region_statistic(&self, region_id: RegionId) -> Option<RegionStatistic> {
1595        self.get_region_statistic(region_id)
1596    }
1597
1598    fn set_region_role(&self, region_id: RegionId, role: RegionRole) -> Result<(), BoxedError> {
1599        self.inner
1600            .set_region_role(region_id, role)
1601            .map_err(BoxedError::new)
1602    }
1603
1604    fn set_region_role_with_lease(
1605        &self,
1606        region_id: RegionId,
1607        role: RegionRole,
1608        sent_at: Instant,
1609        deadline: Instant,
1610    ) -> Result<bool, BoxedError> {
1611        let region = self.inner.find_region(region_id).map_err(BoxedError::new)?;
1612        // Before the role, and even if the role leaves the region's state unchanged.
1613        if role.writable() {
1614            region.manifest_ctx.leader_deadline.extend(deadline);
1615        }
1616        if role == RegionRole::Leader {
1617            return Ok(region
1618                .manifest_ctx
1619                .promote_unless_downgrade_started_after(sent_at, region_id));
1620        }
1621        region.set_role(role);
1622        Ok(true)
1623    }
1624
1625    async fn set_region_role_state_gracefully(
1626        &self,
1627        region_id: RegionId,
1628        region_role_state: SettableRegionRoleState,
1629    ) -> Result<SetRegionRoleStateResponse, BoxedError> {
1630        let _timer = HANDLE_REQUEST_ELAPSED
1631            .with_label_values(&["set_region_role_state_gracefully"])
1632            .start_timer();
1633
1634        self.inner
1635            .set_region_role_state_gracefully(region_id, region_role_state)
1636            .await
1637            .map_err(BoxedError::new)
1638    }
1639
1640    async fn sync_region(
1641        &self,
1642        region_id: RegionId,
1643        request: SyncRegionFromRequest,
1644    ) -> Result<SyncRegionFromResponse, BoxedError> {
1645        let manifest_info = request
1646            .into_region_manifest_info()
1647            .context(UnexpectedSnafu {
1648                err_msg: "Expected a manifest info request",
1649            })
1650            .map_err(BoxedError::new)?;
1651        let (_, synced) = self
1652            .inner
1653            .sync_region(region_id, manifest_info)
1654            .await
1655            .map_err(BoxedError::new)?;
1656
1657        Ok(SyncRegionFromResponse::Mito { synced })
1658    }
1659
1660    async fn remap_manifests(
1661        &self,
1662        request: RemapManifestsRequest,
1663    ) -> Result<RemapManifestsResponse, BoxedError> {
1664        self.inner
1665            .remap_manifests(request)
1666            .await
1667            .map_err(BoxedError::new)
1668    }
1669
1670    fn role(&self, region_id: RegionId) -> Option<RegionRole> {
1671        self.inner.role(region_id)
1672    }
1673
1674    fn as_any(&self) -> &dyn Any {
1675        self
1676    }
1677}
1678
1679impl MitoEngine {
1680    fn handle_alter_response(
1681        &self,
1682        region_id: RegionId,
1683        response: &mut RegionResponse,
1684    ) -> Result<()> {
1685        if let Some(statistic) = self.region_statistic(region_id) {
1686            Self::encode_manifest_info_to_extensions(
1687                &region_id,
1688                statistic.manifest,
1689                &mut response.extensions,
1690            )?;
1691        }
1692        let column_metadatas = self
1693            .inner
1694            .find_region(region_id)
1695            .ok()
1696            .map(|r| r.metadata().column_metadatas.clone());
1697        if let Some(column_metadatas) = column_metadatas {
1698            Self::encode_column_metadatas_to_extensions(
1699                &region_id,
1700                column_metadatas,
1701                &mut response.extensions,
1702            )?;
1703        }
1704        Ok(())
1705    }
1706
1707    fn handle_create_response(
1708        &self,
1709        region_id: RegionId,
1710        response: &mut RegionResponse,
1711    ) -> Result<()> {
1712        let column_metadatas = self
1713            .inner
1714            .find_region(region_id)
1715            .ok()
1716            .map(|r| r.metadata().column_metadatas.clone());
1717        if let Some(column_metadatas) = column_metadatas {
1718            Self::encode_column_metadatas_to_extensions(
1719                &region_id,
1720                column_metadatas,
1721                &mut response.extensions,
1722            )?;
1723        }
1724        Ok(())
1725    }
1726}
1727
1728// Tests methods.
1729#[cfg(any(test, feature = "test"))]
1730#[allow(clippy::too_many_arguments)]
1731impl MitoEngine {
1732    /// Returns a new [MitoEngine] for tests.
1733    pub async fn new_for_test<S: LogStore>(
1734        data_home: &str,
1735        mut config: MitoConfig,
1736        log_store: Arc<S>,
1737        object_store_manager: ObjectStoreManagerRef,
1738        write_buffer_manager: Option<crate::flush::WriteBufferManagerRef>,
1739        listener: Option<crate::engine::listener::EventListenerRef>,
1740        time_provider: crate::time_provider::TimeProviderRef,
1741        schema_metadata_manager: SchemaMetadataManagerRef,
1742        file_ref_manager: FileReferenceManagerRef,
1743        partition_expr_fetcher: PartitionExprFetcherRef,
1744    ) -> Result<MitoEngine> {
1745        config.sanitize(data_home)?;
1746
1747        let config = Arc::new(config);
1748        let wal_raw_entry_reader = Arc::new(LogStoreRawEntryReader::new(log_store.clone()));
1749        let total_memory = get_total_memory_bytes().max(0) as u64;
1750        let scan_memory_limit = config.scan_memory_limit.resolve(total_memory) as usize;
1751        let scan_memory_tracker = QueryMemoryTracker::builder(scan_memory_limit)
1752            .on_update(|usage| {
1753                SCAN_MEMORY_USAGE_BYTES.set(usage as i64);
1754            })
1755            .on_exhausted(|| {
1756                SCAN_MEMORY_EXHAUSTED_TOTAL.inc();
1757            })
1758            .on_reject(|| {
1759                SCAN_REQUESTS_REJECTED_TOTAL.inc();
1760            })
1761            .build();
1762        Ok(MitoEngine {
1763            inner: Arc::new(EngineInner {
1764                workers: WorkerGroup::start_for_test(
1765                    data_home,
1766                    config.clone(),
1767                    log_store,
1768                    object_store_manager,
1769                    write_buffer_manager,
1770                    listener,
1771                    schema_metadata_manager,
1772                    file_ref_manager,
1773                    time_provider,
1774                    partition_expr_fetcher,
1775                )
1776                .await?,
1777                config,
1778                wal_raw_entry_reader,
1779                scan_memory_tracker,
1780                region_hook: None,
1781                #[cfg(feature = "enterprise")]
1782                extension_range_provider_factory: None,
1783            }),
1784        })
1785    }
1786
1787    /// Returns the purge scheduler.
1788    pub fn purge_scheduler(&self) -> &crate::schedule::scheduler::SchedulerRef {
1789        self.inner.workers.purge_scheduler()
1790    }
1791}
1792
1793/// Evaluates region-constant predicates before visiting any of its files.
1794fn manifest_region_matches(
1795    region_id: RegionId,
1796    node_id: Option<u64>,
1797    filters: &MetadataFilter,
1798) -> bool {
1799    filters.matches(|column| {
1800        Some(match column {
1801            sst_entry::REGION_ID => ScalarValue::UInt64(Some(region_id.as_u64())),
1802            sst_entry::TABLE_ID => ScalarValue::UInt32(Some(region_id.table_id())),
1803            sst_entry::REGION_NUMBER => ScalarValue::UInt32(Some(region_id.region_number())),
1804            sst_entry::REGION_GROUP => ScalarValue::UInt8(Some(region_id.region_group())),
1805            sst_entry::REGION_SEQUENCE => ScalarValue::UInt32(Some(region_id.region_sequence())),
1806            sst_entry::NODE_ID => ScalarValue::UInt64(node_id),
1807            _ => return None,
1808        })
1809    })
1810}
1811
1812#[cfg(test)]
1813mod tests {
1814    use std::time::Duration;
1815
1816    use super::*;
1817    use crate::sst::file::FileMeta;
1818
1819    #[test]
1820    fn test_manifest_region_predicates() {
1821        use super::manifest_region_matches;
1822        use common_recordbatch::filter::MetadataFilter;
1823        use datafusion_expr::{col, lit};
1824        let region = store_api::storage::RegionId::new(42, 3);
1825        for (expr, expected) in [
1826            (col("region_id").eq(lit(region.as_u64())), true),
1827            (col("region_id").eq(lit(region.as_u64() + 1)), false),
1828            (col("table_id").eq(lit(42u32)), true),
1829            (col("node_id").is_null(), true),
1830            (col("file_size").gt(lit(100u64)), true),
1831            // A type mismatch is left to the residual filter, never pruned.
1832            (col("region_id").eq(lit("invalid")), true),
1833        ] {
1834            let filter = MetadataFilter::new(std::slice::from_ref(&expr));
1835            assert_eq!(
1836                manifest_region_matches(region, None, &filter),
1837                expected,
1838                "{expr}"
1839            );
1840        }
1841    }
1842
1843    #[test]
1844    fn test_is_valid_region_edit() {
1845        // Valid: has only "files_to_add"
1846        let edit = RegionEdit {
1847            files_to_add: vec![FileMeta::default()],
1848            files_to_remove: vec![],
1849            timestamp_ms: None,
1850            compaction_time_window: None,
1851            flushed_entry_id: None,
1852            flushed_sequence: None,
1853            committed_sequence: None,
1854        };
1855        assert!(is_valid_region_edit(&edit));
1856
1857        // Invalid: "files_to_add" and "files_to_remove" are both empty
1858        let edit = RegionEdit {
1859            files_to_add: vec![],
1860            files_to_remove: vec![],
1861            timestamp_ms: None,
1862            compaction_time_window: None,
1863            flushed_entry_id: None,
1864            flushed_sequence: None,
1865            committed_sequence: None,
1866        };
1867        assert!(!is_valid_region_edit(&edit));
1868
1869        // Valid: has only "files_to_remove"
1870        let edit = RegionEdit {
1871            files_to_add: vec![],
1872            files_to_remove: vec![FileMeta::default()],
1873            timestamp_ms: None,
1874            compaction_time_window: None,
1875            flushed_entry_id: None,
1876            flushed_sequence: None,
1877            committed_sequence: None,
1878        };
1879        assert!(is_valid_region_edit(&edit));
1880
1881        // Valid: both "files_to_add" and "files_to_remove" are not empty
1882        let edit = RegionEdit {
1883            files_to_add: vec![FileMeta::default()],
1884            files_to_remove: vec![FileMeta::default()],
1885            timestamp_ms: None,
1886            compaction_time_window: None,
1887            flushed_entry_id: None,
1888            flushed_sequence: None,
1889            committed_sequence: None,
1890        };
1891        assert!(is_valid_region_edit(&edit));
1892
1893        // Invalid: other fields are not all "None"s
1894        let edit = RegionEdit {
1895            files_to_add: vec![FileMeta::default()],
1896            files_to_remove: vec![],
1897            timestamp_ms: None,
1898            compaction_time_window: Some(Duration::from_secs(1)),
1899            flushed_entry_id: None,
1900            flushed_sequence: None,
1901            committed_sequence: None,
1902        };
1903        assert!(!is_valid_region_edit(&edit));
1904        let edit = RegionEdit {
1905            files_to_add: vec![FileMeta::default()],
1906            files_to_remove: vec![],
1907            timestamp_ms: None,
1908            compaction_time_window: None,
1909            flushed_entry_id: Some(1),
1910            flushed_sequence: None,
1911            committed_sequence: None,
1912        };
1913        assert!(!is_valid_region_edit(&edit));
1914        let edit = RegionEdit {
1915            files_to_add: vec![FileMeta::default()],
1916            files_to_remove: vec![],
1917            timestamp_ms: None,
1918            compaction_time_window: None,
1919            flushed_entry_id: None,
1920            flushed_sequence: Some(1),
1921            committed_sequence: None,
1922        };
1923        assert!(!is_valid_region_edit(&edit));
1924    }
1925}