1mod cache;
10mod discovery;
11mod events;
12mod messenger;
13mod nixl;
14mod object;
15mod offload;
16mod onboard;
17mod rayon;
18mod tokio;
19
20pub use cache::{CacheConfig, DiskCacheConfig, HostCacheConfig, ParallelismMode};
21pub use discovery::{
22 DiscoveryConfig, EtcdDiscoveryConfig, FilesystemDiscoveryConfig, P2pDiscoveryConfig,
23};
24pub use events::{BatchingConfig as EventsBatchingConfig, EventPolicyConfig, EventsConfig};
25pub use messenger::{MessengerBackendConfig, MessengerConfig};
26pub use nixl::NixlConfig;
27pub use object::{NixlObjectConfig, ObjectClientConfig, ObjectConfig, S3ObjectConfig};
28pub use offload::{
29 OffloadConfig, PolicyType, PresenceFilterConfig, PresenceLfuFilterConfig, TierOffloadConfig,
30};
31pub use onboard::{OnboardConfig, OnboardMode};
32pub use rayon::RayonConfig;
33pub use tokio::TokioConfig;
34
35use figment::{
36 Figment, Metadata, Profile, Provider,
37 providers::{Env, Format, Json, Serialized, Toml},
38 value::{Dict, Map},
39};
40use serde::{Deserialize, Serialize};
41use thiserror::Error;
42use validator::{Validate, ValidationErrors};
43
44#[derive(Debug, Error)]
46pub enum ConfigError {
47 #[error("Failed to extract configuration: {0}")]
48 Extraction(#[from] Box<figment::Error>),
49
50 #[error("Configuration validation failed: {0}")]
51 Validation(#[from] ValidationErrors),
52
53 #[error("Configuration error: {0}")]
54 Other(#[from] anyhow::Error),
55}
56
57#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
63pub struct KvbmConfig {
64 #[validate(nested)]
65 pub tokio: TokioConfig,
66
67 #[validate(nested)]
68 pub rayon: RayonConfig,
69
70 #[validate(nested)]
71 pub messenger: MessengerConfig,
72
73 #[validate(nested)]
75 #[serde(default)]
76 pub nixl: Option<NixlConfig>,
77
78 #[validate(nested)]
80 #[serde(default)]
81 pub cache: CacheConfig,
82
83 #[validate(nested)]
85 #[serde(default)]
86 pub offload: OffloadConfig,
87
88 #[serde(default)]
90 pub onboard: OnboardConfig,
91
92 #[validate(nested)]
95 #[serde(default)]
96 pub object: Option<ObjectConfig>,
97
98 #[validate(nested)]
100 #[serde(default)]
101 pub events: EventsConfig,
102}
103
104impl KvbmConfig {
105 pub fn figment() -> Figment {
113 let config_path = std::env::var("KVBM_CONFIG_PATH").unwrap_or_default();
114
115 Figment::new()
116 .merge(Serialized::defaults(KvbmConfig::default()))
117 .merge(Toml::file("/opt/dynamo/etc/kvbm.toml"))
118 .merge(Toml::file(&config_path))
119 .merge(
121 Env::prefixed("KVBM_TOKIO_")
122 .map(|k| format!("tokio.{}", k.as_str().to_lowercase()).into()),
123 )
124 .merge(
126 Env::prefixed("KVBM_RAYON_")
127 .map(|k| format!("rayon.{}", k.as_str().to_lowercase()).into()),
128 )
129 .merge(
131 Env::prefixed("KVBM_MESSENGER_BACKEND_")
132 .map(|k| format!("messenger.backend.{}", k.as_str().to_lowercase()).into()),
133 )
134 .merge(
136 Env::prefixed("KVBM_MESSENGER_DISCOVERY_")
137 .map(|k| format!("messenger.discovery.{}", k.as_str().to_lowercase()).into()),
138 )
139 .merge(
141 Env::prefixed("KVBM_NIXL_")
142 .map(|k| format!("nixl.{}", k.as_str().to_lowercase()).into()),
143 )
144 .merge(
146 Env::prefixed("KVBM_CACHE_HOST_")
147 .map(|k| format!("cache.host.{}", k.as_str().to_lowercase()).into()),
148 )
149 .merge(
151 Env::prefixed("KVBM_CACHE_DISK_")
152 .map(|k| format!("cache.disk.{}", k.as_str().to_lowercase()).into()),
153 )
154 .merge(Env::prefixed("KVBM_CACHE_PARALLELISM").map(|_| "cache.parallelism".into()))
156 .merge(
158 Env::prefixed("KVBM_EVENTS_")
159 .map(|k| format!("events.{}", k.as_str().to_lowercase()).into()),
160 )
161 .merge(
163 Env::prefixed("KVBM_EVENTS_BATCHING_")
164 .map(|k| format!("events.batching.{}", k.as_str().to_lowercase()).into()),
165 )
166 }
167
168 pub fn from_env() -> Result<Self, ConfigError> {
170 Self::extract_from(Self::figment())
171 }
172
173 pub fn extract_from<T: Provider>(provider: T) -> Result<Self, ConfigError> {
187 let config: Self = Figment::from(provider)
188 .extract()
189 .map_err(|e| ConfigError::Extraction(Box::new(e)))?;
190 config.validate()?;
191 Ok(config)
192 }
193
194 pub fn figment_with<T: Provider>(extra: T) -> Figment {
204 Self::figment().merge(extra)
205 }
206
207 pub fn from_figment_with_json(json: &str) -> Result<Self, ConfigError> {
218 Self::extract_from(Self::figment().merge(Json::string(json)))
219 }
220
221 pub fn figment_for_leader() -> Figment {
247 Self::figment().select(Profile::new("leader"))
248 }
249
250 pub fn figment_for_worker() -> Figment {
255 Self::figment().select(Profile::new("worker"))
256 }
257
258 pub fn from_env_for_leader() -> Result<Self, ConfigError> {
260 Self::extract_from(Self::figment_for_leader())
261 }
262
263 pub fn from_env_for_worker() -> Result<Self, ConfigError> {
265 Self::extract_from(Self::figment_for_worker())
266 }
267
268 pub fn from_figment_with_json_for_leader(json: &str) -> Result<Self, ConfigError> {
283 Self::extract_from(Self::figment_for_leader().merge(Json::string(json).nested()))
284 }
285
286 pub fn from_figment_with_json_for_worker(json: &str) -> Result<Self, ConfigError> {
298 Self::extract_from(Self::figment_for_worker().merge(Json::string(json).nested()))
299 }
300}
301
302impl Provider for KvbmConfig {
308 fn metadata(&self) -> Metadata {
309 Metadata::named("KvbmConfig")
310 }
311
312 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
313 Serialized::defaults(self).data()
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_default_config() {
323 let config = KvbmConfig::default();
324 assert_eq!(config.tokio.worker_threads, Some(1));
326 assert!(config.tokio.max_blocking_threads.is_none());
327 assert!(config.rayon.num_threads.is_none());
328 }
329
330 #[test]
331 fn test_figment_defaults() {
332 temp_env::with_vars_unset(
333 vec![
334 "KVBM_CONFIG_PATH",
335 "KVBM_TOKIO_WORKER_THREADS",
336 "KVBM_RAYON_NUM_THREADS",
337 "KVBM_MESSENGER_BACKEND_TCP_ADDR",
338 "KVBM_MESSENGER_DISCOVERY_CLUSTER_ID",
339 ],
340 || {
341 let figment = KvbmConfig::figment();
342 let config: KvbmConfig = figment.extract().unwrap();
343 assert_eq!(config.tokio.worker_threads, Some(1));
345 },
346 );
347 }
348
349 #[test]
350 fn test_env_override_tokio() {
351 temp_env::with_vars(
352 vec![
353 ("KVBM_TOKIO_WORKER_THREADS", Some("2")),
354 ("KVBM_TOKIO_MAX_BLOCKING_THREADS", Some("32")),
355 ],
356 || {
357 let figment = KvbmConfig::figment();
358 let config: KvbmConfig = figment.extract().unwrap();
359 assert_eq!(config.tokio.worker_threads, Some(2));
360 assert_eq!(config.tokio.max_blocking_threads, Some(32));
361 },
362 );
363 }
364
365 #[test]
366 fn test_extract_from_with_tuple_override() {
367 temp_env::with_vars_unset(
368 vec![
369 "KVBM_CONFIG_PATH",
370 "KVBM_TOKIO_WORKER_THREADS",
371 "KVBM_MESSENGER_BACKEND_TCP_PORT",
372 ],
373 || {
374 let figment = KvbmConfig::figment()
376 .merge(("tokio.worker_threads", 2usize))
377 .merge(("messenger.backend.tcp_port", 9090u16));
378
379 let config = KvbmConfig::extract_from(figment).unwrap();
380 assert_eq!(config.tokio.worker_threads, Some(2));
381 assert_eq!(config.messenger.backend.tcp_port, 9090);
382 },
383 );
384 }
385
386 #[test]
387 fn test_figment_with_helper() {
388 temp_env::with_vars_unset(vec!["KVBM_CONFIG_PATH", "KVBM_RAYON_NUM_THREADS"], || {
389 let figment = KvbmConfig::figment_with(("rayon.num_threads", 8usize));
390 let config = KvbmConfig::extract_from(figment).unwrap();
391 assert_eq!(config.rayon.num_threads, Some(8));
392 });
393 }
394
395 #[test]
396 fn test_config_as_provider() {
397 let original = KvbmConfig {
399 tokio: TokioConfig {
400 worker_threads: Some(4),
401 max_blocking_threads: Some(128),
402 },
403 ..Default::default()
404 };
405
406 let figment = Figment::from(&original);
408 let extracted: KvbmConfig = figment.extract().unwrap();
409
410 assert_eq!(extracted.tokio.worker_threads, Some(4));
411 assert_eq!(extracted.tokio.max_blocking_threads, Some(128));
412 }
413
414 #[test]
415 fn test_from_figment_with_json() {
416 temp_env::with_vars_unset(
417 vec![
418 "KVBM_CONFIG_PATH",
419 "KVBM_TOKIO_WORKER_THREADS",
420 "KVBM_MESSENGER_BACKEND_TCP_PORT",
421 ],
422 || {
423 let json = r#"{"tokio": {"worker_threads": 2}, "messenger": {"backend": {"tcp_port": 9090}}}"#;
424 let config = KvbmConfig::from_figment_with_json(json).unwrap();
425
426 assert_eq!(config.tokio.worker_threads, Some(2));
427 assert_eq!(config.messenger.backend.tcp_port, 9090);
428 },
429 );
430 }
431
432 #[test]
433 fn test_from_figment_with_json_overrides_env() {
434 temp_env::with_vars(vec![("KVBM_TOKIO_WORKER_THREADS", Some("1"))], || {
436 let json = r#"{"tokio": {"worker_threads": 2}}"#;
437 let config = KvbmConfig::from_figment_with_json(json).unwrap();
438
439 assert_eq!(config.tokio.worker_threads, Some(2));
441 });
442 }
443
444 #[test]
445 fn test_from_figment_with_empty_json() {
446 temp_env::with_vars_unset(["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"], || {
447 let config = KvbmConfig::from_figment_with_json("{}");
448 assert!(config.is_ok(), "Empty JSON should not cause errors");
449 });
450 }
451
452 #[test]
458 fn test_profile_selection_leader_vs_worker() {
459 temp_env::with_vars_unset(
462 vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
463 || {
464 let json = r#"{
466 "default": {"tokio": {"worker_threads": 4}},
467 "leader": {"tokio": {"worker_threads": 2}},
468 "worker": {"tokio": {"worker_threads": 8}}
469 }"#;
470
471 let leader_config = KvbmConfig::from_figment_with_json_for_leader(json).unwrap();
473 assert_eq!(
474 leader_config.tokio.worker_threads,
475 Some(2),
476 "Leader should get leader profile's tokio.worker_threads"
477 );
478
479 let worker_config = KvbmConfig::from_figment_with_json_for_worker(json).unwrap();
481 assert_eq!(
482 worker_config.tokio.worker_threads,
483 Some(8),
484 "Worker should get worker profile's tokio.worker_threads"
485 );
486 },
487 );
488 }
489
490 #[test]
491 fn test_profile_no_override_uses_default() {
492 temp_env::with_vars_unset(
494 vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
495 || {
496 let json = r#"{"default": {"tokio": {"worker_threads": 4}}}"#;
498
499 let leader_config = KvbmConfig::from_figment_with_json_for_leader(json).unwrap();
501 assert_eq!(
502 leader_config.tokio.worker_threads,
503 Some(4),
504 "Leader should use default when no leader profile exists"
505 );
506
507 let worker_config = KvbmConfig::from_figment_with_json_for_worker(json).unwrap();
508 assert_eq!(
509 worker_config.tokio.worker_threads,
510 Some(4),
511 "Worker should use default when no worker profile exists"
512 );
513 },
514 );
515 }
516
517 #[test]
518 fn test_profile_with_defaults_and_overlay() {
519 temp_env::with_vars_unset(
521 vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
522 || {
523 let json = r#"{
526 "default": {"cache": {"host": {"cache_size_gb": 2.0}}},
527 "leader": {"tokio": {"worker_threads": 2}}
528 }"#;
529
530 let leader_config = KvbmConfig::from_figment_with_json_for_leader(json).unwrap();
532 assert_eq!(leader_config.cache.host.cache_size_gb, Some(2.0));
533 assert_eq!(leader_config.tokio.worker_threads, Some(2));
534
535 let worker_config = KvbmConfig::from_figment_with_json_for_worker(json).unwrap();
537 assert_eq!(worker_config.cache.host.cache_size_gb, Some(2.0));
538 assert_eq!(
540 worker_config.tokio.worker_threads,
541 Some(1),
542 "Worker should get default tokio, not leader's override"
543 );
544 },
545 );
546 }
547
548 #[test]
549 fn test_from_env_for_leader_and_worker() {
550 temp_env::with_vars_unset(
552 vec!["KVBM_CONFIG_PATH", "KVBM_TOKIO_WORKER_THREADS"],
553 || {
554 let leader_config = KvbmConfig::from_env_for_leader();
556 assert!(leader_config.is_ok(), "from_env_for_leader should succeed");
557
558 let worker_config = KvbmConfig::from_env_for_worker();
559 assert!(worker_config.is_ok(), "from_env_for_worker should succeed");
560 },
561 );
562 }
563}