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