Skip to main content

hyperactor_mesh/
config.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//! Configuration for Hyperactor Mesh.
10//!
11//! This module provides hyperactor_mesh-specific configuration attributes that extend
12//! the base hyperactor configuration system.
13
14use std::net::SocketAddr;
15use std::time::Duration;
16
17use hyperactor_config::AttrValue;
18use hyperactor_config::CONFIG;
19use hyperactor_config::ConfigAttr;
20use hyperactor_config::NonZeroUsize;
21use hyperactor_config::attrs::declare_attrs;
22use serde::Deserialize;
23use serde::Serialize;
24use typeuri::Named;
25
26/// A socket address string usable as a `declare_attrs!` default.
27///
28/// Follows the [`hyperactor::config::Pem`] pattern: the `Static`
29/// variant holds a `&'static str` so it can appear in a `static`
30/// item, while `Value` holds a runtime `String` from environment
31/// variables or Python `configure()`.
32#[derive(Clone, Debug, Serialize, Named)]
33#[named("hyperactor_mesh::config::SocketAddrStr")]
34pub enum SocketAddrStr {
35    /// Compile-time default (const-constructible).
36    Static(&'static str),
37    /// Runtime value from env / config.
38    Value(String),
39}
40
41impl<'de> Deserialize<'de> for SocketAddrStr {
42    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43    where
44        D: serde::Deserializer<'de>,
45    {
46        #[derive(Deserialize)]
47        enum Helper {
48            Static(String),
49            Value(String),
50        }
51        match Helper::deserialize(deserializer)? {
52            Helper::Static(s) | Helper::Value(s) => Ok(SocketAddrStr::Value(s)),
53        }
54    }
55}
56
57impl From<String> for SocketAddrStr {
58    fn from(s: String) -> Self {
59        SocketAddrStr::Value(s)
60    }
61}
62
63impl From<SocketAddrStr> for String {
64    fn from(s: SocketAddrStr) -> Self {
65        s.as_ref().to_owned()
66    }
67}
68
69impl AsRef<str> for SocketAddrStr {
70    fn as_ref(&self) -> &str {
71        match self {
72            SocketAddrStr::Static(s) => s,
73            SocketAddrStr::Value(s) => s,
74        }
75    }
76}
77
78impl std::fmt::Display for SocketAddrStr {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.write_str(self.as_ref())
81    }
82}
83
84impl AttrValue for SocketAddrStr {
85    fn display(&self) -> String {
86        self.as_ref().to_owned()
87    }
88
89    fn parse(value: &str) -> Result<Self, anyhow::Error> {
90        value.parse::<SocketAddr>()?;
91        Ok(SocketAddrStr::Value(value.to_string()))
92    }
93}
94
95impl SocketAddrStr {
96    /// Parse the contained string as a `SocketAddr`.
97    pub fn parse_socket_addr(&self) -> Result<SocketAddr, std::net::AddrParseError> {
98        self.as_ref().parse()
99    }
100}
101
102// Declare hyperactor_mesh-specific configuration keys
103declare_attrs! {
104    /// The maximium for a dimension size allowed for a folded shape
105    /// when reshaping during casting to limit fanout.
106    /// usize::MAX means no reshaping as any shape will always be below
107    /// the limit so no dimension needs to be folded.
108    @meta(CONFIG = ConfigAttr::new(
109        Some("HYPERACTOR_MESH_MAX_CAST_DIMENSION_SIZE".to_string()),
110        Some("max_cast_dimension_size".to_string()),
111    ))
112    pub attr MAX_CAST_DIMENSION_SIZE: usize = 16;
113
114    /// Which builtin process launcher backend to use.
115    /// Accepted values: "native" (default), "systemd".
116    /// Trimmed and lowercased before matching.
117    ///
118    /// **Precedence:** Python spawner (via SetProcSpawner) overrides this.
119    @meta(CONFIG = ConfigAttr::new(
120        Some("HYPERACTOR_MESH_PROC_LAUNCHER_KIND".to_string()),
121        Some("proc_launcher_kind".to_string()),
122    ))
123    pub attr MESH_PROC_LAUNCHER_KIND: String = String::new();
124
125    /// Default socket address for the mesh admin HTTP server.
126    ///
127    /// Parsed as a `SocketAddr` (e.g. `[::]:1729`, `0.0.0.0:8080`).
128    /// Used as the bind address when no explicit address is provided
129    /// to `MeshAdminAgent`.
130    @meta(CONFIG = ConfigAttr::new(
131        Some("HYPERACTOR_MESH_ADMIN_ADDR".to_string()),
132        Some("mesh_admin_addr".to_string()),
133    ))
134    pub attr MESH_ADMIN_ADDR: SocketAddrStr = SocketAddrStr::Static("[::]:1729");
135
136    /// Timeout for fallback queries to actors/procs that may have been
137    /// recently destroyed. The second-chance paths in `resolve_proc_node`
138    /// and `resolve_actor_node` fire after the fast QueryChild lookup
139    /// fails. A short budget here prevents dead actors from blocking the
140    /// single-threaded MeshAdminAgent message loop.
141    @meta(CONFIG = ConfigAttr::new(
142        Some("HYPERACTOR_MESH_ADMIN_RESOLVE_ACTOR_TIMEOUT".to_string()),
143        Some("mesh_admin_resolve_actor_timeout".to_string()),
144    ))
145    pub attr MESH_ADMIN_RESOLVE_ACTOR_TIMEOUT: Duration = Duration::from_millis(200);
146
147    /// Maximum number of concurrent resolve requests the HTTP bridge
148    /// forwards to the MeshAdminAgent. Excess requests receive 503
149    /// immediately. Protects the shared tokio runtime from query floods
150    /// (e.g. multiple TUI clients, rapid polling). Increase if the admin
151    /// server serves many concurrent clients that need low-latency
152    /// responses; decrease if introspection queries interfere with the
153    /// actor workload under churn.
154    @meta(CONFIG = ConfigAttr::new(
155        Some("HYPERACTOR_MESH_ADMIN_MAX_CONCURRENT_RESOLVES".to_string()),
156        Some("mesh_admin_max_concurrent_resolves".to_string()),
157    ))
158    pub attr MESH_ADMIN_MAX_CONCURRENT_RESOLVES: usize = 2;
159
160    /// Timeout for the config-push barrier during `HostMesh::attach()`.
161    ///
162    /// When attaching to pre-existing workers (simple bootstrap), the
163    /// client pushes its propagatable config to each host agent and
164    /// waits for confirmation. If the barrier does not complete
165    /// within this duration, attach fails closed.
166    @meta(CONFIG = ConfigAttr::new(
167        Some("HYPERACTOR_MESH_ATTACH_CONFIG_TIMEOUT".to_string()),
168        Some("mesh_attach_config_timeout".to_string()),
169    ))
170    pub attr MESH_ATTACH_CONFIG_TIMEOUT: Duration = Duration::from_secs(60);
171
172    /// Timeout for targeted introspection queries that hit a single,
173    /// specific host. Kept short so a slow or dying actor cannot block
174    /// the single-threaded MeshAdminAgent message loop.
175    @meta(CONFIG = ConfigAttr::new(
176        Some("HYPERACTOR_MESH_ADMIN_SINGLE_HOST_TIMEOUT".to_string()),
177        Some("mesh_admin_single_host_timeout".to_string()),
178    ))
179    pub attr MESH_ADMIN_SINGLE_HOST_TIMEOUT: Duration = Duration::from_secs(3);
180
181    /// Timeout for QueryChild snapshot lookups in resolve_actor_node.
182    /// QueryChild is handled by a synchronous callback — it either
183    /// returns immediately or returns Error. A short budget ensures
184    /// the total time for resolve_actor_node stays well under
185    /// `MESH_ADMIN_SINGLE_HOST_TIMEOUT`.
186    @meta(CONFIG = ConfigAttr::new(
187        Some("HYPERACTOR_MESH_ADMIN_QUERY_CHILD_TIMEOUT".to_string()),
188        Some("mesh_admin_query_child_timeout".to_string()),
189    ))
190    pub attr MESH_ADMIN_QUERY_CHILD_TIMEOUT: Duration = Duration::from_millis(100);
191
192    /// Timeout for the end-to-end `/v1/config/{proc}` bridge reply.
193    /// The config-dump path forwards a `ConfigDump` message through
194    /// the HostAgent bridge and waits for `ConfigDumpResult`. This is
195    /// inter-process actor messaging — fundamentally slower than local
196    /// `QueryChild` snapshot lookups (which use
197    /// `MESH_ADMIN_QUERY_CHILD_TIMEOUT`). During startup, the
198    /// HostAgent message loop may be busy processing actor
199    /// registrations, so bridge latency can exceed several seconds.
200    @meta(CONFIG = ConfigAttr::new(
201        Some("HYPERACTOR_MESH_ADMIN_CONFIG_DUMP_BRIDGE_TIMEOUT".to_string()),
202        Some("mesh_admin_config_dump_bridge_timeout".to_string()),
203    ))
204    pub attr MESH_ADMIN_CONFIG_DUMP_BRIDGE_TIMEOUT: Duration = Duration::from_secs(5);
205
206    /// Timeout for py-spy dump requests. See PS-5 in `introspect`
207    /// module doc. With `--native --native-all`, py-spy unwinds native
208    /// stacks via libunwind which is significantly slower than
209    /// Python-only capture (~100ms). 10s accommodates native unwinding
210    /// on heavily loaded hosts. Independent of
211    /// `MESH_ADMIN_SINGLE_HOST_TIMEOUT` because py-spy does real I/O
212    /// (subprocess + ptrace) rather than actor messaging.
213    @meta(CONFIG = ConfigAttr::new(
214        Some("HYPERACTOR_MESH_ADMIN_PYSPY_TIMEOUT".to_string()),
215        Some("mesh_admin_pyspy_timeout".to_string()),
216    ))
217    pub attr MESH_ADMIN_PYSPY_TIMEOUT: Duration = Duration::from_secs(10);
218
219    /// Timeout for the `/v1/tree` fan-out. Kept generous because the
220    /// tree dump walks every host and proc in the mesh.
221    @meta(CONFIG = ConfigAttr::new(
222        Some("HYPERACTOR_MESH_ADMIN_TREE_TIMEOUT".to_string()),
223        Some("mesh_admin_tree_timeout".to_string()),
224    ))
225    pub attr MESH_ADMIN_TREE_TIMEOUT: Duration = Duration::from_secs(10);
226
227    /// Bridge-side timeout for py-spy dump requests. Must exceed
228    /// `MESH_ADMIN_PYSPY_TIMEOUT` to allow the subprocess kill/reap
229    /// and reply delivery to arrive before declaring `gateway_timeout`.
230    /// See PS-6 in `introspect` module doc.
231    @meta(CONFIG = ConfigAttr::new(
232        Some("HYPERACTOR_MESH_ADMIN_PYSPY_BRIDGE_TIMEOUT".to_string()),
233        Some("mesh_admin_pyspy_bridge_timeout".to_string()),
234    ))
235    pub attr MESH_ADMIN_PYSPY_BRIDGE_TIMEOUT: Duration = Duration::from_secs(13);
236
237    /// Client-side timeout for py-spy requests. Must exceed
238    /// `MESH_ADMIN_PYSPY_BRIDGE_TIMEOUT` so the server can return a
239    /// structured `PySpyResult` even when the subprocess uses the
240    /// full budget. See PS-6 in `introspect` module doc.
241    @meta(CONFIG = ConfigAttr::new(
242        Some("HYPERACTOR_MESH_ADMIN_PYSPY_CLIENT_TIMEOUT".to_string()),
243        Some("mesh_admin_pyspy_client_timeout".to_string()),
244    ))
245    pub attr MESH_ADMIN_PYSPY_CLIENT_TIMEOUT: Duration = Duration::from_secs(20);
246
247    /// Maximum allowed profile duration. Requests exceeding this
248    /// are rejected with a 400. Protects against runaway profile
249    /// captures. See PP-1 in `introspect` module doc.
250    @meta(CONFIG = ConfigAttr::new(
251        Some("HYPERACTOR_MESH_ADMIN_PYSPY_MAX_PROFILE_DURATION".to_string()),
252        Some("mesh_admin_pyspy_max_profile_duration".to_string()),
253    ))
254    pub attr MESH_ADMIN_PYSPY_MAX_PROFILE_DURATION: Duration = Duration::from_secs(300);
255
256    /// Path to the py-spy binary. When non-empty, tried before
257    /// the fallback `"py-spy"` PATH lookup. See PS-3 in
258    /// `introspect` module doc.
259    ///
260    /// Note: env var is `PYSPY_BIN` (not `HYPERACTOR_MESH_PYSPY_BIN`)
261    /// to preserve backward compatibility with existing deployments
262    /// that already set `PYSPY_BIN`.
263    @meta(CONFIG = ConfigAttr::new(
264        Some("PYSPY_BIN".to_string()),
265        Some("pyspy_bin".to_string()),
266    ))
267    pub attr PYSPY_BIN: String = String::new();
268
269    /// When the cast domain has fewer ranks than this threshold,
270    /// v1 casting sends messages point-to-point instead of through the
271    /// comm actor tree. 0 disables the optimization.
272    @meta(CONFIG = ConfigAttr::new(
273        Some("HYPERACTOR_MESH_V1_CAST_POINT_TO_POINT_THRESHOLD".to_string()),
274        Some("v1_cast_point_to_point_threshold".to_string()),
275    ))
276    pub attr V1_CAST_POINT_TO_POINT_THRESHOLD: usize = 0;
277
278
279    @meta(CONFIG = ConfigAttr::new(
280        Some("HYPERACTOR_MESH_MAX_CAST_FANOUT".to_string()),
281        Some("max_cast_fanout".to_string()),
282    ))
283    pub attr MAX_CAST_FANOUT: NonZeroUsize =
284        NonZeroUsize::new(16).expect("16 is non-zero");
285}