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