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