Skip to content

Commit f191ad5

Browse files
: config: global: change layer rep in readiness for stacked test overrides (#1456)
Summary: relates to part 2 of the refactors discussed in D83778023 refactor(config/global.rs): introduce `Layer` enum and recover tests - switch `Layer` representation from `struct`+`Source` to `enum` with variants: `File`, `Env`, `Runtime`, and `TestOverride { attrs, stacks }`. - updated helpers (`layer_attrs`, `layer_attrs_mut`, `test_override_index`). - adjusted `Drop` impls for `ConfigValueGuard` and `ConfigLock` to compile and preserve all existing behavior. all unit tests pass; ready for semantic refactor of `TestOverride` logic next. Reviewed By: mariusae Differential Revision: D84072524
1 parent 7bd6ee5 commit f191ad5

File tree

1 file changed

+230
-61
lines changed

1 file changed

+230
-61
lines changed

hyperactor/src/config/global.rs

Lines changed: 230 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
//! // ... test logic here ...
4444
//! }
4545
//! ```
46+
use std::collections::HashMap;
4647
use std::marker::PhantomData;
4748

4849
use super::*;
@@ -87,20 +88,6 @@ fn priority(s: Source) -> u8 {
8788
}
8889
}
8990

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)]
96-
struct 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-
10491
/// The full set of configuration layers in priority order.
10592
///
10693
/// `Layers` wraps a vector of [`Layer`]s, always kept sorted by
@@ -115,6 +102,209 @@ struct Layers {
115102
ordered: Vec<Layer>,
116103
}
117104

