Skip to main content

mito2/
config.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//! Configurations.
16
17use std::cmp;
18use std::path::Path;
19use std::time::Duration;
20
21use common_base::memory_limit::MemoryLimit;
22use common_base::readable_size::ReadableSize;
23use common_memory_manager::OnExhaustedPolicy;
24use common_stat::{get_total_cpu_cores, get_total_memory_readable};
25use common_telemetry::warn;
26use serde::{Deserialize, Serialize};
27use serde_with::serde_as;
28
29use crate::cache::file_cache::DEFAULT_INDEX_CACHE_PERCENT;
30use crate::error::Result;
31use crate::gc::GcConfig;
32use crate::sst::DEFAULT_WRITE_BUFFER_SIZE;
33
34const MULTIPART_UPLOAD_MINIMUM_SIZE: ReadableSize = ReadableSize::mb(5);
35const DEFAULT_SERIES_INDEX_MAINTENANCE_INTERVAL: Duration = Duration::from_secs(5 * 60);
36/// Default maximum number of SST files to scan concurrently.
37pub(crate) const DEFAULT_MAX_CONCURRENT_SCAN_FILES: usize = 384;
38
39// Use `1/GLOBAL_WRITE_BUFFER_SIZE_FACTOR` of OS memory as global write buffer size in default mode
40const GLOBAL_WRITE_BUFFER_SIZE_FACTOR: u64 = 8;
41/// Use `1/SST_META_CACHE_SIZE_FACTOR` of OS memory size as SST meta cache size in default mode
42const SST_META_CACHE_SIZE_FACTOR: u64 = 8;
43/// Use `1/PREFILTER_RESULT_CACHE_SIZE_FACTOR` of OS memory size as prefilter result cache size in default mode
44const PREFILTER_RESULT_CACHE_SIZE_FACTOR: u64 = 32;
45/// Use `1/INDEX_METADATA_CACHE_SIZE_FACTOR` of OS memory size as index metadata cache size in default mode
46const INDEX_METADATA_CACHE_SIZE_FACTOR: u64 = 32;
47/// Use `1/MEM_CACHE_SIZE_FACTOR` of OS memory size as mem cache size in default mode
48const MEM_CACHE_SIZE_FACTOR: u64 = 16;
49/// Use `1/PAGE_CACHE_SIZE_FACTOR` of OS memory size as page cache size in default mode
50const PAGE_CACHE_SIZE_FACTOR: u64 = 8;
51/// Use `1/INDEX_CREATE_MEM_THRESHOLD_FACTOR` of OS memory size as mem threshold for creating index
52const INDEX_CREATE_MEM_THRESHOLD_FACTOR: u64 = 16;
53
54/// Memory accounted against the shared scan limit.
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum ScanMemoryMode {
58    /// Count projected Parquet columns' uncompressed row-group sizes.
59    #[default]
60    Basic,
61    /// Also count cumulative streamed batches.
62    Extended,
63}
64
65/// Fetch option timeout
66pub(crate) const FETCH_OPTION_TIMEOUT: Duration = Duration::from_secs(3);
67
68/// Configuration for [MitoEngine](crate::engine::MitoEngine).
69/// Before using the config, call [`MitoConfig::sanitize()`] to validate and normalize it.
70#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
71#[serde(default)]
72pub struct MitoConfig {
73    // Worker configs:
74    /// Number of region workers (default: 1/2 of cpu cores).
75    /// Sets to 0 to use the default value.
76    pub num_workers: usize,
77    /// Request channel size of each worker (default 128).
78    pub worker_channel_size: usize,
79    /// Max batch size for a worker to handle requests (default 64).
80    pub worker_request_batch_size: usize,
81
82    // Manifest configs:
83    /// Number of meta action updated to trigger a new checkpoint
84    /// for the manifest (default 10).
85    pub manifest_checkpoint_distance: u64,
86    /// Number of removed files to keep in manifest's `removed_files` field before also
87    /// remove them from `removed_files`. Mostly for debugging purpose.
88    /// If set to 0, it will only use `keep_removed_file_ttl` to decide when to remove files
89    /// from `removed_files` field.
90    pub experimental_manifest_keep_removed_file_count: usize,
91    /// How long to keep removed files in the `removed_files` field of manifest
92    /// after they are removed from manifest.
93    /// files will only be removed from `removed_files` field
94    /// if both `keep_removed_file_count` and `keep_removed_file_ttl` is reached.
95    #[serde(with = "humantime_serde")]
96    pub experimental_manifest_keep_removed_file_ttl: Duration,
97    /// Whether to compress manifest and checkpoint file by gzip (default false).
98    pub compress_manifest: bool,
99
100    // Background job configs:
101    /// Max number of running background index build jobs (default: 1/8 of cpu cores).
102    pub max_background_index_builds: usize,
103    /// Under development; do not enable. Whether to enable series indexes (default false).
104    /// Indexes are stored on the local filesystem under `{data_home}/series_index`.
105    pub experimental_enable_series_index: bool,
106    /// Approximate series and range index size limit in open regions (default: 5 GiB).
107    /// Workers share a periodically refreshed estimate and skip maintenance when full.
108    /// In-flight reconciliation can exceed the limit; closed-region files are not counted.
109    pub experimental_series_index_max_size: ReadableSize,
110    /// Whether to build and query range indexes when series indexes are enabled (default false).
111    /// Obsolete range-index metadata and files are still cleaned up when disabled.
112    pub experimental_enable_range_index: bool,
113    /// Interval between series-index maintenance runs (default 5 min). Zero uses the default.
114    #[serde(with = "humantime_serde")]
115    pub experimental_series_index_maintenance_interval: Duration,
116    /// Under development; do not enable. Requested minimum bucket width for series indexes.
117    /// It is rounded up to an exact multiple of each region's compaction time window.
118    #[serde(with = "humantime_serde")]
119    pub experimental_series_index_bucket_width: Duration,
120    /// Max number of running background flush jobs (default: 1/2 of cpu cores).
121    pub max_background_flushes: usize,
122    /// Max number of running background compaction jobs (default: 1/4 of cpu cores).
123    pub max_background_compactions: usize,
124    /// Max number of running background purge jobs (default: number of cpu cores).
125    pub max_background_purges: usize,
126    /// Memory budget for compaction tasks.
127    /// Supports absolute size (e.g., "2GiB", "512MB") or percentage of system memory (e.g., "50%").
128    /// Setting it to 0 or "unlimited" disables the limit.
129    pub experimental_compaction_memory_limit: MemoryLimit,
130    /// Behavior when compaction cannot acquire memory from the budget.
131    pub experimental_compaction_on_exhausted: OnExhaustedPolicy,
132    /// Default per-output parallelism for input decoding and SST column encoding,
133    /// bounded independently per stage. Values below 2 disable both.
134    /// Tables can override it with `compaction.experimental_output_parallelism`.
135    pub experimental_compaction_output_parallelism: usize,
136
137    // Flush configs:
138    /// Interval to auto flush a region if it has not flushed yet (default 10 min).
139    #[serde(with = "humantime_serde")]
140    pub auto_flush_interval: Duration,
141    /// Global write buffer size threshold to trigger flush.
142    pub global_write_buffer_size: ReadableSize,
143    /// Global write buffer size threshold to reject write requests.
144    pub global_write_buffer_reject_size: ReadableSize,
145    /// Default write buffer size for each region. Regions stall at this size and
146    /// reject writes at twice this size. Setting it to 0 disables both limits
147    /// unless the table specifies `write_buffer_size`.
148    pub default_region_write_buffer_size: ReadableSize,
149
150    // Cache configs:
151    /// Cache size for SST metadata. Setting it to 0 to disable the cache.
152    pub sst_meta_cache_size: ReadableSize,
153    /// Cache size for vectors and arrow arrays. Setting it to 0 to disable the cache.
154    pub vector_cache_size: ReadableSize,
155    /// Cache size for pages of SST row groups. Setting it to 0 to disable the cache.
156    pub page_cache_size: ReadableSize,
157    /// Cache size for time series selector (e.g. `last_value()`). Setting it to 0 to disable the cache.
158    pub selector_result_cache_size: ReadableSize,
159    /// Cache size for flat range scan results. Setting it to 0 to disable the cache.
160    pub range_result_cache_size: ReadableSize,
161    /// Cache size for prefilter results. Setting it to 0 to disable the cache.
162    pub prefilter_result_cache_size: ReadableSize,
163    /// Whether to enable the write cache.
164    pub enable_write_cache: bool,
165    /// File system path for write cache dir's root, defaults to `{data_home}`.
166    pub write_cache_path: String,
167    /// Capacity for write cache.
168    pub write_cache_size: ReadableSize,
169    /// TTL for write cache.
170    #[serde(with = "humantime_serde")]
171    pub write_cache_ttl: Option<Duration>,
172    /// Preload index (puffin) files into cache on region open (default: true).
173    pub preload_index_cache: bool,
174    /// Percentage of write cache capacity allocated for index (puffin) files (default: 20).
175    /// The remaining capacity is used for data (parquet) files.
176    /// Must be between 0 and 100 (exclusive).
177    pub index_cache_percent: u8,
178    /// Enable background downloading of files to the local cache when accessed during queries (default: true).
179    /// When enabled, files will be asynchronously downloaded to improve performance for subsequent reads.
180    pub enable_refill_cache_on_read: bool,
181    /// Capacity for manifest cache (default: 256MB).
182    pub manifest_cache_size: ReadableSize,
183
184    // Other configs:
185    /// Buffer size for SST writing.
186    // TODO(#9240): Support per-object-store write buffer sizes for backend-specific tuning.
187    pub sst_write_buffer_size: ReadableSize,
188    /// Maximum number of SST files to scan concurrently when memory accounting
189    /// does not cover the scan or the limit is disabled (default 384).
190    pub max_concurrent_scan_files: usize,
191    /// Whether to allow stale entries read during replay.
192    pub allow_stale_entries: bool,
193    /// Memory limit for table scans across all queries (default: 25% of system memory).
194    /// Setting it to 0 or "unlimited" disables the limit.
195    /// Supports absolute size (e.g., "2GB") or percentage of system memory (e.g., "50%").
196    pub scan_memory_limit: MemoryLimit,
197    /// Scan memory accounting: basic counts projected Parquet row groups;
198    /// extended also counts cumulative streamed batches. Limited reservations round up to KB.
199    /// Internal series merges use the query runtime pool in both modes.
200    pub scan_memory_mode: ScanMemoryMode,
201    /// Scan memory exhaustion always fails immediately (default: `fail`).
202    /// Legacy `wait` and `wait(<duration>)` values are accepted but deprecated:
203    /// sanitization warns and replaces them with [`OnExhaustedPolicy::Fail`].
204    pub scan_memory_on_exhausted: OnExhaustedPolicy,
205
206    /// Index configs.
207    pub index: IndexConfig,
208    /// Inverted index configs.
209    pub inverted_index: InvertedIndexConfig,
210    /// Full-text index configs.
211    pub fulltext_index: FulltextIndexConfig,
212    /// Bloom filter index configs.
213    pub bloom_filter_index: BloomFilterConfig,
214
215    /// Minimum time interval between two compactions.
216    /// To align with the old behavior, the default value is 0 (no restrictions).
217    #[serde(with = "humantime_serde")]
218    pub min_compaction_interval: Duration,
219    /// Whether to schedule compaction after applying a region edit.
220    pub schedule_compaction_after_edit: bool,
221
222    /// Whether to enable flat format as the default SST format.
223    /// When enabled, forces using BulkMemtable and BulkMemtableBuilder.
224    pub default_flat_format: bool,
225
226    /// Whether to enable the experimental two-phase mode for eligible metric series scans.
227    pub experimental_series_scan_v2: bool,
228
229    pub gc: GcConfig,
230}
231
232impl Default for MitoConfig {
233    fn default() -> Self {
234        let mut mito_config = MitoConfig {
235            num_workers: divide_num_cpus(2),
236            worker_channel_size: 128,
237            worker_request_batch_size: 64,
238            manifest_checkpoint_distance: 10,
239            experimental_manifest_keep_removed_file_count: 256,
240            experimental_manifest_keep_removed_file_ttl: Duration::from_secs(60 * 60),
241            compress_manifest: false,
242            max_background_index_builds: divide_num_cpus(8),
243            experimental_enable_series_index: false,
244            experimental_series_index_max_size: ReadableSize::gb(5),
245            experimental_enable_range_index: false,
246            experimental_series_index_maintenance_interval:
247                DEFAULT_SERIES_INDEX_MAINTENANCE_INTERVAL,
248            experimental_series_index_bucket_width: Duration::from_secs(5 * 24 * 60 * 60),
249            max_background_flushes: divide_num_cpus(2),
250            max_background_compactions: divide_num_cpus(4),
251            max_background_purges: get_total_cpu_cores(),
252            experimental_compaction_memory_limit: MemoryLimit::Unlimited,
253            experimental_compaction_on_exhausted: OnExhaustedPolicy::default(),
254            experimental_compaction_output_parallelism: 1,
255            auto_flush_interval: Duration::from_secs(10 * 60),
256            global_write_buffer_size: ReadableSize::gb(1),
257            global_write_buffer_reject_size: ReadableSize::gb(2),
258            default_region_write_buffer_size: ReadableSize::mb(0),
259            sst_meta_cache_size: ReadableSize::mb(128),
260            vector_cache_size: ReadableSize::mb(512),
261            page_cache_size: ReadableSize::mb(512),
262            selector_result_cache_size: ReadableSize::mb(512),
263            range_result_cache_size: ReadableSize::mb(512),
264            prefilter_result_cache_size: ReadableSize::mb(128),
265            enable_write_cache: false,
266            write_cache_path: String::new(),
267            write_cache_size: ReadableSize::gb(5),
268            write_cache_ttl: None,
269            preload_index_cache: true,
270            index_cache_percent: DEFAULT_INDEX_CACHE_PERCENT,
271            enable_refill_cache_on_read: true,
272            manifest_cache_size: ReadableSize::mb(256),
273            sst_write_buffer_size: DEFAULT_WRITE_BUFFER_SIZE,
274            max_concurrent_scan_files: DEFAULT_MAX_CONCURRENT_SCAN_FILES,
275            allow_stale_entries: false,
276            scan_memory_limit: MemoryLimit::Percentage(25),
277            scan_memory_mode: ScanMemoryMode::Basic,
278            scan_memory_on_exhausted: OnExhaustedPolicy::Fail,
279            index: IndexConfig::default(),
280            inverted_index: InvertedIndexConfig::default(),
281            fulltext_index: FulltextIndexConfig::default(),
282            bloom_filter_index: BloomFilterConfig::default(),
283            min_compaction_interval: Duration::from_secs(0),
284            schedule_compaction_after_edit: true,
285            default_flat_format: true,
286            // FIXME(#9435): Keep v2 opt-in while long-range scan memory usage is investigated.
287            experimental_series_scan_v2: false,
288            gc: GcConfig::default(),
289        };
290
291        // Adjust buffer and cache size according to system memory if we can.
292        if let Some(sys_memory) = get_total_memory_readable() {
293            mito_config.adjust_buffer_and_cache_size(sys_memory);
294        }
295
296        mito_config
297    }
298}
299
300impl MitoConfig {
301    /// Sanitize incorrect configurations.
302    ///
303    /// Returns an error if there is a configuration that unable to sanitize.
304    pub fn sanitize(&mut self, data_home: &str) -> Result<()> {
305        if matches!(
306            self.scan_memory_on_exhausted,
307            OnExhaustedPolicy::Wait { .. }
308        ) {
309            warn!(
310                "scan_memory_on_exhausted = {:?} is deprecated and is treated as fail; \
311                 scan memory exhaustion now fails immediately. Set scan_memory_on_exhausted = \"fail\"",
312                self.scan_memory_on_exhausted
313            );
314            self.scan_memory_on_exhausted = OnExhaustedPolicy::Fail;
315        }
316
317        if self.experimental_enable_series_index {
318            snafu::ensure!(
319                self.experimental_series_index_max_size.as_bytes() >= 1024,
320                crate::error::InvalidConfigSnafu {
321                    reason: "experimental_series_index_max_size must be at least 1KiB"
322                }
323            );
324        }
325        // Use default value if `num_workers` is 0.
326        if self.num_workers == 0 {
327            self.num_workers = divide_num_cpus(2);
328        }
329
330        // Sanitize channel size.
331        if self.worker_channel_size == 0 {
332            warn!("Sanitize channel size 0 to 1");
333            self.worker_channel_size = 1;
334        }
335
336        if self.max_background_flushes == 0 {
337            warn!(
338                "Sanitize max background flushes 0 to {}",
339                divide_num_cpus(2)
340            );
341            self.max_background_flushes = divide_num_cpus(2);
342        }
343        if self.max_background_compactions == 0 {
344            warn!(
345                "Sanitize max background compactions 0 to {}",
346                divide_num_cpus(4)
347            );
348            self.max_background_compactions = divide_num_cpus(4);
349        }
350        if self.max_background_purges == 0 {
351            let cpu_cores = get_total_cpu_cores();
352            warn!("Sanitize max background purges 0 to {}", cpu_cores);
353            self.max_background_purges = cpu_cores;
354        }
355
356        if self
357            .experimental_series_index_maintenance_interval
358            .is_zero()
359        {
360            warn!("Sanitize series-index maintenance interval 0 to 5 minutes");
361            self.experimental_series_index_maintenance_interval =
362                DEFAULT_SERIES_INDEX_MAINTENANCE_INTERVAL;
363        }
364
365        if self.global_write_buffer_reject_size <= self.global_write_buffer_size {
366            self.global_write_buffer_reject_size = self.global_write_buffer_size * 2;
367            warn!(
368                "Sanitize global write buffer reject size to {}",
369                self.global_write_buffer_reject_size
370            );
371        }
372
373        if self.sst_write_buffer_size < MULTIPART_UPLOAD_MINIMUM_SIZE {
374            self.sst_write_buffer_size = MULTIPART_UPLOAD_MINIMUM_SIZE;
375            warn!(
376                "Sanitize sst write buffer size to {}",
377                self.sst_write_buffer_size
378            );
379        }
380
381        // Sets write cache path if it is empty.
382        if self.write_cache_path.trim().is_empty() {
383            self.write_cache_path = data_home.to_string();
384        }
385
386        // Validate index_cache_percent is within valid range (0, 100)
387        if self.index_cache_percent == 0 || self.index_cache_percent >= 100 {
388            warn!(
389                "Invalid index_cache_percent {}, resetting to default {}",
390                self.index_cache_percent, DEFAULT_INDEX_CACHE_PERCENT
391            );
392            self.index_cache_percent = DEFAULT_INDEX_CACHE_PERCENT;
393        }
394
395        self.index.sanitize(data_home, &self.inverted_index)?;
396
397        Ok(())
398    }
399
400    fn adjust_buffer_and_cache_size(&mut self, sys_memory: ReadableSize) {
401        // shouldn't be greater than 1G in default mode.
402        let global_write_buffer_size = cmp::min(
403            sys_memory / GLOBAL_WRITE_BUFFER_SIZE_FACTOR,
404            ReadableSize::gb(1),
405        );
406        // Use 2x of global write buffer size as global write buffer reject size.
407        let global_write_buffer_reject_size = global_write_buffer_size * 2;
408        // Page-index-bearing SST metadata can be much larger than footers alone.
409        // Keep the auto-sized default bounded, but allow a larger warm working set.
410        let sst_meta_cache_size = cmp::min(
411            sys_memory / SST_META_CACHE_SIZE_FACTOR,
412            ReadableSize::mb(512),
413        );
414        let prefilter_result_cache_size = cmp::min(
415            sys_memory / PREFILTER_RESULT_CACHE_SIZE_FACTOR,
416            ReadableSize::mb(128),
417        );
418        // shouldn't be greater than 512MB in default mode.
419        let mem_cache_size = cmp::min(sys_memory / MEM_CACHE_SIZE_FACTOR, ReadableSize::mb(512));
420        let page_cache_size = sys_memory / PAGE_CACHE_SIZE_FACTOR;
421
422        self.global_write_buffer_size = global_write_buffer_size;
423        self.global_write_buffer_reject_size = global_write_buffer_reject_size;
424        self.sst_meta_cache_size = sst_meta_cache_size;
425        self.vector_cache_size = mem_cache_size;
426        self.page_cache_size = page_cache_size;
427        self.selector_result_cache_size = mem_cache_size;
428        self.range_result_cache_size = mem_cache_size;
429        // Use a smaller cache size because prefilter result usually should be small.
430        self.prefilter_result_cache_size = prefilter_result_cache_size;
431
432        self.index.adjust_buffer_and_cache_size(sys_memory);
433    }
434
435    /// Enable write cache.
436    #[cfg(test)]
437    pub fn enable_write_cache(
438        mut self,
439        path: String,
440        size: ReadableSize,
441        ttl: Option<Duration>,
442    ) -> Self {
443        self.enable_write_cache = true;
444        self.write_cache_path = path;
445        self.write_cache_size = size;
446        self.write_cache_ttl = ttl;
447        self
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454
455    #[test]
456    fn test_scan_memory_policy_upgrade() {
457        for policy in [
458            None,
459            Some("fail"),
460            Some("skip"),
461            Some("wait"),
462            Some("wait(30s)"),
463        ] {
464            let input = policy
465                .map(|policy| format!("scan_memory_on_exhausted = {policy:?}"))
466                .unwrap_or_default();
467            let mut config: MitoConfig = toml::from_str(&input).unwrap();
468            let compaction_policy = config.experimental_compaction_on_exhausted;
469            config.sanitize("/data").unwrap();
470            assert_eq!(OnExhaustedPolicy::Fail, config.scan_memory_on_exhausted);
471            assert_eq!(
472                compaction_policy,
473                config.experimental_compaction_on_exhausted
474            );
475            let serialized = toml::to_string(&config).unwrap();
476            assert!(serialized.contains("scan_memory_on_exhausted = \"fail\""));
477            config.sanitize("/data").unwrap();
478            assert_eq!(serialized, toml::to_string(&config).unwrap());
479        }
480        for policy in ["unknown", "wait(invalid)", "wait()"] {
481            assert!(
482                toml::from_str::<MitoConfig>(&format!("scan_memory_on_exhausted = {policy:?}"))
483                    .is_err()
484            );
485        }
486    }
487
488    #[test]
489    fn test_default_auto_flush_interval() {
490        assert_eq!(
491            Duration::from_secs(10 * 60),
492            MitoConfig::default().auto_flush_interval
493        );
494    }
495
496    #[test]
497    fn test_adjust_sst_metadata_and_prefilter_cache_caps_independently() {
498        let mut config = MitoConfig::default();
499
500        config.adjust_buffer_and_cache_size(ReadableSize::gb(1));
501        assert_eq!(ReadableSize::mb(128), config.sst_meta_cache_size);
502        assert_eq!(ReadableSize::mb(32), config.prefilter_result_cache_size);
503
504        config.adjust_buffer_and_cache_size(ReadableSize::gb(64));
505        assert_eq!(ReadableSize::mb(512), config.sst_meta_cache_size);
506        assert_eq!(ReadableSize::mb(128), config.prefilter_result_cache_size);
507    }
508
509    #[test]
510    fn test_series_index_config() {
511        assert!(!MitoConfig::default().experimental_enable_series_index);
512        assert!(!MitoConfig::default().experimental_enable_range_index);
513        assert_eq!(
514            MitoConfig::default().experimental_series_index_bucket_width,
515            Duration::from_secs(5 * 24 * 60 * 60)
516        );
517        let mut config: MitoConfig = toml::from_str(
518            "experimental_enable_series_index = true
519             experimental_enable_range_index = false
520             experimental_series_index_max_size = '64MiB'
521             experimental_series_index_maintenance_interval = '30s'
522             experimental_series_index_bucket_width = '2days'",
523        )
524        .unwrap();
525        config.sanitize("/data").unwrap();
526        assert!(config.experimental_enable_series_index);
527        assert_eq!(
528            ReadableSize::mb(64),
529            config.experimental_series_index_max_size
530        );
531        assert!(!config.experimental_enable_range_index);
532        assert_eq!(
533            config.experimental_series_index_maintenance_interval,
534            Duration::from_secs(30)
535        );
536        assert_eq!(
537            config.experimental_series_index_bucket_width,
538            Duration::from_secs(2 * 24 * 60 * 60)
539        );
540        let restored: MitoConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
541        assert_eq!(config, restored);
542        config.experimental_series_index_max_size = ReadableSize(1023);
543        assert!(config.sanitize("/data").is_err());
544        config.experimental_series_index_max_size = ReadableSize(1024);
545        config.sanitize("/data").unwrap();
546
547        let mut config: MitoConfig =
548            toml::from_str("experimental_series_index_maintenance_interval = '0s'").unwrap();
549        config.sanitize("/data").unwrap();
550        assert_eq!(
551            config.experimental_series_index_maintenance_interval,
552            MitoConfig::default().experimental_series_index_maintenance_interval
553        );
554    }
555
556    #[test]
557    fn test_range_index_config() {
558        let config: MitoConfig = toml::from_str("experimental_enable_series_index = true").unwrap();
559        assert!(!config.experimental_enable_range_index);
560        for enabled in [false, true] {
561            let config: MitoConfig =
562                toml::from_str(&format!("experimental_enable_range_index = {enabled}")).unwrap();
563            assert_eq!(enabled, config.experimental_enable_range_index);
564            let restored: MitoConfig = toml::from_str(&toml::to_string(&config).unwrap()).unwrap();
565            assert_eq!(config, restored);
566        }
567    }
568}
569
570/// Index build mode.
571#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
572#[serde(rename_all = "snake_case")]
573pub enum IndexBuildMode {
574    /// Build index synchronously.
575    #[default]
576    Sync,
577    /// Build index asynchronously.
578    Async,
579}
580
581#[serde_as]
582#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
583#[serde(default)]
584pub struct IndexConfig {
585    /// Auxiliary directory path for the index in filesystem, used to
586    /// store intermediate files for creating the index and staging files
587    /// for searching the index, defaults to `{data_home}/index_intermediate`.
588    ///
589    /// This path contains two subdirectories:
590    /// - `__intm`: for storing intermediate files used during creating index.
591    /// - `staging`: for storing staging files used during searching index.
592    ///
593    /// The default name for this directory is `index_intermediate` for backward compatibility.
594    pub aux_path: String,
595
596    /// The max capacity of the staging directory.
597    pub staging_size: ReadableSize,
598    /// The TTL of the staging directory.
599    /// Defaults to 7 days.
600    /// Setting it to "0s" to disable TTL.
601    #[serde(with = "humantime_serde")]
602    pub staging_ttl: Option<Duration>,
603
604    /// Index Build Mode
605    pub build_mode: IndexBuildMode,
606
607    /// Write buffer size for creating the index.
608    pub write_buffer_size: ReadableSize,
609
610    /// Cache size for metadata of puffin files. Setting it to 0 to disable the cache.
611    pub metadata_cache_size: ReadableSize,
612    /// Cache size for inverted index content. Setting it to 0 to disable the cache.
613    pub content_cache_size: ReadableSize,
614    /// Page size for inverted index content.
615    pub content_cache_page_size: ReadableSize,
616    /// Cache size for index result. Setting it to 0 to disable the cache.
617    pub result_cache_size: ReadableSize,
618}
619
620impl Default for IndexConfig {
621    fn default() -> Self {
622        Self {
623            aux_path: String::new(),
624            staging_size: ReadableSize::gb(2),
625            staging_ttl: Some(Duration::from_secs(7 * 24 * 60 * 60)),
626            build_mode: IndexBuildMode::default(),
627            write_buffer_size: ReadableSize::mb(8),
628            metadata_cache_size: ReadableSize::mb(64),
629            content_cache_size: ReadableSize::mb(128),
630            content_cache_page_size: ReadableSize::kb(64),
631            result_cache_size: ReadableSize::mb(128),
632        }
633    }
634}
635
636impl IndexConfig {
637    pub fn sanitize(
638        &mut self,
639        data_home: &str,
640        inverted_index: &InvertedIndexConfig,
641    ) -> Result<()> {
642        #[allow(deprecated)]
643        if self.aux_path.is_empty() && !inverted_index.intermediate_path.is_empty() {
644            self.aux_path.clone_from(&inverted_index.intermediate_path);
645            warn!(
646                "`inverted_index.intermediate_path` is deprecated, use
647                 `index.aux_path` instead. Set `index.aux_path` to {}",
648                &inverted_index.intermediate_path
649            )
650        }
651        if self.aux_path.is_empty() {
652            let path = Path::new(data_home).join("index_intermediate");
653            self.aux_path = path.as_os_str().to_string_lossy().to_string();
654        }
655
656        if self.write_buffer_size < MULTIPART_UPLOAD_MINIMUM_SIZE {
657            self.write_buffer_size = MULTIPART_UPLOAD_MINIMUM_SIZE;
658            warn!(
659                "Sanitize index write buffer size to {}",
660                self.write_buffer_size
661            );
662        }
663
664        if self.staging_ttl.map(|ttl| ttl.is_zero()).unwrap_or(false) {
665            self.staging_ttl = None;
666        }
667
668        Ok(())
669    }
670
671    pub fn adjust_buffer_and_cache_size(&mut self, sys_memory: ReadableSize) {
672        let cache_size = cmp::min(sys_memory / MEM_CACHE_SIZE_FACTOR, ReadableSize::mb(128));
673        self.result_cache_size = cmp::min(self.result_cache_size, cache_size);
674        self.content_cache_size = cmp::min(self.content_cache_size, cache_size);
675
676        let metadata_cache_size = cmp::min(
677            sys_memory / INDEX_METADATA_CACHE_SIZE_FACTOR,
678            ReadableSize::mb(64),
679        );
680        self.metadata_cache_size = cmp::min(self.metadata_cache_size, metadata_cache_size);
681    }
682}
683
684/// Operational mode for certain actions.
685#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
686#[serde(rename_all = "snake_case")]
687pub enum Mode {
688    /// The action is performed automatically based on internal criteria.
689    #[default]
690    Auto,
691    /// The action is explicitly disabled.
692    Disable,
693}
694
695impl Mode {
696    /// Whether the action is disabled.
697    pub fn disabled(&self) -> bool {
698        matches!(self, Mode::Disable)
699    }
700
701    /// Whether the action is automatic.
702    pub fn auto(&self) -> bool {
703        matches!(self, Mode::Auto)
704    }
705}
706
707/// Memory threshold for performing certain actions.
708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
709#[serde(rename_all = "snake_case")]
710pub enum MemoryThreshold {
711    /// Automatically determine the threshold based on internal criteria.
712    #[default]
713    Auto,
714    /// Unlimited memory.
715    Unlimited,
716    /// Fixed memory threshold.
717    #[serde(untagged)]
718    Size(ReadableSize),
719}
720
721/// Configuration options for the inverted index.
722#[serde_as]
723#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
724#[serde(default)]
725pub struct InvertedIndexConfig {
726    /// Whether to create the index on flush: automatically or never.
727    pub create_on_flush: Mode,
728    /// Whether to create the index on compaction: automatically or never.
729    pub create_on_compaction: Mode,
730    /// Whether to apply the index on query: automatically or never.
731    pub apply_on_query: Mode,
732
733    /// Memory threshold for performing an external sort during index creation.
734    pub mem_threshold_on_create: MemoryThreshold,
735
736    #[deprecated = "use [IndexConfig::aux_path] instead"]
737    #[serde(skip_serializing)]
738    pub intermediate_path: String,
739
740    #[deprecated = "use [IndexConfig::write_buffer_size] instead"]
741    #[serde(skip_serializing)]
742    pub write_buffer_size: ReadableSize,
743}
744
745impl Default for InvertedIndexConfig {
746    #[allow(deprecated)]
747    fn default() -> Self {
748        Self {
749            create_on_flush: Mode::Auto,
750            create_on_compaction: Mode::Auto,
751            apply_on_query: Mode::Auto,
752            mem_threshold_on_create: MemoryThreshold::Auto,
753            write_buffer_size: ReadableSize::mb(8),
754            intermediate_path: String::new(),
755        }
756    }
757}
758
759impl InvertedIndexConfig {
760    pub fn mem_threshold_on_create(&self) -> Option<usize> {
761        match self.mem_threshold_on_create {
762            MemoryThreshold::Auto => {
763                if let Some(sys_memory) = get_total_memory_readable() {
764                    Some((sys_memory / INDEX_CREATE_MEM_THRESHOLD_FACTOR).as_bytes() as usize)
765                } else {
766                    Some(ReadableSize::mb(64).as_bytes() as usize)
767                }
768            }
769            MemoryThreshold::Unlimited => None,
770            MemoryThreshold::Size(size) => Some(size.as_bytes() as usize),
771        }
772    }
773}
774
775/// Configuration options for the full-text index.
776#[serde_as]
777#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
778#[serde(default)]
779pub struct FulltextIndexConfig {
780    /// Whether to create the index on flush: automatically or never.
781    pub create_on_flush: Mode,
782    /// Whether to create the index on compaction: automatically or never.
783    pub create_on_compaction: Mode,
784    /// Whether to apply the index on query: automatically or never.
785    pub apply_on_query: Mode,
786    /// Memory threshold for creating the index.
787    pub mem_threshold_on_create: MemoryThreshold,
788    /// Whether to compress the index data.
789    pub compress: bool,
790}
791
792impl Default for FulltextIndexConfig {
793    fn default() -> Self {
794        Self {
795            create_on_flush: Mode::Auto,
796            create_on_compaction: Mode::Auto,
797            apply_on_query: Mode::Auto,
798            mem_threshold_on_create: MemoryThreshold::Auto,
799            compress: true,
800        }
801    }
802}
803
804impl FulltextIndexConfig {
805    pub fn mem_threshold_on_create(&self) -> usize {
806        match self.mem_threshold_on_create {
807            MemoryThreshold::Auto => {
808                if let Some(sys_memory) = get_total_memory_readable() {
809                    (sys_memory / INDEX_CREATE_MEM_THRESHOLD_FACTOR).as_bytes() as _
810                } else {
811                    ReadableSize::mb(64).as_bytes() as _
812                }
813            }
814            MemoryThreshold::Unlimited => usize::MAX,
815            MemoryThreshold::Size(size) => size.as_bytes() as _,
816        }
817    }
818}
819
820/// Configuration options for the bloom filter.
821#[serde_as]
822#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
823#[serde(default)]
824pub struct BloomFilterConfig {
825    /// Whether to create the index on flush: automatically or never.
826    pub create_on_flush: Mode,
827    /// Whether to create the index on compaction: automatically or never.
828    pub create_on_compaction: Mode,
829    /// Whether to apply the index on query: automatically or never.
830    pub apply_on_query: Mode,
831    /// Memory threshold for creating the index.
832    pub mem_threshold_on_create: MemoryThreshold,
833}
834
835impl Default for BloomFilterConfig {
836    fn default() -> Self {
837        Self {
838            create_on_flush: Mode::Auto,
839            create_on_compaction: Mode::Auto,
840            apply_on_query: Mode::Auto,
841            mem_threshold_on_create: MemoryThreshold::Auto,
842        }
843    }
844}
845
846impl BloomFilterConfig {
847    pub fn mem_threshold_on_create(&self) -> Option<usize> {
848        match self.mem_threshold_on_create {
849            MemoryThreshold::Auto => {
850                if let Some(sys_memory) = get_total_memory_readable() {
851                    Some((sys_memory / INDEX_CREATE_MEM_THRESHOLD_FACTOR).as_bytes() as usize)
852                } else {
853                    Some(ReadableSize::mb(64).as_bytes() as usize)
854                }
855            }
856            MemoryThreshold::Unlimited => None,
857            MemoryThreshold::Size(size) => Some(size.as_bytes() as usize),
858        }
859    }
860}
861
862/// Divide cpu num by a non-zero `divisor` and returns at least 1.
863fn divide_num_cpus(divisor: usize) -> usize {
864    debug_assert!(divisor > 0);
865    let cores = get_total_cpu_cores();
866    debug_assert!(cores > 0);
867
868    cores.div_ceil(divisor)
869}