Skip to main content

cmd/
standalone.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::net::SocketAddr;
17use std::path::Path;
18use std::sync::Arc;
19use std::{fs, path};
20
21use async_trait::async_trait;
22use cache::{build_fundamental_cache_registry, with_default_composite_cache_registry};
23use catalog::CatalogManagerRef;
24use catalog::information_schema::InformationExtensionRef;
25use catalog::kvbackend::{CatalogManagerConfiguratorRef, KvBackendCatalogManagerBuilder};
26use catalog::process_manager::ProcessManager;
27use clap::Parser;
28use common_base::Plugins;
29use common_catalog::consts::{MIN_USER_FLOW_ID, MIN_USER_TABLE_ID};
30use common_config::{Configurable, metadata_store_dir};
31use common_datasource::object_store::{LocalFileAccess, configured_local_path};
32use common_error::ext::BoxedError;
33use common_meta::DatanodeId;
34use common_meta::cache::{LayeredCacheRegistryBuilder, LayeredCacheRegistryRef};
35use common_meta::ddl::flow_meta::FlowMetadataAllocator;
36use common_meta::ddl::table_meta::TableMetadataAllocator;
37use common_meta::ddl::{DdlContext, NoopRegionFailureDetectorControl};
38use common_meta::ddl_manager::{DdlManager, DdlManagerConfiguratorRef, DdlManagerRef};
39use common_meta::key::flow::FlowMetadataManager;
40use common_meta::key::{TableMetadataManager, TableMetadataManagerRef};
41use common_meta::kv_backend::KvBackendRef;
42use common_meta::node_manager::{FlownodeRef, NodeManagerRef};
43use common_meta::procedure_executor::{LocalProcedureExecutor, ProcedureExecutorRef};
44use common_meta::region_keeper::MemoryRegionKeeper;
45use common_meta::region_registry::LeaderRegionRegistry;
46use common_meta::sequence::{Sequence, SequenceBuilder};
47use common_meta::wal_provider::{WalProviderRef, build_wal_provider};
48use common_options::plugin_options::StandaloneFlag;
49use common_procedure::ProcedureManagerRef;
50use common_query::prelude::set_default_prefix;
51use common_telemetry::info;
52use common_telemetry::logging::{DEFAULT_LOGGING_DIR, TracingOptions};
53use common_time::timezone::set_default_timezone;
54use common_version::{short_version, verbose_version};
55use datanode::config::{DatanodeOptions, StorageConfig};
56use datanode::datanode::{Datanode, DatanodeBuilder};
57use datanode::region_server::RegionServer;
58use flow::{
59    FlowDualEngineRef, FlownodeBuilder, FlownodeInstance, FlownodeOptions, FrontendClient,
60    FrontendInvoker, GrpcQueryHandlerWithBoxedError,
61};
62use frontend::frontend::Frontend;
63use frontend::instance::builder::FrontendBuilder;
64use frontend::server::Services;
65use meta_srv::metasrv::{FLOW_ID_SEQ, TABLE_ID_SEQ};
66use plugins::PluginOptions;
67use plugins::frontend::context::{
68    CatalogManagerConfigureContext, StandaloneCatalogManagerConfigureContext,
69};
70use plugins::standalone::context::DdlManagerConfigureContext;
71use servers::tls::{TlsMode, TlsOption, merge_tls_option};
72use snafu::{OptionExt, ResultExt};
73use standalone::options::StandaloneOptions;
74use standalone::{
75    StandaloneDatanodeManager, StandaloneInformationExtension,
76    StandaloneRepartitionProcedureFactory,
77};
78use tracing_appender::non_blocking::WorkerGuard;
79
80use crate::error::{OtherSnafu, Result, StartFlownodeSnafu};
81use crate::options::{GlobalOptions, GreptimeOptions};
82use crate::{App, create_resource_limit_metrics, error, log_versions, maybe_activate_heap_profile};
83
84pub const APP_NAME: &str = "greptime-standalone";
85
86fn standalone_local_file_access(
87    storage: &StorageConfig,
88) -> common_datasource::error::Result<LocalFileAccess> {
89    let data_home = configured_local_path(&storage.data_home)?;
90    let copy_root = match &storage.copy_root {
91        Some(root) => configured_local_path(root)?.with_context(|| {
92            common_datasource::error::InvalidLocalFileRootConfigSnafu {
93                root: root.clone(),
94                reason: "copy_root must be a local path or file URL".to_string(),
95            }
96        })?,
97        None => {
98            let Some(data_home) = &data_home else {
99                info!(
100                    "SQL access to local files is disabled because storage.data_home is not a local path and storage.copy_root is unset"
101                );
102                return Ok(LocalFileAccess::Disabled);
103            };
104            data_home.join("copy")
105        }
106    };
107
108    let access = LocalFileAccess::sandboxed(&copy_root)?;
109    if let Some(data_home) = data_home {
110        let canonical_data_home = data_home.canonicalize().with_context(|_| {
111            common_datasource::error::InvalidLocalFileRootSnafu {
112                root: data_home.display().to_string(),
113            }
114        })?;
115        let canonical_copy_root = access.sandbox_root().with_context(|| {
116            common_datasource::error::InvalidLocalFileRootConfigSnafu {
117                root: copy_root.display().to_string(),
118                reason: "sandboxed local file access has no root".to_string(),
119            }
120        })?;
121        let default_copy_root = canonical_data_home.join("copy");
122        let exposes_internal_files = canonical_data_home.starts_with(canonical_copy_root)
123            || (canonical_copy_root.starts_with(&canonical_data_home)
124                && !canonical_copy_root.starts_with(default_copy_root));
125        if exposes_internal_files {
126            return common_datasource::error::InvalidLocalFileRootConfigSnafu {
127                root: copy_root.display().to_string(),
128                reason: "copy_root must not expose files in data_home outside data_home/copy"
129                    .to_string(),
130            }
131            .fail();
132        }
133    }
134
135    Ok(access)
136}
137
138#[derive(Parser)]
139pub struct Command {
140    #[clap(subcommand)]
141    subcmd: SubCommand,
142}
143
144impl Command {
145    pub async fn build(&self, opts: GreptimeOptions<StandaloneOptions>) -> Result<Instance> {
146        self.subcmd.build(opts).await
147    }
148
149    pub fn load_options(
150        &self,
151        global_options: &GlobalOptions,
152    ) -> Result<GreptimeOptions<StandaloneOptions>> {
153        self.subcmd.load_options(global_options)
154    }
155}
156
157#[derive(Parser)]
158enum SubCommand {
159    Start(StartCommand),
160}
161
162impl SubCommand {
163    async fn build(&self, opts: GreptimeOptions<StandaloneOptions>) -> Result<Instance> {
164        match self {
165            SubCommand::Start(cmd) => cmd.build(opts).await,
166        }
167    }
168
169    fn load_options(
170        &self,
171        global_options: &GlobalOptions,
172    ) -> Result<GreptimeOptions<StandaloneOptions>> {
173        match self {
174            SubCommand::Start(cmd) => cmd.load_options(global_options),
175        }
176    }
177}
178
179pub struct Instance {
180    datanode: Datanode,
181    frontend: Frontend,
182    flownode: FlownodeInstance,
183    procedure_manager: ProcedureManagerRef,
184    leader_services_controller: Box<dyn StandaloneLeaderServicesController>,
185    leader_services_context: LeaderServicesContext,
186    // Keep the logging guard to prevent the worker from being dropped.
187    _guard: Vec<WorkerGuard>,
188}
189
190impl Instance {
191    /// Find the socket addr of a server by its `name`.
192    pub fn server_addr(&self, name: &str) -> Option<SocketAddr> {
193        self.frontend.server_handlers().addr(name)
194    }
195
196    /// Get the mutable Frontend component of this Standalone instance for externally modification
197    /// by others (might not be in this code base, so don't delete this function).
198    pub fn mut_frontend(&mut self) -> &mut Frontend {
199        &mut self.frontend
200    }
201
202    /// Get the Datanode component of this Standalone instance for externally usage
203    /// by others (might not be in this code base, so don't delete this function).
204    pub fn datanode(&self) -> &Datanode {
205        &self.datanode
206    }
207}
208
209#[async_trait]
210impl App for Instance {
211    fn name(&self) -> &str {
212        APP_NAME
213    }
214
215    async fn start(&mut self) -> Result<()> {
216        self.datanode.start_telemetry();
217
218        self.leader_services_controller
219            .start(self.leader_services_context.clone())
220            .await?;
221
222        plugins::start_frontend_plugins(&self.frontend.instance)
223            .await
224            .context(error::StartFrontendSnafu)?;
225
226        self.frontend
227            .start()
228            .await
229            .context(error::StartFrontendSnafu)?;
230
231        self.flownode.start().await.context(StartFlownodeSnafu)?;
232
233        Ok(())
234    }
235
236    async fn stop(&mut self) -> Result<()> {
237        self.frontend
238            .shutdown()
239            .await
240            .context(error::ShutdownFrontendSnafu)?;
241
242        self.leader_services_controller
243            .stop(
244                self.procedure_manager.clone(),
245                self.datanode.region_server(),
246            )
247            .await?;
248
249        self.datanode
250            .shutdown()
251            .await
252            .context(error::ShutdownDatanodeSnafu)?;
253
254        self.flownode
255            .shutdown()
256            .await
257            .context(error::ShutdownFlownodeSnafu)?;
258
259        info!("Datanode instance stopped.");
260
261        Ok(())
262    }
263}
264
265#[derive(Debug, Default, Parser)]
266pub struct StartCommand {
267    #[clap(long)]
268    http_addr: Option<String>,
269    #[clap(long = "grpc-bind-addr", alias = "rpc-bind-addr", alias = "rpc-addr")]
270    grpc_bind_addr: Option<String>,
271    #[clap(long)]
272    mysql_addr: Option<String>,
273    #[clap(long)]
274    postgres_addr: Option<String>,
275    #[clap(short, long)]
276    influxdb_enable: bool,
277    #[clap(short, long)]
278    pub config_file: Option<String>,
279    #[clap(long)]
280    tls_mode: Option<TlsMode>,
281    #[clap(long)]
282    tls_cert_path: Option<String>,
283    #[clap(long)]
284    tls_key_path: Option<String>,
285    #[clap(long)]
286    tls_watch: bool,
287    #[clap(long)]
288    user_provider: Option<String>,
289    #[clap(long, default_value = "GREPTIMEDB_STANDALONE")]
290    pub env_prefix: String,
291    /// The working home directory of this standalone instance.
292    #[clap(long)]
293    data_home: Option<String>,
294}
295
296impl StartCommand {
297    /// Load the GreptimeDB options from various sources (command line, config file or env).
298    pub fn load_options(
299        &self,
300        global_options: &GlobalOptions,
301    ) -> Result<GreptimeOptions<StandaloneOptions>> {
302        let mut opts = GreptimeOptions::<StandaloneOptions>::load_layered_options(
303            self.config_file.as_deref(),
304            self.env_prefix.as_ref(),
305        )
306        .context(error::LoadLayeredConfigSnafu)?;
307
308        self.merge_with_cli_options(global_options, &mut opts.component)?;
309        opts.component.sanitize();
310
311        Ok(opts)
312    }
313
314    // The precedence order is: cli > config file > environment variables > default values.
315    pub fn merge_with_cli_options(
316        &self,
317        global_options: &GlobalOptions,
318        opts: &mut StandaloneOptions,
319    ) -> Result<()> {
320        if let Some(dir) = &global_options.log_dir {
321            opts.logging.dir.clone_from(dir);
322        }
323
324        if global_options.log_level.is_some() {
325            opts.logging.level.clone_from(&global_options.log_level);
326        }
327
328        opts.tracing = TracingOptions {
329            #[cfg(feature = "tokio-console")]
330            tokio_console_addr: global_options.tokio_console_addr.clone(),
331        };
332
333        let tls_opts = TlsOption::new(
334            self.tls_mode,
335            self.tls_cert_path.clone(),
336            self.tls_key_path.clone(),
337            self.tls_watch,
338        );
339
340        if let Some(addr) = &self.http_addr {
341            opts.http.addr.clone_from(addr);
342        }
343
344        if let Some(data_home) = &self.data_home {
345            opts.storage.data_home.clone_from(data_home);
346        }
347
348        // If the logging dir is not set, use the default logs dir in the data home.
349        if opts.logging.dir.is_empty() {
350            opts.logging.dir = Path::new(&opts.storage.data_home)
351                .join(DEFAULT_LOGGING_DIR)
352                .to_string_lossy()
353                .to_string();
354        }
355
356        if let Some(addr) = &self.grpc_bind_addr {
357            // frontend grpc addr conflict with datanode default grpc addr
358            let datanode_grpc_addr = DatanodeOptions::default().grpc.bind_addr;
359            if addr.eq(&datanode_grpc_addr) {
360                return error::IllegalConfigSnafu {
361                    msg: format!(
362                        "gRPC listen address conflicts with datanode reserved gRPC addr: {datanode_grpc_addr}",
363                    ),
364                }.fail();
365            }
366            opts.grpc.bind_addr.clone_from(addr);
367            opts.grpc.tls = merge_tls_option(&opts.grpc.tls, tls_opts.clone());
368        }
369
370        if let Some(addr) = &self.mysql_addr {
371            opts.mysql.enable = true;
372            opts.mysql.addr.clone_from(addr);
373            opts.mysql.tls = merge_tls_option(&opts.mysql.tls, tls_opts.clone());
374        }
375
376        if let Some(addr) = &self.postgres_addr {
377            opts.postgres.enable = true;
378            opts.postgres.addr.clone_from(addr);
379            opts.postgres.tls = merge_tls_option(&opts.postgres.tls, tls_opts.clone());
380        }
381
382        if self.influxdb_enable {
383            opts.influxdb.enable = self.influxdb_enable;
384        }
385
386        if let Some(user_provider) = &self.user_provider {
387            opts.user_provider = Some(user_provider.clone());
388        }
389
390        Ok(())
391    }
392
393    #[allow(unreachable_code)]
394    #[allow(unused_variables)]
395    #[allow(clippy::diverging_sub_expression)]
396    /// Build GreptimeDB instance with the loaded options.
397    pub async fn build(&self, opts: GreptimeOptions<StandaloneOptions>) -> Result<Instance> {
398        common_runtime::init_global_runtimes(&opts.runtime);
399
400        let guard = common_telemetry::init_global_logging(
401            APP_NAME,
402            &opts.component.logging,
403            &opts.component.tracing,
404            None,
405            Some(&opts.component.slow_query),
406        );
407
408        crate::options::flush_dropped_plugin_warnings();
409        log_versions(verbose_version(), short_version(), APP_NAME);
410        maybe_activate_heap_profile(&opts.component.memory);
411        create_resource_limit_metrics(APP_NAME);
412
413        info!("Standalone start command: {:#?}", self);
414        info!("Standalone options: {opts:#?}");
415
416        let (mut instance, _) =
417            Self::build_with(opts.component, opts.plugins, InstanceCreator::default()).await?;
418        instance._guard.extend(guard);
419        Ok(instance)
420    }
421
422    pub async fn build_with(
423        mut opts: StandaloneOptions,
424        plugin_opts: Vec<PluginOptions>,
425        creator: InstanceCreator,
426    ) -> Result<(Instance, InstanceCreatorResult)> {
427        let mut plugins = Plugins::new();
428        plugins.insert(StandaloneFlag);
429        set_default_prefix(opts.default_column_prefix.as_deref())
430            .map_err(BoxedError::new)
431            .context(error::BuildCliSnafu)?;
432
433        opts.grpc.detect_server_addr();
434        let fe_opts = opts.frontend_options();
435        let dn_opts = opts.datanode_options();
436        let node_id = dn_opts.node_id;
437        let init_regions_parallelism = dn_opts.init_regions_parallelism;
438
439        plugins::setup_frontend_plugins_pre_build(&mut plugins, &plugin_opts, &fe_opts, None)
440            .await
441            .context(error::StartFrontendSnafu)?;
442
443        plugins::setup_datanode_plugins_pre_build(&mut plugins, &plugin_opts, &dn_opts)
444            .await
445            .context(error::StartDatanodeSnafu)?;
446
447        set_default_timezone(fe_opts.default_timezone.as_deref())
448            .context(error::InitTimezoneSnafu)?;
449
450        let data_home = &dn_opts.storage.data_home;
451        // Ensure the data_home directory exists.
452        fs::create_dir_all(path::Path::new(data_home))
453            .context(error::CreateDirSnafu { dir: data_home })?;
454        let local_file_access = standalone_local_file_access(&dn_opts.storage)
455            .map_err(BoxedError::new)
456            .context(OtherSnafu)?;
457
458        let metadata_dir = metadata_store_dir(data_home);
459        let kv_backend = creator
460            .metadata_kv_backend_creator
461            .create(metadata_dir, &opts)
462            .await?;
463        let (procedure_manager, event_recorder_handle) =
464            standalone::build_procedure_manager(kv_backend.clone(), opts.procedure);
465
466        plugins::setup_standalone_plugins(&mut plugins, &plugin_opts, &opts, kv_backend.clone())
467            .await
468            .context(error::SetupStandalonePluginsSnafu)?;
469
470        // Builds cache registry
471        let layered_cache_builder = LayeredCacheRegistryBuilder::default();
472        let fundamental_cache_registry = build_fundamental_cache_registry(kv_backend.clone());
473        let mut layered_cache_builder = with_default_composite_cache_registry(
474            layered_cache_builder.add_cache_registry(fundamental_cache_registry),
475        )
476        .context(error::BuildCacheRegistrySnafu)?;
477
478        if let Some(plugin_cache_builder) = plugins::standalone::configure_cache_registry(&plugins)
479        {
480            layered_cache_builder =
481                layered_cache_builder.add_cache_registry(plugin_cache_builder.build());
482        }
483
484        let layered_cache_registry = Arc::new(layered_cache_builder.build());
485
486        let mut builder = DatanodeBuilder::new(dn_opts, plugins.clone(), kv_backend.clone());
487        builder.with_cache_registry(layered_cache_registry.clone());
488        builder.with_local_file_access(local_file_access.clone());
489        if let Some(writable) = creator.open_regions_writable_override {
490            builder.with_open_regions_writable_override(writable);
491        }
492
493        plugins::setup_datanode_plugins_post_build(&mut plugins, &plugin_opts, &builder)
494            .await
495            .context(error::StartDatanodeSnafu)?;
496        builder.set_plugins(plugins.clone());
497
498        let datanode = builder.build().await.context(error::StartDatanodeSnafu)?;
499
500        let information_extension = Arc::new(StandaloneInformationExtension::new(
501            datanode.region_server(),
502            procedure_manager.clone(),
503        ));
504
505        plugins.insert::<InformationExtensionRef>(information_extension.clone());
506
507        let process_manager = Arc::new(ProcessManager::new(opts.grpc.server_addr.clone(), None));
508
509        // for standalone not use grpc, but get a handler to frontend grpc client without
510        // actually make a connection
511        let (frontend_client, frontend_instance_handler) =
512            FrontendClient::from_empty_grpc_handler(opts.query.clone());
513        let frontend_client = Arc::new(frontend_client);
514
515        let builder = KvBackendCatalogManagerBuilder::new(
516            information_extension.clone(),
517            kv_backend.clone(),
518            layered_cache_registry.clone(),
519        )
520        .with_procedure_manager(procedure_manager.clone())
521        .with_process_manager(process_manager.clone());
522        let builder = if let Some(configurator) =
523            plugins.get::<CatalogManagerConfiguratorRef<CatalogManagerConfigureContext>>()
524        {
525            let ctx = StandaloneCatalogManagerConfigureContext {
526                fe_client: frontend_client.clone(),
527            };
528            let ctx = CatalogManagerConfigureContext::Standalone(ctx);
529            configurator
530                .configure(builder, ctx)
531                .await
532                .context(OtherSnafu)?
533        } else {
534            builder
535        };
536        let catalog_manager = builder.build();
537
538        let table_metadata_manager =
539            Self::create_table_metadata_manager(kv_backend.clone()).await?;
540
541        let flow_metadata_manager = Arc::new(FlowMetadataManager::new(kv_backend.clone()));
542        let flownode_options = FlownodeOptions {
543            flow: opts.flow.clone(),
544            ..Default::default()
545        };
546
547        let mut flow_builder = FlownodeBuilder::new(
548            flownode_options,
549            plugins.clone(),
550            table_metadata_manager.clone(),
551            catalog_manager.clone(),
552            flow_metadata_manager.clone(),
553            frontend_client.clone(),
554        );
555
556        plugins::setup_flownode_plugins_post_build(&mut plugins, &plugin_opts, &flow_builder)
557            .await
558            .context(error::StartFlownodeSnafu)?;
559        flow_builder.set_plugins(plugins.clone());
560
561        let flownode = flow_builder
562            .build()
563            .await
564            .map_err(BoxedError::new)
565            .context(error::OtherSnafu)?;
566        let flow_engine = flownode.flow_engine();
567
568        // set the ref to query for the local flow state
569        {
570            information_extension
571                .set_flow_engine(flow_engine.clone())
572                .await;
573        }
574
575        let node_manager = creator
576            .node_manager_creator
577            .create(&kv_backend, datanode.region_server(), flow_engine.clone())
578            .await?;
579
580        let table_id_allocator = creator.table_id_allocator_creator.create(&kv_backend);
581        let flow_id_sequence = Arc::new(
582            SequenceBuilder::new(FLOW_ID_SEQ, kv_backend.clone())
583                .initial(MIN_USER_FLOW_ID as u64)
584                .step(10)
585                .build(),
586        );
587        let kafka_options = opts
588            .wal
589            .clone()
590            .try_into()
591            .context(error::InvalidWalProviderSnafu)?;
592        let wal_provider = build_wal_provider(&kafka_options, kv_backend.clone())
593            .await
594            .context(error::BuildWalProviderSnafu)?;
595        let wal_provider = Arc::new(wal_provider);
596        let table_metadata_allocator = Arc::new(TableMetadataAllocator::new(
597            table_id_allocator.clone(),
598            wal_provider.clone(),
599        ));
600        let flow_metadata_allocator = Arc::new(FlowMetadataAllocator::with_noop_peer_allocator(
601            flow_id_sequence,
602        ));
603
604        let ddl_context = DdlContext {
605            node_manager: node_manager.clone(),
606            cache_invalidator: layered_cache_registry.clone(),
607            memory_region_keeper: Arc::new(MemoryRegionKeeper::default()),
608            leader_region_registry: Arc::new(LeaderRegionRegistry::default()),
609            table_metadata_manager: table_metadata_manager.clone(),
610            table_metadata_allocator: table_metadata_allocator.clone(),
611            flow_metadata_manager: flow_metadata_manager.clone(),
612            flow_metadata_allocator: flow_metadata_allocator.clone(),
613            region_failure_detector_controller: Arc::new(NoopRegionFailureDetectorControl),
614            soft_drop_enabled: false,
615            soft_drop_retention: None,
616            create_database_metadata_committer: None,
617        };
618
619        let ddl_manager = DdlManager::new(
620            ddl_context,
621            procedure_manager.clone(),
622            Arc::new(StandaloneRepartitionProcedureFactory),
623        );
624
625        let ddl_manager = if let Some(configurator) =
626            plugins.get::<DdlManagerConfiguratorRef<DdlManagerConfigureContext>>()
627        {
628            let ctx = DdlManagerConfigureContext {
629                kv_backend: kv_backend.clone(),
630                fe_client: frontend_client.clone(),
631                catalog_manager: catalog_manager.clone(),
632            };
633            configurator
634                .configure(ddl_manager, ctx)
635                .await
636                .context(OtherSnafu)?
637        } else {
638            ddl_manager
639        };
640        ddl_manager
641            .register_loaders()
642            .context(error::InitDdlManagerSnafu)?;
643
644        let procedure_executor = creator
645            .procedure_executor_creator
646            .create(Arc::new(ddl_manager), procedure_manager.clone())
647            .await?;
648
649        let fe_instance = FrontendBuilder::new(
650            fe_opts.clone(),
651            kv_backend.clone(),
652            layered_cache_registry.clone(),
653            catalog_manager.clone(),
654            node_manager.clone(),
655            procedure_executor.clone(),
656            process_manager,
657        )
658        .with_local_file_access(local_file_access);
659
660        plugins::setup_frontend_plugins_post_build(&mut plugins, &plugin_opts, &fe_instance)
661            .await
662            .context(error::StartFrontendSnafu)?;
663
664        let fe_instance = fe_instance
665            .with_plugin(plugins.clone())
666            .try_build()
667            .await
668            .context(error::StartFrontendSnafu)?;
669        let fe_instance = Arc::new(fe_instance);
670
671        event_recorder_handle.install(fe_instance.event_recorder());
672
673        // set the frontend client for flownode
674        let grpc_handler = fe_instance.clone() as Arc<dyn GrpcQueryHandlerWithBoxedError>;
675        let weak_grpc_handler = Arc::downgrade(&grpc_handler);
676        frontend_instance_handler
677            .set_handler(weak_grpc_handler)
678            .await;
679
680        // set the frontend invoker for flownode
681        let flow_streaming_engine = flow_engine.streaming_engine();
682        // flow server need to be able to use frontend to write insert requests back
683        let invoker = FrontendInvoker::build_from(
684            flow_streaming_engine.clone(),
685            catalog_manager.clone(),
686            kv_backend.clone(),
687            layered_cache_registry.clone(),
688            procedure_executor,
689            node_manager.clone(),
690            fe_instance.frontend_peer_addr().to_string(),
691        )
692        .await
693        .context(StartFlownodeSnafu)?;
694        flow_streaming_engine.set_frontend_invoker(invoker).await;
695
696        let servers = Services::new(opts, fe_instance.clone(), plugins.clone())
697            .build()
698            .context(error::StartFrontendSnafu)?;
699
700        let frontend = Frontend {
701            instance: fe_instance,
702            servers,
703            heartbeat_task: None,
704        };
705        let leader_services_context = LeaderServicesContext {
706            procedure_manager: procedure_manager.clone(),
707            wal_provider: wal_provider.clone(),
708            region_server: datanode.region_server(),
709            kv_backend: kv_backend.clone(),
710            cache_registry: layered_cache_registry,
711            catalog_manager,
712            flow_engine,
713            frontend_client,
714            node_id,
715            init_regions_parallelism,
716            plugin_options: plugin_opts,
717        };
718
719        let instance = Instance {
720            datanode,
721            frontend,
722            flownode,
723            procedure_manager,
724            leader_services_controller: creator.leader_services_controller,
725            leader_services_context,
726            _guard: vec![],
727        };
728        let result = InstanceCreatorResult {
729            kv_backend,
730            node_manager,
731            table_id_allocator,
732        };
733        Ok((instance, result))
734    }
735
736    pub async fn create_table_metadata_manager(
737        kv_backend: KvBackendRef,
738    ) -> Result<TableMetadataManagerRef> {
739        let table_metadata_manager = Arc::new(TableMetadataManager::new(kv_backend));
740
741        table_metadata_manager
742            .init()
743            .await
744            .context(error::InitMetadataSnafu)?;
745
746        Ok(table_metadata_manager)
747    }
748}
749
750#[async_trait]
751pub trait NodeManagerCreator: Send + Sync {
752    async fn create(
753        &self,
754        kv_backend: &KvBackendRef,
755        region_server: RegionServer,
756        flow_server: FlownodeRef,
757    ) -> Result<NodeManagerRef>;
758}
759
760pub struct DefaultNodeManagerCreator;
761
762#[async_trait]
763impl NodeManagerCreator for DefaultNodeManagerCreator {
764    async fn create(
765        &self,
766        _: &KvBackendRef,
767        region_server: RegionServer,
768        flow_server: FlownodeRef,
769    ) -> Result<NodeManagerRef> {
770        Ok(Arc::new(StandaloneDatanodeManager {
771            region_server,
772            flow_server,
773        }))
774    }
775}
776
777/// Customizes how standalone opens its metadata KV backend.
778///
779/// The default implementation preserves the built-in raft-engine path. Other
780/// callers can provide a custom implementation without changing standalone
781/// configuration types.
782#[async_trait]
783pub trait MetadataKvBackendCreator: Send + Sync {
784    async fn create(&self, metadata_dir: String, opts: &StandaloneOptions) -> Result<KvBackendRef>;
785}
786
787pub struct DefaultMetadataKvBackendCreator;
788
789#[async_trait]
790impl MetadataKvBackendCreator for DefaultMetadataKvBackendCreator {
791    async fn create(&self, metadata_dir: String, opts: &StandaloneOptions) -> Result<KvBackendRef> {
792        standalone::build_metadata_kvbackend(metadata_dir, opts.metadata_store)
793            .context(error::BuildMetadataKvbackendSnafu)
794    }
795}
796
797pub trait TableIdAllocatorCreator: Send + Sync {
798    fn create(&self, kv_backend: &KvBackendRef) -> Arc<Sequence>;
799}
800
801struct DefaultTableIdAllocatorCreator;
802
803impl TableIdAllocatorCreator for DefaultTableIdAllocatorCreator {
804    fn create(&self, kv_backend: &KvBackendRef) -> Arc<Sequence> {
805        Arc::new(
806            SequenceBuilder::new(TABLE_ID_SEQ, kv_backend.clone())
807                .initial(MIN_USER_TABLE_ID as u64)
808                .step(10)
809                .build(),
810        )
811    }
812}
813
814#[async_trait]
815pub trait ProcedureExecutorCreator: Send + Sync {
816    async fn create(
817        &self,
818        ddl_manager: DdlManagerRef,
819        procedure_manager: ProcedureManagerRef,
820    ) -> Result<ProcedureExecutorRef>;
821}
822
823pub struct DefaultProcedureExecutorCreator;
824
825#[async_trait]
826impl ProcedureExecutorCreator for DefaultProcedureExecutorCreator {
827    async fn create(
828        &self,
829        ddl_manager: DdlManagerRef,
830        procedure_manager: ProcedureManagerRef,
831    ) -> Result<ProcedureExecutorRef> {
832        Ok(Arc::new(LocalProcedureExecutor::new(
833            ddl_manager,
834            procedure_manager,
835        )))
836    }
837}
838
839#[async_trait]
840pub trait StandaloneLeaderServicesController: Send + Sync {
841    /// Starts leader services that manage standalone metadata or WAL state.
842    ///
843    /// The default implementation starts the procedure manager and WAL provider
844    /// during instance startup.
845    async fn start(&self, context: LeaderServicesContext) -> Result<()>;
846
847    /// Stops services started by [`StandaloneLeaderServicesController::start`].
848    async fn stop(
849        &self,
850        procedure_manager: ProcedureManagerRef,
851        region_server: RegionServer,
852    ) -> Result<()>;
853}
854
855#[derive(Clone)]
856/// Additional runtime handles for custom leader-service controllers.
857///
858/// The default standalone startup only needs to start/stop the procedure
859/// manager and WAL provider. Some embedders need to do more work around
860/// leader-service startup, for example reconciling metadata-backed runtime
861/// state before publishing writable leadership. Grouping those handles here
862/// keeps `Instance` small and avoids expanding
863/// [`StandaloneLeaderServicesController::start`] every time a custom lifecycle
864/// needs one more standalone component.
865pub struct LeaderServicesContext {
866    pub procedure_manager: ProcedureManagerRef,
867    pub wal_provider: WalProviderRef,
868    pub region_server: RegionServer,
869    pub kv_backend: KvBackendRef,
870    pub cache_registry: LayeredCacheRegistryRef,
871    pub catalog_manager: CatalogManagerRef,
872    pub flow_engine: FlowDualEngineRef,
873    pub frontend_client: Arc<FrontendClient>,
874    pub node_id: Option<DatanodeId>,
875    pub init_regions_parallelism: usize,
876    pub plugin_options: Vec<PluginOptions>,
877}
878
879pub struct DefaultStandaloneLeaderServicesController;
880
881#[async_trait]
882impl StandaloneLeaderServicesController for DefaultStandaloneLeaderServicesController {
883    async fn start(&self, context: LeaderServicesContext) -> Result<()> {
884        context
885            .procedure_manager
886            .start()
887            .await
888            .context(error::StartProcedureManagerSnafu)?;
889        context
890            .wal_provider
891            .start()
892            .await
893            .context(error::StartWalProviderSnafu)
894    }
895
896    async fn stop(
897        &self,
898        procedure_manager: ProcedureManagerRef,
899        _region_server: RegionServer,
900    ) -> Result<()> {
901        procedure_manager
902            .stop()
903            .await
904            .context(error::StopProcedureManagerSnafu)
905    }
906}
907
908/// `InstanceCreator` is used for grouping various component creators for building the
909/// Standalone instance, suitable for customizing how the instance can be built.
910pub struct InstanceCreator {
911    /// Hook for replacing metadata KV construction while reusing the rest of the
912    /// standalone build flow.
913    metadata_kv_backend_creator: Box<dyn MetadataKvBackendCreator>,
914    node_manager_creator: Box<dyn NodeManagerCreator>,
915    table_id_allocator_creator: Box<dyn TableIdAllocatorCreator>,
916    procedure_executor_creator: Box<dyn ProcedureExecutorCreator>,
917    leader_services_controller: Box<dyn StandaloneLeaderServicesController>,
918    open_regions_writable_override: Option<bool>,
919}
920
921impl InstanceCreator {
922    pub fn new(
923        node_manager_creator: Box<dyn NodeManagerCreator>,
924        table_id_allocator_creator: Box<dyn TableIdAllocatorCreator>,
925        procedure_executor_creator: Box<dyn ProcedureExecutorCreator>,
926    ) -> Self {
927        Self {
928            metadata_kv_backend_creator: Box::new(DefaultMetadataKvBackendCreator),
929            node_manager_creator,
930            table_id_allocator_creator,
931            procedure_executor_creator,
932            leader_services_controller: Box::new(DefaultStandaloneLeaderServicesController),
933            open_regions_writable_override: None,
934        }
935    }
936
937    pub fn with_metadata_kv_backend_creator(
938        mut self,
939        metadata_kv_backend_creator: Box<dyn MetadataKvBackendCreator>,
940    ) -> Self {
941        self.metadata_kv_backend_creator = metadata_kv_backend_creator;
942        self
943    }
944
945    /// Wraps the metadata backend creator while retaining the default creator.
946    ///
947    /// This is useful for callers that need to add runtime behavior around
948    /// metadata access without reimplementing backend selection.
949    pub fn map_metadata_kv_backend_creator<F>(mut self, f: F) -> Self
950    where
951        F: FnOnce(Box<dyn MetadataKvBackendCreator>) -> Box<dyn MetadataKvBackendCreator>,
952    {
953        self.metadata_kv_backend_creator = f(self.metadata_kv_backend_creator);
954        self
955    }
956
957    /// Wraps node-manager creation while preserving the selected standalone node manager.
958    pub fn map_node_manager_creator<F>(mut self, f: F) -> Self
959    where
960        F: FnOnce(Box<dyn NodeManagerCreator>) -> Box<dyn NodeManagerCreator>,
961    {
962        self.node_manager_creator = f(self.node_manager_creator);
963        self
964    }
965
966    /// Wraps procedure-executor creation while preserving the current setup.
967    pub fn map_procedure_executor_creator<F>(mut self, f: F) -> Self
968    where
969        F: FnOnce(Box<dyn ProcedureExecutorCreator>) -> Box<dyn ProcedureExecutorCreator>,
970    {
971        self.procedure_executor_creator = f(self.procedure_executor_creator);
972        self
973    }
974
975    /// Replaces startup/shutdown ownership for procedure manager and WAL provider.
976    pub fn with_leader_services_controller(
977        mut self,
978        leader_services_controller: Box<dyn StandaloneLeaderServicesController>,
979    ) -> Self {
980        self.leader_services_controller = leader_services_controller;
981        self
982    }
983
984    /// Overrides whether regions opened during startup should become writable.
985    ///
986    /// `None` keeps the default startup behavior (regions open writable).
987    ///
988    /// Warning: setting this to `false` in standalone mode will leave reopened regions
989    /// permanently read-only. Standalone has no metasrv heartbeat or region-role
990    /// reconciliation, so there is no path to promote regions to Leader after startup.
991    pub fn with_open_regions_writable_override(mut self, writable: bool) -> Self {
992        self.open_regions_writable_override = Some(writable);
993        self
994    }
995}
996
997impl Default for InstanceCreator {
998    fn default() -> Self {
999        Self {
1000            metadata_kv_backend_creator: Box::new(DefaultMetadataKvBackendCreator),
1001            node_manager_creator: Box::new(DefaultNodeManagerCreator),
1002            table_id_allocator_creator: Box::new(DefaultTableIdAllocatorCreator),
1003            procedure_executor_creator: Box::new(DefaultProcedureExecutorCreator),
1004            leader_services_controller: Box::new(DefaultStandaloneLeaderServicesController),
1005            open_regions_writable_override: None,
1006        }
1007    }
1008}
1009
1010/// `InstanceCreatorResult` is expected to be used paired with [InstanceCreator].
1011/// It stores the created and other important components for further reusing.
1012pub struct InstanceCreatorResult {
1013    pub kv_backend: KvBackendRef,
1014    pub node_manager: NodeManagerRef,
1015    pub table_id_allocator: Arc<Sequence>,
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use std::default::Default;
1021    use std::io::Write;
1022    use std::time::Duration;
1023
1024    use auth::{Identity, Password, UserProviderRef};
1025    use clap::{CommandFactory, Parser};
1026    use common_base::readable_size::ReadableSize;
1027    use common_config::ENV_VAR_SEP;
1028    use common_options::plugin_options::StandaloneFlag;
1029    use common_test_util::temp_dir::{create_named_temp_file, create_temp_dir};
1030    use common_wal::config::DatanodeWalConfig;
1031    use frontend::frontend::FrontendOptions;
1032    use object_store::config::{FileConfig, GcsConfig};
1033    use servers::grpc::GrpcOptions;
1034
1035    use super::*;
1036    use crate::options::GlobalOptions;
1037
1038    #[test]
1039    fn test_standalone_local_file_access_config() {
1040        let data_home = create_temp_dir("standalone_copy_root");
1041        let storage = StorageConfig {
1042            data_home: data_home.path().display().to_string(),
1043            ..Default::default()
1044        };
1045        let access = standalone_local_file_access(&storage).unwrap();
1046        assert_eq!(
1047            access.sandbox_root().unwrap(),
1048            data_home.path().join("copy").canonicalize().unwrap()
1049        );
1050
1051        let remote_data_home = StorageConfig {
1052            data_home: "s3://bucket/data".to_string(),
1053            ..Default::default()
1054        };
1055        assert!(matches!(
1056            standalone_local_file_access(&remote_data_home).unwrap(),
1057            LocalFileAccess::Disabled
1058        ));
1059
1060        let explicit_root = create_temp_dir("standalone_explicit_copy_root");
1061        let remote_with_explicit_root = StorageConfig {
1062            data_home: "s3://bucket/data".to_string(),
1063            copy_root: Some(explicit_root.path().display().to_string()),
1064            ..Default::default()
1065        };
1066        assert_eq!(
1067            standalone_local_file_access(&remote_with_explicit_root)
1068                .unwrap()
1069                .sandbox_root()
1070                .unwrap(),
1071            explicit_root.path().canonicalize().unwrap()
1072        );
1073
1074        let remote_copy_root = StorageConfig {
1075            data_home: data_home.path().display().to_string(),
1076            copy_root: Some("s3://bucket/copy".to_string()),
1077            ..Default::default()
1078        };
1079        assert!(matches!(
1080            standalone_local_file_access(&remote_copy_root),
1081            Err(common_datasource::error::Error::InvalidLocalFileRootConfig { .. })
1082        ));
1083
1084        let exposes_internal = StorageConfig {
1085            data_home: data_home.path().display().to_string(),
1086            copy_root: Some(data_home.path().join("data").display().to_string()),
1087            ..Default::default()
1088        };
1089        assert!(matches!(
1090            standalone_local_file_access(&exposes_internal),
1091            Err(common_datasource::error::Error::InvalidLocalFileRootConfig { .. })
1092        ));
1093
1094        let exposes_data_home = StorageConfig {
1095            data_home: data_home.path().display().to_string(),
1096            copy_root: Some(data_home.path().parent().unwrap().display().to_string()),
1097            ..Default::default()
1098        };
1099        assert!(matches!(
1100            standalone_local_file_access(&exposes_data_home),
1101            Err(common_datasource::error::Error::InvalidLocalFileRootConfig { .. })
1102        ));
1103    }
1104
1105    #[tokio::test]
1106    async fn test_try_from_start_command_to_anymap() {
1107        let fe_opts = FrontendOptions {
1108            user_provider: Some("static_user_provider:cmd:test=test".to_string()),
1109            ..Default::default()
1110        };
1111
1112        let mut plugins = Plugins::new();
1113        plugins.insert(StandaloneFlag);
1114        plugins::setup_frontend_plugins_pre_build(&mut plugins, &[], &fe_opts, None)
1115            .await
1116            .unwrap();
1117
1118        let provider = plugins.get::<UserProviderRef>().unwrap();
1119        let result = provider
1120            .authenticate(
1121                Identity::UserId("test", None),
1122                Password::PlainText("test".to_string().into()),
1123            )
1124            .await;
1125        let _ = result.unwrap();
1126    }
1127
1128    #[test]
1129    fn test_toml() {
1130        let opts = StandaloneOptions::default();
1131        let toml_string = toml::to_string(&opts).unwrap();
1132        let _parsed: StandaloneOptions = toml::from_str(&toml_string).unwrap();
1133    }
1134
1135    #[test]
1136    fn test_read_from_config_file() {
1137        let mut file = create_named_temp_file();
1138        let toml_str = r#"
1139            enable_memory_catalog = true
1140
1141            [wal]
1142            provider = "raft_engine"
1143            dir = "./greptimedb_data/test/wal"
1144            file_size = "1GB"
1145            purge_threshold = "50GB"
1146            purge_interval = "10m"
1147            read_batch_size = 128
1148            sync_write = false
1149
1150            [storage]
1151            data_home = "./greptimedb_data/"
1152            type = "File"
1153
1154            [[storage.providers]]
1155            type = "Gcs"
1156            bucket = "foo"
1157            endpoint = "bar"
1158
1159            [[storage.providers]]
1160            type = "S3"
1161            access_key_id = "access_key_id"
1162            secret_access_key = "secret_access_key"
1163
1164            [storage.compaction]
1165            max_inflight_tasks = 3
1166            max_files_in_level0 = 7
1167            max_purge_tasks = 32
1168
1169            [storage.manifest]
1170            checkpoint_margin = 9
1171            gc_duration = '7s'
1172
1173            [http]
1174            addr = "127.0.0.1:4000"
1175            timeout = "33s"
1176            body_limit = "128MB"
1177
1178            [opentsdb]
1179            enable = true
1180
1181            [logging]
1182            level = "debug"
1183            dir = "./greptimedb_data/test/logs"
1184        "#;
1185        write!(file, "{}", toml_str).unwrap();
1186        let cmd = StartCommand {
1187            config_file: Some(file.path().to_str().unwrap().to_string()),
1188            user_provider: Some("static_user_provider:cmd:test=test".to_string()),
1189            ..Default::default()
1190        };
1191
1192        let options = cmd
1193            .load_options(&GlobalOptions::default())
1194            .unwrap()
1195            .component;
1196        let fe_opts = options.frontend_options();
1197        let dn_opts = options.datanode_options();
1198        let logging_opts = options.logging;
1199        assert_eq!("127.0.0.1:4000".to_string(), fe_opts.http.addr);
1200        assert_eq!(Duration::from_secs(33), fe_opts.http.timeout);
1201        assert_eq!(ReadableSize::mb(128), fe_opts.http.body_limit);
1202        assert_eq!("127.0.0.1:4001".to_string(), fe_opts.grpc.bind_addr);
1203        assert!(fe_opts.mysql.enable);
1204        assert_eq!("127.0.0.1:4002", fe_opts.mysql.addr);
1205        assert_eq!(2, fe_opts.mysql.runtime_size);
1206        assert_eq!(None, fe_opts.mysql.reject_no_database);
1207        assert!(fe_opts.influxdb.enable);
1208        assert!(fe_opts.opentsdb.enable);
1209
1210        let DatanodeWalConfig::RaftEngine(raft_engine_config) = dn_opts.wal else {
1211            unreachable!()
1212        };
1213        assert_eq!(
1214            "./greptimedb_data/test/wal",
1215            raft_engine_config.dir.unwrap()
1216        );
1217
1218        assert!(matches!(
1219            &dn_opts.storage.store,
1220            object_store::config::ObjectStoreConfig::File(FileConfig { .. })
1221        ));
1222        assert_eq!(dn_opts.storage.providers.len(), 2);
1223        assert!(matches!(
1224            dn_opts.storage.providers[0],
1225            object_store::config::ObjectStoreConfig::Gcs(GcsConfig { .. })
1226        ));
1227        match &dn_opts.storage.providers[1] {
1228            object_store::config::ObjectStoreConfig::S3(s3_config) => {
1229                assert_eq!(
1230                    "SecretBox<alloc::string::String>([REDACTED])".to_string(),
1231                    format!("{:?}", s3_config.connection.access_key_id)
1232                );
1233            }
1234            _ => {
1235                unreachable!()
1236            }
1237        }
1238
1239        assert_eq!("debug", logging_opts.level.as_ref().unwrap());
1240        assert_eq!("./greptimedb_data/test/logs".to_string(), logging_opts.dir);
1241    }
1242
1243    #[test]
1244    fn test_load_log_options_from_cli() {
1245        let cmd = StartCommand {
1246            user_provider: Some("static_user_provider:cmd:test=test".to_string()),
1247            mysql_addr: Some("127.0.0.1:4002".to_string()),
1248            postgres_addr: Some("127.0.0.1:4003".to_string()),
1249            ..Default::default()
1250        };
1251
1252        let opts = cmd
1253            .load_options(&GlobalOptions {
1254                log_dir: Some("./greptimedb_data/test/logs".to_string()),
1255                log_level: Some("debug".to_string()),
1256
1257                #[cfg(feature = "tokio-console")]
1258                tokio_console_addr: None,
1259            })
1260            .unwrap()
1261            .component;
1262
1263        assert_eq!("./greptimedb_data/test/logs", opts.logging.dir);
1264        assert_eq!("debug", opts.logging.level.unwrap());
1265    }
1266
1267    #[test]
1268    fn test_config_precedence_order() {
1269        let mut file = create_named_temp_file();
1270        let toml_str = r#"
1271            [http]
1272            addr = "127.0.0.1:4000"
1273
1274            [logging]
1275            level = "debug"
1276        "#;
1277        write!(file, "{}", toml_str).unwrap();
1278
1279        let env_prefix = "STANDALONE_UT";
1280        temp_env::with_vars(
1281            [
1282                (
1283                    // logging.dir = /other/log/dir
1284                    [
1285                        env_prefix.to_string(),
1286                        "logging".to_uppercase(),
1287                        "dir".to_uppercase(),
1288                    ]
1289                    .join(ENV_VAR_SEP),
1290                    Some("/other/log/dir"),
1291                ),
1292                (
1293                    // logging.level = info
1294                    [
1295                        env_prefix.to_string(),
1296                        "logging".to_uppercase(),
1297                        "level".to_uppercase(),
1298                    ]
1299                    .join(ENV_VAR_SEP),
1300                    Some("info"),
1301                ),
1302                (
1303                    // http.addr = 127.0.0.1:24000
1304                    [
1305                        env_prefix.to_string(),
1306                        "http".to_uppercase(),
1307                        "addr".to_uppercase(),
1308                    ]
1309                    .join(ENV_VAR_SEP),
1310                    Some("127.0.0.1:24000"),
1311                ),
1312            ],
1313            || {
1314                let command = StartCommand {
1315                    config_file: Some(file.path().to_str().unwrap().to_string()),
1316                    http_addr: Some("127.0.0.1:14000".to_string()),
1317                    env_prefix: env_prefix.to_string(),
1318                    ..Default::default()
1319                };
1320
1321                let opts = command.load_options(&Default::default()).unwrap().component;
1322
1323                // Should be read from env, env > default values.
1324                assert_eq!(opts.logging.dir, "/other/log/dir");
1325
1326                // Should be read from config file, config file > env > default values.
1327                assert_eq!(opts.logging.level.as_ref().unwrap(), "debug");
1328
1329                // Should be read from cli, cli > config file > env > default values.
1330                let fe_opts = opts.frontend_options();
1331                assert_eq!(fe_opts.http.addr, "127.0.0.1:14000");
1332                assert_eq!(ReadableSize::mb(64), fe_opts.http.body_limit);
1333
1334                // Should be default value.
1335                assert_eq!(fe_opts.grpc.bind_addr, GrpcOptions::default().bind_addr);
1336            },
1337        );
1338    }
1339
1340    #[test]
1341    fn test_parse_grpc_bind_addr_aliases() {
1342        let command =
1343            StartCommand::try_parse_from(["standalone", "--grpc-bind-addr", "127.0.0.1:14001"])
1344                .unwrap();
1345        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:14001"));
1346
1347        let command =
1348            StartCommand::try_parse_from(["standalone", "--rpc-bind-addr", "127.0.0.1:24001"])
1349                .unwrap();
1350        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:24001"));
1351
1352        let command =
1353            StartCommand::try_parse_from(["standalone", "--rpc-addr", "127.0.0.1:34001"]).unwrap();
1354        assert_eq!(command.grpc_bind_addr.as_deref(), Some("127.0.0.1:34001"));
1355    }
1356
1357    #[test]
1358    fn test_help_uses_grpc_option_names() {
1359        let mut cmd = StartCommand::command();
1360        let mut help = Vec::new();
1361        cmd.write_long_help(&mut help).unwrap();
1362        let help = String::from_utf8(help).unwrap();
1363
1364        assert!(help.contains("--grpc-bind-addr"));
1365        assert!(!help.contains("--rpc-bind-addr"));
1366        assert!(!help.contains("--rpc-addr"));
1367    }
1368
1369    #[test]
1370    fn test_load_default_standalone_options() {
1371        let options =
1372            StandaloneOptions::load_layered_options(None, "GREPTIMEDB_STANDALONE").unwrap();
1373        let default_options = StandaloneOptions::default();
1374        assert_eq!(options.enable_telemetry, default_options.enable_telemetry);
1375        assert_eq!(options.http, default_options.http);
1376        assert_eq!(options.grpc, default_options.grpc);
1377        assert_eq!(options.mysql, default_options.mysql);
1378        assert_eq!(options.postgres, default_options.postgres);
1379        assert_eq!(options.opentsdb, default_options.opentsdb);
1380        assert_eq!(options.influxdb, default_options.influxdb);
1381        assert_eq!(options.prom_store, default_options.prom_store);
1382        assert_eq!(options.wal, default_options.wal);
1383        assert_eq!(options.metadata_store, default_options.metadata_store);
1384        assert_eq!(options.procedure, default_options.procedure);
1385        assert_eq!(options.logging, default_options.logging);
1386        assert_eq!(options.region_engine, default_options.region_engine);
1387    }
1388
1389    #[test]
1390    fn test_cache_config() {
1391        let toml_str = r#"
1392            [storage]
1393            data_home = "test_data_home"
1394            type = "S3"
1395            [storage.cache_config]
1396            enable_read_cache = true
1397        "#;
1398        let mut opts: StandaloneOptions = toml::from_str(toml_str).unwrap();
1399        opts.sanitize();
1400        assert!(opts.storage.store.cache_config().unwrap().enable_read_cache);
1401        assert_eq!(
1402            opts.storage.store.cache_config().unwrap().cache_path,
1403            "test_data_home"
1404        );
1405    }
1406
1407    #[test]
1408    #[cfg(not(feature = "enterprise"))]
1409    fn test_load_options_ignores_unknown_plugin_options() {
1410        // Plugin options that are not recognized by the current build (for example,
1411        // an enterprise plugin option seen by an open-source build) must not abort
1412        // startup. They should be dropped with a warning instead.
1413        let mut file = create_named_temp_file();
1414        write!(
1415            file,
1416            r#"
1417[[plugins]]
1418SomeUnknownPlugin = {{ feature = "foo", count = 5 }}
1419
1420[[plugins]]
1421AnotherUnknownPlugin = {{}}
1422"#
1423        )
1424        .unwrap();
1425
1426        let opts = GreptimeOptions::<StandaloneOptions>::load_layered_options(
1427            Some(file.path().to_str().unwrap()),
1428            "GREPTIMEDB_STANDALONE_UT",
1429        )
1430        .expect(
1431            "loading a config with unrecognized plugin options should succeed, \
1432             ignoring the unknown ones",
1433        );
1434        // Unknown plugin options are dropped; the recognized list is empty in the
1435        // open-source build.
1436        assert!(opts.plugins.is_empty());
1437    }
1438
1439    #[test]
1440    fn test_load_options_errors_on_malformed_known_plugin_option() {
1441        // A *known* variant with a malformed payload must NOT be silently
1442        // dropped; config loading must fail so a misconfigured plugin is not
1443        // disabled without notice. Here `Dummy` (a unit variant) is given a map
1444        // payload, which is invalid.
1445        let mut file = create_named_temp_file();
1446        write!(
1447            file,
1448            r#"
1449[[plugins]]
1450Dummy = {{ unexpected = "payload" }}
1451"#
1452        )
1453        .unwrap();
1454
1455        let result = GreptimeOptions::<StandaloneOptions>::load_layered_options(
1456            Some(file.path().to_str().unwrap()),
1457            "GREPTIMEDB_STANDALONE_UT",
1458        );
1459        assert!(
1460            result.is_err(),
1461            "a malformed payload for a known plugin variant must fail config loading"
1462        );
1463    }
1464
1465    #[test]
1466    #[cfg(not(feature = "enterprise"))]
1467    fn test_load_options_ignores_multi_key_unknown_plugin_entry() {
1468        // A single plugin table carrying several *unknown* keys must not abort
1469        // startup with serde's "expected map with a single key" error; it is
1470        // dropped like any other unrecognized plugin option.
1471        let mut file = create_named_temp_file();
1472        write!(
1473            file,
1474            r#"
1475[[plugins]]
1476FirstUnknownPlugin = {{ a = 1 }}
1477SecondUnknownPlugin = {{ b = 2 }}
1478"#
1479        )
1480        .unwrap();
1481
1482        let opts = GreptimeOptions::<StandaloneOptions>::load_layered_options(
1483            Some(file.path().to_str().unwrap()),
1484            "GREPTIMEDB_STANDALONE_UT",
1485        )
1486        .expect(
1487            "loading a config with a multi-key unknown plugin entry should succeed, \
1488             ignoring the unknown ones",
1489        );
1490        assert!(opts.plugins.is_empty());
1491    }
1492}