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}