Skip to content

Commit 34e9810

Browse files
: 'configure' should merge to global config not set (#1913)
Summary: this diff fixes incorrect overwrite semantics in the python configuration path. previously, each kwarg passed to `configure()` called `global::set(Source::Runtime, …)`, which replaces the entire `Runtime` layer. as a result, setting multiple keys in separate calls-or even within a single call-would drop previously set `Runtime` values. the fix introduces `Attrs::merge` and `global::create_or_merge`, which perform an in‑place update of an existing layer while preserving keys not mentioned in the update. the python bridge now uses `create_or_merge` instead of `set`, giving `Runtime` the incremental semantics expected by the python API. a shared `make_layer` helper removes duplicated construction logic. python‑exposed config keys for log forwarding and capture are enabled, and new tests in rust and python exercise single and multi-key configuration. Reviewed By: mariusae Differential Revision: D87339094
1 parent f4deec6 commit 34e9810

File tree

6 files changed

+106
-14
lines changed

6 files changed

+106
-14
lines changed

hyperactor/src/attrs.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,15 @@ impl Attrs {
556556
pub(crate) fn get_value_by_name(&self, name: &'static str) -> Option<&dyn SerializableValue> {
557557
self.values.get(name).map(|b| b.as_ref())
558558
}
559+
560+
/// Merge all attributes from `other` into this set, consuming
561+
/// `other`.
562+
///
563+
/// For each key in `other`, moves its value into `self`,
564+
/// overwriting any existing value for the same key.
565+
pub(crate) fn merge(&mut self, other: Attrs) {
566+
self.values.extend(other.values);
567+
}
559568
}
560569

561570
impl Clone for Attrs {

hyperactor/src/config/global.rs

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,24 @@ pub fn try_get_cloned<T: AttrValue>(key: Key<T>) -> Option<T> {
440440
key.default().cloned()
441441
}
442442

443+
/// Construct a [`Layer`] for the given [`Source`] using the provided
444+
/// `attrs`.
445+
///
446+
/// Used by [`set`] and [`create_or_merge`] when installing a new
447+
/// configuration layer.
448+
fn make_layer(source: Source, attrs: Attrs) -> Layer {
449+
match source {
450+
Source::File => Layer::File(attrs),
451+
Source::Env => Layer::Env(attrs),
452+
Source::Runtime => Layer::Runtime(attrs),
453+
Source::TestOverride => Layer::TestOverride {
454+
attrs,
455+
stacks: HashMap::new(),
456+
},
457+
Source::ClientOverride => Layer::ClientOverride(attrs),
458+
}
459+
}
460+
443461
/// Insert or replace a configuration layer for the given source.
444462
///
445463
/// If a layer with the same [`Source`] already exists, its
@@ -457,16 +475,34 @@ pub fn set(source: Source, attrs: Attrs) {
457475
if let Some(l) = g.ordered.iter_mut().find(|l| layer_source(l) == source) {
458476
*layer_attrs_mut(l) = attrs;
459477
} else {
460-
g.ordered.push(match source {
461-
Source::File => Layer::File(attrs),
462-
Source::Env => Layer::Env(attrs),
463-
Source::Runtime => Layer::Runtime(attrs),
464-
Source::TestOverride => Layer::TestOverride {
465-
attrs,
466-
stacks: HashMap::new(),
467-
},
468-
Source::ClientOverride => Layer::ClientOverride(attrs),
469-
});
478+
g.ordered.push(make_layer(source, attrs));
479+
}
480+
g.ordered.sort_by_key(|l| priority(layer_source(l))); // TestOverride < Runtime < Env < File < ClientOverride
481+
}
482+
483+
/// Insert or update a configuration layer for the given [`Source`].
484+
///
485+
/// If a layer with the same [`Source`] already exists, its attributes
486+
/// are **updated in place**: all keys present in `attrs` are absorbed
487+
/// into the existing layer, overwriting any previous values for those
488+
/// keys while leaving all other keys in that layer unchanged.
489+
///
490+
/// If no layer for `source` exists yet, this behaves like [`set`]: a
491+
/// new layer is created with the provided `attrs`.
492+
///
493+
/// This is useful for incremental / additive updates (for example,
494+
/// runtime configuration driven by a Python API), where callers want
495+
/// to change a subset of keys without discarding previously installed
496+
/// values in the same layer.
497+
///
498+
/// By contrast, [`set`] replaces the entire layer for `source` with
499+
/// `attrs`, discarding any existing values in that layer.
500+
pub fn create_or_merge(source: Source, attrs: Attrs) {
501+
let mut g = LAYERS.write().unwrap();
502+
if let Some(layer) = g.ordered.iter_mut().find(|l| layer_source(l) == source) {
503+
layer_attrs_mut(layer).merge(attrs);
504+
} else {
505+
g.ordered.push(make_layer(source, attrs));
470506
}
471507
g.ordered.sort_by_key(|l| priority(layer_source(l))); // TestOverride < Runtime < Env < File < ClientOverride
472508
}
@@ -1206,4 +1242,37 @@ mod tests {
12061242
assert!(priority(Env) < priority(File));
12071243
assert!(priority(File) < priority(ClientOverride));
12081244
}
1245+
1246+
#[test]
1247+
fn test_create_or_merge_runtime_merges_keys() {
1248+
let _lock = lock();
1249+
reset_to_defaults();
1250+
1251+
// Seed Runtime with one key.
1252+
let mut rt = Attrs::new();
1253+
rt[MESSAGE_TTL_DEFAULT] = 10;
1254+
set(Source::Runtime, rt);
1255+
1256+
// Now update Runtime with a different key via
1257+
// `create_or_merge`.
1258+
let mut update = Attrs::new();
1259+
update[MESSAGE_ACK_EVERY_N_MESSAGES] = 123;
1260+
create_or_merge(Source::Runtime, update);
1261+
1262+
// Both keys should now be visible from Runtime.
1263+
assert_eq!(get(MESSAGE_TTL_DEFAULT), 10);
1264+
assert_eq!(get(MESSAGE_ACK_EVERY_N_MESSAGES), 123);
1265+
}
1266+
1267+
#[test]
1268+
fn test_create_or_merge_runtime_creates_layer_if_missing() {
1269+
let _lock = lock();
1270+
reset_to_defaults();
1271+
1272+
let mut rt = Attrs::new();
1273+
rt[MESSAGE_TTL_DEFAULT] = 42;
1274+
create_or_merge(Source::Runtime, rt);
1275+
1276+
assert_eq!(get(MESSAGE_TTL_DEFAULT), 42);
1277+
}
12091278
}

hyperactor_mesh/src/bootstrap.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ declare_attrs! {
9696
/// piping) or via [`StreamFwder`] when piping is active.
9797
@meta(CONFIG = ConfigAttr {
9898
env_name: Some("HYPERACTOR_MESH_ENABLE_LOG_FORWARDING".to_string()),
99-
py_name: None,
99+
py_name: Some("enable_log_forwarding".to_string()),
100100
})
101101
pub attr MESH_ENABLE_LOG_FORWARDING: bool = false;
102102

@@ -121,7 +121,7 @@ declare_attrs! {
121121
/// buffer used for peeking—independent of file capture.
122122
@meta(CONFIG = ConfigAttr {
123123
env_name: Some("HYPERACTOR_MESH_ENABLE_FILE_CAPTURE".to_string()),
124-
py_name: None,
124+
py_name: Some("enable_file_capture".to_string()),
125125
})
126126
pub attr MESH_ENABLE_FILE_CAPTURE: bool = false;
127127

@@ -130,7 +130,7 @@ declare_attrs! {
130130
/// pipes. Default: 100
131131
@meta(CONFIG = ConfigAttr {
132132
env_name: Some("HYPERACTOR_MESH_TAIL_LOG_LINES".to_string()),
133-
py_name: None,
133+
py_name: Some("tail_log_lines".to_string()),
134134
})
135135
pub attr MESH_TAIL_LOG_LINES: usize = 0;
136136

monarch_hyperactor/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ fn set_global_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T
9797
let key = key.downcast_ref().expect("cannot fail");
9898
let mut attrs = Attrs::new();
9999
attrs.set(key.clone(), value);
100-
hyperactor::config::global::set(Source::Runtime, attrs);
100+
hyperactor::config::global::create_or_merge(Source::Runtime, attrs);
101101
Ok(())
102102
}
103103

python/monarch/_rust_bindings/monarch_hyperactor/config.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,8 @@ def reload_config_from_env() -> None:
2525

2626
def configure(
2727
default_transport: ChannelTransport = ChannelTransport.Unix,
28+
enable_log_forwarding: bool = False,
29+
enable_file_capture: bool = False,
30+
tail_log_lines: int = 0,
2831
) -> None: ...
2932
def get_configuration() -> Dict[str, Any]: ...

python/tests/test_config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,14 @@ def test_get_set_transport() -> None:
3939
def test_nonexistent_config_key() -> None:
4040
with pytest.raises(ValueError):
4141
configure(does_not_exist=42) # type: ignore
42+
43+
44+
def test_get_set_multiple() -> None:
45+
configure(default_transport=ChannelTransport.TcpWithLocalhost)
46+
configure(enable_log_forwarding=True, enable_file_capture=True, tail_log_lines=100)
47+
config = get_configuration()
48+
49+
assert config["enable_log_forwarding"]
50+
assert config["enable_file_capture"]
51+
assert config["tail_log_lines"] == 100
52+
assert config["default_transport"] == ChannelTransport.TcpWithLocalhost

0 commit comments

Comments
 (0)