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::path::Path;
16use std::sync::Arc;
17use std::time::Duration;
18
19use async_trait::async_trait;
20use cache::{build_fundamental_cache_registry, with_default_composite_cache_registry};
21use catalog::information_extension::DistributedInformationExtension;
22use catalog::kvbackend::{CachedKvBackendBuilder, KvBackendCatalogManagerBuilder, MetaKvBackend};
23use catalog::process_manager::ProcessManager;
24use clap::Parser;
25use client::client_manager::NodeClients;
26use common_base::Plugins;
27use common_config::{Configurable, DEFAULT_DATA_HOME};
28use common_grpc::channel_manager::ChannelConfig;
29use common_meta::cache::{CacheRegistryBuilder, LayeredCacheRegistryBuilder};
30use common_meta::heartbeat::handler::invalidate_table_cache::InvalidateCacheHandler;
31use common_meta::heartbeat::handler::parse_mailbox_message::ParseMailboxMessageHandler;
32use common_meta::heartbeat::handler::HandlerGroupExecutor;
33use common_telemetry::info;
34use common_telemetry::logging::{TracingOptions, DEFAULT_LOGGING_DIR};
35use common_time::timezone::set_default_timezone;
36use common_version::{short_version, verbose_version};
37use frontend::frontend::Frontend;
38use frontend::heartbeat::HeartbeatTask;
39use frontend::instance::builder::FrontendBuilder;
40use frontend::server::Services;
41use meta_client::{MetaClientOptions, MetaClientType};
42use servers::addrs;
43use servers::export_metrics::ExportMetricsTask;
44use servers::grpc::GrpcOptions;
45use servers::tls::{TlsMode, TlsOption};
46use snafu::{OptionExt, ResultExt};
47use tracing_appender::non_blocking::WorkerGuard;
48
49use crate::error::{self, Result};
50use crate::options::{GlobalOptions, GreptimeOptions};
51use crate::{create_resource_limit_metrics, log_versions, maybe_activate_heap_profile, App};
52
53type FrontendOptions = GreptimeOptions<frontend::frontend::FrontendOptions>;
54
55pub struct Instance {
56    frontend: Frontend,
57    // Keep the logging guard to prevent the worker from being dropped.
58    _guard: Vec<WorkerGuard>,
59}
60
61pub const APP_NAME: &str = "greptime-frontend";
62
63impl Instance {
64    pub fn new(frontend: Frontend, _guard: Vec<WorkerGuard>) -> Self {
65        Self { frontend, _guard }
66    }
67
68    pub fn inner(&self) -> &Frontend {
69        &self.frontend
70    }
71
72    pub fn mut_inner(&mut self) -> &mut Frontend {
73        &mut self.frontend
74    }
75}
76
77#[async_trait]
78impl App for Instance {
79    fn name(&self) -> &str {
80        APP_NAME
81    }
82
83    async fn start(&mut self) -> Result<()> {
84        let plugins = self.frontend.instance.plugins().clone();
85        plugins::start_frontend_plugins(plugins)
86            .await
87            .context(error::StartFrontendSnafu)?;
88
89        self.frontend
90            .start()
91            .await
92            .context(error::StartFrontendSnafu)
93    }
94
95    async fn stop(&mut self) -> Result<()> {
96        self.frontend
97            .shutdown()
98            .await
99            .context(error::ShutdownFrontendSnafu)
100    }
101}
102
103#[derive(Parser)]
104pub struct Command {
105    #[clap(subcommand)]
106    pub subcmd: SubCommand,
107}
108
109impl Command {
110    pub async fn build(&self, opts: FrontendOptions) -> Result<Instance> {
111        self.subcmd.build(opts).await
112    }
113
114    pub fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
115        self.subcmd.load_options(global_options)
116    }
117}
118
119#[derive(Parser)]
120pub enum SubCommand {
121    Start(StartCommand),
122}
123
124impl SubCommand {
125    async fn build(&self, opts: FrontendOptions) -> Result<Instance> {
126        match self {
127            SubCommand::Start(cmd) => cmd.build(opts).await,
128        }
129    }
130
131    fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
132        match self {
133            SubCommand::Start(cmd) => cmd.load_options(global_options),
134        }
135    }
136}
137
138#[derive(Debug, Default, Parser)]
139pub struct StartCommand {
140    /// The address to bind the gRPC server.
141    #[clap(long, alias = "rpc-addr")]
142    rpc_bind_addr: Option<String>,
143    /// The address advertised to the metasrv, and used for connections from outside the host.
144    /// If left empty or unset, the server will automatically use the IP address of the first network interface
145    /// on the host, with the same port number as the one specified in `rpc_bind_addr`.
146    #[clap(long, alias = "rpc-hostname")]
147    rpc_server_addr: Option<String>,
148    /// The address to bind the internal gRPC server.
149    #[clap(long, alias = "internal-rpc-addr")]
150    internal_rpc_bind_addr: Option<String>,
151    /// The address advertised to the metasrv, and used for connections from outside the host.
152    /// If left empty or unset, the server will automatically use the IP address of the first network interface
153    /// on the host, with the same port number as the one specified in `internal_rpc_bind_addr`.
154    #[clap(long, alias = "internal-rpc-hostname")]
155    internal_rpc_server_addr: Option<String>,
156    #[clap(long)]
157    http_addr: Option<String>,
158    #[clap(long)]
159    http_timeout: Option<u64>,
160    #[clap(long)]
161    mysql_addr: Option<String>,
162    #[clap(long)]
163    postgres_addr: Option<String>,
164    #[clap(short, long)]
165    pub config_file: Option<String>,
166    #[clap(short, long)]
167    influxdb_enable: Option<bool>,
168    #[clap(long, value_delimiter = ',', num_args = 1..)]
169    metasrv_addrs: Option<Vec<String>>,
170    #[clap(long)]
171    tls_mode: Option<TlsMode>,
172    #[clap(long)]
173    tls_cert_path: Option<String>,
174    #[clap(long)]
175    tls_key_path: Option<String>,
176    #[clap(long)]
177    user_provider: Option<String>,
178    #[clap(long)]
179    disable_dashboard: Option<bool>,
180    #[clap(long, default_value = "GREPTIMEDB_FRONTEND")]
181    pub env_prefix: String,
182}
183
184impl StartCommand {
185    fn load_options(&self, global_options: &GlobalOptions) -> Result<FrontendOptions> {
186        let mut opts = FrontendOptions::load_layered_options(
187            self.config_file.as_deref(),
188            self.env_prefix.as_ref(),
189        )
190        .context(error::LoadLayeredConfigSnafu)?;
191
192        self.merge_with_cli_options(global_options, &mut opts)?;
193
194        Ok(opts)
195    }
196
197    // The precedence order is: cli > config file > environment variables > default values.
198    fn merge_with_cli_options(
199        &self,
200        global_options: &GlobalOptions,
201        opts: &mut FrontendOptions,
202    ) -> Result<()> {
203        let opts = &mut opts.component;
204
205        if let Some(dir) = &global_options.log_dir {
206            opts.logging.dir.clone_from(dir);
207        }
208
209        // If the logging dir is not set, use the default logs dir in the data home.
210        if opts.logging.dir.is_empty() {
211            opts.logging.dir = Path::new(DEFAULT_DATA_HOME)
212                .join(DEFAULT_LOGGING_DIR)
213                .to_string_lossy()
214                .to_string();
215        }
216
217        if global_options.log_level.is_some() {
218            opts.logging.level.clone_from(&global_options.log_level);
219        }
220
221        opts.tracing = TracingOptions {
222            #[cfg(feature = "tokio-console")]
223            tokio_console_addr: global_options.tokio_console_addr.clone(),
224        };
225
226        let tls_opts = TlsOption::new(
227            self.tls_mode.clone(),
228            self.tls_cert_path.clone(),
229            self.tls_key_path.clone(),
230        );
231
232        if let Some(addr) = &self.http_addr {
233            opts.http.addr.clone_from(addr);
234        }
235
236        if let Some(http_timeout) = self.http_timeout {
237            opts.http.timeout = Duration::from_secs(http_timeout)
238        }
239
240        if let Some(disable_dashboard) = self.disable_dashboard {
241            opts.http.disable_dashboard = disable_dashboard;
242        }
243
244        if let Some(addr) = &self.rpc_bind_addr {
245            opts.grpc.bind_addr.clone_from(addr);
246            opts.grpc.tls = tls_opts.clone();
247        }
248
249        if let Some(addr) = &self.rpc_server_addr {
250            opts.grpc.server_addr.clone_from(addr);
251        }
252
253        if let Some(addr) = &self.internal_rpc_bind_addr {
254            if let Some(internal_grpc) = &mut opts.internal_grpc {
255                internal_grpc.bind_addr = addr.to_string();
256            } else {
257                let grpc_options = GrpcOptions {
258                    bind_addr: addr.to_string(),
259                    ..Default::default()
260                };
261
262                opts.internal_grpc = Some(grpc_options);
263            }
264        }
265
266        if let Some(addr) = &self.internal_rpc_server_addr {
267            if let Some(internal_grpc) = &mut opts.internal_grpc {
268                internal_grpc.server_addr = addr.to_string();
269            } else {
270                let grpc_options = GrpcOptions {
271                    server_addr: addr.to_string(),
272                    ..Default::default()
273                };
274                opts.internal_grpc = Some(grpc_options);
275            }
276        }
277
278        if let Some(addr) = &self.mysql_addr {
279            opts.mysql.enable = true;
280            opts.mysql.addr.clone_from(addr);
281            opts.mysql.tls = tls_opts.clone();
282        }
283
284        if let Some(addr) = &self.postgres_addr {
285            opts.postgres.enable = true;
286            opts.postgres.addr.clone_from(addr);
287            opts.postgres.tls = tls_opts;
288        }
289
290        if let Some(enable) = self.influxdb_enable {
291            opts.influxdb.enable = enable;
292        }
293
294        if let Some(metasrv_addrs) = &self.metasrv_addrs {
295            opts.meta_client
296                .get_or_insert_with(MetaClientOptions::default)
297                .metasrv_addrs
298                .clone_from(metasrv_addrs);
299        }
300
301        if let Some(user_provider) = &self.user_provider {
302            opts.user_provider = Some(user_provider.clone());
303        }
304
305        Ok(())
306    }
307
308    async fn build(&self, opts: FrontendOptions) -> Result<Instance> {
309        common_runtime::init_global_runtimes(&opts.runtime);
310
311        let guard = common_telemetry::init_global_logging(
312            APP_NAME,
313            &opts.component.logging,
314            &opts.component.tracing,
315            opts.component.node_id.clone(),
316            Some(&opts.component.slow_query),
317        );
318
319        log_versions(verbose_version(), short_version(), APP_NAME);
320        maybe_activate_heap_profile(&opts.component.memory);
321        create_resource_limit_metrics(APP_NAME);
322
323        info!("Frontend start command: {:#?}", self);
324        info!("Frontend options: {:#?}", opts);
325
326        let plugin_opts = opts.plugins;
327        let mut opts = opts.component;
328        opts.grpc.detect_server_addr();
329        let mut plugins = Plugins::new();
330        plugins::setup_frontend_plugins(&mut plugins, &plugin_opts, &opts)
331            .await
332            .context(error::StartFrontendSnafu)?;
333
334        set_default_timezone(opts.default_timezone.as_deref()).context(error::InitTimezoneSnafu)?;
335
336        let meta_client_options = opts
337            .meta_client
338            .as_ref()
339            .context(error::MissingConfigSnafu {
340                msg: "'meta_client'",
341            })?;
342
343        let cache_max_capacity = meta_client_options.metadata_cache_max_capacity;
344        let cache_ttl = meta_client_options.metadata_cache_ttl;
345        let cache_tti = meta_client_options.metadata_cache_tti;
346
347        let meta_client = meta_client::create_meta_client(
348            MetaClientType::Frontend,
349            meta_client_options,
350            Some(&plugins),
351            None,
352        )
353        .await
354        .context(error::MetaClientInitSnafu)?;
355
356        // TODO(discord9): add helper function to ease the creation of cache registry&such
357        let cached_meta_backend =
358            CachedKvBackendBuilder::new(Arc::new(MetaKvBackend::new(meta_client.clone())))
359                .cache_max_capacity(cache_max_capacity)
360                .cache_ttl(cache_ttl)
361                .cache_tti(cache_tti)
362                .build();
363        let cached_meta_backend = Arc::new(cached_meta_backend);
364
365        // Builds cache registry
366        let layered_cache_builder = LayeredCacheRegistryBuilder::default().add_cache_registry(
367            CacheRegistryBuilder::default()
368                .add_cache(cached_meta_backend.clone())
369                .build(),
370        );
371        let fundamental_cache_registry =
372            build_fundamental_cache_registry(Arc::new(MetaKvBackend::new(meta_client.clone())));
373        let layered_cache_registry = Arc::new(
374            with_default_composite_cache_registry(
375                layered_cache_builder.add_cache_registry(fundamental_cache_registry),
376            )
377            .context(error::BuildCacheRegistrySnafu)?
378            .build(),
379        );
380
381        let information_extension =
382            Arc::new(DistributedInformationExtension::new(meta_client.clone()));
383
384        let process_manager = Arc::new(ProcessManager::new(
385            addrs::resolve_addr(&opts.grpc.bind_addr, Some(&opts.grpc.server_addr)),
386            Some(meta_client.clone()),
387        ));
388
389        let builder = KvBackendCatalogManagerBuilder::new(
390            information_extension,
391            cached_meta_backend.clone(),
392            layered_cache_registry.clone(),
393        )
394        .with_process_manager(process_manager.clone());
395        #[cfg(feature = "enterprise")]
396        let builder = if let Some(factories) = plugins.get() {
397            builder.with_extra_information_table_factories(factories)
398        } else {
399            builder
400        };
401        let catalog_manager = builder.build();
402
403        let executor = HandlerGroupExecutor::new(vec![
404            Arc::new(ParseMailboxMessageHandler),
405            Arc::new(InvalidateCacheHandler::new(layered_cache_registry.clone())),
406        ]);
407
408        let heartbeat_task = HeartbeatTask::new(
409            &opts,
410            meta_client.clone(),
411            opts.heartbeat.clone(),
412            Arc::new(executor),
413        );
414        let heartbeat_task = Some(heartbeat_task);
415
416        // frontend to datanode need not timeout.
417        // Some queries are expected to take long time.
418        let mut channel_config = ChannelConfig {
419            timeout: None,
420            tcp_nodelay: opts.datanode.client.tcp_nodelay,
421            connect_timeout: Some(opts.datanode.client.connect_timeout),
422            ..Default::default()
423        };
424        if opts.grpc.flight_compression.transport_compression() {
425            channel_config.accept_compression = true;
426            channel_config.send_compression = true;
427        }
428        let client = NodeClients::new(channel_config);
429
430        let instance = FrontendBuilder::new(
431            opts.clone(),
432            cached_meta_backend.clone(),
433            layered_cache_registry.clone(),
434            catalog_manager,
435            Arc::new(client),
436            meta_client,
437            process_manager,
438        )
439        .with_plugin(plugins.clone())
440        .with_local_cache_invalidator(layered_cache_registry)
441        .try_build()
442        .await
443        .context(error::StartFrontendSnafu)?;
444        let instance = Arc::new(instance);
445
446        let export_metrics_task = ExportMetricsTask::try_new(&opts.export_metrics, Some(&plugins))
447            .context(error::ServersSnafu)?;
448
449        let servers = Services::new(opts, instance.clone(), plugins)
450            .build()
451            .context(error::StartFrontendSnafu)?;
452
453        let frontend = Frontend {
454            instance,
455            servers,
456            heartbeat_task,
457            export_metrics_task,
458        };
459
460        Ok(Instance::new(frontend, guard))
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use std::io::Write;
467    use std::time::Duration;
468
469    use auth::{Identity, Password, UserProviderRef};
470    use common_base::readable_size::ReadableSize;
471    use common_config::ENV_VAR_SEP;
472    use common_test_util::temp_dir::create_named_temp_file;
473    use servers::grpc::GrpcOptions;
474    use servers::http::HttpOptions;
475
476    use super::*;
477    use crate::options::GlobalOptions;
478
479    #[test]
480    fn test_try_from_start_command() {
481        let command = StartCommand {
482            http_addr: Some("127.0.0.1:1234".to_string()),
483            mysql_addr: Some("127.0.0.1:5678".to_string()),
484            postgres_addr: Some("127.0.0.1:5432".to_string()),
485            internal_rpc_bind_addr: Some("127.0.0.1:4010".to_string()),
486            internal_rpc_server_addr: Some("10.0.0.24:4010".to_string()),
487            influxdb_enable: Some(false),
488            disable_dashboard: Some(false),
489            ..Default::default()
490        };
491
492        let opts = command.load_options(&Default::default()).unwrap().component;
493
494        assert_eq!(opts.http.addr, "127.0.0.1:1234");
495        assert_eq!(ReadableSize::mb(64), opts.http.body_limit);
496        assert_eq!(opts.mysql.addr, "127.0.0.1:5678");
497        assert_eq!(opts.postgres.addr, "127.0.0.1:5432");
498
499        let internal_grpc = opts.internal_grpc.as_ref().unwrap();
500        assert_eq!(internal_grpc.bind_addr, "127.0.0.1:4010");
501        assert_eq!(internal_grpc.server_addr, "10.0.0.24:4010");
502
503        let default_opts = FrontendOptions::default().component;
504
505        assert_eq!(opts.grpc.bind_addr, default_opts.grpc.bind_addr);
506        assert!(opts.mysql.enable);
507        assert_eq!(opts.mysql.runtime_size, default_opts.mysql.runtime_size);
508        assert!(opts.postgres.enable);
509        assert_eq!(
510            opts.postgres.runtime_size,
511            default_opts.postgres.runtime_size
512        );
513        assert!(opts.opentsdb.enable);
514
515        assert!(!opts.influxdb.enable);
516    }
517
518    #[test]
519    fn test_read_from_config_file() {
520        let mut file = create_named_temp_file();
521        let toml_str = r#"
522            [http]
523            addr = "127.0.0.1:4000"
524            timeout = "0s"
525            body_limit = "2GB"
526
527            [opentsdb]
528            enable = false
529
530            [logging]
531            level = "debug"
532            dir = "./greptimedb_data/test/logs"
533        "#;
534        write!(file, "{}", toml_str).unwrap();
535
536        let command = StartCommand {
537            config_file: Some(file.path().to_str().unwrap().to_string()),
538            disable_dashboard: Some(false),
539            ..Default::default()
540        };
541
542        let fe_opts = command.load_options(&Default::default()).unwrap().component;
543
544        assert_eq!("127.0.0.1:4000".to_string(), fe_opts.http.addr);
545        assert_eq!(Duration::from_secs(0), fe_opts.http.timeout);
546
547        assert_eq!(ReadableSize::gb(2), fe_opts.http.body_limit);
548
549        assert_eq!("debug", fe_opts.logging.level.as_ref().unwrap());
550        assert_eq!(
551            "./greptimedb_data/test/logs".to_string(),
552            fe_opts.logging.dir
553        );
554        assert!(!fe_opts.opentsdb.enable);
555    }
556
557    #[tokio::test]
558    async fn test_try_from_start_command_to_anymap() {
559        let fe_opts = frontend::frontend::FrontendOptions {
560            http: HttpOptions {
561                disable_dashboard: false,
562                ..Default::default()
563            },
564            user_provider: Some("static_user_provider:cmd:test=test".to_string()),
565            ..Default::default()
566        };
567
568        let mut plugins = Plugins::new();
569        plugins::setup_frontend_plugins(&mut plugins, &[], &fe_opts)
570            .await
571            .unwrap();
572
573        let provider = plugins.get::<UserProviderRef>().unwrap();
574        let result = provider
575            .authenticate(
576                Identity::UserId("test", None),
577                Password::PlainText("test".to_string().into()),
578            )
579            .await;
580        let _ = result.unwrap();
581    }
582
583    #[test]
584    fn test_load_log_options_from_cli() {
585        let cmd = StartCommand {
586            disable_dashboard: Some(false),
587            ..Default::default()
588        };
589
590        let options = cmd
591            .load_options(&GlobalOptions {
592                log_dir: Some("./greptimedb_data/test/logs".to_string()),
593                log_level: Some("debug".to_string()),
594
595                #[cfg(feature = "tokio-console")]
596                tokio_console_addr: None,
597            })
598            .unwrap()
599            .component;
600
601        let logging_opt = options.logging;
602        assert_eq!("./greptimedb_data/test/logs", logging_opt.dir);
603        assert_eq!("debug", logging_opt.level.as_ref().unwrap());
604    }
605
606    #[test]
607    fn test_config_precedence_order() {
608        let mut file = create_named_temp_file();
609        let toml_str = r#"
610            [http]
611            addr = "127.0.0.1:4000"
612
613            [meta_client]
614            timeout = "3s"
615            connect_timeout = "5s"
616            tcp_nodelay = true
617
618            [mysql]
619            addr = "127.0.0.1:4002"
620        "#;
621        write!(file, "{}", toml_str).unwrap();
622
623        let env_prefix = "FRONTEND_UT";
624        temp_env::with_vars(
625            [
626                (
627                    // mysql.addr = 127.0.0.1:14002
628                    [
629                        env_prefix.to_string(),
630                        "mysql".to_uppercase(),
631                        "addr".to_uppercase(),
632                    ]
633                    .join(ENV_VAR_SEP),
634                    Some("127.0.0.1:14002"),
635                ),
636                (
637                    // mysql.runtime_size = 11
638                    [
639                        env_prefix.to_string(),
640                        "mysql".to_uppercase(),
641                        "runtime_size".to_uppercase(),
642                    ]
643                    .join(ENV_VAR_SEP),
644                    Some("11"),
645                ),
646                (
647                    // http.addr = 127.0.0.1:24000
648                    [
649                        env_prefix.to_string(),
650                        "http".to_uppercase(),
651                        "addr".to_uppercase(),
652                    ]
653                    .join(ENV_VAR_SEP),
654                    Some("127.0.0.1:24000"),
655                ),
656                (
657                    // meta_client.metasrv_addrs = 127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003
658                    [
659                        env_prefix.to_string(),
660                        "meta_client".to_uppercase(),
661                        "metasrv_addrs".to_uppercase(),
662                    ]
663                    .join(ENV_VAR_SEP),
664                    Some("127.0.0.1:3001,127.0.0.1:3002,127.0.0.1:3003"),
665                ),
666            ],
667            || {
668                let command = StartCommand {
669                    config_file: Some(file.path().to_str().unwrap().to_string()),
670                    http_addr: Some("127.0.0.1:14000".to_string()),
671                    env_prefix: env_prefix.to_string(),
672                    ..Default::default()
673                };
674
675                let fe_opts = command.load_options(&Default::default()).unwrap().component;
676
677                // Should be read from env, env > default values.
678                assert_eq!(fe_opts.mysql.runtime_size, 11);
679                assert_eq!(
680                    fe_opts.meta_client.unwrap().metasrv_addrs,
681                    vec![
682                        "127.0.0.1:3001".to_string(),
683                        "127.0.0.1:3002".to_string(),
684                        "127.0.0.1:3003".to_string()
685                    ]
686                );
687
688                // Should be read from config file, config file > env > default values.
689                assert_eq!(fe_opts.mysql.addr, "127.0.0.1:4002");
690
691                // Should be read from cli, cli > config file > env > default values.
692                assert_eq!(fe_opts.http.addr, "127.0.0.1:14000");
693
694                // Should be default value.
695                assert_eq!(fe_opts.grpc.bind_addr, GrpcOptions::default().bind_addr);
696            },
697        );
698    }
699}