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