Skip to content

Commit 7ef1e51

Browse files
: python/tests/test_actors.py: more contextmgr improvements (meta-pytorch#1982)
Summary: this refactors the Python-side config helpers so `configured()` actually behaves like a "save/restore Runtime overrides" wrapper instead of copying the merged config back into the Runtime layer. it introduces Runtime-only helpers and rewrites `configured()` / `configured_with_redirected_stdio()` to snapshot and restore just the Runtime layer while composing cleanly with stdio redirection. the logging semantics and test expectations are unchanged; only the way we stage and clean up runtime overrides is different (and now deterministic / nestable). Differential Revision: D87795973
1 parent 44210e0 commit 7ef1e51

File tree

4 files changed

+560
-338
lines changed

4 files changed

+560
-338
lines changed

hyperactor/src/config/global.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,8 +524,7 @@ pub fn create_or_merge(source: Source, attrs: Attrs) {
524524
/// contribute to resolution in [`get`], [`get_cloned`], or
525525
/// [`attrs`]. Defaults and any remaining layers continue to apply
526526
/// in their normal priority order.
527-
#[allow(dead_code)]
528-
pub(crate) fn clear(source: Source) {
527+
pub fn clear(source: Source) {
529528
let mut g = LAYERS.write().unwrap();
530529
g.ordered.retain(|l| layer_source(l) != source);
531530
}
@@ -586,6 +585,34 @@ pub fn attrs() -> Attrs {
586585
merged
587586
}
588587

588+
/// Return a snapshot of the attributes for a specific configuration
589+
/// source.
590+
///
591+
/// If a layer with the given [`Source`] exists, this clones and
592+
/// returns its [`Attrs`]. Otherwise an empty [`Attrs`] is returned.
593+
/// The returned map is detached from the global store – mutating it
594+
/// does **not** affect the underlying layer; use [`set`] or
595+
/// [`create_or_merge`] to modify layers.
596+
fn layer_attrs_for(source: Source) -> Attrs {
597+
let layers = LAYERS.read().unwrap();
598+
if let Some(layer) = layers.ordered.iter().find(|l| layer_source(l) == source) {
599+
layer_attrs(layer).clone()
600+
} else {
601+
Attrs::new()
602+
}
603+
}
604+
605+
/// Snapshot the current attributes in the **Runtime** configuration
606+
/// layer.
607+
///
608+
/// This returns a cloned [`Attrs`] containing only values explicitly
609+
/// set in the [`Source::Runtime`] layer (no merging with
610+
/// Env/File/Defaults). If no Runtime layer is present, an empty
611+
/// [`Attrs`] is returned.
612+
pub fn runtime_attrs() -> Attrs {
613+
layer_attrs_for(Source::Runtime)
614+
}
615+
589616
/// Reset the global configuration to only Defaults (for testing).
590617
///
591618
/// This clears all explicit layers (`File`, `Env`, `Runtime`, and

monarch_hyperactor/src/config.rs

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,34 @@ where
9999
val.map(|v| v.into_py_any(py)).transpose()
100100
}
101101

102-
fn set_global_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T) -> PyResult<()> {
102+
/// Fetch a config value from the **Runtime** layer only and convert
103+
/// it to Python.
104+
///
105+
/// This mirrors [`get_global_config`] but restricts the lookup to the
106+
/// `Source::Runtime` layer (ignoring TestOverride/Env/File/defaults).
107+
/// If the key has a runtime override, it is cloned as `T`, converted
108+
/// to `P`, then to a `PyObject`; otherwise `Ok(None)` is returned.
109+
fn get_runtime_config<'py, P, T>(
110+
py: Python<'py>,
111+
key: &'static dyn ErasedKey,
112+
) -> PyResult<Option<PyObject>>
113+
where
114+
T: AttrValue + TryInto<P>,
115+
P: IntoPyObjectExt<'py>,
116+
PyErr: From<<T as TryInto<P>>::Error>,
117+
{
118+
let key = key.downcast_ref::<T>().expect("cannot fail");
119+
let runtime = hyperactor::config::global::runtime_attrs();
120+
let val: Option<P> = runtime
121+
.get(key.clone())
122+
.cloned()
123+
.map(|v| v.try_into())
124+
.transpose()?;
125+
val.map(|v| v.into_py_any(py)).transpose()
126+
}
127+
128+
/// Note that this function writes strictly into the `Runtime` layer.
129+
fn set_runtime_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T) -> PyResult<()> {
103130
// Again, can't fail unless there's a bug in the code in this file.
104131
let key = key.downcast_ref().expect("cannot fail");
105132
let mut attrs = Attrs::new();
@@ -108,7 +135,7 @@ fn set_global_config<T: AttrValue + Debug>(key: &'static dyn ErasedKey, value: T
108135
Ok(())
109136
}
110137

111-
fn set_global_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> PyResult<()> {
138+
fn set_runtime_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> PyResult<()> {
112139
// Get the `ErasedKey` from the kwarg `name` passed to `monarch.configure(...)`.
113140
let key = match KEY_BY_NAME.get(name) {
114141
None => {
@@ -128,7 +155,7 @@ fn set_global_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> P
128155
name,
129156
key.typename()
130157
))),
131-
Some(info) => (info.set_global_config)(py, key, val),
158+
Some(info) => (info.set_runtime_config)(py, key, val),
132159
}
133160
}
134161

