Skip to main content

hyperactor_remote/
actor_spawner.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//! Actor interface for remotely spawning actors.
10//!
11//! [`ActorSpawner`] is a spawn factory actor. A proc that wants to accept
12//! dynamic actor spawns runs an `ActorSpawner` locally and exposes an
13//! [`ActorRef<ActorSpawner>`] to callers, for example through a token
14//! rendezvous. A caller can post [`SpawnActor`] directly, but most callers
15//! should use [`ActorSpawnerEndpoint`]. The extension trait builds the spawn
16//! request, starts the local supervisor that owns the remote actor lifetime,
17//! and returns the [`ActorRef`] for the actor that will be created on the
18//! remote proc.
19//!
20//! Spawning through [`ActorSpawnerEndpoint`] makes the spawning actor the
21//! supervisor of the spawned remote actor. Logically, the remote actor becomes
22//! part of the spawner's supervision tree even though it runs on another proc:
23//!
24//! ```text
25//! caller proc                         remote proc
26//!
27//! caller actor
28//! `- spawned actor A  -------- runs on -------->  ActorSpawner's proc
29//! ```
30//!
31//! The implementation realizes that logical edge with a caller-side supervisor,
32//! a remote worker, and a cross-proc session. Boxes are actors. Arrow labels
33//! are protocol messages.
34//!
35//! ```text
36//! caller proc                                      remote proc
37//!
38//! caller actor
39//! `- [Supervisor] -- SpawnActor ---------------->  [ActorSpawner]
40//!                                                `- [Worker]
41//!                                                   `- [spawned actor A]
42//!
43//! [Supervisor] <-------- SupervisorEvent -------- [Worker]
44//! ```
45//!
46//! The caller-side [`Supervisor`] is spawned as a child of the actor that
47//! requested the remote spawn. Its bootstrap posts [`SpawnActor`] to the remote
48//! [`ActorSpawner`]. The remote [`ActorSpawner`] spawns a [`Worker`] child and asks it
49//! to deserialize the [`Gspawn`] specification, spawn the requested actor as its
50//! own child, and attach it to the caller-side supervisor. The caller-side
51//! supervisor also owns the private liveness machinery that lets either proc
52//! observe when the other side disappears.
53//!
54//! The resulting lifetime is rooted in the caller's actor tree. If the caller
55//! tree stops, `Supervisor` forwards a stop request to `Worker`, and `Worker`
56//! stops the spawned actor. If the spawned actor fails, `Worker` reports the
57//! supervision event to `Supervisor`, which re-raises it into the caller's
58//! supervision tree. If the supervisor session fails, the surviving side turns
59//! that into a terminal supervision event instead of leaving an orphaned remote
60//! actor.
61//!
62//! A typical caller only needs the extension trait. The returned [`ActorRef`]
63//! names where the actor will run and is usable immediately: the remote proc
64//! queues messages for the actor until it starts. They are returned as
65//! undeliverable only if the actor does not start within
66//! [`hyperactor::config::PENDING_ACTOR_DELIVERY_TIMEOUT`]:
67//!
68//! ```rust,ignore
69//! use hyperactor::ActorRef;
70//! use hyperactor::Context;
71//! use hyperactor::Handler;
72//! use hyperactor::Label;
73//! use hyperactor::RemoteSpawn;
74//! use hyperactor::Uid;
75//! use hyperactor_remote::ActorSpawner;
76//! use hyperactor_remote::ActorSpawnerEndpoint;
77//!
78//! struct Driver {
79//!     actor_spawner: ActorRef<ActorSpawner>,
80//! }
81//!
82//! impl Handler<Start> for Driver {
83//!     async fn handle(&mut self, cx: &Context<Self>, _start: Start) -> anyhow::Result<()> {
84//!         let calculator = self.actor_spawner.spawn_uid::<Calculator>(
85//!             cx,
86//!             Uid::instance(Label::new("calculator").unwrap()),
87//!             CalculatorParams { initial_value: 0 },
88//!         )?;
89//!         calculator.post(cx, Add { lhs: 40, rhs: 2 });
90//!         Ok(())
91//!     }
92//! }
93//!
94//! # struct Start;
95//! # struct Calculator;
96//! # struct CalculatorParams {
97//! #     initial_value: i64,
98//! # }
99//! # struct Add {
100//! #     lhs: i64,
101//! #     rhs: i64,
102//! # }
103//! # impl RemoteSpawn for Calculator {
104//! #     type Params = CalculatorParams;
105//! # }
106//! ```
107
108use async_trait::async_trait;
109use hyperactor::Actor;
110use hyperactor::ActorEnvironment;
111use hyperactor::ActorHandle;
112use hyperactor::ActorRef;
113use hyperactor::Context;
114use hyperactor::Endpoint;
115use hyperactor::Handler;
116use hyperactor::Instance;
117use hyperactor::OncePortHandle;
118use hyperactor::RemoteSpawn;
119use hyperactor::Uid;
120use hyperactor::context;
121use hyperactor_config::Flattrs;
122
123use crate::Gspawn;
124use crate::KeepaliveLink;
125use crate::SpawnActor as SpawnActorMessage;
126use crate::SupervisionOptions;
127use crate::Supervisor;
128use crate::Worker;
129use crate::supervision::GspawnAndSupervise;
130
131// Actor-spawn interface exposed by an actor spawner.
132hyperactor::behavior!(SpawnActor, SpawnActorMessage);
133
134/// Remote actor that accepts dynamic remote spawn requests.
135#[derive(Debug, Default)]
136#[hyperactor::export(SpawnActorMessage)]
137#[hyperactor::spawnable]
138pub struct ActorSpawner;
139
140#[async_trait]
141impl Actor for ActorSpawner {
142    async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
143        this.set_system();
144        Ok(())
145    }
146}
147
148#[async_trait]
149impl Handler<SpawnActorMessage> for ActorSpawner {
150    async fn handle(
151        &mut self,
152        cx: &Context<Self>,
153        message: SpawnActorMessage,
154    ) -> anyhow::Result<()> {
155        let SpawnActorMessage { gspawn, supervise } = message;
156        let worker = context::Actor::instance(cx).spawn(Worker::new());
157        worker.post(cx, GspawnAndSupervise::new(gspawn, supervise));
158        Ok(())
159    }
160}
161
162hyperactor::assert_behaves!(ActorSpawner as SpawnActor);
163
164/// Convenience methods for endpoints that accept [`SpawnActor`] requests.
165pub trait ActorSpawnerEndpoint {
166    /// Spawn a registered actor with a fresh actor uid.
167    fn spawn<A>(&self, cx: &impl context::Actor, params: A::Params) -> anyhow::Result<ActorRef<A>>
168    where
169        A: RemoteSpawn,
170        Self: Clone + Send + 'static,
171        for<'a> &'a Self: Endpoint<SpawnActorMessage>,
172    {
173        self.spawn_uid::<A>(cx, Uid::anonymous(), params)
174    }
175
176    /// Spawn a registered actor and return its local supervisor handle.
177    ///
178    /// The returned actor ref identifies the eventual remote actor. The handle
179    /// controls its local supervisor proxy and can be stopped immediately,
180    /// including before the remote supervision session links.
181    fn spawn_with_handle<A>(
182        &self,
183        cx: &impl context::Actor,
184        params: A::Params,
185    ) -> anyhow::Result<(ActorRef<A>, ActorHandle<Supervisor>)>
186    where
187        A: RemoteSpawn,
188        Self: Clone + Send + 'static,
189        for<'a> &'a Self: Endpoint<SpawnActorMessage>,
190    {
191        self.spawn_uid_with_link_and_ready::<A>(
192            cx,
193            Uid::anonymous(),
194            params,
195            KeepaliveLink::default(),
196            None,
197            cx.instance().actor_environment().clone(),
198            Flattrs::new(),
199        )
200    }
201
202    /// Spawn a registered actor with an explicit actor uid.
203    fn spawn_uid<A>(
204        &self,
205        cx: &impl context::Actor,
206        uid: Uid,
207        params: A::Params,
208    ) -> anyhow::Result<ActorRef<A>>
209    where
210        A: RemoteSpawn,
211        Self: Clone + Send + 'static,
212        for<'a> &'a Self: Endpoint<SpawnActorMessage>,
213    {
214        self.spawn_uid_with_link::<A>(cx, uid, params, KeepaliveLink::default())
215    }
216
217    /// Spawn a registered actor and report when it has linked.
218    ///
219    /// The returned [`ActorRef`] is usable immediately; see the
220    /// [module docs](self). A bare readiness signal is posted to `ready` once
221    /// the actor has linked. The address is already known from the
222    /// synchronously returned [`ActorRef`], so `ready` carries no payload.
223    fn spawn_uid_with_ready<A>(
224        &self,
225        cx: &impl context::Actor,
226        uid: Uid,
227        params: A::Params,
228        ready: OncePortHandle<()>,
229    ) -> anyhow::Result<ActorRef<A>>
230    where
231        A: RemoteSpawn,
232        Self: Clone + Send + 'static,
233        for<'a> &'a Self: Endpoint<SpawnActorMessage>,
234    {
235        self.spawn_uid_with_link_and_ready::<A>(
236            cx,
237            uid,
238            params,
239            KeepaliveLink::default(),
240            Some(ready),
241            cx.instance().actor_environment().clone(),
242            Flattrs::new(),
243        )
244        .map(|(actor_ref, _supervisor)| actor_ref)
245    }
246
247    /// Spawn a registered actor with an explicit actor uid and liveness link.
248    fn spawn_uid_with_link<A>(
249        &self,
250        cx: &impl context::Actor,
251        uid: Uid,
252        params: A::Params,
253        liveness: KeepaliveLink,
254    ) -> anyhow::Result<ActorRef<A>>
255    where
256        A: RemoteSpawn,
257        Self: Clone + Send + 'static,
258        for<'a> &'a Self: Endpoint<SpawnActorMessage>,
259    {
260        self.spawn_uid_with_link_and_ready::<A>(
261            cx,
262            uid,
263            params,
264            liveness,
265            None,
266            cx.instance().actor_environment().clone(),
267            Flattrs::new(),
268        )
269        .map(|(actor_ref, _supervisor)| actor_ref)
270    }
271
272    /// Spawn a registered actor with an explicit actor uid, liveness link,
273    /// optional readiness port, persistent environment, and supervision event
274    /// labels. This is the general form behind the other `spawn*` methods,
275    /// which pass the caller's own environment and no labels; see
276    /// [`spawn_uid_with_ready`](Self::spawn_uid_with_ready) for the readiness
277    /// semantics and [`Supervisor::with_labels`] for the labels.
278    fn spawn_uid_with_link_and_ready<A>(
279        &self,
280        cx: &impl context::Actor,
281        uid: Uid,
282        params: A::Params,
283        liveness: KeepaliveLink,
284        ready: Option<OncePortHandle<()>>,
285        environment: ActorEnvironment,
286        labels: Flattrs,
287    ) -> anyhow::Result<(ActorRef<A>, ActorHandle<Supervisor>)>
288    where
289        A: RemoteSpawn,
290        Self: Clone + Send + 'static,
291        for<'a> &'a Self: Endpoint<SpawnActorMessage>,
292    {
293        // We can safely compute the resulting actor ref: by definition it
294        // will be on the same proc as the actor spawner itself, and we use a
295        // Uid to identify it.
296        anyhow::ensure!(
297            uid.is_instance(),
298            "ActorSpawner-spawned actors cannot be singletons"
299        );
300        let actor_ref = ActorRef::attest(
301            self.endpoint_location()
302                .actor_addr()
303                .proc_addr()
304                .actor_addr_uid(uid.clone()),
305        );
306        let actor_spawner = self.clone();
307        let gspawn = Gspawn::for_actor_uid_in_environment::<A>(uid, params, environment)?;
308        let supervisor = cx.instance().spawn(
309            Supervisor::bootstrap_uid(
310                liveness,
311                SupervisionOptions::default(),
312                Uid::anonymous(),
313                actor_ref.actor_addr().clone(),
314                ready,
315                move |cx, supervise| {
316                    actor_spawner.post(cx, SpawnActorMessage { gspawn, supervise });
317                    Ok(())
318                },
319            )
320            .with_labels(labels),
321        );
322        Ok((actor_ref, supervisor))
323    }
324}
325
326impl<T> ActorSpawnerEndpoint for T where for<'a> &'a T: Endpoint<SpawnActorMessage> {}
327
328#[cfg(test)]
329mod tests {
330    use std::time::Duration;
331
332    use async_trait::async_trait;
333    use hyperactor::Actor;
334    use hyperactor::ActorAddr;
335    use hyperactor::ActorEnvironment;
336    use hyperactor::ActorHandle;
337    use hyperactor::Context;
338    use hyperactor::Handler;
339    use hyperactor::Instance;
340    use hyperactor::Label;
341    use hyperactor::OncePortHandle;
342    use hyperactor::OncePortRef;
343    use hyperactor::PortRef;
344    use hyperactor::Proc;
345    use hyperactor::RemoteSpawn;
346    use hyperactor::Uid;
347    use hyperactor::supervision::ActorSupervisionEvent;
348    use hyperactor_config::attrs::declare_attrs;
349    use serde::Deserialize;
350    use serde::Serialize;
351    use typeuri::Named;
352
353    use super::*;
354    use crate::Gspawn;
355    use crate::KeepaliveLink;
356    use crate::RemoteActorDisposition;
357    use crate::Supervise;
358    use crate::SupervisionOptions;
359    use crate::SupervisorEvent;
360
361    #[derive(Clone, Debug, Serialize, Deserialize, Named)]
362    struct Fail;
363    wirevalue::register_type!(Fail);
364
365    #[derive(Debug)]
366    #[hyperactor::export(Fail)]
367    struct TestChild;
368
369    #[async_trait]
370    impl Actor for TestChild {}
371
372    #[async_trait]
373    impl RemoteSpawn for TestChild {
374        type Params = ();
375
376        async fn new(_params: (), _environment: &ActorEnvironment) -> anyhow::Result<Self> {
377            Ok(Self)
378        }
379    }
380
381    #[async_trait]
382    impl Handler<Fail> for TestChild {
383        async fn handle(&mut self, _cx: &Context<Self>, _message: Fail) -> anyhow::Result<()> {
384            anyhow::bail!("test child failed")
385        }
386    }
387
388    hyperactor::register_spawnable!(TestChild);
389
390    #[derive(Debug)]
391    #[hyperactor::export(handlers = [])]
392    struct FailingChild;
393
394    #[async_trait]
395    impl Actor for FailingChild {
396        async fn init(&mut self, _this: &Instance<Self>) -> anyhow::Result<()> {
397            anyhow::bail!("failing child init")
398        }
399    }
400
401    #[async_trait]
402    impl RemoteSpawn for FailingChild {
403        type Params = ();
404
405        async fn new(_params: (), _environment: &ActorEnvironment) -> anyhow::Result<Self> {
406            Ok(Self)
407        }
408    }
409
410    hyperactor::register_spawnable!(FailingChild);
411
412    declare_attrs! {
413        attr REMOTE_ENVIRONMENT_TAG: u64;
414        attr REMOTE_SERVER_ONLY_TAG: u64;
415    }
416
417    #[derive(Debug)]
418    #[hyperactor::export(handlers = [])]
419    struct EnvironmentChild {
420        reply: PortRef<(u64, u64, Option<u64>, Option<u64>)>,
421        constructor_value: u64,
422        constructor_server_only: Option<u64>,
423    }
424
425    #[async_trait]
426    impl Actor for EnvironmentChild {
427        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
428            let stored_value = this
429                .actor_environment()
430                .get(REMOTE_ENVIRONMENT_TAG)
431                .unwrap_or_default();
432            self.reply.post(
433                this,
434                (
435                    self.constructor_value,
436                    stored_value,
437                    self.constructor_server_only,
438                    this.actor_environment().get(REMOTE_SERVER_ONLY_TAG),
439                ),
440            );
441            Ok(())
442        }
443    }
444
445    #[async_trait]
446    impl RemoteSpawn for EnvironmentChild {
447        type Params = PortRef<(u64, u64, Option<u64>, Option<u64>)>;
448
449        async fn new(reply: Self::Params, environment: &ActorEnvironment) -> anyhow::Result<Self> {
450            Ok(Self {
451                reply,
452                constructor_value: environment.get(REMOTE_ENVIRONMENT_TAG).unwrap_or_default(),
453                constructor_server_only: environment.get(REMOTE_SERVER_ONLY_TAG),
454            })
455        }
456    }
457
458    hyperactor::register_spawnable!(EnvironmentChild);
459
460    #[derive(Debug)]
461    struct PlainSpawner {
462        actor_spawner: ActorHandle<ActorSpawner>,
463        spawned: PortRef<ActorAddr>,
464        events: PortRef<ActorSupervisionEvent>,
465    }
466
467    #[async_trait]
468    impl Actor for PlainSpawner {
469        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
470            let actor_ref = self.actor_spawner.spawn_uid::<FailingChild>(
471                this,
472                Uid::instance(Label::new("failing_child").unwrap()),
473                (),
474            )?;
475            self.spawned.post(this, actor_ref.actor_addr().clone());
476            Ok(())
477        }
478
479        async fn handle_supervision_event(
480            &mut self,
481            this: &Instance<Self>,
482            event: &ActorSupervisionEvent,
483        ) -> anyhow::Result<bool> {
484            self.events.post(this, event.clone());
485            this.exit("supervision event observed")?;
486            Ok(true)
487        }
488    }
489
490    #[derive(Debug)]
491    struct ReadySpawner {
492        actor_spawner: ActorHandle<ActorSpawner>,
493        spawned: PortRef<ActorAddr>,
494        ready: Option<OncePortHandle<()>>,
495    }
496
497    #[async_trait]
498    impl Actor for ReadySpawner {
499        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
500            let ready = self.ready.take().expect("ready spawner initialized once");
501            let actor_ref = self.actor_spawner.spawn_uid_with_ready::<TestChild>(
502                this,
503                Uid::instance(Label::new("ready_child").unwrap()),
504                (),
505                ready,
506            )?;
507            self.spawned.post(this, actor_ref.actor_addr().clone());
508            Ok(())
509        }
510
511        async fn handle_supervision_event(
512            &mut self,
513            _this: &Instance<Self>,
514            event: &ActorSupervisionEvent,
515        ) -> anyhow::Result<bool> {
516            // Absorb teardown events so cleanup doesn't surface as a root failure.
517            Ok(!event.is_error())
518        }
519    }
520
521    #[derive(Clone, Debug, Serialize, Deserialize, Named)]
522    struct Ping(OncePortRef<()>);
523    wirevalue::register_type!(Ping);
524
525    #[derive(Debug)]
526    #[hyperactor::export(Ping)]
527    struct StoppingSpawner {
528        actor_spawner: ActorHandle<ActorSpawner>,
529        stopped: Option<OncePortHandle<()>>,
530    }
531
532    #[async_trait]
533    impl Actor for StoppingSpawner {
534        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
535            let (_child, supervisor) = self
536                .actor_spawner
537                .spawn_with_handle::<TestChild>(this, ())?;
538            supervisor.stop("test complete")?;
539            let _status = supervisor.await;
540            self.stopped
541                .take()
542                .expect("stopping spawner initialized once")
543                .post(this, ());
544            Ok(())
545        }
546    }
547
548    #[async_trait]
549    impl Handler<Ping> for StoppingSpawner {
550        async fn handle(&mut self, cx: &Context<Self>, message: Ping) -> anyhow::Result<()> {
551            message.0.post(cx, ());
552            Ok(())
553        }
554    }
555
556    #[derive(Debug)]
557    #[hyperactor::export(Ping)]
558    struct SlowChild;
559
560    #[async_trait]
561    impl Actor for SlowChild {}
562
563    #[async_trait]
564    impl RemoteSpawn for SlowChild {
565        type Params = ();
566
567        async fn new(_params: (), _environment: &ActorEnvironment) -> anyhow::Result<Self> {
568            // Keeps the child unbound on its proc while its spawner messages it.
569            tokio::time::sleep(Duration::from_millis(500)).await;
570            Ok(Self)
571        }
572    }
573
574    #[async_trait]
575    impl Handler<Ping> for SlowChild {
576        async fn handle(&mut self, cx: &Context<Self>, message: Ping) -> anyhow::Result<()> {
577            message.0.post(cx, ());
578            Ok(())
579        }
580    }
581
582    hyperactor::register_spawnable!(SlowChild);
583
584    #[derive(Debug)]
585    struct EagerSpawner {
586        actor_spawner: ActorHandle<ActorSpawner>,
587        pong: Option<OncePortRef<()>>,
588    }
589
590    #[async_trait]
591    impl Actor for EagerSpawner {
592        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
593            let child = self.actor_spawner.spawn_uid::<SlowChild>(
594                this,
595                Uid::instance(Label::new("slow_child").unwrap()),
596                (),
597            )?;
598            let pong = self.pong.take().expect("eager spawner initialized once");
599            child.post(this, Ping(pong));
600            Ok(())
601        }
602
603        async fn handle_supervision_event(
604            &mut self,
605            _this: &Instance<Self>,
606            event: &ActorSupervisionEvent,
607        ) -> anyhow::Result<bool> {
608            // Absorb teardown events so cleanup doesn't surface as a root failure.
609            Ok(!event.is_error())
610        }
611    }
612
613    fn encoded_unit() -> Vec<u8> {
614        bincode::serde::encode_to_vec((), bincode::config::legacy()).unwrap()
615    }
616
617    async fn spawn_test_child(
618        actor_spawner: &ActorHandle<ActorSpawner>,
619        client: &hyperactor::Client,
620        supervisor: PortRef<SupervisorEvent>,
621        session_id: Uid,
622    ) {
623        let (_link_handle, liveness) =
624            KeepaliveLink::new(Duration::from_secs(60), Duration::from_secs(60))
625                .spawn_supervisor(client)
626                .unwrap();
627        actor_spawner.post(
628            client,
629            SpawnActorMessage {
630                gspawn: Gspawn::for_actor_uid::<TestChild>(
631                    client,
632                    Uid::instance(Label::new("test_child").unwrap()),
633                    (),
634                )
635                .unwrap(),
636                supervise: Supervise {
637                    session_id,
638                    supervisor,
639                    parent: client.self_addr().clone(),
640                    liveness,
641                    options: SupervisionOptions::default(),
642                    labels: Flattrs::new(),
643                },
644            },
645        );
646    }
647
648    #[tokio::test]
649    async fn test_spawn_actor_endpoint_works_from_plain_actor() {
650        let proc = Proc::isolated();
651        let client = proc.client("client");
652        let actor_spawner = proc.spawn(ActorSpawner);
653        let (spawned, mut spawned_rx) = client.open_port::<ActorAddr>();
654        let (events, mut events_rx) = client.open_port::<ActorSupervisionEvent>();
655        let spawner = proc.spawn(PlainSpawner {
656            actor_spawner: actor_spawner.clone(),
657            spawned: spawned.bind(),
658            events: events.bind(),
659        });
660
661        let spawned_actor = tokio::time::timeout(Duration::from_secs(5), spawned_rx.recv())
662            .await
663            .unwrap()
664            .unwrap();
665        let event = tokio::time::timeout(Duration::from_secs(5), events_rx.recv())
666            .await
667            .unwrap()
668            .unwrap();
669        assert!(event.is_error());
670        assert_eq!(event.actor_id, spawned_actor);
671
672        actor_spawner.stop("test").unwrap();
673        tokio::time::timeout(Duration::from_secs(5), spawner)
674            .await
675            .unwrap();
676        tokio::time::timeout(Duration::from_secs(5), actor_spawner)
677            .await
678            .unwrap();
679    }
680
681    #[tokio::test]
682    async fn test_spawn_uid_with_ready_signals_when_child_links() {
683        let proc = Proc::isolated();
684        let client = proc.client("client");
685        let actor_spawner = proc.spawn(ActorSpawner);
686        let (spawned, mut spawned_rx) = client.open_port::<ActorAddr>();
687        let (ready, ready_rx) = client.open_once_port::<()>();
688        let spawner = proc.spawn(ReadySpawner {
689            actor_spawner: actor_spawner.clone(),
690            spawned: spawned.bind(),
691            ready: Some(ready),
692        });
693
694        let _attested = tokio::time::timeout(Duration::from_secs(5), spawned_rx.recv())
695            .await
696            .unwrap()
697            .unwrap();
698        // The readiness port fires once the child links and becomes reachable.
699        // It carries no payload — the caller already holds the address from the
700        // synchronously returned ref — so receiving it is the reachability signal.
701        tokio::time::timeout(Duration::from_secs(5), ready_rx.recv())
702            .await
703            .unwrap()
704            .unwrap();
705
706        spawner.stop("test").unwrap();
707        tokio::time::timeout(Duration::from_secs(5), spawner)
708            .await
709            .unwrap();
710        actor_spawner.stop("test").unwrap();
711        tokio::time::timeout(Duration::from_secs(5), actor_spawner)
712            .await
713            .unwrap();
714    }
715
716    #[tokio::test]
717    async fn test_spawn_uid_delivers_messages_sent_before_child_starts() {
718        let proc = Proc::isolated();
719        let client = proc.client("client");
720        let actor_spawner = proc.spawn(ActorSpawner);
721        let (pong, pong_rx) = client.open_once_port::<()>();
722        let spawner = proc.spawn(EagerSpawner {
723            actor_spawner: actor_spawner.clone(),
724            pong: Some(pong.bind()),
725        });
726
727        tokio::time::timeout(Duration::from_secs(5), pong_rx.recv())
728            .await
729            .unwrap()
730            .unwrap();
731
732        spawner.stop("test").unwrap();
733        tokio::time::timeout(Duration::from_secs(5), spawner)
734            .await
735            .unwrap();
736        actor_spawner.stop("test").unwrap();
737        tokio::time::timeout(Duration::from_secs(5), actor_spawner)
738            .await
739            .unwrap();
740    }
741
742    #[tokio::test]
743    async fn test_spawn_with_handle_stops_only_spawned_actor() {
744        let proc = Proc::isolated();
745        let client = proc.client("client");
746        let actor_spawner = proc.spawn(ActorSpawner);
747        let (stopped, stopped_rx) = client.open_once_port::<()>();
748        let parent = proc.spawn(StoppingSpawner {
749            actor_spawner: actor_spawner.clone(),
750            stopped: Some(stopped),
751        });
752
753        tokio::time::timeout(Duration::from_secs(5), stopped_rx.recv())
754            .await
755            .expect("spawned actor stop timed out")
756            .expect("spawned actor stop signal closed");
757        assert!(
758            !parent.status().borrow().is_terminal(),
759            "stopping the spawned actor should not stop its parent"
760        );
761        assert!(
762            !actor_spawner.status().borrow().is_terminal(),
763            "stopping the spawned actor should not stop the actor spawner"
764        );
765
766        let (pong, pong_rx) = client.open_once_port::<()>();
767        parent.post(&client, Ping(pong.bind()));
768        tokio::time::timeout(Duration::from_secs(5), pong_rx.recv())
769            .await
770            .expect("parent ping timed out")
771            .expect("parent ping port closed");
772
773        parent.stop("test complete").expect("stop parent");
774        tokio::time::timeout(Duration::from_secs(5), parent)
775            .await
776            .expect("parent stop timed out");
777        actor_spawner.stop("test").expect("stop actor spawner");
778        tokio::time::timeout(Duration::from_secs(5), actor_spawner)
779            .await
780            .expect("actor spawner stop timed out");
781    }
782
783    #[tokio::test]
784    async fn actor_spawner_carries_callers_actor_environment() {
785        const SENTINEL: u64 = 0xA0FE;
786
787        let proc = Proc::isolated();
788        let client = proc.client("client");
789        let mut server_environment = ActorEnvironment::default();
790        server_environment
791            .set(REMOTE_ENVIRONMENT_TAG, SENTINEL + 1)
792            .expect("seed conflicting server environment");
793        server_environment
794            .set(REMOTE_SERVER_ONLY_TAG, 99)
795            .expect("seed server-only environment");
796        let server = proc
797            .actor_instance_in_environment::<TestChild>("seeded_server", server_environment)
798            .expect("create seeded server instance");
799        let actor_spawner = server.instance.spawn(ActorSpawner);
800        let (reply, mut reply_rx) = client.open_port::<(u64, u64, Option<u64>, Option<u64>)>();
801        let (ready, ready_rx) = client.open_once_port::<()>();
802
803        let mut environment = ActorEnvironment::default();
804        environment
805            .set(REMOTE_ENVIRONMENT_TAG, SENTINEL)
806            .expect("seed caller environment");
807        let caller = proc
808            .actor_instance_in_environment::<TestChild>("seeded_caller", environment)
809            .expect("create seeded caller instance");
810
811        actor_spawner
812            .spawn_uid_with_ready::<EnvironmentChild>(
813                &caller.instance,
814                Uid::instance(Label::new("environment_child").unwrap()),
815                reply.bind(),
816                ready,
817            )
818            .expect("request remote child");
819        tokio::time::timeout(Duration::from_secs(5), ready_rx.recv())
820            .await
821            .expect("remote child readiness timed out")
822            .expect("remote child readiness port closed");
823        assert_eq!(
824            tokio::time::timeout(Duration::from_secs(5), reply_rx.recv())
825                .await
826                .expect("environment reply timed out")
827                .expect("environment reply port closed"),
828            (SENTINEL, SENTINEL, None, None),
829            "constructor and stored instance must receive only the caller's environment",
830        );
831
832        actor_spawner.stop("test").expect("stop actor spawner");
833        tokio::time::timeout(Duration::from_secs(5), actor_spawner)
834            .await
835            .expect("actor spawner stop timed out");
836    }
837
838    #[test]
839    fn gspawn_serializes_actor_environment() {
840        let mut environment = ActorEnvironment::default();
841        environment
842            .set(REMOTE_ENVIRONMENT_TAG, 17)
843            .expect("seed actor environment");
844        let gspawn = Gspawn::with_uid(
845            TestChild::typename(),
846            Uid::instance(Label::new("serialized_child").unwrap()),
847            encoded_unit(),
848            environment,
849        );
850
851        let encoded = bincode::serde::encode_to_vec(&gspawn, bincode::config::legacy())
852            .expect("serialize gspawn");
853        let restored: Gspawn =
854            bincode::serde::decode_from_slice(&encoded, bincode::config::legacy())
855                .map(|(value, _)| value)
856                .expect("deserialize gspawn");
857
858        assert_eq!(restored, gspawn);
859        assert_eq!(
860            restored.actor_environment().get(REMOTE_ENVIRONMENT_TAG),
861            Some(17),
862        );
863    }
864
865    #[tokio::test]
866    async fn test_actor_spawner_worker_reports_child_failure() {
867        let proc = Proc::isolated();
868        let client = proc.client("client");
869        let actor_spawner = proc.spawn(ActorSpawner);
870        let (supervisor, mut supervisor_rx) = client.open_port::<SupervisorEvent>();
871        let session_id = Uid::instance(Label::new("session").unwrap());
872
873        spawn_test_child(
874            &actor_spawner,
875            &client,
876            supervisor.bind(),
877            session_id.clone(),
878        )
879        .await;
880        let linked = tokio::time::timeout(Duration::from_secs(5), supervisor_rx.recv())
881            .await
882            .unwrap()
883            .unwrap();
884        let (session_id, child) = match linked {
885            SupervisorEvent::Linked {
886                session_id, child, ..
887            } => (session_id, child),
888            message => panic!("expected linked message, got {:?}", message),
889        };
890        PortRef::<Fail>::attest_handler_port(&child).post(&client, Fail);
891
892        let event = tokio::time::timeout(Duration::from_secs(5), supervisor_rx.recv())
893            .await
894            .unwrap()
895            .unwrap();
896        match event {
897            SupervisorEvent::SupervisionEvent {
898                session_id: event_session_id,
899                event: ActorSupervisionEvent { actor_id, .. },
900                disposition: RemoteActorDisposition::Terminal,
901            } if event_session_id == session_id && actor_id == child => {}
902            message => panic!("expected terminal supervision event, got {:?}", message),
903        }
904
905        actor_spawner.stop("test").unwrap();
906        tokio::time::timeout(Duration::from_secs(5), actor_spawner)
907            .await
908            .unwrap();
909    }
910
911    #[tokio::test]
912    async fn test_actor_spawner_rejects_unknown_actor_type() {
913        let proc = Proc::isolated();
914        let client = proc.client("client");
915        let actor_spawner = proc.spawn(ActorSpawner);
916        let (supervisor, mut supervisor_rx) = client.open_port::<SupervisorEvent>();
917        let (_link_handle, liveness) =
918            KeepaliveLink::new(Duration::from_secs(60), Duration::from_secs(60))
919                .spawn_supervisor(&client)
920                .unwrap();
921        let session_id = Uid::instance(Label::new("session").unwrap());
922
923        actor_spawner.post(
924            &client,
925            SpawnActorMessage {
926                gspawn: Gspawn::with_uid(
927                    "unknown::Actor",
928                    Uid::anonymous(),
929                    encoded_unit(),
930                    ActorEnvironment::default(),
931                ),
932                supervise: Supervise {
933                    session_id: session_id.clone(),
934                    supervisor: supervisor.bind(),
935                    parent: client.self_addr().clone(),
936                    liveness,
937                    options: SupervisionOptions::default(),
938                    labels: Flattrs::new(),
939                },
940            },
941        );
942
943        let rejected = tokio::time::timeout(Duration::from_secs(5), supervisor_rx.recv())
944            .await
945            .unwrap()
946            .unwrap();
947        match rejected {
948            SupervisorEvent::SuperviseRejected {
949                session_id: rejected_session_id,
950                reason,
951            } if rejected_session_id == session_id
952                && reason.contains("actor type unknown::Actor not registered") => {}
953            message => panic!("expected supervise rejection, got {:?}", message),
954        }
955
956        actor_spawner.stop("test").unwrap();
957        tokio::time::timeout(Duration::from_secs(5), actor_spawner)
958            .await
959            .unwrap();
960    }
961}