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