@@ -137,10 +164,15 @@ fn set_global_config_from_py_obj(py: Python<'_>, name: &str, val: PyObject) -> P
137164
/// `T::typehash() == PythonConfigTypeInfo::typehash()`.
138165
struct PythonConfigTypeInfo {
139166
typehash: fn() -> u64,
140-
set_global_config:
141-
fn(py: Python<'_>, key: &'static dyn ErasedKey, val: PyObject) -> PyResult<()>,
167+
142168
get_global_config:
143169
fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<PyObject>>,
170+
171+
set_runtime_config:
172+
fn(py: Python<'_>, key: &'static dyn ErasedKey, val: PyObject) -> PyResult<()>,
173+
174+
get_runtime_config:
175+
fn(py: Python<'_>, key: &'static dyn ErasedKey) -> PyResult<Option<PyObject>>,
144176
}
145177

146178
inventory::collect!(PythonConfigTypeInfo);
@@ -160,15 +192,18 @@ macro_rules! declare_py_config_type {
160192
hyperactor::submit! {
161193
PythonConfigTypeInfo {
162194
typehash: $ty::typehash,
163-
set_global_config: |py, key, val| {
195+
set_runtime_config: |py, key, val| {
164196
let val: $ty = val.extract::<$ty>(py).map_err(|err| PyTypeError::new_err(format!(
165197
"invalid value `{}` for configuration key `{}` ({})",
166198
val, key.name(), err
167199
)))?;
168-
set_global_config(key, val)
200+
set_runtime_config(key, val)
169201
},
170202
get_global_config: |py, key| {
171203
get_global_config::<$ty, $ty>(py, key)
204+
},
205+
get_runtime_config: |py, key| {
206+
get_runtime_config::<$ty, $ty>(py, key)
172207
}
173208
}
174209
}
@@ -180,15 +215,18 @@ macro_rules! declare_py_config_type {
180215
hyperactor::submit! {
181216
PythonConfigTypeInfo {
182217
typehash: $ty::typehash,
183-
set_global_config: |py, key, val| {
218+
set_runtime_config: |py, key, val| {
184219
let val: $ty = val.extract::<$py_ty>(py).map_err(|err| PyTypeError::new_err(format!(
185220
"invalid value `{}` for configuration key `{}` ({})",
186221
val, key.name(), err
187222
)))?.into();
188-
set_global_config(key, val)
223+
set_runtime_config(key, val)
189224
},
190225
get_global_config: |py, key| {
191226
get_global_config::<$py_ty, $ty>(py, key)
227+
},
228+
get_runtime_config: |py, key| {
229+
get_runtime_config::<$py_ty, $ty>(py, key)
192230
}
193231
}
194232
}
@@ -212,7 +250,7 @@ fn configure(py: Python<'_>, kwargs: Option<HashMap<String, PyObject>>) -> PyRes
212250
.map(|kwargs| {
213251
kwargs
214252
.into_iter()
215-
.try_for_each(|(key, val)| set_global_config_from_py_obj(py, &key, val))
253+
.try_for_each(|(key, val)| set_runtime_config_from_py_obj(py, &key, val))
216254
})
217255
.transpose()?;
218256
Ok(())
@@ -236,6 +274,62 @@ fn get_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
236274
.collect()
237275
}
238276

277+
/// Get only the Runtime layer configuration (Python-exposed keys).
278+
///
279+
/// The Runtime layer is effectively the "Python configuration layer",
280+
/// populated exclusively via `configure(**kwargs)` from Python. This
281+
/// function returns only the Python-exposed keys (those with
282+
/// `@meta(CONFIG = ConfigAttr { py_name: Some(...), .. })`) that are
283+
/// currently set in the Runtime layer.
284+
///
285+
/// This is used by Python's `configured()` context manager to
286+
/// snapshot and restore the Runtime layer for composable, nested
287+
/// configuration overrides:
288+
///
289+
/// ```python
290+
/// prev = get_runtime_configuration()
291+
/// try:
292+
/// configure(**overrides)
293+
/// yield get_configuration().copy()
294+
/// finally:
295+
/// clear_runtime_configuration()
296+
/// configure(**prev)
297+
/// ```
298+
///
299+
/// Unlike `get_configuration()`, which returns the merged view across
300+
/// all layers (File, Env, Runtime, TestOverride), this returns only
301+
/// what's explicitly set in the Runtime layer.
302+
#[pyfunction]
303+
fn get_runtime_configuration(py: Python<'_>) -> PyResult<HashMap<String, PyObject>> {
304+
KEY_BY_NAME
305+
.iter()
306+
.filter_map(|(name, key)| match TYPEHASH_TO_INFO.get(&key.typehash()) {
307+
None => None,
308+
Some(info) => match (info.get_runtime_config)(py, *key) {
309+
Err(err) => Some(Err(err)),
310+
Ok(val) => val.map(|val| Ok(((*name).into(), val))),
311+
},
312+
})
313+
.collect()
314+
}
315+
316+
/// Clear runtime configuration overrides.
317+
///
318+
/// This removes all entries from the Runtime config layer for this
319+
/// process. The Runtime layer is exclusively populated via Python's
320+
/// `configure(**kwargs)`, so clearing it is SAFE — it will not
321+
/// destroy configuration from other sources (environment variables,
322+
/// config files, or built-in defaults).
323+
///
324+
/// This is primarily used by Python's `configured()` context manager
325+
/// to restore configuration state after applying temporary overrides.
326+
/// Other layers (Env, File, TestOverride, defaults) are unaffected.
327+
#[pyfunction]
328+
fn clear_runtime_configuration(_py: Python<'_>) -> PyResult<()> {
329+
hyperactor::config::global::clear(Source::Runtime);
330+
Ok(())
331+
}
332+
239333
/// Register Python bindings for the config module
240334
pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
241335
let reload = wrap_pyfunction!(reload_config_from_env, module)?;
@@ -266,5 +360,19 @@ pub fn register_python_bindings(module: &Bound<'_, PyModule>) -> PyResult<()> {
266360
)?;
267361
module.add_function(get_configuration)?;
268362

363+
let get_runtime_configuration = wrap_pyfunction!(get_runtime_configuration, module)?;
364+
get_runtime_configuration.setattr(
365+
"__module__",
366+
"monarch._rust_bindings.monarch_hyperactor.config",
367+
)?;
368+
module.add_function(get_runtime_configuration)?;
369+
370+
let clear_runtime_configuration = wrap_pyfunction!(clear_runtime_configuration, module)?;
371+
clear_runtime_configuration.setattr(
372+
"__module__",
373+
"monarch._rust_bindings.monarch_hyperactor.config",
374+
)?;
375+
module.add_function(clear_runtime_configuration)?;
376+
269377
Ok(())
270378
}

python/monarch/_rust_bindings/monarch_hyperactor/config.pyi

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,42 @@ def configure(
3838
tail_log_lines: int = ...,
3939
**kwargs: object,
4040
) -> None:
41-
"""Change a configuration value in the global configuration. If called with
42-
no arguments, makes no changes. Does not reset any configuration"""
41+
"""Configure Hyperactor runtime defaults for this process.
42+
43+
This updates the **runtime** configuration layer from Python,
44+
setting the default channel transport and optional logging
45+
behaviour (forwarding, file capture, and how many lines to tail).
46+
"""
47+
...
48+
49+
def get_configuration() -> Dict[str, Any]:
50+
"""Return a snapshot of the current Hyperactor configuration.
51+
52+
The result is a plain dictionary view of the merged configuration
53+
(defaults plus any overrides from environment or Python), useful
54+
for debugging and tests.
55+
"""
4356
...
4457

45-
def get_configuration() -> Dict[str, Any]: ...
58+
def get_runtime_configuration() -> Dict[str, Any]:
59+
"""Return a snapshot of the Runtime layer configuration.
60+
61+
The Runtime layer contains only configuration values set from
62+
Python via configure(). This returns only those Python-exposed
63+
keys currently in the Runtime layer (not merged across all layers
64+
like get_configuration).
65+
66+
This can be used to snapshot/restore Runtime state.
67+
"""
68+
...
69+
70+
def clear_runtime_configuration() -> None:
71+
"""Clear all Runtime layer configuration overrides.
72+
73+
Safely removes all entries from the Runtime config layer. Since
74+
the Runtime layer is exclusively populated via Python's
75+
configure(), this will not affect configuration from environment
76+
variables, config files, or built-in defaults.
77+
"""
78+
79+
...

0 commit comments

Comments
 (0)