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