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