1use eyre::{Report, eyre};
2use once_cell::sync::Lazy;
3use std::cell::RefCell;
4use std::{path::Path, sync::RwLock, time::Duration};
5thread_local! {
6 static TLS_CONFIG_DIR: RefCell<Option<String>> = const { RefCell::new(None) };
7}
8
9#[derive(Debug, Clone, Default)]
10pub struct EnvironmentConfig {
11 pub quic_idle_ms: Option<u64>,
12 pub backoff_ms: Option<u64>,
13 pub interval_ms: Option<u64>,
14 pub connect_timeout_ms: Option<u64>,
15 pub auth_timeout_secs: Option<u64>,
16 pub heartbeat_secs: Option<u64>,
17 pub join_timeout_secs: Option<u64>,
18 pub peer_cleanup_secs: Option<u64>,
19 pub peer_check_secs: Option<u64>,
20 pub rebalance_improvement: Option<f64>,
22 pub worker_idle_multiplier: Option<u64>,
24 pub rebalance_startup_grace_secs: Option<u64>,
26 pub rebalance_check_interval_secs: Option<u64>,
28 pub rebalance_min_interval_secs: Option<u64>,
30 pub migration_check_interval_secs: Option<u64>,
32 pub migration_connection_timeout_secs: Option<u64>,
34 pub migration_handoff_timeout_secs: Option<u64>,
36 pub rebalance_good_score_cutoff: Option<f64>,
38 pub rebalance_periodic_improvement: Option<f64>,
40 pub rebalance_consecutive_signals: Option<u32>,
42 pub rebalance_max_per_hour: Option<u32>,
44 pub rebalance_weight_health: Option<f64>,
46 pub rebalance_weight_history: Option<f64>,
47 pub rebalance_weight_breaker: Option<f64>,
48 pub rebalance_empty_delta_threshold: Option<f64>,
50 pub rebalance_empty_bonus_weight: Option<f64>,
52 pub rebalance_empty_health_cutoff: Option<f64>,
54 pub rebalance_health_stale_secs: Option<u64>,
56 pub rebalance_health_stale_factor: Option<f64>,
58}
59
60impl EnvironmentConfig {
61 pub const DEFAULT_HEARTBEAT_SECS: u64 = 2;
62 pub const DEFAULT_AUTH_TIMEOUT_SECS: u64 = 5;
63 pub const DEFAULT_JOIN_TIMEOUT_SECS: u64 = 30;
64 pub const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
65
66 fn from_env() -> Self {
67 let mut cfg = Self {
68 quic_idle_ms: std::env::var("VOLLI_TEST_QUIC_IDLE_MS")
69 .ok()
70 .and_then(|v| v.parse().ok()),
71 backoff_ms: std::env::var("VOLLI_TEST_BACKOFF_MS")
72 .ok()
73 .and_then(|v| v.parse().ok()),
74 interval_ms: std::env::var("VOLLI_TEST_INTERVAL_MS")
75 .ok()
76 .and_then(|v| v.parse().ok()),
77 connect_timeout_ms: std::env::var("VOLLI_CONNECT_TIMEOUT_MS")
78 .ok()
79 .and_then(|v| v.parse().ok()),
80 auth_timeout_secs: std::env::var("VOLLI_TEST_AUTH_TIMEOUT_SECS")
81 .ok()
82 .and_then(|v| v.parse().ok()),
83 heartbeat_secs: std::env::var("VOLLI_TEST_HEARTBEAT_SECS")
84 .ok()
85 .and_then(|v| v.parse().ok()),
86 join_timeout_secs: std::env::var("VOLLI_JOIN_TIMEOUT_SECS")
87 .ok()
88 .and_then(|v| v.parse().ok()),
89 peer_cleanup_secs: std::env::var("VOLLI_PEER_CLEANUP_SECS")
90 .ok()
91 .and_then(|v| v.parse().ok()),
92 peer_check_secs: std::env::var("VOLLI_PEER_CHECK_SECS")
93 .ok()
94 .and_then(|v| v.parse().ok()),
95 rebalance_improvement: std::env::var("VOLLI_REBALANCE_IMPROVEMENT")
96 .ok()
97 .and_then(|v| v.parse().ok()),
98 worker_idle_multiplier: std::env::var("VOLLI_WORKER_IDLE_MULTIPLIER")
99 .ok()
100 .and_then(|v| v.parse().ok()),
101 rebalance_startup_grace_secs: std::env::var("VOLLI_REBALANCE_STARTUP_GRACE_SECS")
102 .ok()
103 .and_then(|v| v.parse().ok()),
104 rebalance_check_interval_secs: std::env::var("VOLLI_REBALANCE_CHECK_INTERVAL_SECS")
105 .ok()
106 .and_then(|v| v.parse().ok()),
107 rebalance_min_interval_secs: std::env::var("VOLLI_REBALANCE_MIN_INTERVAL_SECS")
108 .ok()
109 .and_then(|v| v.parse().ok()),
110 migration_check_interval_secs: std::env::var("VOLLI_MIGRATION_CHECK_INTERVAL_SECS")
111 .ok()
112 .and_then(|v| v.parse().ok()),
113 migration_connection_timeout_secs: std::env::var(
114 "VOLLI_MIGRATION_CONNECTION_TIMEOUT_SECS",
115 )
116 .ok()
117 .and_then(|v| v.parse().ok()),
118 migration_handoff_timeout_secs: std::env::var("VOLLI_MIGRATION_HANDOFF_TIMEOUT_SECS")
119 .ok()
120 .and_then(|v| v.parse().ok()),
121 rebalance_good_score_cutoff: std::env::var("VOLLI_REBALANCE_GOOD_SCORE_CUTOFF")
122 .ok()
123 .and_then(|v| v.parse().ok()),
124 rebalance_periodic_improvement: std::env::var("VOLLI_REBALANCE_PERIODIC_IMPROVEMENT")
125 .ok()
126 .and_then(|v| v.parse().ok()),
127 rebalance_consecutive_signals: std::env::var("VOLLI_REBALANCE_CONSECUTIVE_SIGNALS")
128 .ok()
129 .and_then(|v| v.parse().ok()),
130 rebalance_max_per_hour: std::env::var("VOLLI_REBALANCE_MAX_PER_HOUR")
131 .ok()
132 .and_then(|v| v.parse().ok()),
133 rebalance_weight_health: std::env::var("VOLLI_REBALANCE_WEIGHT_HEALTH")
134 .ok()
135 .and_then(|v| v.parse().ok()),
136 rebalance_weight_history: std::env::var("VOLLI_REBALANCE_WEIGHT_HISTORY")
137 .ok()
138 .and_then(|v| v.parse().ok()),
139 rebalance_weight_breaker: std::env::var("VOLLI_REBALANCE_WEIGHT_BREAKER")
140 .ok()
141 .and_then(|v| v.parse().ok()),
142 rebalance_empty_delta_threshold: std::env::var("VOLLI_REBALANCE_EMPTY_DELTA")
143 .ok()
144 .and_then(|v| v.parse().ok()),
145 rebalance_empty_bonus_weight: std::env::var("VOLLI_REBALANCE_EMPTY_BONUS_WEIGHT")
146 .ok()
147 .and_then(|v| v.parse().ok()),
148 rebalance_empty_health_cutoff: std::env::var("VOLLI_REBALANCE_EMPTY_HEALTH_CUTOFF")
149 .ok()
150 .and_then(|v| v.parse().ok()),
151 rebalance_health_stale_secs: std::env::var("VOLLI_REBALANCE_HEALTH_STALE_SECS")
152 .ok()
153 .and_then(|v| v.parse().ok()),
154 rebalance_health_stale_factor: std::env::var("VOLLI_REBALANCE_HEALTH_STALE_FACTOR")
155 .ok()
156 .and_then(|v| v.parse().ok()),
157 };
158 let fast = std::env::var("VOLLI_FAST_TESTS")
161 .map(|v| v != "0")
162 .unwrap_or_else(|_| std::env::var("NEXTEST").is_ok());
163 if fast {
164 if cfg.connect_timeout_ms.is_none() {
165 cfg.connect_timeout_ms = Some(50);
166 }
167 if cfg.backoff_ms.is_none() {
168 cfg.backoff_ms = Some(0);
169 }
170 if cfg.interval_ms.is_none() {
171 cfg.interval_ms = Some(1);
172 }
173 if cfg.auth_timeout_secs.is_none() {
174 cfg.auth_timeout_secs = Some(1);
175 }
176 if cfg.heartbeat_secs.is_none() {
177 cfg.heartbeat_secs = Some(1);
178 }
179 }
180 cfg
181 }
182
183 pub fn heartbeat_secs(&self) -> u64 {
184 self.heartbeat_secs.unwrap_or(Self::DEFAULT_HEARTBEAT_SECS)
185 }
186
187 pub fn auth_timeout_secs(&self) -> u64 {
188 self.auth_timeout_secs
189 .unwrap_or(Self::DEFAULT_AUTH_TIMEOUT_SECS)
190 }
191
192 pub fn join_timeout_secs(&self) -> u64 {
193 self.join_timeout_secs
194 .unwrap_or(Self::DEFAULT_JOIN_TIMEOUT_SECS)
195 }
196
197 pub fn connect_timeout_ms(&self) -> u64 {
198 self.connect_timeout_ms
199 .unwrap_or(Self::DEFAULT_CONNECT_TIMEOUT_MS)
200 }
201
202 pub fn quic_idle_duration(&self) -> Result<Duration, Report> {
203 let hb = self.heartbeat_secs();
204 let ms = self.quic_idle_ms.unwrap_or(hb * 3 * 1000);
205 if ms <= hb * 1000 {
206 return Err(eyre!(
207 "quic idle timeout {ms}ms must exceed heartbeat interval {hb}s"
208 ));
209 }
210 Ok(Duration::from_millis(ms))
211 }
212
213 pub fn worker_idle_multiplier(&self) -> u64 {
215 self.worker_idle_multiplier.unwrap_or(5)
216 }
217
218 pub fn rebalance_startup_grace_secs(&self) -> u64 {
220 self.rebalance_startup_grace_secs.unwrap_or(300)
221 }
222
223 pub fn rebalance_check_interval_secs(&self) -> u64 {
225 self.rebalance_check_interval_secs.unwrap_or(300)
226 }
227
228 pub fn rebalance_min_interval_secs(&self) -> u64 {
230 self.rebalance_min_interval_secs.unwrap_or(300)
231 }
232
233 pub fn migration_check_interval_secs(&self) -> u64 {
235 self.migration_check_interval_secs.unwrap_or(180)
236 }
237
238 pub fn migration_connection_timeout_secs(&self) -> u64 {
240 self.migration_connection_timeout_secs.unwrap_or(15)
241 }
242
243 pub fn migration_handoff_timeout_secs(&self) -> u64 {
245 self.migration_handoff_timeout_secs.unwrap_or(30)
246 }
247
248 pub fn rebalance_good_score_cutoff(&self) -> f64 {
250 self.rebalance_good_score_cutoff.unwrap_or(0.6)
251 }
252
253 pub fn rebalance_periodic_improvement(&self) -> f64 {
255 self.rebalance_periodic_improvement.unwrap_or(0.30)
256 }
257
258 pub fn rebalance_consecutive_signals(&self) -> u32 {
260 self.rebalance_consecutive_signals.unwrap_or(1)
261 }
262
263 pub fn rebalance_max_per_hour(&self) -> u32 {
265 self.rebalance_max_per_hour.unwrap_or(3)
266 }
267
268 pub fn rebalance_weight_health(&self) -> f64 {
270 self.rebalance_weight_health.unwrap_or(0.6)
271 }
272 pub fn rebalance_weight_history(&self) -> f64 {
273 self.rebalance_weight_history.unwrap_or(0.2)
274 }
275 pub fn rebalance_weight_breaker(&self) -> f64 {
276 self.rebalance_weight_breaker.unwrap_or(0.2)
277 }
278
279 pub fn rebalance_empty_delta_threshold(&self) -> f64 {
281 self.rebalance_empty_delta_threshold.unwrap_or(0.20)
282 }
283
284 pub fn rebalance_empty_bonus_weight(&self) -> f64 {
286 self.rebalance_empty_bonus_weight.unwrap_or(0.5)
287 }
288
289 pub fn rebalance_empty_health_cutoff(&self) -> f64 {
291 self.rebalance_empty_health_cutoff.unwrap_or(0.6)
292 }
293
294 pub fn rebalance_health_stale_secs(&self) -> u64 {
295 self.rebalance_health_stale_secs.unwrap_or(60)
296 }
297
298 pub fn rebalance_health_stale_factor(&self) -> f64 {
299 self.rebalance_health_stale_factor.unwrap_or(0.75)
300 }
301}
302
303static CONFIG: Lazy<RwLock<EnvironmentConfig>> =
304 Lazy::new(|| RwLock::new(EnvironmentConfig::from_env()));
305
306static CONFIG_DIR: Lazy<RwLock<Option<String>>> =
307 Lazy::new(|| RwLock::new(std::env::var("VOLLI_CONFIG_DIR").ok()));
308
309pub fn env_config() -> EnvironmentConfig {
310 CONFIG.read().unwrap().clone()
311}
312
313pub fn config_dir_env() -> Option<String> {
314 if let Some(dir) = TLS_CONFIG_DIR.with(|c| c.borrow().clone()) {
316 return Some(dir);
317 }
318 CONFIG_DIR.read().unwrap().clone()
319}
320
321pub fn profile_env() -> Option<String> {
322 std::env::var("VOLLI_PROFILE").ok()
323}
324
325pub fn tcp_port_env() -> Option<u16> {
326 std::env::var("VOLLI_TCP_PORT")
327 .ok()
328 .and_then(|v| v.parse().ok())
329}
330
331pub fn quic_port_env() -> Option<u16> {
332 std::env::var("VOLLI_QUIC_PORT")
333 .ok()
334 .and_then(|v| v.parse().ok())
335}
336
337pub fn editor_env() -> Option<String> {
338 std::env::var("EDITOR").ok()
339}
340
341pub struct ConfigDirGuard {
342 prev_thread: Option<String>,
343 prev_global: Option<String>,
344}
345
346pub fn override_config_dir<P: AsRef<Path>>(dir: Option<P>) -> ConfigDirGuard {
347 let new = dir.map(|p| p.as_ref().to_string_lossy().to_string());
348 let prev_thread = TLS_CONFIG_DIR.with(|c| {
349 let mut b = c.borrow_mut();
350 std::mem::replace(&mut *b, new.clone())
351 });
352 let prev_global = {
353 let mut lock = CONFIG_DIR.write().unwrap();
354 std::mem::replace(&mut *lock, new)
355 };
356 ConfigDirGuard {
357 prev_thread,
358 prev_global,
359 }
360}
361
362pub struct ConfigGuard(Option<EnvironmentConfig>);
363
364pub fn override_env_config(cfg: EnvironmentConfig) -> ConfigGuard {
365 let prev = {
366 let mut lock = CONFIG.write().unwrap();
367 std::mem::replace(&mut *lock, cfg)
368 };
369 ConfigGuard(Some(prev))
370}
371
372pub fn override_env_config_patch(patch: EnvironmentConfig) -> ConfigGuard {
379 let prev = CONFIG.read().unwrap().clone();
380 let merged = EnvironmentConfig {
382 quic_idle_ms: patch.quic_idle_ms.or(prev.quic_idle_ms),
383 backoff_ms: patch.backoff_ms.or(prev.backoff_ms),
384 interval_ms: patch.interval_ms.or(prev.interval_ms),
385 connect_timeout_ms: patch.connect_timeout_ms.or(prev.connect_timeout_ms),
386 auth_timeout_secs: patch.auth_timeout_secs.or(prev.auth_timeout_secs),
387 heartbeat_secs: patch.heartbeat_secs.or(prev.heartbeat_secs),
388 join_timeout_secs: patch.join_timeout_secs.or(prev.join_timeout_secs),
389 peer_cleanup_secs: patch.peer_cleanup_secs.or(prev.peer_cleanup_secs),
390 peer_check_secs: patch.peer_check_secs.or(prev.peer_check_secs),
391 rebalance_improvement: patch.rebalance_improvement.or(prev.rebalance_improvement),
392 worker_idle_multiplier: patch.worker_idle_multiplier.or(prev.worker_idle_multiplier),
393 rebalance_startup_grace_secs: patch
394 .rebalance_startup_grace_secs
395 .or(prev.rebalance_startup_grace_secs),
396 rebalance_check_interval_secs: patch
397 .rebalance_check_interval_secs
398 .or(prev.rebalance_check_interval_secs),
399 rebalance_min_interval_secs: patch
400 .rebalance_min_interval_secs
401 .or(prev.rebalance_min_interval_secs),
402 migration_check_interval_secs: patch
403 .migration_check_interval_secs
404 .or(prev.migration_check_interval_secs),
405 migration_connection_timeout_secs: patch
406 .migration_connection_timeout_secs
407 .or(prev.migration_connection_timeout_secs),
408 migration_handoff_timeout_secs: patch
409 .migration_handoff_timeout_secs
410 .or(prev.migration_handoff_timeout_secs),
411 rebalance_good_score_cutoff: patch
412 .rebalance_good_score_cutoff
413 .or(prev.rebalance_good_score_cutoff),
414 rebalance_periodic_improvement: patch
415 .rebalance_periodic_improvement
416 .or(prev.rebalance_periodic_improvement),
417 rebalance_consecutive_signals: patch
418 .rebalance_consecutive_signals
419 .or(prev.rebalance_consecutive_signals),
420 rebalance_max_per_hour: patch.rebalance_max_per_hour.or(prev.rebalance_max_per_hour),
421 rebalance_weight_health: patch
422 .rebalance_weight_health
423 .or(prev.rebalance_weight_health),
424 rebalance_weight_history: patch
425 .rebalance_weight_history
426 .or(prev.rebalance_weight_history),
427 rebalance_weight_breaker: patch
428 .rebalance_weight_breaker
429 .or(prev.rebalance_weight_breaker),
430 rebalance_empty_delta_threshold: patch
431 .rebalance_empty_delta_threshold
432 .or(prev.rebalance_empty_delta_threshold),
433 rebalance_empty_bonus_weight: patch
434 .rebalance_empty_bonus_weight
435 .or(prev.rebalance_empty_bonus_weight),
436 rebalance_empty_health_cutoff: patch
437 .rebalance_empty_health_cutoff
438 .or(prev.rebalance_empty_health_cutoff),
439 rebalance_health_stale_secs: patch
440 .rebalance_health_stale_secs
441 .or(prev.rebalance_health_stale_secs),
442 rebalance_health_stale_factor: patch
443 .rebalance_health_stale_factor
444 .or(prev.rebalance_health_stale_factor),
445 };
446 let old = {
447 let mut lock = CONFIG.write().unwrap();
448 std::mem::replace(&mut *lock, merged)
449 };
450 ConfigGuard(Some(old))
451}
452
453impl Drop for ConfigGuard {
454 fn drop(&mut self) {
455 if let Some(prev) = self.0.take() {
456 let mut lock = CONFIG.write().unwrap();
457 *lock = prev;
458 }
459 }
460}
461
462impl Drop for ConfigDirGuard {
463 fn drop(&mut self) {
464 TLS_CONFIG_DIR.with(|c| {
465 *c.borrow_mut() = self.prev_thread.take();
466 });
467 let mut lock = CONFIG_DIR.write().unwrap();
468 *lock = self.prev_global.take();
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use std::time::Duration;
476
477 #[test]
478 fn quic_idle_default() {
479 let cfg = EnvironmentConfig {
480 heartbeat_secs: Some(2),
481 ..Default::default()
482 };
483 assert_eq!(cfg.quic_idle_duration().unwrap(), Duration::from_secs(6));
484 }
485
486 #[test]
487 fn quic_idle_invalid() {
488 let cfg = EnvironmentConfig {
489 heartbeat_secs: Some(5),
490 quic_idle_ms: Some(4000),
491 ..Default::default()
492 };
493 assert!(cfg.quic_idle_duration().is_err());
494 }
495
496 #[test]
497 fn quic_idle_valid() {
498 let cfg = EnvironmentConfig {
499 heartbeat_secs: Some(5),
500 quic_idle_ms: Some(11000),
501 ..Default::default()
502 };
503 assert_eq!(
504 cfg.quic_idle_duration().unwrap(),
505 Duration::from_millis(11000)
506 );
507 }
508}