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