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