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