105+
/// A single configuration layer in the global configuration model.
106+
///
107+
/// Layers are consulted in priority order (`TestOverride → Runtime →
108+
/// Env → File → Default`) when resolving configuration values. Each
109+
/// variant holds an [`Attrs`] map of key/value pairs.
110+
///
111+
/// The `TestOverride` variant additionally maintains per-key override
112+
/// stacks to support nested and out-of-order test overrides. These
113+
/// stacks are currently placeholders for a future refactor; for now,
114+
/// only the `attrs` field is used.
115+
///
116+
/// Variants:
117+
/// - [`Layer::File`] — Values loaded from configuration files (lowest
118+
/// explicit priority).
119+
/// - [`Layer::Env`] — Values sourced from process environment
120+
/// variables.
121+
/// - [`Layer::Runtime`] — Programmatically set runtime overrides.
122+
/// - [`Layer::TestOverride`] — Temporary in-test overrides applied
123+
/// under [`ConfigLock`].
124+
///
125+
/// Layers are stored in [`Layers::ordered`], kept sorted by their
126+
/// effective [`Source`] priority (`TestOverride` first, `File` last).
127+
enum Layer {
128+
/// Values loaded from configuration files. Lowest explicit
129+
/// priority; only overridden by Env, Runtime, or TestOverride.
130+
File(Attrs),
131+
132+
/// Values read from process environment variables. Typically
133+
/// installed at startup via [`init_from_env`].
134+
Env(Attrs),
135+
136+
/// Values set programmatically at runtime. Stable high-priority
137+
/// layer used by parent/child bootstrap and dynamic updates.
138+
Runtime(Attrs),
139+
140+
/// Ephemeral values inserted during tests via
141+
/// [`ConfigLock::override_key`]. Always takes precedence over all
142+
/// other layers. Currently holds both the active `attrs` map and
143+
/// a per-key `stacks` table (used to support nested or
144+
/// out-of-order test overrides in future refactors).
145+
TestOverride {
146+
attrs: Attrs,
147+
stacks: HashMap<&'static str, OverrideStack>,
148+
},
149+
}
150+
151+
/// A per-key stack of test overrides used by the
152+
/// [`Layer::TestOverride`] layer.
153+
///
154+
/// Each stack tracks the sequence of active overrides applied to a
155+
/// single configuration key. The topmost frame represents the
156+
/// currently effective override; earlier frames represent older
157+
/// (still live) guards that may drop out of order.
158+
///
159+
/// Fields:
160+
/// - `env_var`: The associated process environment variable name, if
161+
/// any.
162+
/// - `saved_env`: The original environment variable value before the
163+
/// first override was applied (or `None` if it did not exist).
164+
/// - `frames`: The stack of active override frames, with the top
165+
/// being the last element in the vector.
166+
///
167+
/// The full stack mechanism is not yet active; it is introduced
168+
/// incrementally to prepare for robust out-of-order test override
169+
/// restoration.
170+
struct OverrideStack {
171+
/// The name of the process environment variable associated with
172+
/// this configuration key, if any.
173+
///
174+
/// Used to mirror changes to the environment when overrides are
175+
/// applied or removed. `None` if the key has no
176+
/// `CONFIG.env_name`.
177+
env_var: Option<String>,
178+
179+
/// The original value of the environment variable before the
180+
/// first override was applied.
181+
///
182+
/// Stored so it can be restored once the last frame is dropped.
183+
/// `None` means the variable did not exist prior to overriding.
184+
saved_env: Option<String>,
185+
186+
/// The sequence of active override frames for this key.
187+
///
188+
/// Each frame represents one active test override; the last
189+
/// element (`frames.last()`) is the current top-of-stack and
190+
/// defines the effective value seen in the configuration and
191+
/// environment.
192+
frames: Vec<OverrideFrame>,
193+
}
194+
195+
/// A single entry in a per-key override stack.
196+
///
197+
/// Each `OverrideFrame` represents one active test override applied
198+
/// via [`ConfigLock::override_key`]. Frames are uniquely identified
199+
/// by a monotonically increasing token and record both the value
200+
/// being overridden and its string form for environment mirroring.
201+
///
202+
/// When a guard drops, its frame is removed from the stack; if it was
203+
/// the top, the next frame (if any) becomes active, or the original
204+
/// environment is restored when the stack becomes empty.
205+
struct OverrideFrame {
206+
/// A unique, monotonically increasing identifier for this
207+
/// override frame.
208+
///
209+
/// Used to associate a dropping [`ConfigValueGuard`] with its
210+
/// corresponding entry in the stack, even if drops occur out of
211+
/// order.
212+
token: u64,
213+
214+
/// The serialized configuration value active while this frame is
215+
/// on top of its stack.
216+
///
217+
/// Stored as a boxed [`SerializableValue`] to match how values
218+
/// are kept within [`Attrs`].
219+
value: Box<dyn crate::attrs::SerializableValue>,
220+
221+
/// Pre-rendered string form of the value, used for environment
222+
/// variable updates when this frame becomes active.
223+
///
224+
/// Avoids recomputing `value.display()` on every push or pop.
225+
env_str: String,
226+
}
227+
228+
/// Return the [`Source`] corresponding to a given [`Layer`].
229+
///
230+
/// This provides a uniform way to retrieve a layer's logical source
231+
/// (File, Env, Runtime, or TestOverride) regardless of its internal
232+
/// representation. Used for sorting layers by priority and for
233+
/// source-based lookups or removals.
234+
fn layer_source(l: &Layer) -> Source {
235+
match l {
236+
Layer::File(_) => Source::File,
237+
Layer::Env(_) => Source::Env,
238+
Layer::Runtime(_) => Source::Runtime,
239+
Layer::TestOverride { .. } => Source::TestOverride,
240+
}
241+
}
242+
243+
/// Return an immutable reference to the [`Attrs`] contained in a
244+
/// [`Layer`].
245+
///
246+
/// This abstracts over the specific layer variant so callers can read
247+
/// configuration values uniformly without needing to pattern-match on
248+
/// the layer type. For `TestOverride`, this returns the current
249+
/// top-level attributes reflecting the active overrides.
250+
fn layer_attrs(l: &Layer) -> &Attrs {
251+
match l {
252+
Layer::File(a) | Layer::Env(a) | Layer::Runtime(a) => a,
253+
Layer::TestOverride { attrs, .. } => attrs,
254+
}
255+
}
256+
257+
/// Return a mutable reference to the [`Attrs`] contained in a
258+
/// [`Layer`].
259+
///
260+
/// This allows callers to modify configuration values within any
261+
/// layer without needing to pattern-match on its variant. For
262+
/// `TestOverride`, the returned [`Attrs`] always reflect the current
263+
/// top-of-stack overrides for each key.
264+
fn layer_attrs_mut(l: &mut Layer) -> &mut Attrs {
265+
match l {
266+
Layer::File(a) | Layer::Env(a) | Layer::Runtime(a) => a,
267+
Layer::TestOverride { attrs, .. } => attrs,
268+
}
269+
}
270+
271+
/// Return the index of the [`Layer::TestOverride`] within the
272+
/// [`Layers`] vector.
273+
///
274+
/// If a TestOverride layer is present, its position in the ordered
275+
/// list is returned; otherwise, `None` is returned. This is used to
276+
/// locate the active test override layer for inserting or restoring
277+
/// temporary configuration values.
278+
fn test_override_index(layers: &Layers) -> Option<usize> {
279+
layers
280+
.ordered
281+
.iter()
282+
.position(|l| matches!(l, Layer::TestOverride { .. }))
283+
}
284+
285+
/// Ensure a [`Layer::TestOverride`] exists and return a mutable
286+
/// reference to its [`Attrs`].
287+
///
288+
/// If the TestOverride layer already exists, a mutable reference to
289+
/// its attributes is returned directly. Otherwise, a new TestOverride
290+
/// layer (with empty `Attrs` and stacks) is created, inserted into
291+
/// the ordered layers according to priority, and then returned.
292+
///
293+
/// This helper is used by [`ConfigLock::override_key`] to guarantee
294+
/// that test overrides always have a dedicated writable layer.
295+
fn ensure_test_override_layer_mut<'a>(layers: &'a mut Layers) -> &'a mut Attrs {
296+
if let Some(i) = test_override_index(layers) {
297+
return layer_attrs_mut(&mut layers.ordered[i]);
298+
}
299+
layers.ordered.push(Layer::TestOverride {
300+
attrs: Attrs::new(),
301+
stacks: HashMap::new(),
302+
});
303+
layers.ordered.sort_by_key(|l| priority(layer_source(l)));
304+
let i = test_override_index(layers).expect("just inserted TestOverride layer");
305+
layer_attrs_mut(&mut layers.ordered[i])
306+
}
307+
118308
/// Global layered configuration store.
119309
///
120310
/// This is the single authoritative store for configuration in
@@ -138,10 +328,7 @@ struct Layers {
138328
static LAYERS: LazyLock<Arc<RwLock<Layers>>> = LazyLock::new(|| {
139329
let env = super::from_env();
140330
let layers = Layers {
141-
ordered: vec![Layer {
142-
source: Source::Env,
143-
attrs: env,
144-
}],
331+
ordered: vec![Layer::Env(env)],
145332
};
146333
Arc::new(RwLock::new(layers))
147334
});
@@ -214,8 +401,9 @@ pub fn init_from_yaml<P: AsRef<Path>>(path: P) -> Result<(), anyhow::Error> {
214401
pub fn get<T: AttrValue + Copy>(key: Key<T>) -> T {
215402
let layers = LAYERS.read().unwrap();
216403
for layer in &layers.ordered {
217-
if layer.attrs.contains_key(key) {
218-
return *layer.attrs.get(key).unwrap();
404+
let a = layer_attrs(layer);
405+
if let Some(value) = a.get(key) {
406+
return *value;
219407
}
220408
}
221409
*key.default().expect("key must have a default")
@@ -240,8 +428,9 @@ pub fn get_cloned<T: AttrValue>(key: Key<T>) -> T {
240428
pub fn try_get_cloned<T: AttrValue>(key: Key<T>) -> Option<T> {
241429
let layers = LAYERS.read().unwrap();
242430
for layer in &layers.ordered {
243-
if layer.attrs.contains_key(key) {
244-
return layer.attrs.get(key).cloned();
431+
let a = layer_attrs(layer);
432+
if a.contains_key(key) {
433+
return a.get(key).cloned();
245434
}
246435
}
247436
key.default().cloned()
@@ -261,12 +450,20 @@ pub fn try_get_cloned<T: AttrValue>(key: Key<T>) -> Option<T> {
261450
/// overriding configuration values.
262451
pub fn set(source: Source, attrs: Attrs) {
263452
let mut g = LAYERS.write().unwrap();
264-
if let Some(l) = g.ordered.iter_mut().find(|l| l.source == source) {
265-
l.attrs = attrs;
453+
if let Some(l) = g.ordered.iter_mut().find(|l| layer_source(l) == source) {
454+
*layer_attrs_mut(l) = attrs;
266455
} else {
267-
g.ordered.push(Layer { source, attrs });
456+
g.ordered.push(match source {
457+
Source::File => Layer::File(attrs),
458+
Source::Env => Layer::Env(attrs),
459+
Source::Runtime => Layer::Runtime(attrs),
460+
Source::TestOverride => Layer::TestOverride {
461+
attrs,
462+
stacks: HashMap::new(),
463+
},
464+
});
268465
}
269-
g.ordered.sort_by_key(|l| priority(l.source)); // TestOverride < Runtime < Env < File
466+
g.ordered.sort_by_key(|l| priority(layer_source(l))); // TestOverride < Runtime < Env < File
270467
}
271468

272469
/// Remove the configuration layer for the given [`Source`], if
@@ -279,7 +476,7 @@ pub fn set(source: Source, attrs: Attrs) {
279476
#[allow(dead_code)]
280477
pub(crate) fn clear(source: Source) {
281478
let mut g = LAYERS.write().unwrap();
282-
g.ordered.retain(|l| l.source != source);
479+
g.ordered.retain(|l| layer_source(l) != source);
283480
}
284481

285482
/// Return a complete, merged snapshot of the effective configuration
@@ -311,7 +508,7 @@ pub fn attrs() -> Attrs {
311508
// Try to resolve from highest -> lowest priority layer.
312509
let mut chosen: Option<Box<dyn crate::attrs::SerializableValue>> = None;
313510
for layer in &layers.ordered {
314-
if let Some(v) = layer.attrs.get_value_by_name(name) {
511+
if let Some(v) = layer_attrs(layer).get_value_by_name(name) {
315512
chosen = Some(v.cloned());
316513
break;
317514
}
@@ -351,26 +548,6 @@ pub fn reset_to_defaults() {
351548
g.ordered.clear();
352549
}
353550

354-
fn test_override_index(layers: &Layers) -> Option<usize> {
355-
layers
356-
.ordered
357-
.iter()
358-
.position(|l| matches!(l.source, Source::TestOverride))
359-
}
360-
361-
fn ensure_test_override_layer_mut(layers: &mut Layers) -> &mut Attrs {
362-
if let Some(i) = test_override_index(layers) {
363-
return &mut layers.ordered[i].attrs;
364-
}
365-
layers.ordered.push(Layer {
366-
source: Source::TestOverride,
367-
attrs: Attrs::new(),
368-
});
369-
layers.ordered.sort_by_key(|l| priority(l.source));
370-
let i = test_override_index(layers).expect("just inserted TestOverride layer");
371-
&mut layers.ordered[i].attrs
372-
}
373-
374551
/// A guard that holds the global configuration lock and provides
375552
/// override functionality.
376553
///
@@ -443,15 +620,9 @@ impl ConfigLock {
443620
impl Drop for ConfigLock {
444621
fn drop(&mut self) {
445622
let mut guard = LAYERS.write().unwrap();
446-
if let Some(pos) = guard
447-
.ordered
448-
.iter()
449-
.position(|l| matches!(l.source, Source::TestOverride))
450-
{
623+
if let Some(pos) = test_override_index(&guard) {
451624
guard.ordered.remove(pos);
452625
}
453-
// No need to restore anything else; underlying layers
454-
// remain intact.
455626
}
456627
}
457628

@@ -486,7 +657,7 @@ impl<T: 'static> Drop for ConfigValueGuard<'_, T> {
486657
let mut guard = LAYERS.write().unwrap();
487658

488659
if let Some(i) = test_override_index(&guard) {
489-
let layer_attrs = &mut guard.ordered[i].attrs;
660+
let layer_attrs = &mut layer_attrs_mut(&mut guard.ordered[i]);
490661

491662
if let Some(prev) = self.orig.take() {
492663
layer_attrs.insert_value(self.key, prev);
@@ -497,11 +668,9 @@ impl<T: 'static> Drop for ConfigValueGuard<'_, T> {
497668
}
498669

499670
if let Some((k, v)) = self.orig_env.take() {
500-
// SAFETY: process-global environment variables are
501-
// not thread-safe to mutate. This override/restore
502-
// path is only ever used in single-threaded test
503-
// code, and is serialized by the global ConfigLock to
504-
// avoid races between tests.
671+
// SAFETY: only ever used in single-threaded test code and
672+
// serialized by the global ConfigLock to avoid races
673+
// between tests.
505674
unsafe {
506675
if let Some(v) = v {
507676
std::env::set_var(k, v);

0 commit comments

Comments
 (0)