Skip to content

Commit 1ffac3c

Browse files
: config: global: robust to drop order in test overrides
Differential Revision: D84085259
1 parent 5520fa1 commit 1ffac3c

File tree

2 files changed

+247
-62
lines changed

2 files changed

+247
-62
lines changed

hyperactor/src/attrs.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ pub struct Key<T: 'static> {
153153
attrs: &'static LazyLock<Attrs>,
154154
}
155155

156+
impl<T> Key<T> {
157+
/// Returns the name of this key.
158+
pub fn name(&self) -> &'static str {
159+
self.name
160+
}
161+
}
162+
156163
impl<T: Named + 'static> Key<T> {
157164
/// Creates a new key with the given name.
158165
pub const fn new(
@@ -167,11 +174,6 @@ impl<T: Named + 'static> Key<T> {
167174
}
168175
}
169176

170-
/// Returns the name of this key.
171-
pub fn name(&self) -> &'static str {
172-
self.name
173-
}
174-
175177
/// Returns a reference to the default value for this key, if one exists.
176178
pub fn default(&self) -> Option<&'static T> {
177179
self.default_value

hyperactor/src/config/global.rs

Lines changed: 240 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
//! ```
4646
use std::collections::HashMap;
4747
use std::marker::PhantomData;
48+
use std::sync::atomic::AtomicU64;
49+
use std::sync::atomic::Ordering;
4850

4951
use super::*;
5052
use crate::attrs::AttrValue;
@@ -333,6 +335,8 @@ static LAYERS: LazyLock<Arc<RwLock<Layers>>> = LazyLock::new(|| {
333335
Arc::new(RwLock::new(layers))
334336
});
335337

