Skip to main content

cmd/
frontend.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 std::fmt::Debug;
16use std::path::Path;
17use std::sync::Arc;
18use std::time::Duration;
19
20use async_trait::async_trait;
21use cache::{build_fundamental_cache_registry, with_default_composite_cache_registry};
22use catalog::information_extension::DistributedInformationExtension;
23use catalog::information_schema::InformationExtensionRef;
24use catalog::kvbackend::{
25    CachedKvBackendBuilder, CatalogManagerConfiguratorRef, KvBackendCatalogManagerBuilder,
26    new_read_only_meta_kv_backend,
27};
28use catalog::process_manager::ProcessManager;
29use clap::Parser;
30use client::client_manager::NodeClients;
31use common_base::Plugins;
32use common_config::{Configurable, DEFAULT_DATA_HOME};
33use common_error::ext::BoxedError;
34use common_grpc::channel_manager::ChannelConfig;
35use common_meta::cache::{CacheRegistryBuilder, LayeredCacheRegistryBuilder};
36use common_meta::heartbeat::handler::HandlerGroupExecutor;
37use common_meta::heartbeat::handler::invalidate_table_cache::InvalidateCacheHandler;
38use common_meta::heartbeat::handler::parse_mailbox_message::ParseMailboxMessageHandler;
39use common_meta::heartbeat::handler::suspend::SuspendHandler;
40use common_query::prelude::set_default_prefix;
41use common_stat::ResourceStatImpl;
42use common_telemetry::info;
43use common_telemetry::logging::{DEFAULT_LOGGING_DIR, TracingOptions};
44use common_time::timezone::set_default_timezone;
45use common_version::{short_version, verbose_version};
46use frontend::frontend::Frontend;
47use frontend::heartbeat::HeartbeatTask;
48use frontend::instance::builder::FrontendBuilder;
49use frontend::server::Services;
50use meta_client::{MetaClientOptions, MetaClientRef, MetaClientType};
51use plugins::PluginOptions;
52use plugins::frontend::context::{
53    CatalogManagerConfigureContext, DistributedCatalogManagerConfigureContext,
54};
55use plugins::frontend::setup_frontend_dynamic_plugins;
56use plugins::options::PluginOptionsDeserializerImpl;
57use servers::addrs;
58use servers::grpc::GrpcOptions;
59use servers::tls::{TlsMode, TlsOption, merge_tls_option};
60use snafu::{OptionExt, ResultExt};
61use tracing_appender::non_blocking::WorkerGuard;
62
63use crate::error::{self, OtherSnafu, Result};
64use crate::options::{GlobalOptions, GreptimeOptions};
65use crate::{App, create_resource_limit_metrics, log_versions, maybe_activate_heap_profile};
66
67type FrontendOptions = GreptimeOptions<frontend::frontend::FrontendOptions>;
68
69pub struct Instance {
70    frontend: Frontend,
71    // Keep the logging guard to prevent the worker from being dropped.
72    _guard: Vec<WorkerGuard>,
73}
74
75pub const APP_NAME: &str = "greptime-frontend";
76
77impl Instance {
78    pub fn new(frontend: Frontend, _guard: Vec<WorkerGuard>) -> Self {
79        Self { frontend, _guard }
80    }
81
82    pub fn inner(&self) -> &Frontend {
83        &self.frontend
84    }
85
86    pub fn mut_inner(&mut self) -> &mut Frontend {
87        &mut self.frontend
88    }
89}
90
91#[async_trait]
92impl App for Instance {
93    fn name(&self) -> &str {
94        APP_NAME
95    }
96
97    async fn start(&mut self) -> Result<()> {
98        let plugins = self.frontend.instance.plugins().clone();
99        plugins::start_frontend_plugins(plugins)
100            .await
101            .context(error::StartFrontendSnafu)?;
102
103        self.frontend
104            .start()
105            .await
106            .context(error::StartFrontendSnafu)
107    }
108
109    async fn stop(&mut self) -> Result<()> {
110        self.frontend
111            .shutdown()
112            .await
113            .context(error::ShutdownFrontendSnafu)
114    }
115}
116
117#[derive(Parser)]
118pub struct Command {
119    #[clap(subcommand)]
120    pub subcmd: SubCommand,
121}
122
123impl Command {
124    pub async fn build(&self, opts: FrontendOptions) -> Result<Instance> {
125        self.subcmd.build(opts).await
126    }
127
128    pub fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
129        self.subcmd.load_options(global_options)
130    }
131}
132
133#[derive(Parser)]
134pub enum SubCommand {
135    Start(StartCommand),
136}
137
138impl SubCommand {
139    async fn build(&self, opts: FrontendOptions) -> Result<Instance> {
140        match self {
141            SubCommand::Start(cmd) => cmd.build(opts).await,
142        }
143    }
144
145    fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
146        match self {
147            SubCommand::Start(cmd) => cmd.load_options(global_options),
148        }
149    }
150}
151
152#[derive(Debug, Default, Parser)]
153pub struct StartCommand {
154    /// The address to bind the gRPC server.
155    #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")]
156    grpc_bind_addr: Option<String>,
157    /// The address advertised to the metasrv, and used for connections from outside the host.
158    /// If left empty or unset, the server will automatically use the IP address of the first network interface
159    /// on the host, with the same port number as the one specified in `grpc_bind_addr`.
160    #[clap(
161        long = "grpc-server-addr",
162        alias = "rpc-server-addr",
163        alias = "rpc-hostname"
164    )]
165    grpc_server_addr: Option<String>,
166    /// The address to bind the internal gRPC server.
167    #[clap(
168        long = "internal-grpc-bind-addr",
169        alias = "internal-rpc-bind-addr",
170        alias = "internal-rpc-addr"
171    )]
172    internal_grpc_bind_addr: Option<String>,
173    /// The address advertised to the metasrv, and used for connections from outside the host.
174    /// If left empty or unset, the server will automatically use the IP address of the first network interface
175    /// on the host, with the same port number as the one specified in `internal_grpc_bind_addr`.
176    #[clap(
177        long = "internal-grpc-server-addr",
178        alias = "internal-rpc-server-addr",
179        alias = "internal-rpc-hostname"
180    )]
181    internal_grpc_server_addr: Option<String>,
182    #[clap(long)]
183    http_addr: Option<String>,
184    #[clap(long)]
185    http_timeout: Option<u64>,
186    #[clap(long)]
187    mysql_addr: Option<String>,
188    #[clap(long)]
189    postgres_addr: Option<String>,
190    #[clap(short, long)]
191    pub config_file: Option<String>,
192    #[clap(short, long)]
193    influxdb_enable: Option<bool>,
194    #[clap(long, value_delimiter = ',', num_args = 1..)]
195    metasrv_addrs: Option<Vec<String>>,
196    #[clap(long)]
197    tls_mode: Option<TlsMode>,
198    #[clap(long)]
199    tls_cert_path: Option<String>,
200    #[clap(long)]
201    tls_key_path: Option<String>,
202    #[clap(long)]
203    tls_watch: bool,
204    #[clap(long)]
205    user_provider: Option<String>,
206    #[clap(long)]
207    disable_dashboard: Option<bool>,
208    #[clap(long, default_value = "GREPTIMEDB_FRONTEND")]
209    pub env_prefix: String,
210}
211
212impl StartCommand {
213    fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
214        let mut opts = FrontendOptions::load_layered_options(
215            self.config_file.as_deref(),
216            self.env_prefix.as_ref(),
217        )
218        .context(error::LoadLayeredConfigSnafu)?;
219
220        self.merge_with_cli_options(global_options, &mut opts)?;
221
222        Ok(opts)
223    }
224
225    // The precedence order is: cli > config file > environment variables > default values.
226    fn merge_with_cli_options(
227        &self,
228        global_options: &GlobalOptions,
229        opts: &mut FrontendOptions,
230    ) -> Result<()> {
231        let opts = &mut opts.component;
232
233        if let Some(dir) = &global_options.log_dir {
234            opts.logging.dir.clone_from(dir);
235        }
236
237        // If the logging dir is not set, use the default logs dir in the data home.
238        if opts.logging.dir.is_empty() {
239            opts.logging.dir = Path::new(DEFAULT_DATA_HOME)
240                .join(DEFAULT_LOGGING_DIR)
241                .to_string_lossy()
242                .to_string();
243        }
244
245        if global_options.log_level.is_some() {
246            opts.logging.level.clone_from(&global_options.log_level);
247        }
248
249        opts.tracing = TracingOptions {
250            #[cfg(feature = "tokio-console")]
251            tokio_console_addr: global_options.tokio_console_addr.clone(),
252        };
253
254        let tls_opts = TlsOption::new(
255            self.tls_mode,
256            self.tls_cert_path.clone(),
257            self.tls_key_path.clone(),
258            self.tls_watch,
259        );
260
261        if let Some(addr) = &self.http_addr {
262            opts.http.addr.clone_from(addr);
263        }
264
265        if let Some(http_timeout) = self.http_timeout {
266            opts.http.timeout = Duration::from_secs(http_timeout)
267        }
268
269        if let Some(disable_dashboard) = self.disable_dashboard {
270            opts.http.disable_dashboard = disable_dashboard;
271        }
272
273        if let Some(addr) = &self.grpc_bind_addr {
274            opts.grpc.bind_addr.clone_from(addr);
275            opts.grpc.tls = merge_tls_option(&opts.grpc.tls, tls_opts.clone());
276        }
277
278        if let Some(addr) = &self.grpc_server_addr {
279            opts.grpc.server_addr.clone_from(addr);
280        }
281
282        if let Some(addr) = &self.internal_grpc_bind_addr {
283            if let Some(internal_grpc) = &mut opts.internal_grpc {
284                internal_grpc.bind_addr = addr.clone();
285            } else {
286                let grpc_options = GrpcOptions {
287                    bind_addr: addr.clone(),
288                    ..Default::default()
289                };
290
291                opts.internal_grpc = Some(grpc_options);
292            }
293        }
294
295        if let Some(addr) = &self.internal_grpc_server_addr {
296            if let Some(internal_grpc) = &mut opts.internal_grpc {
297                internal_grpc.server_addr = addr.clone();
298            } else {
299                let grpc_options = GrpcOptions {
300                    server_addr: addr.clone(),
301                    ..Default::default()
302                };
303                opts.internal_grpc = Some(grpc_options);
304            }
305        }
306
307        if let Some(addr) = &self.mysql_addr {
308            opts.mysql.enable = true;
309            opts.mysql.addr.clone_from(addr);
310            opts.mysql.tls = merge_tls_option(&opts.mysql.tls, tls_opts.clone());
311        }
312
313        if let Some(addr) = &self.postgres_addr {
314            opts.postgres.enable = true;
315            opts.postgres.addr.clone_from(addr);
316            opts.postgres.tls = merge_tls_option(&opts.postgres.tls, tls_opts.clone());
317        }
318
319        if let Some(enable) = self.influxdb_enable {
320            opts.influxdb.enable = enable;
321        }
322
323        if let Some(metasrv_addrs) = &self.metasrv_addrs {
324            opts.meta_client
325                .get_or_insert_with(MetaClientOptions::default)
326                .metasrv_addrs
327                .clone_from(metasrv_addrs);
328        }
329
330        if let Some(user_provider) = &self.user_provider {
331            opts.user_provider = Some(user_provider.clone());
332        }
333
334        Ok(())
335    }
336
337    async fn build(&self, opts: FrontendOptions) -> Result<Instance> {
338        common_runtime::init_global_runtimes(&opts.runtime);
339
340        let guard = common_telemetry::init_global_logging(
341            APP_NAME,
342            &opts.component.logging,
343            &opts.component.tracing,
344            opts.component.node_id.clone(),
345            Some(&opts.component.slow_query),
346        );
347
348        log_versions(verbose_version(), short_version(), APP_NAME);
349        maybe_activate_heap_profile(&opts.component.memory);
350        create_resource_limit_metrics(APP_NAME);
351
352        info!("Frontend start command: {:#?}", self);
353        info!("Frontend options: {:#?}", opts);
354
355        let plugin_opts = opts.plugins;
356        let mut opts = opts.component;
357        opts.grpc.detect_server_addr();
358        let mut plugins = Plugins::new();
359        plugins::setup_frontend_plugins(&mut plugins, &plugin_opts, &opts)
360            .await
361            .context(error::StartFrontendSnafu)?;
362
363        set_default_timezone(opts.default_timezone.as_deref()).context(error::InitTimezoneSnafu)?;
364        set_default_prefix(opts.default_column_prefix.as_deref())
365            .map_err(BoxedError::new)
366            .context(error::BuildCliSnafu)?;
367
368        let meta_client_options = opts
369            .meta_client
370            .as_ref()
371            .context(error::MissingConfigSnafu {
372                msg: "'meta_client'",
373            })?;
374
375        let cache_max_capacity = meta_client_options.metadata_cache_max_capacity;
376        let cache_ttl = meta_client_options.metadata_cache_ttl;
377        let cache_tti = meta_client_options.metadata_cache_tti;
378
379        let meta_client = meta_client::create_meta_client(
380            MetaClientType::Frontend,
381            meta_client_options,
382            Some(&plugins),
383            None,
384        )
385        .await
386        .context(error::MetaClientInitSnafu)?;
387
388        let meta_config: Vec<PluginOptions> = meta_client
389            .pull_config(PluginOptionsDeserializerImpl)
390            .await
391            .context(error::MetaClientInitSnafu)?;
392        setup_frontend_dynamic_plugins(meta_config, &mut plugins)
393            .await
394            .context(error::StartFrontendSnafu)?;
395
396        let readonly_meta_backend = new_read_only_meta_kv_backend(meta_client.clone());
397
398        // TODO(discord9): add helper function to ease the creation of cache registry&such
399        let cached_meta_backend = CachedKvBackendBuilder::new(readonly_meta_backend.clone())
400            .cache_max_capacity(cache_max_capacity)
401            .cache_ttl(cache_ttl)
402            .cache_tti(cache_tti)
403            .build();
404        let cached_meta_backend = Arc::new(cached_meta_backend);
405
406        // Builds cache registry
407        let layered_cache_builder = LayeredCacheRegistryBuilder::default().add_cache_registry(
408            CacheRegistryBuilder::default()
409                .add_cache(cached_meta_backend.clone())
410                .build(),
411        );
412        let fundamental_cache_registry =
413            build_fundamental_cache_registry(readonly_meta_backend.clone());
414        let layered_cache_registry = Arc::new(
415            with_default_composite_cache_registry(
416                layered_cache_builder.add_cache_registry(fundamental_cache_registry),
417            )
418            .context(error::BuildCacheRegistrySnafu)?
419            .build(),
420        );
421
422        // frontend to datanode need not timeout.
423        // Some queries are expected to take long time.
424        let mut channel_config = ChannelConfig {
425            timeout: None,
426            tcp_nodelay: opts.datanode.client.tcp_nodelay,
427            connect_timeout: Some(opts.datanode.client.connect_timeout),
428            ..Default::default()
429        };
430        if opts.grpc.flight_compression.transport_compression() {
431            channel_config.accept_compression = true;
432            channel_config.send_compression = true;
433        }
434        let client = Arc::new(NodeClients::new(channel_config));
435
436        let information_extension = Arc::new(DistributedInformationExtension::new(
437            meta_client.clone(),
438            client.clone(),
439        ));
440        plugins.insert::<InformationExtensionRef>(information_extension.clone());
441
442        let process_manager = Arc::new(ProcessManager::new(
443            addrs::resolve_addr(&opts.grpc.bind_addr, Some(&opts.grpc.server_addr)),
444            Some(meta_client.clone()),
445        ));
446
447        let builder = KvBackendCatalogManagerBuilder::new(
448            information_extension,
449            cached_meta_backend.clone(),
450            layered_cache_registry.clone(),
451        )
452        .with_process_manager(process_manager.clone());
453        let builder = if let Some(configurator) =
454            plugins.get::<CatalogManagerConfiguratorRef<CatalogManagerConfigureContext>>()
455        {
456            let ctx = DistributedCatalogManagerConfigureContext {
457                meta_client: meta_client.clone(),
458            };
459            let ctx = CatalogManagerConfigureContext::Distributed(ctx);
460
461            configurator
462                .configure(builder, ctx)
463                .await
464                .context(OtherSnafu)?
465        } else {
466            builder
467        };
468        let catalog_manager = builder.build();
469
470        let instance = FrontendBuilder::new(
471            opts.clone(),
472            cached_meta_backend.clone(),
473            layered_cache_registry.clone(),
474            catalog_manager,
475            client,
476            meta_client.clone(),
477            process_manager,
478        )
479        .with_plugin(plugins.clone())
480        .with_local_cache_invalidator(layered_cache_registry)
481        .try_build()
482        .await
483        .context(error::StartFrontendSnafu)?;
484
485        let heartbeat_task = Some(create_heartbeat_task(&opts, meta_client, &instance));
486
487        let instance = Arc::new(instance);
488
489        let servers = Services::new(opts, instance.clone(), plugins)
490            .build()
491            .context(error::StartFrontendSnafu)?;
492
493        let frontend = Frontend {
494            instance,
495            servers,
496            heartbeat_task,
497        };
498
499        Ok(Instance::new(frontend, guard))
500    }
501}
502
503pub fn create_heartbeat_task(
504    options: &frontend::frontend::FrontendOptions,
505    meta_client: MetaClientRef,
506    instance: &frontend::instance::Instance,
507) -> HeartbeatTask {
508    let executor = Arc::new(HandlerGroupExecutor::new(vec![
509        Arc::new(ParseMailboxMessageHandler),
510        Arc::new(SuspendHandler::new(instance.suspend_state())),
511        Arc::new(InvalidateCacheHandler::new(
512            instance.cache_invalidator().clone(),
513        )),
514    ]));
515
516    let stat = {
517        let mut stat = ResourceStatImpl::default();
518        stat.start_collect_cpu_usage();
519        Arc::new(stat)
520    };
521
522    HeartbeatTask::new(
523        instance.frontend_peer_addr().to_string(),
524        options,
525        meta_client,
526        executor,
527        stat,
528    )
529}
530
531#[cfg(test)]
532mod tests {
533    use std::io::Write;
534    use std::time::Duration;
535
536    use auth::{Identity, Password, UserProviderRef};
537    use clap::{CommandFactory, Parser};
538    use common_base::readable_size::ReadableSize;
539    use common_config::ENV_VAR_SEP;
540    use common_test_util::temp_dir::create_named_temp_file;
541    use servers::grpc::GrpcOptions;
542    use servers::http::HttpOptions;
543
544    use super::*;
545    use crate::options::GlobalOptions;
546
547    #[test]
548    fn test_try_from_start_command() {
549        let command = StartCommand {
550            http_addr: Some("127.0.0.1:1234".to_string()),
551            mysql_addr: Some("127.0.0.1:5678".to_string()),
552            postgres_addr: Some("127.0.0.1:5432".to_string()),
553            internal_grpc_bind_addr: Some("127.0.0.1:4010".to_string()),
554            internal_grpc_server_addr: Some("10.0.0.24:4010".to_string()),
555            influxdb_enable: Some(false),
556            disable_dashboard: Some(false),
557            ..Default::default()
558        };
559
560        let opts = command.load_options(&Default::default()).unwrap().component;
561
562        assert_eq!(opts.http.addr, "127.0.0.1:1234");
563        assert_eq!(ReadableSize::mb(64), opts.http.body_limit);
564        assert_eq!(opts.mysql.addr, "127.0.0.1:5678");
565        assert_eq!(opts.postgres.addr, "127.0.0.1:5432");
566
567        let internal_grpc = opts.internal_grpc.as_ref().unwrap();
568        assert_eq!(internal_grpc.bind_addr, "127.0.0.1:4010");
569        assert_eq!(internal_grpc.server_addr, "10.0.0.24:4010");
570
571        let default_opts = FrontendOptions::default().component;
572
573        assert_eq!(opts.grpc.bind_addr, default_opts.grpc.bind_addr);
574        assert!(opts.mysql.enable);
575        assert_eq!(opts.mysql.runtime_size, default_opts.mysql.runtime_size);
576        assert!(opts.postgres.enable);
577        assert_eq!(
578            opts.postgres.runtime_size,
579            default_opts.postgres.runtime_size
580        );
581        assert!(opts.opentsdb.enable);
582
583        assert!(!opts.influxdb.enable);
584    }
585
586    #[test]
587    fn test_read_from_config_file() {
588        let mut file = create_named_temp_file();
589        let toml_str = r#"
590            [http]
591            addr = "127.0.0.1:4000"
592            timeout = "0s"
593            body_limit = "2GB"
594
595            [opentsdb]
596            enable = false
597
598            [logging]
599            level = "debug"
600            dir = "./greptimedb_data/test/logs"
601        "#;
602        write!(file, "{}", toml_str).unwrap();
603
604        let command = StartCommand {
605            config_file: Some(file.path().to_str().unwrap().to_string()),
606            disable_dashboard: Some(false),
607            ..Default::default()
608        };
609
610        let fe_opts = command.load_options(&Default::default()).unwrap().component;
611
612        assert_eq!("127.0.0.1:4000".to_string(), fe_opts.http.addr);
613        assert_eq!(Duration::from_secs(0), fe_opts.http.timeout);
614
615        assert_eq!(ReadableSize::gb(2), fe_opts.http.body_limit);
616
617        assert_eq!("debug", fe_opts.logging.level.as_ref().unwrap());
618        assert_eq!(
619            "./greptimedb_data/test/logs".to_string(),
620            fe_opts.logging.dir
621        );
622        assert!(!fe_opts.opentsdb.enable);
623    }
624
625    #[tokio::test]
626    async fn test_try_from_start_command_to_anymap() {
627        let fe_opts = frontend::frontend::FrontendOptions {
628            http: HttpOptions {
629                disable_dashboard: false,
630                ..Default::default()
631            },
632            meta_client: Some(MetaClientOptions::default()),
633            user_provider: Some("static_user_provider:cmd:test=test".to_string()),
634            ..Default::default()
635        };
636
637        let mut plugins = Plugins::new();
638        plugins::setup_frontend_plugins(&mut plugins, &[], &fe_opts)
639            .await
640            .unwrap();
641
642        let provider = plugins.get::<UserProviderRef>().unwrap();
643        let result = provider
644            .authenticate(
645                Identity::UserId("test", None),
646                Password::PlainText("test".to_string().into()),
647            )
648            .await;
649        let _ = result.unwrap();
650    }
651
652    #[test]
653    fn test_load_log_options_from_cli() {
654        let cmd = StartCommand {
655            disable_dashboard: Some(false),
656            ..Default::default()
657        };
658
659        let options = cmd
660            .load_options(&GlobalOptions {
661                log_dir: Some("./greptimedb_data/test/logs".to_string()),
662                log_level: Some("debug".to_string()),
663
664                #[cfg(feature = "tokio-console")]
665                tokio_console_addr: None,
666            })
667            .unwrap()
668            .component;
669
670        let logging_opt = options.logging;
671        assert_eq!("./greptimedb_data/test/logs", logging_opt.dir);
672        assert_eq!("debug", logging_opt.level.as_ref().unwrap());
673    }
674
675    #[test]
676    fn test_config_precedence_order() {
677        let mut file = create_named_temp_file();
678        let toml_str = r#"
679            [http]
680            addr = "127.0.0.1:4000"
681
682            [meta_client]
683            timeout = "3s"
684            connect_timeout = "5s"
685            tcp_nodelay = true
686
687            [mysql]
688            addr = "127.0.0.1:4002"
689        "#;
690        write!(file, "{}", toml_str).unwrap();
691
692        let env_prefix = "FRONTEND_UT";
693        temp_env::with_vars(
694            [
695                (
696                    // mysql.addr = 127.0.0.1:14002
697                    [
698                        env_prefix.to_string(),
699                        "mysql".to_uppercase(),
700                        "addr".to_uppercase(),
701                    ]
702                    .join(ENV_VAR_SEP),
703                    Some("127.0.0.1:14002"),
704                ),
705                (
706                    // mysql.runtime_size = 11
707                    [
708                        env_prefix.to_string(),
709                        "mysql".to_uppercase(),
710                        "runtime_size".to_uppercase(),
711                    ]
712                    .join(ENV_VAR_SEP),
713                    Some("11"),
714                ),
715                (
716                    // http.addr = 127.0.0.1:24000
717                    [
718                        env_prefix.to_string(),
719                        "http".to_uppercase(),
720                        "addr".to_uppercase(),
721                    ]
722                    .join(ENV_VAR_SEP),
723                    Some("127.0.0.1:24000"),
724                ),
725                (
726                    // meta_client.metasrv_addrs = 127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003
727                    [
728                        env_prefix.to_string(),
729                        "meta_client".to_uppercase(),
730                        "metasrv_addrs".to_uppercase(),
731                    ]
732                    .join(ENV_VAR_SEP),
733                    Some("127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003"),
734                ),
735            ],
736            || {
737                let command = StartCommand {
738                    config_file: Some(file.path().to_str().unwrap().to_string()),
739                    http_addr: Some("127.0.0.1:14000".to_string()),
740                    env_prefix: env_prefix.to_string(),
741                    ..Default::default()
742                };
743
744                let fe_opts = command.load_options(&Default::default()).unwrap().component;
745
746                // Should be read from env, env > default values.
747                assert_eq!(fe_opts.mysql.runtime_size, 11);
748                assert_eq!(
749                    fe_opts.meta_client.unwrap().metasrv_addrs,
750                    vec![
751                        "127.0.0.1:3001".to_string(),
752                        "127.0.0.1:3002".to_string(),
753                        "127.0.0.1:3003".to_string()
754                    ]
755                );
756
757                // Should be read from config file, config file > env > default values.
758                assert_eq!(fe_opts.mysql.addr, "127.0.0.1:4002");
759
760                // Should be read from cli, cli > config file > env > default values.
761                assert_eq!(fe_opts.http.addr, "127.0.0.1:14000");
762
763                // Should be default value.
764                assert_eq!(fe_opts.grpc.bind_addr, GrpcOptions::default().bind_addr);
765            },
766        );
767    }
768
769    #[test]
770    fn test_parse_grpc_cli_aliases() {
771        let command = StartCommand::try_parse_from([
772            "frontend",
773            "--grpc-bind-addr",
774            "127.0.0.1:14001",
775            "--grpc-server-addr",
776            "10.0.0.1:14001",
777            "--internal-grpc-bind-addr",
778            "127.0.0.1:14010",
779            "--internal-grpc-server-addr",
780            "10.0.0.1:14010",
781        ])
782        .unwrap();
783        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:14001"));
784        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.1:14001"));
785        assert_eq!(
786            command.internal_grpc_bind_addr.as_deref(),
787            Some("127.0.0.1:14010")
788        );
789        assert_eq!(
790            command.internal_grpc_server_addr.as_deref(),
791            Some("10.0.0.1:14010")
792        );
793
794        let command = StartCommand::try_parse_from([
795            "frontend",
796            "--rpc-bind-addr",
797            "127.0.0.1:24001",
798            "--rpc-server-addr",
799            "10.0.0.2:24001",
800            "--internal-rpc-bind-addr",
801            "127.0.0.1:24010",
802            "--internal-rpc-server-addr",
803            "10.0.0.2:24010",
804        ])
805        .unwrap();
806        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:24001"));
807        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.2:24001"));
808        assert_eq!(
809            command.internal_grpc_bind_addr.as_deref(),
810            Some("127.0.0.1:24010")
811        );
812        assert_eq!(
813            command.internal_grpc_server_addr.as_deref(),
814            Some("10.0.0.2:24010")
815        );
816
817        let command = StartCommand::try_parse_from([
818            "frontend",
819            "--rpc-addr",
820            "127.0.0.1:34001",
821            "--rpc-hostname",
822            "10.0.0.3:34001",
823            "--internal-rpc-addr",
824            "127.0.0.1:34010",
825            "--internal-rpc-hostname",
826            "10.0.0.3:34010",
827        ])
828        .unwrap();
829        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:34001"));
830        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.3:34001"));
831        assert_eq!(
832            command.internal_grpc_bind_addr.as_deref(),
833            Some("127.0.0.1:34010")
834        );
835        assert_eq!(
836            command.internal_grpc_server_addr.as_deref(),
837            Some("10.0.0.3:34010")
838        );
839    }
840
841    #[test]
842    fn test_help_uses_grpc_option_names() {
843        let mut cmd = StartCommand::command();
844        let mut help = Vec::new();
845        cmd.write_long_help(&mut help).unwrap();
846        let help = String::from_utf8(help).unwrap();
847
848        assert!(help.contains("--grpc-bind-addr"));
849        assert!(help.contains("--grpc-server-addr"));
850        assert!(help.contains("--internal-grpc-bind-addr"));
851        assert!(help.contains("--internal-grpc-server-addr"));
852        assert!(!help.contains("--rpc-bind-addr"));
853        assert!(!help.contains("--rpc-server-addr"));
854        assert!(!help.contains("--rpc-addr"));
855        assert!(!help.contains("--rpc-hostname"));
856        assert!(!help.contains("--internal-rpc-bind-addr"));
857        assert!(!help.contains("--internal-rpc-server-addr"));
858        assert!(!help.contains("--internal-rpc-addr"));
859        assert!(!help.contains("--internal-rpc-hostname"));
860    }
861}