hyperactor/config/global.rs
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Global layered configuration for Hyperactor.
10//!
11//! This module provides the process-wide configuration store and APIs
12//! to access it. Configuration values are resolved via a **layered
13//! model**: `TestOverride → Runtime → Env → File → Default`.
14//!
15//! - Reads (`get`, `get_cloned`) consult layers in that order, falling
16//! back to defaults if no explicit value is set.
17//! - `attrs()` returns a **complete snapshot** of the effective
18//! configuration at call time: it materializes defaults for keys not
19//! set in any layer, and omits meta-only keys (like `CONFIG_ENV_VAR`)
20//! unless explicitly set.
21//! - In tests, `lock()` and `override_key` allow temporary overrides
22//! that are removed automatically when the guard drops.
23//! - In normal operation, a parent process can capture its effective
24//! config via `attrs()` and pass that snapshot to a child during
25//! bootstrap. The child installs it as a `Runtime` layer so the
26//! parent’s values take precedence over Env/File/Defaults.
27//!
28//! This design provides flexibility (easy test overrides, runtime
29//! updates, YAML/Env baselines) while ensuring type safety and
30//! predictable resolution order.
31//!
32//!
33//! # Testing
34//!
35//! Tests can override global configuration using [`lock`]. This
36//! ensures such tests are serialized (and cannot clobber each other's
37//! overrides).
38//!
39//! ```ignore
40//! #[test]
41//! fn test_my_feature() {
42//! let config = hyperactor::config::global::lock();
43//! let _guard = config.override_key(SOME_CONFIG_KEY, test_value);
44//! // ... test logic here ...
45//! }
46//! ```
47use std::marker::PhantomData;
48
49use super::*;
50use crate::attrs::AttrValue;
51use crate::attrs::Key;
52
53/// Configuration source layers in priority order.
54///
55/// Resolution order is always: **TestOverride -> Runtime -> Env
56/// -> File -> Default**.
57///
58/// Smaller `priority()` number = higher precedence.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
60pub enum Source {
61 /// Values loaded from configuration files (e.g., YAML). This
62 /// is the lowest-priority explicit source.
63 File,
64 /// Values read from environment variables at process startup.
65 /// Higher priority than File, but lower than
66 /// Runtime/TestOverride.
67 Env,
68 /// Values set programmatically at runtime. Highest stable
69 /// priority layer; only overridden by TestOverride.
70 Runtime,
71 /// Ephemeral values inserted by tests via
72 /// `ConfigLock::override_key`. Always wins over all other
73 /// sources; removed when the guard drops.
74 TestOverride,
75}
76
77/// Return the numeric priority for a source.
78///
79/// Smaller number = higher precedence. Matches the documented
80/// order: TestOverride (0) -> Runtime (1) -> Env (2) -> File (3).
81fn priority(s: Source) -> u8 {
82 match s {
83 Source::TestOverride => 0,
84 Source::Runtime => 1,
85 Source::Env => 2,
86 Source::File => 3,
87 }
88}
89
90/// A single configuration layer in the global store.
91///
92/// Each `Layer` wraps a [`Source`] and its associated [`Attrs`]
93/// values. Layers are kept in priority order and consulted during
94/// resolution.
95#[derive(Clone)]
96struct Layer {
97 /// The origin of this layer (File, Env, Runtime, or
98 /// TestOverride).
99 source: Source,
100 /// The set of attributes explicitly provided by this source.
101 attrs: Attrs,
102}
103
104/// The full set of configuration layers in priority order.
105///
106/// `Layers` wraps a vector of [`Layer`]s, always kept sorted by
107/// [`priority`] (lowest number = highest precedence).
108///
109/// Resolution (`get`, `get_cloned`, `attrs`) consults `ordered`
110/// from front to back, returning the first value found for each
111/// key and falling back to defaults if none are set in any layer.
112struct Layers {
113 /// Kept sorted by `priority` (lowest number first = highest
114 /// priority).
115 ordered: Vec<Layer>,
116}
117
118/// Global layered configuration store.
119///
120/// This is the single authoritative store for configuration in
121/// the process. It is always present, protected by an `RwLock`,
122/// and holds a [`Layers`] struct containing all active sources.
123///
124/// On startup it is seeded with a single [`Source::Env`] layer
125/// (values loaded from process environment variables). Additional
126/// layers can be installed later via [`set`] or cleared with
127/// [`clear`]. Reads (`get`, `get_cloned`, `attrs`) consult the
128/// layers in priority order.
129///
130/// In tests, a [`Source::TestOverride`] layer is pushed on demand
131/// by [`ConfigLock::override_key`]. This layer always takes
132/// precedence and is automatically removed when the guard drops.
133///
134/// In normal operation, a parent process may capture its config
135/// with [`attrs`] and pass it to a child during bootstrap. The
136/// child installs this snapshot as its [`Source::Runtime`] layer,
137/// ensuring the parent's values override Env/File/Defaults.
138static LAYERS: LazyLock<Arc<RwLock<Layers>>> = LazyLock::new(|| {
139 let env = super::from_env();
140 let layers = Layers {
141 ordered: vec![Layer {
142 source: Source::Env,
143 attrs: env,
144 }],
145 };
146 Arc::new(RwLock::new(layers))
147});
148
149/// Acquire the global configuration lock.
150///
151/// This lock serializes all mutations of the global
152/// configuration, ensuring they cannot clobber each other. It
153/// returns a [`ConfigLock`] guard, which must be held for the
154/// duration of any mutation (e.g. inserting or overriding
155/// values).
156///
157/// Most commonly used in tests, where it provides exclusive
158/// access to push a [`Source::TestOverride`] layer via
159/// [`ConfigLock::override_key`]. The override layer is
160/// automatically removed when the guard drops, restoring the
161/// original state.
162///
163/// # Example
164/// ```rust,ignore
165/// let lock = hyperactor::config::global::lock();
166/// let _guard = lock.override_key(CONFIG_KEY, "test_value");
167/// // Code under test sees the overridden config.
168/// // On drop, the key is restored.
169/// ```
170pub fn lock() -> ConfigLock {
171 static MUTEX: LazyLock<std::sync::Mutex<()>> = LazyLock::new(|| std::sync::Mutex::new(()));
172 ConfigLock {
173 _guard: MUTEX.lock().unwrap(),
174 }
175}
176
177/// Initialize the global configuration from environment
178/// variables.
179///
180/// Reads values from process environment variables, using the
181/// `CONFIG_ENV_VAR` meta-attribute declared on each key to find
182/// its mapping. The resulting values are installed as the
183/// [`Source::Env`] layer. Keys without a corresponding
184/// environment variable fall back to defaults or higher-priority
185/// sources.
186///
187/// Typically invoked once at process startup to overlay config
188/// values from the environment. Repeated calls replace the
189/// existing Env layer.
190pub fn init_from_env() {
191 set(Source::Env, super::from_env());
192}
193
194/// Initialize the global configuration from a YAML file.
195///
196/// Loads values from the specified YAML file and installs them as
197/// the [`Source::File`] layer. This is the lowest-priority
198/// explicit source: values from Env, Runtime, or TestOverride
199/// layers always take precedence. Keys not present in the file
200/// fall back to their defaults or higher-priority sources.
201///
202/// Typically invoked once at process startup to provide a
203/// baseline configuration. Repeated calls replace the existing
204/// File layer.
205pub fn init_from_yaml<P: AsRef<Path>>(path: P) -> Result<(), anyhow::Error> {
206 let file = super::from_yaml(path)?;
207 set(Source::File, file);
208 Ok(())
209}
210
211/// Get a key from the global configuration (Copy types).
212///
213/// Resolution order: TestOverride -> Runtime -> Env -> File ->
214/// Default. Panics if the key has no default and is not set in
215/// any layer.
216pub fn get<T: AttrValue + Copy>(key: Key<T>) -> T {
217 let layers = LAYERS.read().unwrap();
218 for layer in &layers.ordered {
219 if layer.attrs.contains_key(key) {
220 return *layer.attrs.get(key).unwrap();
221 }
222 }
223 *key.default().expect("key must have a default")
224}
225
226/// Get a key by cloning the value.
227///
228/// Resolution order: TestOverride -> Runtime -> Env -> File ->
229/// Default. Panics if the key has no default and is not set in
230/// any layer.
231pub fn get_cloned<T: AttrValue>(key: Key<T>) -> T {
232 try_get_cloned(key)
233 .expect("key must have a default")
234 .clone()
235}
236
237/// Try to get a key by cloning the value.
238///
239/// Resolution order: TestOverride -> Runtime -> Env -> File ->
240/// Default. Returns None if the key has no default and is not set in
241/// any layer.
242pub fn try_get_cloned<T: AttrValue>(key: Key<T>) -> Option<T> {
243 let layers = LAYERS.read().unwrap();
244 for layer in &layers.ordered {
245 if layer.attrs.contains_key(key) {
246 return layer.attrs.get(key).cloned();
247 }
248 }
249 key.default().cloned()
250}
251
252/// Insert or replace a configuration layer for the given source.
253///
254/// If a layer with the same [`Source`] already exists, its
255/// contents are replaced with the provided `attrs`. Otherwise a
256/// new layer is added. After insertion, layers are re-sorted so
257/// that higher-priority sources (e.g. [`Source::TestOverride`],
258/// [`Source::Runtime`]) appear before lower-priority ones
259/// ([`Source::Env`], [`Source::File`]).
260///
261/// This function is used by initialization routines (e.g.
262/// `init_from_env`, `init_from_yaml`) and by tests when
263/// overriding configuration values.
264pub fn set(source: Source, attrs: Attrs) {
265 let mut g = LAYERS.write().unwrap();
266 if let Some(l) = g.ordered.iter_mut().find(|l| l.source == source) {
267 l.attrs = attrs;
268 } else {
269 g.ordered.push(Layer { source, attrs });
270 }
271 g.ordered.sort_by_key(|l| priority(l.source)); // TestOverride < Runtime < Env < File
272}
273
274/// Remove the configuration layer for the given [`Source`], if
275/// present.
276///
277/// After this call, values from that source will no longer
278/// contribute to resolution in [`get`], [`get_cloned`], or
279/// [`attrs`]. Defaults and any remaining layers continue to apply
280/// in their normal priority order.
281pub(crate) fn clear(source: Source) {
282 let mut g = LAYERS.write().unwrap();
283 g.ordered.retain(|l| l.source != source);
284}
285
286/// Return a complete, merged snapshot of the effective
287/// configuration.
288///
289/// Resolution per key:
290/// 1) First explicit value found in layers (TestOverride →
291/// Runtime → Env → File).
292/// 2) Otherwise, the key's default (if any).
293///
294/// Notes:
295/// - This materializes defaults into the returned Attrs so it's
296/// self-contained.
297pub fn attrs() -> Attrs {
298 let layers = LAYERS.read().unwrap();
299 let mut merged = Attrs::new();
300
301 // Iterate all declared keys (registered via `declare_attrs!`
302 // + inventory).
303 for info in inventory::iter::<AttrKeyInfo>() {
304 let name = info.name;
305
306 // Try to resolve from highest -> lowest priority layer.
307 let mut chosen: Option<Box<dyn crate::attrs::SerializableValue>> = None;
308 for layer in &layers.ordered {
309 if let Some(v) = layer.attrs.get_value_by_name(name) {
310 chosen = Some(v.cloned());
311 break;
312 }
313 }
314
315 // If no explicit value, materialize the default if there
316 // is one.
317 let boxed = match chosen {
318 Some(b) => b,
319 None => {
320 if let Some(default) = info.default {
321 default.cloned()
322 } else {
323 // No explicit value and no default — skip
324 // this key.
325 continue;
326 }
327 }
328 };
329
330 merged.insert_value_by_name_unchecked(name, boxed);
331 }
332
333 merged
334}
335
336/// Reset the global configuration to only Defaults (for testing).
337///
338/// This clears all explicit layers (`File`, `Env`, `Runtime`, and
339/// `TestOverride`). Subsequent lookups will resolve keys entirely
340/// from their declared defaults.
341///
342/// Note: Should be called while holding [`global::lock`] in
343/// tests, to ensure no concurrent modifications happen.
344pub fn reset_to_defaults() {
345 let mut g = LAYERS.write().unwrap();
346 g.ordered.clear();
347}
348
349fn test_override_index(layers: &Layers) -> Option<usize> {
350 layers
351 .ordered
352 .iter()
353 .position(|l| matches!(l.source, Source::TestOverride))
354}
355
356fn ensure_test_override_layer_mut(layers: &mut Layers) -> &mut Attrs {
357 if let Some(i) = test_override_index(layers) {
358 return &mut layers.ordered[i].attrs;
359 }
360 layers.ordered.push(Layer {
361 source: Source::TestOverride,
362 attrs: Attrs::new(),
363 });
364 layers.ordered.sort_by_key(|l| priority(l.source));
365 let i = test_override_index(layers).expect("just inserted TestOverride layer");
366 &mut layers.ordered[i].attrs
367}
368
369/// A guard that holds the global configuration lock and provides
370/// override functionality.
371///
372/// This struct acts as both a lock guard (preventing other tests
373/// from modifying global config) and as the only way to create
374/// configuration overrides. Override guards cannot outlive this
375/// ConfigLock, ensuring proper synchronization.
376pub struct ConfigLock {
377 _guard: std::sync::MutexGuard<'static, ()>,
378}
379
380impl ConfigLock {
381 /// Create a configuration override that will be restored when
382 /// the guard is dropped.
383 ///
384 /// The returned guard must not outlive this ConfigLock.
385 pub fn override_key<'a, T: AttrValue>(
386 &'a self,
387 key: crate::attrs::Key<T>,
388 value: T,
389 ) -> ConfigValueGuard<'a, T> {
390 // Write into the single TestOverride layer (create if
391 // needed).
392 let (prev_in_layer, orig_env) = {
393 let mut guard = LAYERS.write().unwrap();
394 let layer_attrs = ensure_test_override_layer_mut(&mut guard);
395 // Save any previous override for this key in the the
396 // TestOverride layer.
397 let prev = layer_attrs.remove_value(key);
398 // Set new override value.
399 layer_attrs.set(key, value.clone());
400 // Mirror env var.
401 let orig_env = if let Some(env_var) = key.attrs().get(CONFIG_ENV_VAR) {
402 let orig = std::env::var(env_var).ok();
403 // SAFETY: Mutating process-global environment
404 // variables is not thread-safe. This path is used
405 // only in tests while holding the global
406 // ConfigLock, which serializes config mutations
407 // across the process. Tests are single-threaded
408 // with respect to env changes, so there are no
409 // concurrent readers/writers. We also record the
410 // original value and restore it in
411 // ConfigValueGuard::drop.
412 unsafe {
413 std::env::set_var(env_var, value.display());
414 }
415 Some((env_var.clone(), orig))
416 } else {
417 None
418 };
419 (prev, orig_env)
420 };
421
422 ConfigValueGuard {
423 key,
424 orig: prev_in_layer, // previous value for this key *inside* TestOverride layer
425 orig_env,
426 _phantom: PhantomData,
427 }
428 }
429}
430
431/// When a [`ConfigLock`] is dropped, the special
432/// [`Source::TestOverride`] layer (if present) is removed
433/// entirely. This discards all temporary overrides created under
434/// the lock, ensuring they cannot leak into subsequent tests or
435/// callers. Other layers (`Runtime`, `Env`, `File`, and defaults)
436/// are left untouched.
437///
438/// Note: individual values within the TestOverride layer may
439/// already have been restored by [`ConfigValueGuard`]s as they
440/// drop. This final removal guarantees no residual layer remains
441/// once the lock itself is released.
442impl Drop for ConfigLock {
443 fn drop(&mut self) {
444 let mut guard = LAYERS.write().unwrap();
445 if let Some(pos) = guard
446 .ordered
447 .iter()
448 .position(|l| matches!(l.source, Source::TestOverride))
449 {
450 guard.ordered.remove(pos);
451 }
452 // No need to restore anything else; underlying layers
453 // remain intact.
454 }
455}
456
457/// A guard that restores a single configuration value when dropped
458pub struct ConfigValueGuard<'a, T: 'static> {
459 key: crate::attrs::Key<T>,
460 orig: Option<Box<dyn crate::attrs::SerializableValue>>,
461 orig_env: Option<(String, Option<String>)>,
462 // This is here so we can hold onto a 'a lifetime.
463 _phantom: PhantomData<&'a ()>,
464}
465
466/// When a [`ConfigValueGuard`] is dropped, it restores the
467/// configuration state for the key it was guarding:
468///
469/// - If there was a previous override for this key in the
470/// [`Source::TestOverride`] layer, that value is reinserted.
471/// - If this guard was the only override for the key, the entry
472/// is removed from the layer entirely (leaving underlying layers
473/// or defaults to apply).
474/// - If the key declared a `CONFIG_ENV_VAR`, the corresponding
475/// process environment variable is restored to its original value
476/// (or removed if it didn't exist).
477///
478/// This ensures that overrides applied via
479/// [`ConfigLock::override_key`] are always reverted cleanly when
480/// the guard is dropped, without leaking state into subsequent
481/// tests or callers.
482impl<T: 'static> Drop for ConfigValueGuard<'_, T> {
483 fn drop(&mut self) {
484 let mut guard = LAYERS.write().unwrap();
485
486 if let Some(i) = test_override_index(&guard) {
487 let layer_attrs = &mut guard.ordered[i].attrs;
488
489 if let Some(prev) = self.orig.take() {
490 layer_attrs.insert_value(self.key, prev);
491 } else {
492 // remove without needing T: AttrValue
493 let _ = layer_attrs.remove_value(self.key);
494 }
495 }
496
497 if let Some((k, v)) = self.orig_env.take() {
498 // SAFETY: process-global environment variables are
499 // not thread-safe to mutate. This override/restore
500 // path is only ever used in single-threaded test
501 // code, and is serialized by the global ConfigLock to
502 // avoid races between tests.
503 unsafe {
504 if let Some(v) = v {
505 std::env::set_var(k, v);
506 } else {
507 std::env::remove_var(&k);
508 }
509 }
510 }
511 }
512}
513
514#[cfg(test)]
515mod tests {
516
517 use super::*;
518
519 #[test]
520 fn test_global_config() {
521 let config = lock();
522
523 // Reset global config to defaults to avoid interference from other tests
524 reset_to_defaults();
525
526 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
527 {
528 let _guard = config.override_key(CODEC_MAX_FRAME_LENGTH, 1024);
529 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 1024);
530 // The configuration will be automatically restored when _guard goes out of scope
531 }
532
533 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
534 }
535
536 #[test]
537 fn test_overrides() {
538 let config = lock();
539
540 // Reset global config to defaults to avoid interference from other tests
541 reset_to_defaults();
542
543 // Test the new lock/override API for individual config values
544 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
545 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
546
547 // Test single value override
548 {
549 let _guard = config.override_key(CODEC_MAX_FRAME_LENGTH, 2048);
550 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2048);
551 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30)); // Unchanged
552 }
553
554 // Values should be restored after guard is dropped
555 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
556
557 // Test multiple overrides
558 let orig_value = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").ok();
559 {
560 let _guard1 = config.override_key(CODEC_MAX_FRAME_LENGTH, 4096);
561 let _guard2 = config.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(60));
562
563 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 4096);
564 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(60));
565 // This was overridden:
566 assert_eq!(
567 std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(),
568 "1m"
569 );
570 }
571 assert_eq!(
572 std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").ok(),
573 orig_value
574 );
575
576 // All values should be restored
577 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), CODEC_MAX_FRAME_LENGTH_DEFAULT);
578 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
579 }
580
581 #[test]
582 fn test_layer_precedence_env_over_file_and_replacement() {
583 let _lock = lock();
584 reset_to_defaults();
585
586 // File sets a value.
587 let mut file = Attrs::new();
588 file[CODEC_MAX_FRAME_LENGTH] = 1111;
589 set(Source::File, file);
590
591 // Env sets a different value.
592 let mut env = Attrs::new();
593 env[CODEC_MAX_FRAME_LENGTH] = 2222;
594 set(Source::Env, env);
595
596 // Env should win over File.
597 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 2222);
598
599 // Replace Env layer with a new value.
600 let mut env2 = Attrs::new();
601 env2[CODEC_MAX_FRAME_LENGTH] = 3333;
602 set(Source::Env, env2);
603
604 assert_eq!(get(CODEC_MAX_FRAME_LENGTH), 3333);
605 }
606
607 #[test]
608 fn test_runtime_overrides_and_clear_restores_lower_layers() {
609 let _lock = lock();
610 reset_to_defaults();
611
612 // File baseline.
613 let mut file = Attrs::new();
614 file[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(30);
615 set(Source::File, file);
616
617 // Env override.
618 let mut env = Attrs::new();
619 env[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(40);
620 set(Source::Env, env);
621
622 // Runtime beats both.
623 let mut rt = Attrs::new();
624 rt[MESSAGE_DELIVERY_TIMEOUT] = Duration::from_secs(50);
625 set(Source::Runtime, rt);
626
627 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(50));
628
629 // Clearing Runtime should reveal Env again.
630 clear(Source::Runtime);
631
632 // With the Runtime layer gone, Env still wins over File.
633 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
634 }
635
636 #[test]
637 fn test_attrs_snapshot_materializes_defaults_and_omits_meta() {
638 let _lock = lock();
639 reset_to_defaults();
640
641 // No explicit layers: values should come from Defaults.
642 let snap = attrs();
643
644 // A few representative defaults are materialized:
645 assert_eq!(snap[CODEC_MAX_FRAME_LENGTH], 10 * 1024 * 1024 * 1024);
646 assert_eq!(snap[MESSAGE_DELIVERY_TIMEOUT], Duration::from_secs(30));
647
648 // CONFIG_ENV_VAR has no default and wasn't explicitly set:
649 // should be omitted.
650 let json = serde_json::to_string(&snap).unwrap();
651 assert!(
652 !json.contains("config_env_var"),
653 "CONFIG_ENV_VAR must not appear in snapshot unless explicitly set"
654 );
655 }
656
657 #[test]
658 fn test_parent_child_snapshot_as_runtime_layer() {
659 let _lock = lock();
660 reset_to_defaults();
661
662 // Parent effective config (pretend it's a parent process).
663 let mut parent_env = Attrs::new();
664 parent_env[MESSAGE_ACK_EVERY_N_MESSAGES] = 12345;
665 set(Source::Env, parent_env);
666
667 let parent_snap = attrs();
668
669 // "Child" process: start clean, install parent snapshot as
670 // Runtime.
671 reset_to_defaults();
672 set(Source::Runtime, parent_snap);
673
674 // Child should observe parent's effective value (as highest
675 // stable layer).
676 assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 12345);
677 }
678
679 #[test]
680 fn test_testoverride_layer_override_and_env_restore() {
681 let lock = lock();
682 reset_to_defaults();
683
684 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
685
686 // SAFETY: single-threaded test.
687 unsafe {
688 std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
689 }
690
691 {
692 let _guard = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(99));
693 // Override wins:
694 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(99));
695
696 // Env should be mirrored to the same duration (string may
697 // be "1m 39s")
698 let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
699 let parsed = humantime::parse_duration(&s).unwrap();
700 assert_eq!(parsed, Duration::from_secs(99));
701 }
702
703 // After drop, value and env restored:
704 assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
705 assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
706 }
707
708 #[test]
709 fn test_reset_to_defaults_clears_all_layers() {
710 let _lock = lock();
711 reset_to_defaults();
712
713 // Seed multiple layers.
714 let mut file = Attrs::new();
715 file[SPLIT_MAX_BUFFER_SIZE] = 7;
716 set(Source::File, file);
717
718 let mut env = Attrs::new();
719 env[SPLIT_MAX_BUFFER_SIZE] = 8;
720 set(Source::Env, env);
721
722 let mut rt = Attrs::new();
723 rt[SPLIT_MAX_BUFFER_SIZE] = 9;
724 set(Source::Runtime, rt);
725
726 // Sanity: highest wins.
727 assert_eq!(get(SPLIT_MAX_BUFFER_SIZE), 9);
728
729 // Reset clears all explicit layers; defaults apply.
730 reset_to_defaults();
731 assert_eq!(get(SPLIT_MAX_BUFFER_SIZE), 5); // default
732 }
733
734 #[test]
735 fn test_get_cloned_resolution_matches_get() {
736 let _lock = lock();
737 reset_to_defaults();
738
739 let mut env = Attrs::new();
740 env[CHANNEL_MULTIPART] = false;
741 set(Source::Env, env);
742
743 assert!(!get(CHANNEL_MULTIPART));
744 let v = get_cloned(CHANNEL_MULTIPART);
745 assert!(!v);
746 }
747
748 #[test]
749 fn test_attrs_snapshot_respects_layer_precedence_per_key() {
750 let _lock = lock();
751 reset_to_defaults();
752
753 let mut file = Attrs::new();
754 file[MESSAGE_TTL_DEFAULT] = 10;
755 set(Source::File, file);
756
757 let mut env = Attrs::new();
758 env[MESSAGE_TTL_DEFAULT] = 20;
759 set(Source::Env, env);
760
761 let snap = attrs();
762 assert_eq!(snap[MESSAGE_TTL_DEFAULT], 20); // Env beats File
763 }
764}