1use 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
25pub const ENV_VAR_SEP: &str = "__";
27
28pub const ENV_LIST_SEP: &str = ",";
30
31pub trait Configurable: Serialize + DeserializeOwned + Default + Sized {
33 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 let json_str = serde_json::to_string(&default_opts).context(SerdeJsonSnafu)?;
67 let default_config = File::from_str(&json_str, FileFormat::Json);
68
69 let mut layered_config = config::Config::builder().add_source(default_config);
71
72 layered_config = match Self::env_tagged_seq_keys() {
76 Some(keys) if !keys.is_empty() => {
77 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 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 fn validate_sanitize(&mut self) -> Result<()> {
109 Ok(())
110 }
111
112 fn env_list_keys() -> Option<&'static [&'static str]> {
114 None
115 }
116
117 fn env_tagged_seq_keys() -> Option<&'static [&'static str]> {
130 None
131 }
132
133 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 [
204 (
205 [
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 [
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 [
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 [
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 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 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 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 assert_eq!(opts.prefix, Some("".to_string()));
301 assert_eq!(opts.name, None);
303 },
304 );
305 }
306
307 #[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 #[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 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 assert_eq!(opts.region_engine.len(), defaults.region_engine.len());
466 assert_eq!(opts.region_engine[1], defaults.region_engine[1]);
467
468 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 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 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 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 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}