Skip to main content

hyperactor_mesh/
testactor.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//! This module defines a test actor. It is defined in a separate module
10//! (outside of [`crate::testing`]) to ensure that it is compiled into
11//! the bootstrap binary, which is not built in test mode (and anyway, test mode
12//! does not work across crate boundaries)
13
14#[cfg(test)]
15use std::collections::HashMap;
16use std::collections::VecDeque;
17use std::ops::Deref;
18#[cfg(test)]
19use std::time::Duration;
20
21use async_trait::async_trait;
22use hyperactor::Actor;
23use hyperactor::ActorRef;
24use hyperactor::Context;
25use hyperactor::Endpoint as _;
26use hyperactor::Handler;
27use hyperactor::Instance;
28use hyperactor::RefClient;
29#[cfg(test)]
30use hyperactor::context;
31use hyperactor::ordering::SEQ_INFO;
32use hyperactor::ordering::SeqInfo;
33use hyperactor::supervision::ActorSupervisionEvent;
34use hyperactor_config::Flattrs;
35use hyperactor_config::global::Source;
36use ndslice::Point;
37#[cfg(test)]
38use ndslice::ViewExt as _;
39use serde::Deserialize;
40use serde::Serialize;
41use typeuri::Named;
42#[cfg(test)]
43use uuid::Uuid;
44
45use crate::ActorMesh;
46#[cfg(test)]
47use crate::ActorMeshRef;
48use crate::ProcMeshRef;
49use crate::comm::multicast::CastInfo;
50use crate::mesh_id::ActorMeshId;
51use crate::supervision::MeshFailure;
52#[cfg(test)]
53use crate::testing;
54
55/// A simple test actor used by various unit tests.
56#[derive(Default, Debug)]
57#[hyperactor::export(
58    (),
59    GetActorId,
60    GetCastInfo,
61    GetResourceRank,
62    CauseSupervisionEvent,
63    Forward,
64    GetConfigAttrs,
65    SetConfigAttrs,
66)]
67#[hyperactor::spawnable]
68pub struct TestActor;
69
70impl Actor for TestActor {}
71
72/// A message that returns the recipient actor's id and cast message's seq info.
73#[derive(Debug, Clone, Named, Serialize, Deserialize)]
74pub struct GetActorId(pub hyperactor::PortRef<(hyperactor::ActorAddr, Option<SeqInfo>)>);
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub enum SupervisionEventType {
78    Panic,
79    SigSEGV,
80    ProcessExit(i32),
81}
82
83/// A message that causes a supervision event. The one argument determines what
84/// kind of supervision event it'll be.
85#[derive(Debug, Clone, Named, Serialize, Deserialize)]
86pub struct CauseSupervisionEvent {
87    pub kind: SupervisionEventType,
88    pub send_to_children: bool,
89}
90
91impl CauseSupervisionEvent {
92    fn cause_event(&self) -> ! {
93        match self.kind {
94            SupervisionEventType::Panic => {
95                panic!("for testing");
96            }
97            SupervisionEventType::SigSEGV => {
98                tracing::error!("exiting with SIGSEGV");
99                // SAFETY: This is for testing code that explicitly causes a SIGSEGV.
100                unsafe { std::ptr::null_mut::<i32>().write(42) };
101                // While the above should always segfault, we need a hard exit
102                // for the compiler's sake.
103                panic!("should have segfaulted");
104            }
105            SupervisionEventType::ProcessExit(code) => {
106                tracing::error!("exiting process {} with code {}", std::process::id(), code);
107                std::process::exit(code);
108            }
109        }
110    }
111}
112
113#[async_trait]
114impl Handler<()> for TestActor {
115    async fn handle(&mut self, _cx: &Context<Self>, _: ()) -> Result<(), anyhow::Error> {
116        Ok(())
117    }
118}
119
120#[async_trait]
121impl Handler<GetActorId> for TestActor {
122    async fn handle(
123        &mut self,
124        cx: &Context<Self>,
125        GetActorId(reply): GetActorId,
126    ) -> Result<(), anyhow::Error> {
127        let seq_info = cx.headers().get(SEQ_INFO);
128        reply.post(cx, (cx.self_addr().clone(), seq_info));
129        Ok(())
130    }
131}
132
133#[async_trait]
134impl Handler<CauseSupervisionEvent> for TestActor {
135    async fn handle(
136        &mut self,
137        _cx: &Context<Self>,
138        msg: CauseSupervisionEvent,
139    ) -> Result<(), anyhow::Error> {
140        msg.cause_event();
141    }
142}
143
144/// A test actor that handles supervision events.
145/// It should be the parent of TestActor who can panic or cause a SIGSEGV.
146#[derive(Default, Debug)]
147#[hyperactor::export(ActorSupervisionEvent)]
148#[hyperactor::spawnable]
149pub struct TestActorWithSupervisionHandling;
150
151#[async_trait]
152impl Actor for TestActorWithSupervisionHandling {
153    async fn handle_supervision_event(
154        &mut self,
155        _this: &Instance<Self>,
156        event: &ActorSupervisionEvent,
157    ) -> Result<bool, anyhow::Error> {
158        tracing::error!("supervision event: {:?}", event);
159        // Swallow the supervision error to avoid crashing the process.
160        Ok(true)
161    }
162}
163
164#[async_trait]
165impl Handler<ActorSupervisionEvent> for TestActorWithSupervisionHandling {
166    async fn handle(
167        &mut self,
168        _cx: &Context<Self>,
169        _msg: ActorSupervisionEvent,
170    ) -> Result<(), anyhow::Error> {
171        Ok(())
172    }
173}
174
175/// A test actor that sleeps when it receives a Duration message.
176/// Used for testing timeout and abort behavior.
177#[derive(Default, Debug)]
178#[hyperactor::export(std::time::Duration)]
179#[hyperactor::spawnable]
180pub struct SleepActor;
181
182impl Actor for SleepActor {}
183
184#[async_trait]
185impl Handler<std::time::Duration> for SleepActor {
186    async fn handle(
187        &mut self,
188        _cx: &Context<Self>,
189        duration: std::time::Duration,
190    ) -> Result<(), anyhow::Error> {
191        tokio::time::sleep(duration).await;
192        Ok(())
193    }
194}
195
196/// A message to forward to a visit list of ports.
197/// Each port removes the next entry, and adds it to the
198/// 'visited' list.
199#[derive(Debug, Clone, Named, Serialize, Deserialize)]
200pub struct Forward {
201    pub to_visit: VecDeque<hyperactor::PortRef<Forward>>,
202    pub visited: Vec<hyperactor::PortRef<Forward>>,
203}
204
205#[async_trait]
206impl Handler<Forward> for TestActor {
207    async fn handle(
208        &mut self,
209        cx: &Context<Self>,
210        Forward {
211            mut to_visit,
212            mut visited,
213        }: Forward,
214    ) -> Result<(), anyhow::Error> {
215        let Some(this) = to_visit.pop_front() else {
216            anyhow::bail!("unexpected forward chain termination");
217        };
218        visited.push(this);
219        let next = to_visit.front().cloned();
220        anyhow::ensure!(next.is_some(), "unexpected forward chain termination");
221        next.unwrap().post(cx, Forward { to_visit, visited });
222        Ok(())
223    }
224}
225
226/// Just return the cast info of the sender.
227#[derive(Debug, Clone, Named, Serialize, Deserialize, Handler, RefClient)]
228pub struct GetCastInfo {
229    /// Originating actor, point, sender.
230    #[reply]
231    pub cast_info: hyperactor::PortRef<(Point, ActorRef<TestActor>, hyperactor::ActorAddr)>,
232}
233
234#[async_trait]
235impl Handler<GetCastInfo> for TestActor {
236    async fn handle(
237        &mut self,
238        cx: &Context<Self>,
239        GetCastInfo { cast_info }: GetCastInfo,
240    ) -> Result<(), anyhow::Error> {
241        cast_info.post(cx, (cx.cast_point(), cx.bind(), cx.sender().clone()));
242        Ok(())
243    }
244}
245
246#[derive(Debug, Clone, Named, Serialize, Deserialize)]
247pub struct GetResourceRank {
248    pub rank: crate::resource::Rank,
249    pub reply: hyperactor::PortRef<(Point, Option<usize>)>,
250}
251
252#[async_trait]
253impl Handler<GetResourceRank> for TestActor {
254    async fn handle(
255        &mut self,
256        cx: &Context<Self>,
257        GetResourceRank { rank, reply }: GetResourceRank,
258    ) -> Result<(), anyhow::Error> {
259        reply.post(cx, (cx.cast_point(), rank.0));
260
261        Ok(())
262    }
263}
264
265#[derive(Debug)]
266#[hyperactor::export]
267#[hyperactor::spawnable]
268pub struct FailingCreateTestActor;
269
270#[async_trait]
271impl Actor for FailingCreateTestActor {}
272
273#[async_trait]
274impl hyperactor::RemoteSpawn for FailingCreateTestActor {
275    type Params = ();
276
277    async fn new(
278        _params: Self::Params,
279        _environment: Flattrs,
280    ) -> Result<Self, hyperactor::internal_macro_support::anyhow::Error> {
281        Err(anyhow::anyhow!("test failure"))
282    }
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize, Named)]
286pub struct SetConfigAttrs(pub Vec<u8>);
287
288#[async_trait]
289impl Handler<SetConfigAttrs> for TestActor {
290    async fn handle(
291        &mut self,
292        _cx: &Context<Self>,
293        SetConfigAttrs(attrs): SetConfigAttrs,
294    ) -> Result<(), anyhow::Error> {
295        let attrs =
296            bincode::serde::decode_from_slice(&attrs, bincode::config::legacy()).map(|(v, _)| v)?;
297        hyperactor_config::global::set(Source::Runtime, attrs);
298        Ok(())
299    }
300}
301
302#[derive(Clone, Debug, Serialize, Deserialize, Named)]
303pub struct GetConfigAttrs(pub hyperactor::PortRef<Vec<u8>>);
304
305#[async_trait]
306impl Handler<GetConfigAttrs> for TestActor {
307    async fn handle(
308        &mut self,
309        cx: &Context<Self>,
310        GetConfigAttrs(reply): GetConfigAttrs,
311    ) -> Result<(), anyhow::Error> {
312        let attrs = bincode::serde::encode_to_vec(
313            hyperactor_config::global::attrs(),
314            bincode::config::legacy(),
315        )?;
316        reply.post(cx, attrs);
317        Ok(())
318    }
319}
320
321/// A message to request the next supervision event delivered to WrapperActor.
322/// Replies with None if no supervision event is encountered within a timeout
323/// (10 seconds).
324#[derive(Clone, Debug, Serialize, Deserialize, Named)]
325pub struct NextSupervisionFailure(pub hyperactor::PortRef<Option<MeshFailure>>);
326
327/// A small wrapper to handle supervision messages so they don't
328/// need to reach the client. This just wraps and forwards all messages to TestActor.
329/// The supervision events are sent back to "supervisor".
330#[derive(Debug)]
331#[hyperactor::export(CauseSupervisionEvent, MeshFailure, NextSupervisionFailure)]
332#[hyperactor::spawnable]
333pub struct WrapperActor {
334    proc_mesh: ProcMeshRef,
335    // Needs to be a mesh so we own this actor and have a controller for it.
336    mesh: Option<ActorMesh<TestActor>>,
337    supervisor: hyperactor::PortRef<MeshFailure>,
338    test_name: ActorMeshId,
339}
340
341#[async_trait]
342impl hyperactor::RemoteSpawn for WrapperActor {
343    type Params = (ProcMeshRef, hyperactor::PortRef<MeshFailure>, ActorMeshId);
344
345    async fn new(
346        (proc_mesh, supervisor, test_name): Self::Params,
347        _environment: Flattrs,
348    ) -> Result<Self, hyperactor::internal_macro_support::anyhow::Error> {
349        Ok(Self {
350            proc_mesh,
351            mesh: None,
352            supervisor,
353            test_name,
354        })
355    }
356}
357
358#[async_trait]
359impl Actor for WrapperActor {
360    async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
361        self.mesh = Some(
362            self.proc_mesh
363                .spawn_with_name(this, self.test_name.clone(), &(), None, false)
364                .await?,
365        );
366        Ok(())
367    }
368}
369
370#[async_trait]
371impl Handler<CauseSupervisionEvent> for WrapperActor {
372    async fn handle(
373        &mut self,
374        cx: &Context<Self>,
375        msg: CauseSupervisionEvent,
376    ) -> Result<(), anyhow::Error> {
377        // No reply to wait for.
378        if msg.send_to_children {
379            // Send only to children, don't cause the event itself.
380            self.mesh
381                .as_ref()
382                .unwrap()
383                .cast(cx, msg)
384                .map_err(|e| e.into())
385        } else {
386            msg.cause_event()
387        }
388    }
389}
390
391#[async_trait]
392impl Handler<NextSupervisionFailure> for WrapperActor {
393    async fn handle(
394        &mut self,
395        cx: &Context<Self>,
396        msg: NextSupervisionFailure,
397    ) -> Result<(), anyhow::Error> {
398        let mesh = if let Some(mesh) = self.mesh.as_ref() {
399            mesh.deref()
400        } else {
401            msg.0.post(cx, None);
402            return Ok(());
403        };
404        let failure = match tokio::time::timeout(
405            tokio::time::Duration::from_secs(20),
406            mesh.next_supervision_event(cx),
407        )
408        .await
409        {
410            Ok(Ok(failure)) => Some(failure),
411            // Any error in next_supervision_event is treated the same.
412            Ok(Err(_)) => None,
413            // If we timeout, send back None.
414            Err(_) => None,
415        };
416        msg.0.post(cx, failure);
417        Ok(())
418    }
419}
420
421#[async_trait]
422impl Handler<MeshFailure> for WrapperActor {
423    async fn handle(&mut self, cx: &Context<Self>, msg: MeshFailure) -> Result<(), anyhow::Error> {
424        // All supervision events are considered handled so they don't bubble up
425        // to the client (who isn't listening for MeshFailure).
426        tracing::info!("got supervision event from child: {}", msg);
427        // Send to a port so the client can view the messages.
428        // Ignore the error if there is one.
429        let _ = self.supervisor.post(cx, msg.clone());
430        Ok(())
431    }
432}
433
434#[cfg(test)]
435/// Asserts that the provided actor mesh has the expected shape,
436/// and all actors are assigned the correct ranks. We also test
437/// slicing the mesh.
438pub async fn assert_mesh_shape(actor_mesh: ActorMesh<TestActor>) {
439    let instance = testing::instance();
440    // Verify casting to the root actor mesh
441    assert_casting_correctness(&actor_mesh, instance, None).await;
442
443    // Just pick the first dimension. Slice half of it off.
444    // actor_mesh.extent().
445    let label = actor_mesh.extent().labels()[0].clone();
446    let size = actor_mesh.extent().sizes()[0] / 2;
447
448    // Verify casting to the sliced actor mesh
449    let sliced_actor_mesh = actor_mesh.range(&label, 0..size).unwrap();
450    assert_casting_correctness(&sliced_actor_mesh, instance, None).await;
451}
452
453#[cfg(test)]
454/// Cast to the actor mesh, and verify that all actors are reached, and the
455/// sequence numbers, if provided, are correct.
456pub async fn assert_casting_correctness(
457    actor_mesh: &ActorMeshRef<TestActor>,
458    instance: &impl context::Actor,
459    expected_seqs: Option<(Uuid, Vec<u64>)>,
460) {
461    let (port, mut rx) = instance.mailbox().open_port();
462    actor_mesh.cast(instance, GetActorId(port.bind())).unwrap();
463    let expected_actor_ids = actor_mesh
464        .values()
465        .map(|actor_ref| actor_ref.actor_addr().clone())
466        .collect::<Vec<_>>();
467    let mut expected: HashMap<&hyperactor::ActorAddr, Option<SeqInfo>> = match expected_seqs {
468        None => expected_actor_ids
469            .iter()
470            .map(|actor_id| (actor_id, None))
471            .collect(),
472        Some((session_id, seqs)) => expected_actor_ids
473            .iter()
474            .zip(
475                seqs.into_iter()
476                    .map(|seq| Some(SeqInfo::Session { session_id, seq })),
477            )
478            .collect(),
479    };
480
481    while !expected.is_empty() {
482        let (actor_id, rcved) = rx.recv().await.unwrap();
483        let rcv_seq_info = rcved.unwrap();
484        let removed = expected.remove(&actor_id);
485        assert!(
486            removed.is_some(),
487            "got {actor_id}, expect {expected_actor_ids:?}"
488        );
489        if let Some(expected) = removed.unwrap() {
490            assert_eq!(expected, rcv_seq_info, "got different seq for {actor_id}");
491        }
492    }
493
494    // No more messages
495    tokio::time::sleep(Duration::from_secs(1)).await;
496    let result = rx.try_recv();
497    assert!(result.as_ref().unwrap().is_none(), "got {result:?}");
498}