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::future::Future;
17use std::path::Path;
18use std::pin::Pin;
19use std::sync::Arc;
20use std::time::Duration;
21
22use async_trait::async_trait;
23use cache::{build_fundamental_cache_registry, with_default_composite_cache_registry};
24use catalog::information_extension::DistributedInformationExtension;
25use catalog::information_schema::InformationExtensionRef;
26use catalog::kvbackend::{
27    CachedKvBackendBuilder, CatalogManagerConfiguratorRef, KvBackendCatalogManagerBuilder,
28    new_read_only_meta_kv_backend,
29};
30use catalog::process_manager::ProcessManager;
31use clap::Parser;
32use client::client_manager::NodeClients;
33use common_base::Plugins;
34use common_config::{Configurable, DEFAULT_DATA_HOME};
35use common_error::ext::BoxedError;
36use common_meta::cache::{CacheRegistryBuilder, LayeredCacheRegistryBuilder};
37use common_query::prelude::set_default_prefix;
38use common_stat::ResourceStatImpl;
39use common_telemetry::info;
40use common_telemetry::logging::{DEFAULT_LOGGING_DIR, TracingOptions};
41use common_time::timezone::set_default_timezone;
42use common_version::{short_version, verbose_version};
43use frontend::frontend::Frontend;
44use frontend::heartbeat::{
45    FrontendHeartbeatExtensions, HeartbeatTask, heartbeat_response_handler_executor,
46};
47use frontend::instance::builder::FrontendBuilder;
48use frontend::server::Services;
49use meta_client::{MetaClientOptions, MetaClientRef, MetaClientType};
50use plugins::PluginOptions;
51use plugins::frontend::context::{
52    CatalogManagerConfigureContext, DistributedCatalogManagerConfigureContext,
53};
54use plugins::options::PluginOptionsDeserializerImpl;
55use servers::addrs;
56use servers::grpc::GrpcOptions;
57use servers::tls::{TlsMode, TlsOption, merge_tls_option};
58use snafu::{OptionExt, ResultExt};
59use tracing_appender::non_blocking::WorkerGuard;
60
61use crate::error::{self, OtherSnafu, Result};
62use crate::options::{GlobalOptions, GreptimeOptions};
63use crate::{App, create_resource_limit_metrics, log_versions, maybe_activate_heap_profile};
64
65type FrontendOptions = GreptimeOptions<frontend::frontend::FrontendOptions>;
66type HeartbeatExtensionSetupFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
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.build_with_heartbeat_extensions(opts, FrontendHeartbeatExtensions::default())
124            .await
125    }
126
127    /// Builds a frontend with pre-registered heartbeat extensions.
128    ///
129    /// Register extensions before calling this method. The supplied registry is installed before
130    /// normal plugin setup. After plugin heartbeat setup completes, the registry is frozen before
131    /// heartbeat handlers and the heartbeat task are built, so later registrations are rejected
132    /// and every heartbeat consumer observes the same extension membership.
133    pub async fn build_with_heartbeat_extensions(
134        &self,
135        opts: FrontendOptions,
136        heartbeat_extensions: FrontendHeartbeatExtensions,
137    ) -> Result<Instance> {
138        let plugins = plugins_with_heartbeat_extensions(heartbeat_extensions);
139        self.forward_build_with_plugins(opts, plugins, |command, opts, plugins| {
140            command.build_with_plugins(opts, plugins)
141        })
142        .await
143    }
144
145    fn forward_build_with_plugins<'a, T>(
146        &'a self,
147        opts: FrontendOptions,
148        plugins: Plugins,
149        build: impl FnOnce(&'a StartCommand, FrontendOptions, Plugins) -> T,
150    ) -> T {
151        self.subcmd.forward_build_with_plugins(opts, plugins, build)
152    }
153
154    pub fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
155        self.subcmd.load_options(global_options)
156    }
157}
158
159#[derive(Parser)]
160pub enum SubCommand {
161    Start(StartCommand),
162}
163
164impl SubCommand {
165    fn forward_build_with_plugins<'a, T>(
166        &'a self,
167        opts: FrontendOptions,
168        plugins: Plugins,
169        build: impl FnOnce(&'a StartCommand, FrontendOptions, Plugins) -> T,
170    ) -> T {
171        match self {
172            SubCommand::Start(cmd) => build(cmd, opts, plugins),
173        }
174    }
175
176    fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
177        match self {
178            SubCommand::Start(cmd) => cmd.load_options(global_options),
179        }
180    }
181}
182
183#[derive(Debug, Default, Parser)]
184pub struct StartCommand {
185    /// The address to bind the gRPC server.
186    #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")]
187    grpc_bind_addr: Option<String>,
188    /// The address advertised to the metasrv, and used for connections from outside the host.
189    /// If left empty or unset, the server will automatically use the IP address of the first network interface
190    /// on the host, with the same port number as the one specified in `grpc_bind_addr`.
191    #[clap(
192        long = "grpc-server-addr",
193        alias = "rpc-server-addr",
194        alias = "rpc-hostname"
195    )]
196    grpc_server_addr: Option<String>,
197    /// The address to bind the internal gRPC server.
198    #[clap(
199        long = "internal-grpc-bind-addr",
200        alias = "internal-rpc-bind-addr",
201        alias = "internal-rpc-addr"
202    )]
203    internal_grpc_bind_addr: Option<String>,
204    /// The address advertised to the metasrv, and used for connections from outside the host.
205    /// If left empty or unset, the server will automatically use the IP address of the first network interface
206    /// on the host, with the same port number as the one specified in `internal_grpc_bind_addr`.
207    #[clap(
208        long = "internal-grpc-server-addr",
209        alias = "internal-rpc-server-addr",
210        alias = "internal-rpc-hostname"
211    )]
212    internal_grpc_server_addr: Option<String>,
213    #[clap(long)]
214    http_addr: Option<String>,
215    #[clap(long)]
216    http_timeout: Option<u64>,
217    #[clap(long)]
218    mysql_addr: Option<String>,
219    #[clap(long)]
220    postgres_addr: Option<String>,
221    #[clap(short, long)]
222    pub config_file: Option<String>,
223    #[clap(short, long)]
224    influxdb_enable: Option<bool>,
225    #[clap(long, value_delimiter = ',', num_args = 1..)]
226    metasrv_addrs: Option<Vec<String>>,
227    #[clap(long)]
228    tls_mode: Option<TlsMode>,
229    #[clap(long)]
230    tls_cert_path: Option<String>,
231    #[clap(long)]
232    tls_key_path: Option<String>,
233    #[clap(long)]
234    tls_watch: bool,
235    #[clap(long)]
236    user_provider: Option<String>,
237    #[clap(long)]
238    disable_dashboard: Option<bool>,
239    #[clap(long, default_value = "GREPTIMEDB_FRONTEND")]
240    pub env_prefix: String,
241}
242
243impl StartCommand {
244    fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
245        let mut opts = FrontendOptions::load_layered_options(
246            self.config_file.as_deref(),
247            self.env_prefix.as_ref(),
248        )
249        .context(error::LoadLayeredConfigSnafu)?;
250
251        self.merge_with_cli_options(global_options, &mut opts)?;
252
253        Ok(opts)
254    }
255
256    // The precedence order is: cli > config file > environment variables > default values.
257    fn merge_with_cli_options(
258        &self,
259        global_options: &GlobalOptions,
260        opts: &mut FrontendOptions,
261    ) -> Result<()> {
262        let opts = &mut opts.component;
263
264        if let Some(dir) = &global_options.log_dir {
265            opts.logging.dir.clone_from(dir);
266        }
267
268        // If the logging dir is not set, use the default logs dir in the data home.
269        if opts.logging.dir.is_empty() {
270            opts.logging.dir = Path::new(DEFAULT_DATA_HOME)
271                .join(DEFAULT_LOGGING_DIR)
272                .to_string_lossy()
273                .to_string();
274        }
275
276        if global_options.log_level.is_some() {
277            opts.logging.level.clone_from(&global_options.log_level);
278        }
279
280        opts.tracing = TracingOptions {
281            #[cfg(feature = "tokio-console")]
282            tokio_console_addr: global_options.tokio_console_addr.clone(),
283        };
284
285        let tls_opts = TlsOption::new(
286            self.tls_mode,
287            self.tls_cert_path.clone(),
288            self.tls_key_path.clone(),
289            self.tls_watch,
290        );
291
292        if let Some(addr) = &self.http_addr {
293            opts.http.addr.clone_from(addr);
294        }
295
296        if let Some(http_timeout) = self.http_timeout {
297            opts.http.timeout = Duration::from_secs(http_timeout)
298        }
299
300        if let Some(disable_dashboard) = self.disable_dashboard {
301            opts.http.disable_dashboard = disable_dashboard;
302        }
303
304        if let Some(addr) = &self.grpc_bind_addr {
305            opts.grpc.bind_addr.clone_from(addr);
306            opts.grpc.tls = merge_tls_option(&opts.grpc.tls, tls_opts.clone());
307        }
308
309        if let Some(addr) = &self.grpc_server_addr {
310            opts.grpc.server_addr.clone_from(addr);
311        }
312
313        if let Some(addr) = &self.internal_grpc_bind_addr {
314            if let Some(internal_grpc) = &mut opts.internal_grpc {
315                internal_grpc.bind_addr = addr.clone();
316            } else {
317                let grpc_options = GrpcOptions {
318                    bind_addr: addr.clone(),
319                    ..Default::default()
320                };
321
322                opts.internal_grpc = Some(grpc_options);
323            }
324        }
325
326        if let Some(addr) = &self.internal_grpc_server_addr {
327            if let Some(internal_grpc) = &mut opts.internal_grpc {
328                internal_grpc.server_addr = addr.clone();
329            } else {
330                let grpc_options = GrpcOptions {
331                    server_addr: addr.clone(),
332                    ..Default::default()
333                };
334                opts.internal_grpc = Some(grpc_options);
335            }
336        }
337
338        if let Some(addr) = &self.mysql_addr {
339            opts.mysql.enable = true;
340            opts.mysql.addr.clone_from(addr);
341            opts.mysql.tls = merge_tls_option(&opts.mysql.tls, tls_opts.clone());
342        }
343
344        if let Some(addr) = &self.postgres_addr {
345            opts.postgres.enable = true;
346            opts.postgres.addr.clone_from(addr);
347            opts.postgres.tls = merge_tls_option(&opts.postgres.tls, tls_opts.clone());
348        }
349
350        if let Some(enable) = self.influxdb_enable {
351            opts.influxdb.enable = enable;
352        }
353
354        if let Some(metasrv_addrs) = &self.metasrv_addrs {
355            opts.meta_client
356                .get_or_insert_with(MetaClientOptions::default)
357                .metasrv_addrs
358                .clone_from(metasrv_addrs);
359        }
360
361        if let Some(user_provider) = &self.user_provider {
362            opts.user_provider = Some(user_provider.clone());
363        }
364
365        Ok(())
366    }
367
368    async fn build_with_plugins(
369        &self,
370        opts: FrontendOptions,
371        plugins: Plugins,
372    ) -> Result<Instance> {
373        let guard = common_telemetry::init_global_logging(
374            APP_NAME,
375            &opts.component.logging,
376            &opts.component.tracing,
377            opts.component.node_id.clone(),
378            Some(&opts.component.slow_query),
379        );
380
381        common_runtime::init_global_runtimes(&opts.runtime);
382
383        crate::options::flush_dropped_plugin_warnings();
384        log_versions(verbose_version(), short_version(), APP_NAME);
385        maybe_activate_heap_profile(&opts.component.memory);
386        create_resource_limit_metrics(APP_NAME);
387
388        info!("Frontend start command: {:#?}", self);
389        info!("Frontend options: {:#?}", opts);
390
391        let plugin_opts = opts.plugins;
392        let mut opts = opts.component;
393        opts.grpc.detect_server_addr();
394
395        set_default_timezone(opts.default_timezone.as_deref()).context(error::InitTimezoneSnafu)?;
396        set_default_prefix(opts.default_column_prefix.as_deref())
397            .map_err(BoxedError::new)
398            .context(error::BuildCliSnafu)?;
399
400        let meta_client_options = opts
401            .meta_client
402            .as_ref()
403            .context(error::MissingConfigSnafu {
404                msg: "'meta_client'",
405            })?;
406
407        let cache_max_capacity = meta_client_options.metadata_cache_max_capacity;
408        let cache_ttl = meta_client_options.metadata_cache_ttl;
409        let cache_tti = meta_client_options.metadata_cache_tti;
410
411        let meta_config: Vec<PluginOptions> = meta_client::create_meta_client(
412            MetaClientType::Frontend,
413            meta_client_options,
414            None,
415            None,
416        )
417        .await
418        .context(error::MetaClientInitSnafu)?
419        .pull_config(PluginOptionsDeserializerImpl)
420        .await
421        .context(error::MetaClientInitSnafu)?;
422
423        let mut plugins =
424            prepare_frontend_plugins(plugins, &plugin_opts, &opts, Some(&meta_config)).await?;
425
426        // now initialize the meta_client with plugins
427        let meta_client = meta_client::create_meta_client(
428            MetaClientType::Frontend,
429            meta_client_options,
430            Some(&plugins),
431            None,
432        )
433        .await
434        .context(error::MetaClientInitSnafu)?;
435
436        let readonly_meta_backend = new_read_only_meta_kv_backend(meta_client.clone());
437
438        // TODO(discord9): add helper function to ease the creation of cache registry&such
439        let cached_meta_backend = CachedKvBackendBuilder::new(readonly_meta_backend.clone())
440            .cache_max_capacity(cache_max_capacity)
441            .cache_ttl(cache_ttl)
442            .cache_tti(cache_tti)
443            .build();
444        let cached_meta_backend = Arc::new(cached_meta_backend);
445
446        // Builds cache registry
447        let layered_cache_builder = LayeredCacheRegistryBuilder::default().add_cache_registry(
448            CacheRegistryBuilder::default()
449                .add_cache(cached_meta_backend.clone())
450                .build(),
451        );
452        let fundamental_cache_registry =
453            build_fundamental_cache_registry(readonly_meta_backend.clone());
454        let mut layered_cache_builder = with_default_composite_cache_registry(
455            layered_cache_builder.add_cache_registry(fundamental_cache_registry),
456        )
457        .context(error::BuildCacheRegistrySnafu)?;
458
459        if let Some(plugin_cache_builder) = plugins::frontend::configure_cache_registry(&plugins) {
460            layered_cache_builder =
461                layered_cache_builder.add_cache_registry(plugin_cache_builder.build());
462        }
463
464        let layered_cache_registry = Arc::new(layered_cache_builder.build());
465
466        // frontend to datanode need not timeout.
467        // Some queries are expected to take long time.
468        let mut channel_config = opts.datanode.client.channel_config();
469        channel_config.timeout = None;
470        if opts.grpc.flight_compression.transport_compression() {
471            channel_config.accept_compression = true;
472            channel_config.send_compression = true;
473        }
474        let client = Arc::new(NodeClients::new(channel_config));
475
476        let information_extension = Arc::new(DistributedInformationExtension::new(
477            meta_client.clone(),
478            client.clone(),
479        ));
480        plugins.insert::<InformationExtensionRef>(information_extension.clone());
481
482        let process_manager = Arc::new(ProcessManager::new(
483            addrs::resolve_addr(&opts.grpc.bind_addr, Some(&opts.grpc.server_addr)),
484            Some(meta_client.clone()),
485        ));
486
487        let builder = KvBackendCatalogManagerBuilder::new(
488            information_extension,
489            cached_meta_backend.clone(),
490            layered_cache_registry.clone(),
491        )
492        .with_process_manager(process_manager.clone());
493        let builder = if let Some(configurator) =
494            plugins.get::<CatalogManagerConfiguratorRef<CatalogManagerConfigureContext>>()
495        {
496            let ctx = DistributedCatalogManagerConfigureContext {
497                meta_client: meta_client.clone(),
498            };
499            let ctx = CatalogManagerConfigureContext::Distributed(ctx);
500
501            configurator
502                .configure(builder, ctx)
503                .await
504                .context(OtherSnafu)?
505        } else {
506            builder
507        };
508        let catalog_manager = builder.build();
509
510        let builder = FrontendBuilder::new(
511            opts.clone(),
512            cached_meta_backend.clone(),
513            layered_cache_registry.clone(),
514            catalog_manager,
515            client,
516            meta_client.clone(),
517            process_manager,
518        );
519
520        plugins::setup_frontend_plugins_post_build(&mut plugins, &plugin_opts, &builder)
521            .await
522            .context(error::StartFrontendSnafu)?;
523
524        let instance = builder
525            .with_plugin(plugins.clone())
526            .with_local_cache_invalidator(layered_cache_registry)
527            .try_build()
528            .await
529            .context(error::StartFrontendSnafu)?;
530
531        let instance = Arc::new(instance);
532
533        let heartbeat_instance = instance.clone();
534        let heartbeat_extensions =
535            setup_and_freeze_frontend_heartbeat_extensions(&mut plugins, move |plugins| {
536                Box::pin(async move {
537                    plugins::setup_frontend_heartbeat_extensions(
538                        plugins,
539                        &plugin_opts,
540                        &heartbeat_instance,
541                    )
542                    .await
543                    .context(error::StartFrontendSnafu)
544                })
545            })
546            .await?;
547        let heartbeat_task = Some(create_heartbeat_task_with_extensions(
548            &opts,
549            meta_client,
550            &instance,
551            heartbeat_extensions,
552        ));
553
554        let servers = Services::new(opts, instance.clone(), plugins)
555            .build()
556            .context(error::StartFrontendSnafu)?;
557
558        let frontend = Frontend {
559            instance,
560            servers,
561            heartbeat_task,
562        };
563
564        Ok(Instance::new(frontend, guard))
565    }
566}
567
568async fn prepare_frontend_plugins(
569    mut plugins: Plugins,
570    plugin_opts: &[PluginOptions],
571    opts: &frontend::frontend::FrontendOptions,
572    meta_config: Option<&[PluginOptions]>,
573) -> Result<Plugins> {
574    plugins::setup_frontend_plugins_pre_build(&mut plugins, plugin_opts, opts, meta_config)
575        .await
576        .context(error::StartFrontendSnafu)?;
577    Ok(plugins)
578}
579
580fn plugins_with_heartbeat_extensions(heartbeat_extensions: FrontendHeartbeatExtensions) -> Plugins {
581    let plugins = Plugins::new();
582    plugins.insert(heartbeat_extensions);
583    plugins
584}
585
586async fn setup_and_freeze_frontend_heartbeat_extensions(
587    plugins: &mut Plugins,
588    setup: impl for<'a> FnOnce(&'a mut Plugins) -> HeartbeatExtensionSetupFuture<'a>,
589) -> Result<FrontendHeartbeatExtensions> {
590    setup(plugins).await?;
591    let extensions = plugins
592        .get::<FrontendHeartbeatExtensions>()
593        .unwrap_or_default();
594    extensions.freeze();
595    Ok(extensions)
596}
597
598pub fn create_heartbeat_task(
599    options: &frontend::frontend::FrontendOptions,
600    meta_client: MetaClientRef,
601    instance: &frontend::instance::Instance,
602) -> HeartbeatTask {
603    create_heartbeat_task_with_extensions(options, meta_client, instance, Default::default())
604}
605
606fn create_heartbeat_task_with_extensions(
607    options: &frontend::frontend::FrontendOptions,
608    meta_client: MetaClientRef,
609    instance: &frontend::instance::Instance,
610    extensions: FrontendHeartbeatExtensions,
611) -> HeartbeatTask {
612    let executor = heartbeat_response_handler_executor(
613        &extensions,
614        instance.suspend_state(),
615        instance.cache_invalidator().clone(),
616    );
617
618    let stat = {
619        let mut stat = ResourceStatImpl::default();
620        stat.start_collect_cpu_usage();
621        Arc::new(stat)
622    };
623
624    HeartbeatTask::new(
625        instance.frontend_peer_addr().to_string(),
626        options,
627        meta_client,
628        executor,
629        stat,
630    )
631    .with_extensions(extensions)
632}
633
634#[cfg(test)]
635mod tests {
636    use std::io::Write;
637    use std::sync::Arc;
638    use std::time::Duration;
639
640    use auth::{Identity, Password, UserProviderRef};
641    use clap::{CommandFactory, Parser};
642    use common_base::readable_size::ReadableSize;
643    use common_config::ENV_VAR_SEP;
644    use common_test_util::temp_dir::create_named_temp_file;
645    use servers::grpc::GrpcOptions;
646    use servers::http::HttpOptions;
647
648    use super::*;
649    use crate::options::GlobalOptions;
650
651    #[derive(Debug)]
652    struct TestExtension(&'static str);
653
654    #[async_trait]
655    impl frontend::heartbeat::FrontendHeartbeatExtension for TestExtension {
656        fn name(&self) -> &'static str {
657            self.0
658        }
659    }
660
661    #[test]
662    fn test_command_forwards_heartbeat_extensions_to_start_build() {
663        let command = Command {
664            subcmd: SubCommand::Start(StartCommand::default()),
665        };
666        let extensions = FrontendHeartbeatExtensions::default();
667        let shared_extensions = extensions.clone();
668
669        let forwarded_plugins = command.forward_build_with_plugins(
670            FrontendOptions::default(),
671            plugins_with_heartbeat_extensions(extensions),
672            |_, _, plugins| plugins,
673        );
674        let forwarded_extensions = forwarded_plugins
675            .get::<FrontendHeartbeatExtensions>()
676            .unwrap();
677
678        assert_registry_identity(&shared_extensions, &forwarded_extensions);
679    }
680
681    #[tokio::test]
682    async fn test_prefilled_heartbeat_extensions_survive_pre_build_setup() {
683        let extensions = FrontendHeartbeatExtensions::default();
684        let shared_extensions = extensions.clone();
685
686        let options = frontend::frontend::FrontendOptions {
687            meta_client: Some(MetaClientOptions::default()),
688            ..Default::default()
689        };
690        let plugins = prepare_frontend_plugins(
691            plugins_with_heartbeat_extensions(extensions),
692            &[],
693            &options,
694            None,
695        )
696        .await
697        .unwrap();
698
699        let setup_extensions = plugins.get_or_insert(FrontendHeartbeatExtensions::default);
700        assert_registry_identity(&shared_extensions, &setup_extensions);
701    }
702
703    #[tokio::test]
704    async fn test_heartbeat_extension_setup_completes_before_freeze() {
705        let extensions = FrontendHeartbeatExtensions::default();
706        assert_eq!(
707            extensions.try_register(Arc::new(TestExtension("caller"))),
708            Ok(())
709        );
710        let mut plugins = plugins_with_heartbeat_extensions(extensions.clone());
711
712        let finalized = setup_and_freeze_frontend_heartbeat_extensions(&mut plugins, |plugins| {
713            Box::pin(async move {
714                let setup_extensions = plugins.get_or_insert(FrontendHeartbeatExtensions::default);
715                assert_eq!(
716                    setup_extensions.try_register(Arc::new(TestExtension("plugin"))),
717                    Ok(())
718                );
719                Ok(())
720            })
721        })
722        .await
723        .unwrap();
724
725        assert_eq!(
726            finalized
727                .extensions()
728                .iter()
729                .map(|extension| extension.name())
730                .collect::<Vec<_>>(),
731            ["caller", "plugin"]
732        );
733        assert_eq!(
734            extensions.try_register(Arc::new(TestExtension("late"))),
735            Err(frontend::heartbeat::RegistrationError::Frozen)
736        );
737        assert_eq!(finalized.len(), 2);
738    }
739
740    fn assert_registry_identity(
741        expected: &FrontendHeartbeatExtensions,
742        actual: &FrontendHeartbeatExtensions,
743    ) {
744        let extension = Arc::new(TestExtension("cmd-test-extension"));
745        assert!(expected.register(extension.clone()));
746        let registered = actual.extensions();
747        assert_eq!(registered.len(), 1);
748        assert!(Arc::ptr_eq(
749            &(extension as Arc<dyn frontend::heartbeat::FrontendHeartbeatExtension>),
750            &registered[0]
751        ));
752    }
753
754    #[test]
755    fn test_try_from_start_command() {
756        let command = StartCommand {
757            http_addr: Some("127.0.0.1:1234".to_string()),
758            mysql_addr: Some("127.0.0.1:5678".to_string()),
759            postgres_addr: Some("127.0.0.1:5432".to_string()),
760            internal_grpc_bind_addr: Some("127.0.0.1:4010".to_string()),
761            internal_grpc_server_addr: Some("10.0.0.24:4010".to_string()),
762            influxdb_enable: Some(false),
763            disable_dashboard: Some(false),
764            ..Default::default()
765        };
766
767        let opts = command.load_options(&Default::default()).unwrap().component;
768
769        assert_eq!(opts.http.addr, "127.0.0.1:1234");
770        assert_eq!(ReadableSize::mb(64), opts.http.body_limit);
771        assert_eq!(opts.mysql.addr, "127.0.0.1:5678");
772        assert_eq!(opts.postgres.addr, "127.0.0.1:5432");
773
774        let internal_grpc = opts.internal_grpc.as_ref().unwrap();
775        assert_eq!(internal_grpc.bind_addr, "127.0.0.1:4010");
776        assert_eq!(internal_grpc.server_addr, "10.0.0.24:4010");
777
778        let default_opts = FrontendOptions::default().component;
779
780        assert_eq!(opts.grpc.bind_addr, default_opts.grpc.bind_addr);
781        assert!(opts.mysql.enable);
782        assert_eq!(opts.mysql.runtime_size, default_opts.mysql.runtime_size);
783        assert!(opts.postgres.enable);
784        assert_eq!(
785            opts.postgres.runtime_size,
786            default_opts.postgres.runtime_size
787        );
788        assert!(opts.opentsdb.enable);
789
790        assert!(!opts.influxdb.enable);
791    }
792
793    #[test]
794    fn test_read_from_config_file() {
795        let mut file = create_named_temp_file();
796        let toml_str = r#"
797            [http]
798            addr = "127.0.0.1:4000"
799            timeout = "0s"
800            body_limit = "2GB"
801
802            [opentsdb]
803            enable = false
804
805            [logging]
806            level = "debug"
807            dir = "./greptimedb_data/test/logs"
808        "#;
809        write!(file, "{}", toml_str).unwrap();
810
811        let command = StartCommand {
812            config_file: Some(file.path().to_str().unwrap().to_string()),
813            disable_dashboard: Some(false),
814            ..Default::default()
815        };
816
817        let fe_opts = command.load_options(&Default::default()).unwrap().component;
818
819        assert_eq!("127.0.0.1:4000".to_string(), fe_opts.http.addr);
820        assert_eq!(Duration::from_secs(0), fe_opts.http.timeout);
821
822        assert_eq!(ReadableSize::gb(2), fe_opts.http.body_limit);
823
824        assert_eq!("debug", fe_opts.logging.level.as_ref().unwrap());
825        assert_eq!(
826            "./greptimedb_data/test/logs".to_string(),
827            fe_opts.logging.dir
828        );
829        assert!(!fe_opts.opentsdb.enable);
830    }
831
832    #[tokio::test]
833    async fn test_try_from_start_command_to_anymap() {
834        let fe_opts = frontend::frontend::FrontendOptions {
835            http: HttpOptions {
836                disable_dashboard: false,
837                ..Default::default()
838            },
839            meta_client: Some(MetaClientOptions::default()),
840            user_provider: Some("static_user_provider:cmd:test=test".to_string()),
841            ..Default::default()
842        };
843
844        let mut plugins = Plugins::new();
845        plugins::setup_frontend_plugins_pre_build(&mut plugins, &[], &fe_opts, None)
846            .await
847            .unwrap();
848
849        let provider = plugins.get::<UserProviderRef>().unwrap();
850        let result = provider
851            .authenticate(
852                Identity::UserId("test", None),
853                Password::PlainText("test".to_string().into()),
854            )
855            .await;
856        let _ = result.unwrap();
857    }
858
859    #[test]
860    fn test_load_log_options_from_cli() {
861        let cmd = StartCommand {
862            disable_dashboard: Some(false),
863            ..Default::default()
864        };
865
866        let options = cmd
867            .load_options(&GlobalOptions {
868                log_dir: Some("./greptimedb_data/test/logs".to_string()),
869                log_level: Some("debug".to_string()),
870
871                #[cfg(feature = "tokio-console")]
872                tokio_console_addr: None,
873            })
874            .unwrap()
875            .component;
876
877        let logging_opt = options.logging;
878        assert_eq!("./greptimedb_data/test/logs", logging_opt.dir);
879        assert_eq!("debug", logging_opt.level.as_ref().unwrap());
880    }
881
882    #[test]
883    fn test_config_precedence_order() {
884        let mut file = create_named_temp_file();
885        let toml_str = r#"
886            [http]
887            addr = "127.0.0.1:4000"
888
889            [meta_client]
890            timeout = "3s"
891            connect_timeout = "5s"
892            tcp_nodelay = true
893
894            [mysql]
895            addr = "127.0.0.1:4002"
896        "#;
897        write!(file, "{}", toml_str).unwrap();
898
899        let env_prefix = "FRONTEND_UT";
900        temp_env::with_vars(
901            [
902                (
903                    // mysql.addr = 127.0.0.1:14002
904                    [
905                        env_prefix.to_string(),
906                        "mysql".to_uppercase(),
907                        "addr".to_uppercase(),
908                    ]
909                    .join(ENV_VAR_SEP),
910                    Some("127.0.0.1:14002"),
911                ),
912                (
913                    // mysql.runtime_size = 11
914                    [
915                        env_prefix.to_string(),
916                        "mysql".to_uppercase(),
917                        "runtime_size".to_uppercase(),
918                    ]
919                    .join(ENV_VAR_SEP),
920                    Some("11"),
921                ),
922                (
923                    // http.addr = 127.0.0.1:24000
924                    [
925                        env_prefix.to_string(),
926                        "http".to_uppercase(),
927                        "addr".to_uppercase(),
928                    ]
929                    .join(ENV_VAR_SEP),
930                    Some("127.0.0.1:24000"),
931                ),
932                (
933                    // meta_client.metasrv_addrs = 127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003
934                    [
935                        env_prefix.to_string(),
936                        "meta_client".to_uppercase(),
937                        "metasrv_addrs".to_uppercase(),
938                    ]
939                    .join(ENV_VAR_SEP),
940                    Some("127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003"),
941                ),
942            ],
943            || {
944                let command = StartCommand {
945                    config_file: Some(file.path().to_str().unwrap().to_string()),
946                    http_addr: Some("127.0.0.1:14000".to_string()),
947                    env_prefix: env_prefix.to_string(),
948                    ..Default::default()
949                };
950
951                let fe_opts = command.load_options(&Default::default()).unwrap().component;
952
953                // Should be read from env, env > default values.
954                assert_eq!(fe_opts.mysql.runtime_size, 11);
955                assert_eq!(
956                    fe_opts.meta_client.unwrap().metasrv_addrs,
957                    vec![
958                        "127.0.0.1:3001".to_string(),
959                        "127.0.0.1:3002".to_string(),
960                        "127.0.0.1:3003".to_string()
961                    ]
962                );
963
964                // Should be read from config file, config file > env > default values.
965                assert_eq!(fe_opts.mysql.addr, "127.0.0.1:4002");
966
967                // Should be read from cli, cli > config file > env > default values.
968                assert_eq!(fe_opts.http.addr, "127.0.0.1:14000");
969
970                // Should be default value.
971                assert_eq!(fe_opts.grpc.bind_addr, GrpcOptions::default().bind_addr);
972            },
973        );
974    }
975
976    #[test]
977    fn test_parse_grpc_cli_aliases() {
978        let command = StartCommand::try_parse_from([
979            "frontend",
980            "--grpc-bind-addr",
981            "127.0.0.1:14001",
982            "--grpc-server-addr",
983            "10.0.0.1:14001",
984            "--internal-grpc-bind-addr",
985            "127.0.0.1:14010",
986            "--internal-grpc-server-addr",
987            "10.0.0.1:14010",
988        ])
989        .unwrap();
990        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:14001"));
991        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.1:14001"));
992        assert_eq!(
993            command.internal_grpc_bind_addr.as_deref(),
994            Some("127.0.0.1:14010")
995        );
996        assert_eq!(
997            command.internal_grpc_server_addr.as_deref(),
998            Some("10.0.0.1:14010")
999        );
1000
1001        let command = StartCommand::try_parse_from([
1002            "frontend",
1003            "--rpc-bind-addr",
1004            "127.0.0.1:24001",
1005            "--rpc-server-addr",
1006            "10.0.0.2:24001",
1007            "--internal-rpc-bind-addr",
1008            "127.0.0.1:24010",
1009            "--internal-rpc-server-addr",
1010            "10.0.0.2:24010",
1011        ])
1012        .unwrap();
1013        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:24001"));
1014        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.2:24001"));
1015        assert_eq!(
1016            command.internal_grpc_bind_addr.as_deref(),
1017            Some("127.0.0.1:24010")
1018        );
1019        assert_eq!(
1020            command.internal_grpc_server_addr.as_deref(),
1021            Some("10.0.0.2:24010")
1022        );
1023
1024        let command = StartCommand::try_parse_from([
1025            "frontend",
1026            "--rpc-addr",
1027            "127.0.0.1:34001",
1028            "--rpc-hostname",
1029            "10.0.0.3:34001",
1030            "--internal-rpc-addr",
1031            "127.0.0.1:34010",
1032            "--internal-rpc-hostname",
1033            "10.0.0.3:34010",
1034        ])
1035        .unwrap();
1036        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:34001"));
1037        assert_eq!(command.grpc_server_addr.as_deref(), Some("10.0.0.3:34001"));
1038        assert_eq!(
1039            command.internal_grpc_bind_addr.as_deref(),
1040            Some("127.0.0.1:34010")
1041        );
1042        assert_eq!(
1043            command.internal_grpc_server_addr.as_deref(),
1044            Some("10.0.0.3:34010")
1045        );
1046    }
1047
1048    #[test]
1049    fn test_help_uses_grpc_option_names() {
1050        let mut cmd = StartCommand::command();
1051        let mut help = Vec::new();
1052        cmd.write_long_help(&mut help).unwrap();
1053        let help = String::from_utf8(help).unwrap();
1054
1055        assert!(help.contains("--grpc-bind-addr"));
1056        assert!(help.contains("--grpc-server-addr"));
1057        assert!(help.contains("--internal-grpc-bind-addr"));
1058        assert!(help.contains("--internal-grpc-server-addr"));
1059        assert!(!help.contains("--rpc-bind-addr"));
1060        assert!(!help.contains("--rpc-server-addr"));
1061        assert!(!help.contains("--rpc-addr"));
1062        assert!(!help.contains("--rpc-hostname"));
1063        assert!(!help.contains("--internal-rpc-bind-addr"));
1064        assert!(!help.contains("--internal-rpc-server-addr"));
1065        assert!(!help.contains("--internal-rpc-addr"));
1066        assert!(!help.contains("--internal-rpc-hostname"));
1067    }
1068}