338+
static OVERRIDE_TOKEN_SEQ: AtomicU64 = AtomicU64::new(1);
339+
336340
/// Acquire the global configuration lock.
337341
///
338342
/// This lock serializes all mutations of the global
@@ -569,38 +573,67 @@ impl ConfigLock {
569573
key: crate::attrs::Key<T>,
570574
value: T,
571575
) -> ConfigValueGuard<'a, T> {
572-
// Write into the single TestOverride layer (create if
573-
// needed).
574-
let (prev_in_layer, orig_env) = {
575-
let mut guard = LAYERS.write().unwrap();
576-
let layer_attrs = ensure_test_override_layer_mut(&mut guard);
577-
// Save any previous override for this key in the the
578-
// TestOverride layer.
579-
let prev = layer_attrs.remove_value(key);
580-
// Set new override value.
581-
layer_attrs.set(key, value.clone());
582-
// Mirror env var.
583-
let orig_env = if let Some(cfg) = key.attrs().get(crate::config::CONFIG) {
584-
if let Some(env_var) = &cfg.env_name {
585-
let orig = std::env::var(env_var).ok();
586-
// SAFETY: this path is used only in tests under ConfigLock
587-
unsafe {
588-
std::env::set_var(env_var, value.display());
589-
}
590-
Some((env_var.clone(), orig))
591-
} else {
592-
None
593-
}
576+
let token = OVERRIDE_TOKEN_SEQ.fetch_add(1, Ordering::Relaxed);
577+
578+
let mut g = LAYERS.write().unwrap();
579+
580+
// Ensure TestOverride layer exists.
581+
let idx = if let Some(i) = test_override_index(&g) {
582+
i
583+
} else {
584+
g.ordered.push(Layer::TestOverride {
585+
attrs: Attrs::new(),
586+
stacks: HashMap::new(),
587+
});
588+
g.ordered.sort_by_key(|l| priority(layer_source(l)));
589+
test_override_index(&g).expect("just inserted TestOverride layer")
590+
};
591+
592+
// Mutably access TestOverride's attrs + stacks.
593+
let (attrs, stacks) = match &mut g.ordered[idx] {
594+
Layer::TestOverride { attrs, stacks } => (attrs, stacks),
595+
_ => unreachable!(),
596+
};
597+
598+
// Compute env var (if any) for this key once.
599+
let (env_var, env_str) = if let Some(cfg) = key.attrs().get(crate::config::CONFIG) {
600+
if let Some(name) = &cfg.env_name {
601+
(Some(name.clone()), value.display())
594602
} else {
595-
None
596-
};
597-
(prev, orig_env)
603+
(None, String::new())
604+
}
605+
} else {
606+
(None, String::new())
598607
};
599608

609+
// Get per-key stack (by declared name).
610+
let key_name = key.name();
611+
let stack = stacks.entry(key_name).or_insert_with(|| OverrideStack {
612+
env_var: env_var.clone(),
613+
saved_env: env_var.as_ref().and_then(|n| std::env::var(n).ok()),
614+
frames: Vec::new(),
615+
});
616+
617+
// Push the new frame.
618+
let boxed: Box<dyn crate::attrs::SerializableValue> = Box::new(value.clone());
619+
stack.frames.push(OverrideFrame {
620+
token,
621+
value: boxed,
622+
env_str,
623+
});
624+
625+
// Make this frame the active value in TestOverride attrs.
626+
attrs.set(key, value.clone());
627+
628+
// Update process env to reflect new top-of-stack.
629+
if let (Some(var), Some(top)) = (stack.env_var.as_ref(), stack.frames.last()) {
630+
// SAFETY: Under global ConfigLock during tests.
631+
unsafe { std::env::set_var(var, &top.env_str) }
632+
}
633+
600634
ConfigValueGuard {
601635
key,
602-
orig: prev_in_layer, // previous value for this key *inside* TestOverride layer
603-
orig_env,
636+
token,
604637
_phantom: PhantomData,
605638
}
606639
}
@@ -629,55 +662,96 @@ impl Drop for ConfigLock {
629662
/// A guard that restores a single configuration value when dropped
630663
pub struct ConfigValueGuard<'a, T: 'static> {
631664
key: crate::attrs::Key<T>,
632-
orig: Option<Box<dyn crate::attrs::SerializableValue>>,
633-
orig_env: Option<(String, Option<String>)>,
665+
token: u64,
634666
// This is here so we can hold onto a 'a lifetime.
635667
_phantom: PhantomData<&'a ()>,
636668
}
637669

638670
/// When a [`ConfigValueGuard`] is dropped, it restores the
639671
/// configuration state for the key it was guarding:
640672
///
641-
/// - If there was a previous override for this key in the
642-
/// [`Source::TestOverride`] layer, that value is reinserted.
643-
/// - If this guard was the only override for the key, the entry is
644-
/// removed from the layer entirely (leaving underlying layers or
645-
/// defaults to apply).
646-
/// - If the key declared a `CONFIG.env_name` (via `@meta(CONFIG =
647-
/// ConfigAttr { env_name: Some(...), .. })`), the corresponding
648-
/// process environment variable is restored to its original value
649-
/// (or removed if it didn't exist).
673+
/// <insert text here>
650674
///
651675
/// This ensures that overrides applied via
652676
/// [`ConfigLock::override_key`] are always reverted cleanly when the
653677
/// guard is dropped, without leaking state into subsequent tests or
654678
/// callers.
655679
impl<T: 'static> Drop for ConfigValueGuard<'_, T> {
656680
fn drop(&mut self) {
657-
let mut guard = LAYERS.write().unwrap();
658-
659-
if let Some(i) = test_override_index(&guard) {
660-
let layer_attrs = &mut layer_attrs_mut(&mut guard.ordered[i]);
681+
let mut g = LAYERS.write().unwrap();
682+
let i = if let Some(i) = test_override_index(&g) {
683+
i
684+
} else {
685+
return;
686+
};
661687

662-
if let Some(prev) = self.orig.take() {
663-
layer_attrs.insert_value(self.key, prev);
664-
} else {
665-
// remove without needing T: AttrValue
666-
let _ = layer_attrs.remove_value(self.key);
667-
}
668-
}
688+
// Access TestOverride internals
689+
let (attrs, stacks) = match &mut g.ordered[i] {
690+
Layer::TestOverride { attrs, stacks } => (attrs, stacks),
691+
_ => unreachable!("TestOverride index points to non-TestOverride layer"),
692+
};
669693

670-
if let Some((k, v)) = self.orig_env.take() {
671-
// SAFETY: only ever used in single-threaded test code and
672-
// serialized by the global ConfigLock to avoid races
673-
// between tests.
674-
unsafe {
675-
if let Some(v) = v {
676-
std::env::set_var(k, v);
694+
let key_name = self.key.name();
695+
696+
// We need a tiny scope for the &mut borrow of the stack so we
697+
// can call `stacks.remove(key_name)` afterward if it becomes
698+
// empty.
699+
let mut remove_empty_stack = false;
700+
let mut restore_env_var: Option<String> = None;
701+
let mut restore_env_to: Option<String> = None;
702+
703+
if let Some(stack) = stacks.get_mut(key_name) {
704+
// Find this guard's frame by token.
705+
if let Some(pos) = stack.frames.iter().position(|f| f.token == self.token) {
706+
let is_top = pos + 1 == stack.frames.len();
707+
708+
if is_top {
709+
// Pop the active frame
710+
stack.frames.pop();
711+
712+
if let Some(new_top) = stack.frames.last() {
713+
// New top becomes active: update attrs and env.
714+
attrs.insert_value(self.key, (*new_top.value).cloned());
715+
if let Some(var) = stack.env_var.as_ref() {
716+
// SAFETY: Under global ConfigLock during tests.
717+
unsafe { std::env::set_var(var, &new_top.env_str) }
718+
}
719+
} else {
720+
// Stack empty: remove key now, and after we
721+
// drop the&mut borrow of the stack, restore
722+
// env and remove the stack entry.
723+
let _ = attrs.remove_value(self.key);
724+
725+
// Capture restoration details while we still have access to the stack.
726+
if let Some(var) = stack.env_var.as_ref() {
727+
restore_env_var = Some(var.clone());
728+
restore_env_to = stack.saved_env.clone(); // None => unset
729+
}
730+
remove_empty_stack = true
731+
}
677732
} else {
678-
std::env::remove_var(&k);
733+
// Out-of-order drop: remove only that frame: active top stays
734+
stack.frames.remove(pos);
735+
// No changes to attrs or env here.
736+
}
737+
} // else: token already handled; nothing to do
738+
} // &must stack borrow ends here
739+
740+
// If we emptied the stack for this key, restore env and drop
741+
// the stack entry.
742+
if remove_empty_stack {
743+
if let Some(var) = restore_env_var.as_ref() {
744+
// SAFETY: Under global ConfigLock during tests.
745+
unsafe {
746+
if let Some(val) = restore_env_to.as_ref() {
747+
std::env::set_var(var, val);
748+
} else {
749+
std::env::remove_var(var);
750+
}
679751
}
680752
}
753+
// Now it's safe to remove the stack from the map.
754+
let _ = stacks.remove(key_name);
681755
}
682756
}
683757
}
@@ -962,4 +1036,113 @@ mod tests {
9621036
"attrs() should exclude keys without @meta(CONFIG = ...)"
9631037
);
9641038
}
1039+
1040+
#[test]
1041+
fn test_testoverride_multiple_stacked_overrides_lifo() {
1042+
let lock = lock();
1043+
reset_to_defaults();
1044+
1045+
// Baseline sanity.
1046+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1047+
1048+
// Start from a clean env so we can assert restoration to "unset".
1049+
// SAFETY: single-threaded tests.
1050+
unsafe {
1051+
std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1052+
}
1053+
assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1054+
1055+
// Stack A: 40s (becomes top)
1056+
let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40));
1057+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1058+
{
1059+
let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1060+
assert_eq!(
1061+
humantime::parse_duration(&s).unwrap(),
1062+
Duration::from_secs(40)
1063+
);
1064+
}
1065+
1066+
// Stack B: 50s (new top)
1067+
let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50));
1068+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(50));
1069+
{
1070+
let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1071+
assert_eq!(
1072+
humantime::parse_duration(&s).unwrap(),
1073+
Duration::from_secs(50)
1074+
);
1075+
}
1076+
1077+
// Drop B first → should reveal A (LIFO)
1078+
std::mem::drop(guard_b);
1079+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1080+
{
1081+
let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1082+
assert_eq!(
1083+
humantime::parse_duration(&s).unwrap(),
1084+
Duration::from_secs(40)
1085+
);
1086+
}
1087+
1088+
// Drop A → should restore default and unset env.
1089+
std::mem::drop(guard_a);
1090+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1091+
assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1092+
}
1093+
1094+
#[test]
1095+
fn test_testoverride_out_of_order_drop_keeps_top_stable() {
1096+
let lock = lock();
1097+
reset_to_defaults();
1098+
1099+
// Clean env baseline.
1100+
// SAFETY: single-threaded tests.
1101+
unsafe {
1102+
std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT");
1103+
}
1104+
assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1105+
1106+
// Push three frames in order: A=40s, B=50s, C=70s (C is top).
1107+
let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40));
1108+
let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50));
1109+
let guard_c = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(70));
1110+
1111+
// Top is C.
1112+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70));
1113+
{
1114+
let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1115+
assert_eq!(
1116+
humantime::parse_duration(&s).unwrap(),
1117+
Duration::from_secs(70)
1118+
);
1119+
}
1120+
1121+
// Drop the *middle* frame (B) first → top must remain C, env unchanged.
1122+
std::mem::drop(guard_b);
1123+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70));
1124+
{
1125+
let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1126+
assert_eq!(
1127+
humantime::parse_duration(&s).unwrap(),
1128+
Duration::from_secs(70)
1129+
);
1130+
}
1131+
1132+
// Now drop C → A becomes top, env follows A.
1133+
std::mem::drop(guard_c);
1134+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40));
1135+
{
1136+
let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap();
1137+
assert_eq!(
1138+
humantime::parse_duration(&s).unwrap(),
1139+
Duration::from_secs(40)
1140+
);
1141+
}
1142+
1143+
// Drop A → restore default and clear env.
1144+
std::mem::drop(guard_a);
1145+
assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30));
1146+
assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err());
1147+
}
9651148
}

0 commit comments

Comments
 (0)