1use std::future::Future;
18use std::future::IntoFuture;
19use std::pin::Pin;
20
21use futures::future::select_all;
22use hyperactor::ActorAddr;
23use hyperactor::ActorMonitor;
24use hyperactor::RemoteHandles;
25use hyperactor::RemoteMessage;
26use hyperactor::actor::ActorStatus;
27use hyperactor::actor::Referable;
28use hyperactor::context;
29use hyperactor::mailbox::Message;
30use hyperactor::mailbox::PortReceiver;
31use hyperactor::monitor::MonitorActivation;
32use hyperactor::monitor::MonitorFailure;
33use hyperactor::monitor::MonitorStatus;
34use hyperactor::supervision::ActorSupervisionEvent;
35use ndslice::view::MapIntoExt as _;
36use ndslice::view::Ranked as _;
37use ndslice::view::Region;
38
39use crate::ActorMeshRef;
40use crate::ValueMesh;
41use crate::supervision::MeshFailure;
42
43pub struct MeshMonitor {
53 monitors: ValueMesh<ActorMonitor>,
55}
56
57impl MeshMonitor {
58 pub fn spawn_persistent(cx: &impl context::Actor, actors: ValueMesh<ActorAddr>) -> Self {
64 Self::spawn_with(actors, |actor| ActorMonitor::spawn_persistent(cx, actor))
65 }
66
67 pub fn spawn_dormant(cx: &impl context::Actor, actors: ValueMesh<ActorAddr>) -> Self {
73 Self::spawn_with(actors, |actor| ActorMonitor::spawn_dormant(cx, actor))
74 }
75
76 fn spawn_with(actors: ValueMesh<ActorAddr>, spawn: impl Fn(ActorAddr) -> ActorMonitor) -> Self {
77 Self {
78 monitors: actors.map_into(|actor| spawn(actor.clone())),
79 }
80 }
81
82 pub fn activate(&self, cx: &impl context::Actor) -> MeshMonitorActivation {
86 MeshMonitorActivation {
87 activations: self.monitors.map_into(|monitor| monitor.activate(cx)),
88 }
89 }
90
91 pub fn region(&self) -> &Region {
93 self.monitors.region()
94 }
95
96 pub fn status(&self, rank: usize) -> Option<ActorStatus> {
98 self.monitors
99 .get(rank)
100 .map(|monitor| match monitor.status() {
101 MonitorStatus::Checking => ActorStatus::Unknown,
102 MonitorStatus::Alive(status) => status,
103 MonitorStatus::Failed(failure) => monitor_failure_to_actor_status(&failure),
104 })
105 }
106
107 async fn wait_for_failure(&self) -> MeshFailure {
113 if self.region().num_ranks() == 0 {
114 return std::future::pending().await;
115 }
116 let waits = (0..self.region().num_ranks()).map(|rank| {
117 let monitor = self
118 .monitors
119 .get(rank)
120 .expect("dense actor monitor mesh has one value per rank");
121 Box::pin(async move {
122 let failure = monitor
125 .guard(std::future::pending::<()>())
126 .await
127 .expect_err("pending future never completes");
128 (rank, failure)
129 })
130 });
131 let ((rank, failure), _index, _rest) = select_all(waits).await;
132 monitor_failure_to_mesh_failure(rank, &failure)
133 }
134
135 pub async fn guard<F>(&self, fut: F) -> Result<F::Output, MeshFailure>
137 where
138 F: Future,
139 {
140 tokio::pin!(fut);
141 tokio::select! {
142 result = fut => Ok(result),
143 failure = self.wait_for_failure() => Err(failure),
144 }
145 }
146
147 pub async fn cast_and_collect<A, M, R>(
155 &self,
156 actor_mesh: &ActorMeshRef<A>,
157 cx: &impl context::Actor,
158 message: M,
159 receiver: &mut PortReceiver<R>,
160 ) -> crate::Result<Result<Vec<R>, MeshFailure>>
161 where
162 A: Referable + RemoteHandles<M>,
163 M: RemoteMessage + Clone,
164 R: Message,
165 {
166 actor_mesh.cast(cx, message)?;
167 let num_ranks = actor_mesh.region().num_ranks();
168 let replies = async {
169 let mut replies = Vec::with_capacity(num_ranks);
170 for _ in 0..num_ranks {
171 replies.push(receiver.recv().await?);
172 }
173 Ok::<_, hyperactor::mailbox::MailboxError>(replies)
174 };
175
176 match self.guard(replies).await {
177 Ok(Ok(replies)) => Ok(Ok(replies)),
178 Ok(Err(error)) => Err(Box::new(error).into()),
179 Err(failure) => Ok(Err(failure)),
180 }
181 }
182}
183
184#[must_use = "the monitor goes dormant once its last activation is dropped"]
186pub struct MeshMonitorActivation {
187 #[expect(
188 dead_code,
189 reason = "held only so dropping it releases each rank's activation"
190 )]
191 activations: ValueMesh<MonitorActivation>,
192}
193
194impl<'a> IntoFuture for &'a MeshMonitor {
197 type Output = MeshFailure;
198 type IntoFuture = Pin<Box<dyn Future<Output = MeshFailure> + Send + 'a>>;
199
200 fn into_future(self) -> Self::IntoFuture {
201 Box::pin(self.wait_for_failure())
202 }
203}
204
205fn monitor_failure_to_mesh_failure(rank: usize, failure: &MonitorFailure) -> MeshFailure {
206 let actor_id = failure.actor_id().clone();
207 let actor_status = monitor_failure_to_actor_status(failure);
208 MeshFailure {
211 actor_mesh_name: None,
212 event: ActorSupervisionEvent::new(actor_id, None, actor_status, None),
213 crashed_ranks: vec![rank],
214 reporting_controller: None,
217 }
218}
219
220fn monitor_failure_to_actor_status(failure: &MonitorFailure) -> ActorStatus {
221 match failure {
222 MonitorFailure::ActorStopped { status, .. } => status.clone(),
223 MonitorFailure::ActorFailed { status, .. } if status.is_failed() => status.clone(),
224 failure => ActorStatus::generic_failure(failure.to_string()),
225 }
226}
227
228#[cfg(all(test, fbcode_build))]
229mod tests {
230 use std::collections::HashSet;
231
232 use hyperactor::ActorRef;
233 use hyperactor::Proc;
234 use hyperactor::id::Label;
235 use ndslice::Region;
236 use ndslice::extent;
237 use tokio::time::Duration;
238
239 use super::*;
240 use crate::mesh_id::ActorMeshId;
241 use crate::testactor;
242
243 #[tokio::test]
244 async fn test_mesh_monitor_reports_rank_failure() {
245 let proc = Proc::isolated();
246 let client = proc.client("client");
247 let target = client.spawn_with_label("rank0", testactor::TestActor);
248 let region: Region = extent!(replicas = 1).into();
249 let actors = ValueMesh::new(region, vec![target.actor_addr().clone()]).unwrap();
250 let monitor = MeshMonitor::spawn_persistent(&client, actors);
251 assert_eq!(monitor.status(0), Some(ActorStatus::Unknown));
252
253 let mut wait = (&monitor).into_future();
255 tokio::select! {
256 biased;
257 failure = &mut wait => panic!("unexpected failure before stop: {failure:?}"),
258 _ = tokio::task::yield_now() => {}
259 }
260
261 target
262 .drain_and_stop("rank complete")
263 .expect("target should accept stop");
264
265 let failure = tokio::time::timeout(Duration::from_secs(10), wait)
266 .await
267 .expect("timed out waiting for mesh monitor failure");
268 assert_eq!(failure.crashed_ranks, vec![0]);
269 assert!(matches!(
270 failure.event.actor_status,
271 ActorStatus::Stopped(ref reason) if reason == "rank complete"
272 ));
273 assert!(matches!(
274 monitor.status(0).expect("rank 0 should remain monitored"),
275 ActorStatus::Stopped(ref reason) if reason == "rank complete"
276 ));
277
278 tokio::time::timeout(Duration::from_secs(5), target)
279 .await
280 .expect("timed out waiting for target to stop");
281 }
282
283 #[tokio::test]
284 async fn test_cast_and_collect_returns_all_replies_or_rank_failure() {
285 let proc = Proc::isolated();
286 let client = proc.client("client");
287 let rank0 = client.spawn_with_label("rank0", testactor::TestActor);
288 let rank1 = client.spawn_with_label("rank1", testactor::TestActor);
289 let actor_refs: Vec<ActorRef<testactor::TestActor>> = vec![rank0.bind(), rank1.bind()];
290 let expected_actor_addrs: HashSet<_> = actor_refs
291 .iter()
292 .map(|actor_ref| actor_ref.actor_addr().clone())
293 .collect();
294 let region: Region = extent!(replicas = 2).into();
295 let actor_mesh = ActorMeshRef::try_new_data(
296 ActorMeshId::instance(Label::new("replicas").unwrap()),
297 region,
298 actor_refs,
299 )
300 .expect("data actor mesh should be valid");
301 let monitor = actor_mesh.monitor(&client);
302
303 let (port, mut receiver) = client.open_port();
304 let replies = tokio::time::timeout(
305 Duration::from_secs(10),
306 monitor.cast_and_collect(
307 &actor_mesh,
308 &client,
309 testactor::GetActorId(port.bind()),
310 &mut receiver,
311 ),
312 )
313 .await
314 .expect("timed out waiting for all rank replies")
315 .expect("cast and receive should succeed")
316 .expect("all ranks should remain alive");
317 let actor_addrs: HashSet<_> = replies
318 .into_iter()
319 .map(|(actor_addr, _seq_info)| actor_addr)
320 .collect();
321 assert_eq!(actor_addrs, expected_actor_addrs);
322
323 let (_port, mut receiver): (_, PortReceiver<()>) = client.open_port();
324 let mut pending =
325 std::pin::pin!(monitor.cast_and_collect(&actor_mesh, &client, (), &mut receiver,));
326 tokio::select! {
327 biased;
328 result = &mut pending => panic!("cast unexpectedly completed: {result:?}"),
329 _ = tokio::task::yield_now() => {}
330 }
331 rank1
332 .drain_and_stop("rank complete")
333 .expect("rank 1 should accept stop");
334 let failure = tokio::time::timeout(Duration::from_secs(10), pending)
335 .await
336 .expect("timed out waiting for a rank failure")
337 .expect("cast should initiate")
338 .expect_err("rank failure should win before all replies arrive");
339 assert_eq!(failure.crashed_ranks, vec![1]);
340 assert!(matches!(
341 failure.event.actor_status,
342 ActorStatus::Stopped(ref reason) if reason == "rank complete"
343 ));
344
345 rank0
346 .drain_and_stop("test complete")
347 .expect("rank 0 should accept stop");
348 tokio::time::timeout(Duration::from_secs(5), rank0)
349 .await
350 .expect("timed out waiting for rank 0 to stop");
351 tokio::time::timeout(Duration::from_secs(5), rank1)
352 .await
353 .expect("timed out waiting for rank 1 to stop");
354 }
355}