Skip to main content

common_config/
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
15use config::{Environment, File, FileFormat};
16use serde::Serialize;
17use serde::de::DeserializeOwned;
18use snafu::ResultExt;
19
20use crate::error::{
21    LoadLayeredConfigSnafu, Result, SerdeJsonSnafu, TaggedSeqNotTopLevelSnafu, TomlFormatSnafu,
22};
23use crate::tagged_seq::TaggedSeqEnvSource;
24
25/// Separator for environment variables. For example, `DATANODE__STORAGE__MANIFEST__CHECKPOINT_MARGIN`.
26pub const ENV_VAR_SEP: &str = "__";
27
28/// Separator for list values in environment variables. For example, `localhost:3001,localhost:3002,localhost:3003`.
29pub const ENV_LIST_SEP: &str = ",";
30
31/// Configuration trait defines the common interface for configuration that can be loaded from multiple sources and serialized to TOML.
32pub trait Configurable: Serialize + DeserializeOwned + Default + Sized {
33    /// Load the configuration from multiple sources and merge them.
34    /// The precedence order is: config file > environment variables > default values.
35    /// `env_prefix` is the prefix of environment variables, e.g. "FRONTEND__xxx".
36    /// The function will use dunder(double underscore) `__` as the separator for environment variables, for example:
37    /// `DATANODE__STORAGE__MANIFEST__CHECKPOINT_MARGIN` will be mapped to `DatanodeOptions.storage.manifest.checkpoint_margin` field in the configuration.
38    /// `list_keys` is the list of keys that should be parsed as a list, for example, you can pass `Some(&["meta_client_options.metasrv_addrs"]` to parse `GREPTIMEDB_METASRV__META_CLIENT_OPTIONS__METASRV_ADDRS` as a list.
39    /// The function will use comma `,` as the separator for list values, for example: `127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003`.
40    fn load_layered_options(config_file: Option<&str>, env_prefix: &str) -> Result<Self> {
41        let default_opts = Self::default();
42
43        let env_source = {
44            let mut env = Environment::default();
45
46            if !env_prefix.is_empty() {
47                env = env.prefix(env_prefix);
48            }
49
50            if let Some(list_keys) = Self::env_list_keys() {
51                env = env.list_separator(ENV_LIST_SEP);
52                for key in list_keys {
53                    env = env.with_list_parse_key(key);
54                }
55            }
56
57            env.try_parsing(true)
58                .separator(ENV_VAR_SEP)
59                .ignore_empty(false)
60        };
61
62        // Workaround: Replacement for `Config::try_from(&default_opts)` due to
63        // `ConfigSerializer` cannot handle the case of an empty struct contained
64        // within an iterative structure.
65        // See: https://github.com/mehcode/config-rs/issues/461
66        let json_str = serde_json::to_string(&default_opts).context(SerdeJsonSnafu)?;
67        let default_config = File::from_str(&json_str, FileFormat::Json);
68
69        // Add default values and environment variables as the sources of the configuration.
70        let mut layered_config = config::Config::builder().add_source(default_config);
71
72        // A sequence of tagged tables cannot be addressed by plain dotted env keys: the path
73        // walk of `config` drops the default sequence. Such keys are merged onto the default
74        // entries by tag name instead.
75        layered_config = match Self::env_tagged_seq_keys() {
76            Some(keys) if !keys.is_empty() => {
77                // The default entries are looked up by literal key, so a dotted key would find
78                // none of them. Reject it instead of silently dropping the defaults.
79                if let Some(key) = keys.iter().find(|key| key.contains('.')) {
80                    return TaggedSeqNotTopLevelSnafu {
81                        key: (*key).to_string(),
82                    }
83                    .fail();
84                }
85
86                let defaults = serde_json::to_value(&default_opts).context(SerdeJsonSnafu)?;
87                layered_config.add_source(TaggedSeqEnvSource::new(env_source, keys, &defaults))
88            }
89            _ => layered_config.add_source(env_source),
90        };
91
92        // Add config file as the source of the configuration if it is specified.
93        if let Some(config_file) = config_file {
94            layered_config = layered_config.add_source(File::new(config_file, FileFormat::Toml));
95        }
96
97        let mut opts: Self = layered_config
98            .build()
99            .and_then(|x| x.try_deserialize())
100            .context(LoadLayeredConfigSnafu)?;
101
102        opts.validate_sanitize()?;
103
104        Ok(opts)
105    }
106
107    /// Validate(and possibly sanitize) the configuration.
108    fn validate_sanitize(&mut self) -> Result<()> {
109        Ok(())
110    }
111
112    /// List of toml keys that should be parsed as a list.
113    fn env_list_keys() -> Option<&'static [&'static str]> {
114        None
115    }
116
117    /// Keys whose value is a sequence of single-key tagged tables (e.g. `region_engine`,
118    /// serialized from `Vec<Enum>` with externally tagged variants). Environment variables
119    /// address them by tag name (`KEY__<TAG>__<FIELD>`) instead of numeric index, and the
120    /// overrides are merged onto the corresponding default entries.
121    ///
122    /// Keys must name a **top-level** field of the options struct: the default entries to
123    /// merge onto are looked up by literal key in the serialized defaults, so a dotted key is
124    /// rejected instead of silently leaving the defaults out of the merged sequence.
125    ///
126    /// A configuration file that configures such a key as a whole replaces the merged value,
127    /// so the environment variables for it are ineffective then and the file has to spell out
128    /// every entry it wants.
129    fn env_tagged_seq_keys() -> Option<&'static [&'static str]> {
130        None
131    }
132
133    /// Serialize the configuration to a TOML string.
134    fn to_toml(&self) -> Result<String> {
135        toml::to_string(&self).context(TomlFormatSnafu)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use std::io::Write;
142    use std::time::Duration;
143
144    use common_base::readable_size::ReadableSize;
145    use common_telemetry::logging::LoggingOptions;
146    use common_test_util::temp_dir::create_named_temp_file;
147    use common_wal::config::DatanodeWalConfig;
148    use datanode::config::StorageConfig;
149    use meta_client::MetaClientOptions;
150    use serde::{Deserialize, Serialize};
151
152    use super::*;
153
154    #[derive(Debug, Serialize, Deserialize, Default)]
155    struct TestDatanodeConfig {
156        node_id: Option<u64>,
157        logging: LoggingOptions,
158        meta_client: Option<MetaClientOptions>,
159        wal: DatanodeWalConfig,
160        storage: StorageConfig,
161    }
162
163    impl Configurable for TestDatanodeConfig {
164        fn env_list_keys() -> Option<&'static [&'static str]> {
165            Some(&["meta_client.metasrv_addrs"])
166        }
167    }
168
169    #[test]
170    fn test_load_layered_options() {
171        let mut file = create_named_temp_file();
172        let toml_str = r#"
173            enable_memory_catalog = false
174            rpc_addr = "127.0.0.1:3001"
175            rpc_hostname = "127.0.0.1"
176            rpc_runtime_size = 8
177            mysql_addr = "127.0.0.1:4406"
178            mysql_runtime_size = 2
179
180            [meta_client]
181            timeout = "3s"
182            connect_timeout = "5s"
183            tcp_nodelay = true
184
185            [wal]
186            provider = "raft_engine"
187            dir = "./greptimedb_data/wal"
188            file_size = "1GB"
189            purge_threshold = "50GB"
190            purge_interval = "10m"
191            read_batch_size = 128
192            sync_write = false
193
194            [logging]
195            level = "debug"
196            dir = "./greptimedb_data/test/logs"
197        "#;
198        write!(file, "{}", toml_str).unwrap();
199
200        let env_prefix = "DATANODE_UT";
201        temp_env::with_vars(
202            // The following environment variables will be used to override the values in the config file.
203            [
204                (
205                    // storage.type = S3
206                    [
207                        env_prefix.to_string(),
208                        "storage".to_uppercase(),
209                        "type".to_uppercase(),
210                    ]
211                    .join(ENV_VAR_SEP),
212                    Some("S3"),
213                ),
214                (
215                    // storage.bucket = mybucket
216                    [
217                        env_prefix.to_string(),
218                        "storage".to_uppercase(),
219                        "bucket".to_uppercase(),
220                    ]
221                    .join(ENV_VAR_SEP),
222                    Some("mybucket"),
223                ),
224                (
225                    // wal.dir = /other/wal/dir
226                    [
227                        env_prefix.to_string(),
228                        "wal".to_uppercase(),
229                        "dir".to_uppercase(),
230                    ]
231                    .join(ENV_VAR_SEP),
232                    Some("/other/wal/dir"),
233                ),
234                (
235                    // meta_client.metasrv_addrs = 127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003
236                    [
237                        env_prefix.to_string(),
238                        "meta_client".to_uppercase(),
239                        "metasrv_addrs".to_uppercase(),
240                    ]
241                    .join(ENV_VAR_SEP),
242                    Some("127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003"),
243                ),
244            ],
245            || {
246                let opts = TestDatanodeConfig::load_layered_options(
247                    Some(file.path().to_str().unwrap()),
248                    env_prefix,
249                )
250                .unwrap();
251
252                // Check the configs from environment variables.
253                match &opts.storage.store {
254                    object_store::config::ObjectStoreConfig::S3(s3_config) => {
255                        assert_eq!(s3_config.connection.bucket, "mybucket".to_string());
256                    }
257                    _ => panic!("unexpected store type"),
258                }
259                assert_eq!(
260                    opts.meta_client.unwrap().metasrv_addrs,
261                    vec![
262                        "127.0.0.1:3001".to_string(),
263                        "127.0.0.1:3002".to_string(),
264                        "127.0.0.1:3003".to_string()
265                    ]
266                );
267
268                // Should be the values from config file, not environment variables.
269                let DatanodeWalConfig::RaftEngine(raft_engine_config) = opts.wal else {
270                    unreachable!()
271                };
272                assert_eq!(raft_engine_config.dir.unwrap(), "./greptimedb_data/wal");
273
274                // Should be default values.
275                assert_eq!(opts.node_id, None);
276            },
277        );
278    }
279
280    #[derive(Debug, Serialize, Deserialize, Default)]
281    struct SimpleConfig {
282        name: Option<String>,
283        prefix: Option<String>,
284    }
285
286    impl Configurable for SimpleConfig {}
287
288    #[test]
289    fn test_empty_env_var_is_not_ignored() {
290        let env_prefix = "SIMPLE_CFG_UT";
291        temp_env::with_vars(
292            [(
293                [env_prefix.to_string(), "PREFIX".to_string()].join(ENV_VAR_SEP),
294                Some(""),
295            )],
296            || {
297                let opts = SimpleConfig::load_layered_options(None, env_prefix).unwrap();
298                // With ignore_empty(false), an empty env var should yield Some("")
299                // rather than None (which was the previous behavior with ignore_empty(true)).
300                assert_eq!(opts.prefix, Some("".to_string()));
301                // Unset env var should remain None.
302                assert_eq!(opts.name, None);
303            },
304        );
305    }
306
307    /// A sequence of single-key tagged tables, the shape of `Vec<Enum>` with externally
308    /// tagged variants.
309    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
310    #[serde(default)]
311    struct TestIndexConfig {
312        aux_path: String,
313        cache_percent: u8,
314    }
315
316    impl Default for TestIndexConfig {
317        fn default() -> Self {
318            Self {
319                aux_path: "/default/aux".to_string(),
320                cache_percent: 30,
321            }
322        }
323    }
324
325    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326    #[serde(default)]
327    struct TestMitoConfig {
328        global_write_buffer_size: ReadableSize,
329        global_write_buffer_reject_size: ReadableSize,
330        index: TestIndexConfig,
331    }
332
333    impl Default for TestMitoConfig {
334        fn default() -> Self {
335            Self {
336                global_write_buffer_size: ReadableSize::mb(64),
337                global_write_buffer_reject_size: ReadableSize::gb(2),
338                index: TestIndexConfig::default(),
339            }
340        }
341    }
342
343    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344    #[serde(default)]
345    struct TestFileConfig {
346        data_dir: String,
347    }
348
349    impl Default for TestFileConfig {
350        fn default() -> Self {
351            Self {
352                data_dir: "/default/data".to_string(),
353            }
354        }
355    }
356
357    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
358    #[serde(default)]
359    struct TestMetricConfig {
360        #[serde(with = "humantime_serde")]
361        flush_metadata_region_interval: Duration,
362    }
363
364    impl Default for TestMetricConfig {
365        fn default() -> Self {
366            Self {
367                flush_metadata_region_interval: Duration::from_secs(30),
368            }
369        }
370    }
371
372    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
373    #[serde(rename_all = "snake_case")]
374    enum TestRegionEngine {
375        Mito(TestMitoConfig),
376        File(TestFileConfig),
377        Metric(TestMetricConfig),
378    }
379
380    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
381    struct TestTaggedSeqConfig {
382        region_engine: Vec<TestRegionEngine>,
383    }
384
385    impl Default for TestTaggedSeqConfig {
386        fn default() -> Self {
387            Self {
388                region_engine: vec![
389                    TestRegionEngine::Mito(TestMitoConfig::default()),
390                    TestRegionEngine::File(TestFileConfig::default()),
391                ],
392            }
393        }
394    }
395
396    impl Configurable for TestTaggedSeqConfig {
397        fn env_tagged_seq_keys() -> Option<&'static [&'static str]> {
398            Some(&["region_engine"])
399        }
400    }
401
402    /// A config whose tagged sequence key is not a top-level field.
403    #[derive(Debug, Serialize, Deserialize, Default)]
404    struct TestDottedKeyConfig {
405        region_engine: Vec<TestRegionEngine>,
406    }
407
408    impl Configurable for TestDottedKeyConfig {
409        fn env_tagged_seq_keys() -> Option<&'static [&'static str]> {
410            Some(&["region_engine.mito"])
411        }
412    }
413
414    fn test_mito(config: &TestTaggedSeqConfig) -> &TestMitoConfig {
415        config
416            .region_engine
417            .iter()
418            .find_map(|engine| match engine {
419                TestRegionEngine::Mito(config) => Some(config),
420                _ => None,
421            })
422            .expect("the mito entry")
423    }
424
425    fn test_metric(config: &TestTaggedSeqConfig) -> &TestMetricConfig {
426        config
427            .region_engine
428            .iter()
429            .find_map(|engine| match engine {
430                TestRegionEngine::Metric(config) => Some(config),
431                _ => None,
432            })
433            .expect("the metric entry")
434    }
435
436    /// Loads with the given prefix and returns the message of the error, including the
437    /// source of the layered loading error.
438    fn load_error_text(env_prefix: &str) -> String {
439        let err = TestTaggedSeqConfig::load_layered_options(None, env_prefix).unwrap_err();
440        let mut text = err.to_string();
441        let mut source = std::error::Error::source(&err);
442        while let Some(error) = source {
443            text.push_str(&format!(": {error}"));
444            source = std::error::Error::source(error);
445        }
446        text
447    }
448
449    #[test]
450    fn test_tagged_seq_env_overrides_a_field() {
451        let env_prefix = "TAGGED_SEQ_FIELD_UT";
452        let env_key = [
453            env_prefix,
454            "REGION_ENGINE",
455            "MITO",
456            "GLOBAL_WRITE_BUFFER_REJECT_SIZE",
457        ]
458        .join(ENV_VAR_SEP);
459
460        temp_env::with_var(env_key, Some("4GB"), || {
461            let opts = TestTaggedSeqConfig::load_layered_options(None, env_prefix).unwrap();
462            let defaults = TestTaggedSeqConfig::default();
463
464            // The entries of the sequence that the environment does not mention are intact.
465            assert_eq!(opts.region_engine.len(), defaults.region_engine.len());
466            assert_eq!(opts.region_engine[1], defaults.region_engine[1]);
467
468            // The addressed field wins, the fields next to it keep their default value.
469            let mito = test_mito(&opts);
470            assert_eq!(mito.global_write_buffer_reject_size, ReadableSize::gb(4));
471            assert_eq!(
472                mito.global_write_buffer_size,
473                TestMitoConfig::default().global_write_buffer_size
474            );
475            assert_eq!(mito.index, TestIndexConfig::default());
476        });
477    }
478
479    #[test]
480    fn test_tagged_seq_env_appends_an_entry() {
481        let env_prefix = "TAGGED_SEQ_APPEND_UT";
482        let env_key = [
483            env_prefix,
484            "REGION_ENGINE",
485            "METRIC",
486            "FLUSH_METADATA_REGION_INTERVAL",
487        ]
488        .join(ENV_VAR_SEP);
489
490        temp_env::with_var(env_key, Some("1m"), || {
491            let opts = TestTaggedSeqConfig::load_layered_options(None, env_prefix).unwrap();
492            let defaults = TestTaggedSeqConfig::default();
493
494            // The entry is not in the default sequence, so it is appended.
495            assert_eq!(opts.region_engine.len(), defaults.region_engine.len() + 1);
496            assert_eq!(opts.region_engine[0], defaults.region_engine[0]);
497            assert_eq!(opts.region_engine[1], defaults.region_engine[1]);
498            assert_eq!(
499                test_metric(&opts).flush_metadata_region_interval,
500                Duration::from_secs(60)
501            );
502        });
503    }
504
505    #[test]
506    fn test_tagged_seq_env_overrides_a_nested_field() {
507        let env_prefix = "TAGGED_SEQ_NESTED_UT";
508        let env_key = [env_prefix, "REGION_ENGINE", "MITO", "INDEX", "AUX_PATH"].join(ENV_VAR_SEP);
509
510        temp_env::with_var(env_key, Some("/tmp/aux"), || {
511            let opts = TestTaggedSeqConfig::load_layered_options(None, env_prefix).unwrap();
512
513            let mito = test_mito(&opts);
514            assert_eq!(mito.index.aux_path, "/tmp/aux");
515            assert_eq!(
516                mito.index.cache_percent,
517                TestIndexConfig::default().cache_percent
518            );
519            assert_eq!(
520                mito.global_write_buffer_size,
521                TestMitoConfig::default().global_write_buffer_size
522            );
523        });
524    }
525
526    #[test]
527    fn test_tagged_seq_env_rejects_whole_key() {
528        let env_prefix = "TAGGED_SEQ_NO_TAG_UT";
529        let env_key = [env_prefix, "REGION_ENGINE"].join(ENV_VAR_SEP);
530
531        temp_env::with_var(env_key, Some("mito"), || {
532            let text = load_error_text(env_prefix);
533            assert!(text.contains("region_engine"), "{text}");
534            assert!(text.contains("region_engine__<TAG>__<FIELD>"), "{text}");
535        });
536    }
537
538    #[test]
539    fn test_tagged_seq_env_rejects_numeric_index() {
540        let env_prefix = "TAGGED_SEQ_NUMERIC_UT";
541        let env_key = [
542            env_prefix,
543            "REGION_ENGINE",
544            "0",
545            "MITO",
546            "GLOBAL_WRITE_BUFFER_SIZE",
547        ]
548        .join(ENV_VAR_SEP);
549
550        temp_env::with_var(env_key, Some("4GB"), || {
551            let text = load_error_text(env_prefix);
552            assert!(
553                text.contains("region_engine.0.mito.global_write_buffer_size"),
554                "{text}"
555            );
556            assert!(text.contains("addressed by tag name"), "{text}");
557        });
558    }
559
560    #[test]
561    fn test_tagged_seq_env_rejects_scalar_and_field_under_same_key() {
562        let env_prefix = "TAGGED_SEQ_NOT_A_TABLE_UT";
563        let whole_key = [env_prefix, "REGION_ENGINE", "MITO", "INDEX"].join(ENV_VAR_SEP);
564        let below_key =
565            [env_prefix, "REGION_ENGINE", "MITO", "INDEX", "AUX_PATH"].join(ENV_VAR_SEP);
566
567        temp_env::with_vars(
568            [
569                (whole_key, Some("not-a-table")),
570                (below_key, Some("/tmp/aux")),
571            ],
572            || {
573                // One variable sets `index` as a whole, another sets a field below it. The
574                // conflict is reported as such instead of failing later inside serde with a
575                // type error that does not name the variables involved.
576                let text = load_error_text(env_prefix);
577                assert!(
578                    text.contains("another environment variable sets 'region_engine.mito.index'"),
579                    "{text}"
580                );
581                assert!(text.contains("region_engine.mito.index.aux_path"), "{text}");
582            },
583        );
584    }
585
586    #[test]
587    fn test_tagged_seq_env_rejects_signed_and_float_indices() {
588        for index in ["-1", "+1", "1e3"] {
589            let env_prefix = "TAGGED_SEQ_INDEX_SHAPE_UT";
590            let env_key = [
591                env_prefix,
592                "REGION_ENGINE",
593                index,
594                "MITO",
595                "GLOBAL_WRITE_BUFFER_SIZE",
596            ]
597            .join(ENV_VAR_SEP);
598
599            temp_env::with_var(env_key, Some("4GB"), || {
600                let text = load_error_text(env_prefix);
601                assert!(text.contains("addressed by tag name"), "{index}: {text}");
602            });
603        }
604    }
605
606    #[test]
607    fn test_tagged_seq_env_rejects_tag_without_field() {
608        let env_prefix = "TAGGED_SEQ_NO_FIELD_UT";
609        let env_key = [env_prefix, "REGION_ENGINE", "MITO"].join(ENV_VAR_SEP);
610
611        temp_env::with_var(env_key, Some("mito"), || {
612            let text = load_error_text(env_prefix);
613            assert!(text.contains("region_engine.mito"), "{text}");
614            assert!(text.contains("region_engine__<TAG>__<FIELD>"), "{text}");
615        });
616    }
617
618    #[test]
619    fn test_tagged_seq_env_rejects_field_below_scalar_default() {
620        // The `data_dir` field of the file engine defaults to a string, so nothing can be set
621        // below it.
622        let env_prefix = "TAGGED_SEQ_SCALAR_DEFAULT_UT";
623        let env_key = [env_prefix, "REGION_ENGINE", "FILE", "DATA_DIR", "SUB"].join(ENV_VAR_SEP);
624
625        temp_env::with_var(env_key, Some("/tmp/sub"), || {
626            let text = load_error_text(env_prefix);
627            assert!(text.contains("region_engine.file.data_dir"), "{text}");
628            assert!(text.contains("default value is not a table"), "{text}");
629        });
630    }
631
632    #[test]
633    fn test_tagged_seq_env_type_error_names_the_environment() {
634        let env_prefix = "TAGGED_SEQ_TYPE_ERROR_UT";
635        let env_key = [
636            env_prefix,
637            "REGION_ENGINE",
638            "MITO",
639            "INDEX",
640            "CACHE_PERCENT",
641        ]
642        .join(ENV_VAR_SEP);
643
644        temp_env::with_var(env_key, Some("not-a-number"), || {
645            // The override replaces the default value as the environment collected it, so the
646            // type error still says where that value comes from.
647            let text = load_error_text(env_prefix);
648            assert!(text.contains("in the environment"), "{text}");
649        });
650    }
651
652    #[test]
653    fn test_tagged_seq_key_must_be_top_level() {
654        let err = TestDottedKeyConfig::load_layered_options(None, "TAGGED_SEQ_DOTTED_KEY_UT")
655            .unwrap_err();
656        let text = err.to_string();
657        assert!(text.contains("region_engine.mito"), "{text}");
658        assert!(text.contains("top-level"), "{text}");
659    }
660}