Skip to main content

acktor_ipc/
session.rs

1//! Per-connection session actor.
2//!
3//! A [`Session`] wraps a single [`IpcConnection`] and mediates all traffic over it: routing
4//! inbound frames to local actors, forwarding outbound messages from local actors, and tracking
5//! pending request tags for response correlation. Sessions are owned by a
6//! [`Node`][crate::node::Node] and are created through it rather than directly.
7//!
8
9use std::fmt::{self, Debug};
10use std::result::Result as StdResult;
11use std::sync::Arc;
12
13use ahash::HashMap;
14use bytes::Bytes;
15use futures_util::{FutureExt, TryFutureExt};
16use tokio::time::{Duration, Instant};
17use tracing::{Instrument, debug, info, warn};
18
19use acktor::{
20    Actor, ActorContext, Address, ErrorReport, Handler, Message, Sender, SenderInfo,
21    channel::oneshot,
22    message::FutureMessageResult,
23    utils::{ShortName, debug_trace},
24};
25use acktor_ipc_proto::{message, utils as proto_utils};
26
27use crate::actor_ref::ActorRef;
28use crate::codec::{Decode, DecodeContext, DecodeError, Encode, EncodeContext};
29use crate::error::SessionError;
30use crate::ipc_method::IpcConnection;
31use crate::node::actor_mgr::{self, ActorMgr};
32use crate::remote::{
33    BinaryMessage, RemoteAddressable, RemoteMailbox, RemoteMailboxRegistry, RemoteSpawnable,
34};
35
36pub mod command;
37
38mod context;
39use context::SessionContext;
40
41mod proxy;
42use proxy::Proxy;
43
44type Result<T> = StdResult<T, SessionError>;
45
46/// How long a pending request may sit in the response maps before the cleanup sweep resolves
47/// it with [`SessionError::ResponseTimeout`]. The observable timeout is up to
48/// `RESPONSE_TIMEOUT + CLEANUP_INTERVAL` because the sweep is periodic.
49pub(crate) const RESPONSE_TIMEOUT: Duration = Duration::from_secs(60);
50
51/// How often the session runs the response-map cleanup sweep.
52pub(crate) const CLEANUP_INTERVAL: Duration = Duration::from_secs(15);
53
54struct MessageResponse {
55    tag: u64,
56    result: StdResult<Bytes, String>,
57}
58
59impl Debug for MessageResponse {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.debug_tuple("MessageResponse").field(&self.tag).finish()
62    }
63}
64
65impl Message for MessageResponse {
66    type Result = ();
67}
68
69/// An actor which manages the IPC connection to a remote endpoint.
70pub struct Session {
71    connection: Box<dyn IpcConnection>,
72    registry: RemoteMailboxRegistry,
73    actor_mgr: Address<ActorMgr>,
74    tag: u64, // unique tag generator
75    proxy: Option<Arc<Proxy>>,
76    message_res_tx_map: HashMap<u64, (oneshot::Sender<Bytes>, Instant)>,
77}
78
79impl Session {
80    /// Constructs a new [`Session`]. Called internally by [`Node`][crate::node::Node] when it
81    /// accepts or initiates an IPC connection; not intended for direct use.
82    pub(crate) fn new(
83        connection: Box<dyn IpcConnection>,
84        registry: RemoteMailboxRegistry,
85        actor_mgr: Address<ActorMgr>,
86    ) -> Self {
87        Self {
88            connection,
89            registry,
90            actor_mgr,
91            tag: 0,
92            proxy: None,
93            message_res_tx_map: HashMap::default(),
94        }
95    }
96
97    fn cleanup_message_res_tx_map(&mut self) {
98        let now = Instant::now();
99
100        let expired = self.message_res_tx_map.extract_if(|_, (tx, timestamp)| {
101            tx.is_closed() || now.duration_since(*timestamp) >= RESPONSE_TIMEOUT
102        });
103
104        for (tag, (tx, _)) in expired {
105            if tx.is_closed() {
106                debug!(
107                    "The sender of message with tag {} has closed the response rx, remove the \
108                     corresponding response tx",
109                    tag
110                );
111            } else {
112                let _ = tx.send_err(SessionError::ResponseTimeout);
113            }
114        }
115    }
116
117    fn next_tag(&mut self) -> u64 {
118        let tag = self.tag;
119        self.tag = self.tag.wrapping_add(1);
120        tag
121    }
122
123    async fn send_ipc_message(&mut self, ipc_msg: message::IpcMessage) -> Result<()> {
124        let encoded_ipc_msg = ipc_msg.encode_to_bytes(None)?;
125        self.connection
126            .send(encoded_ipc_msg)
127            .await
128            .map_err(SessionError::SendOutboundMessageFailed)?;
129
130        Ok(())
131    }
132
133    fn encode_context(&self) -> Arc<dyn EncodeContext + Send + Sync> {
134        self.proxy
135            .clone()
136            .expect("proxy is always available after session is started")
137            as Arc<dyn EncodeContext + Send + Sync>
138    }
139
140    fn decode_context(&self) -> Arc<dyn DecodeContext + Send + Sync> {
141        self.proxy
142            .clone()
143            .expect("proxy is always available after session is started")
144            as Arc<dyn DecodeContext + Send + Sync>
145    }
146
147    fn find_mailbox(&self, actor_id: u64) -> Result<RemoteMailbox> {
148        self.registry
149            .get(actor_id)
150            .ok_or_else(|| SessionError::ActorNotFound(actor_id.to_string()))
151    }
152
153    async fn find_mailbox_by_label(&self, label: String) -> Result<RemoteMailbox> {
154        self.actor_mgr
155            .send(actor_mgr::GetActor { label })
156            .await?
157            .await?
158    }
159
160    async fn handle_node_message(
161        &mut self,
162        message: message::NodeMessage,
163        ctx: &mut <Self as Actor>::Context,
164    ) -> Result<()> {
165        match message.message {
166            Some(message::NodeMessageType::CreateActor(message::CreateActor {
167                type_id,
168                label,
169                config,
170                tag,
171            })) => {
172                let actor_mgr = self.actor_mgr.clone();
173                let address = ctx.address();
174
175                // spawn a task to handle the potentially time consuming actor creation process
176                tokio::spawn(
177                    async move {
178                        actor_mgr
179                            .send(actor_mgr::CreateActor {
180                                type_id,
181                                label,
182                                config,
183                            })
184                            .await?
185                            .await?
186                    }
187                    .then(move |result| async move {
188                        let result = match result {
189                            Ok(actor_id) => proto_utils::ResultAddress::ok(actor_id.as_local()),
190                            Err(e) => proto_utils::ResultAddress::err(e.report()),
191                        };
192
193                        let bytes: Bytes = prost::Message::encode_to_vec(&result).into();
194
195                        // send the result back to this actor with the MessageResponse message
196                        // the IpcConnection can not be cloned into the spawned task without a
197                        // Arc<Mutex<..>>, so we convert this into a sequential message handling
198                        // process
199                        address
200                            .do_send(MessageResponse {
201                                tag,
202                                result: Ok(bytes),
203                            })
204                            .await
205                    })
206                    .inspect_err(|e| {
207                        // we can not do much if sending the response back to this actor fails,
208                        // just log it
209                        warn!(
210                            "Could not send `NodeMessageResponse::CreateActor` to remote node: {}",
211                            e.report()
212                        );
213                    })
214                    .in_current_span(),
215                );
216
217                Ok(())
218            }
219
220            Some(message::NodeMessageType::GetActor(message::GetActor { actor, tag })) => {
221                let result = match actor {
222                    Some(proto_utils::ActorRef { r#ref }) => match r#ref {
223                        Some(proto_utils::ActorRefType::Index(actor_id)) => {
224                            self.find_mailbox(actor_id)
225                        }
226                        Some(proto_utils::ActorRefType::Label(label)) => {
227                            self.find_mailbox_by_label(label).await
228                        }
229                        None => Err(DecodeError::from("missing field `ref` in `ActorRef`").into()),
230                    },
231                    _ => Err(
232                        DecodeError::from("missing field `actor` in `NodeMessage::GetActor`")
233                            .into(),
234                    ),
235                };
236
237                let result = match result {
238                    Ok(mailbox) => proto_utils::ResultAddress::ok(mailbox.index().as_local()),
239                    Err(e) => proto_utils::ResultAddress::err(e.report()),
240                };
241
242                let bytes: Bytes = prost::Message::encode_to_vec(&result).into();
243                let ipc_msg = message::IpcMessage::message_response(message::MessageResponse::new(
244                    tag,
245                    Ok(bytes),
246                ));
247
248                self.send_ipc_message(ipc_msg).await.inspect_err(|e| {
249                    // we can not do much if sending the response back to the remote node fails,
250                    // just log it
251                    warn!(
252                        "Could not send `NodeMessageResponse::GetActor` to remote node: {}",
253                        e.report()
254                    );
255                })
256            }
257
258            _ => Err(DecodeError::from("missing field `message` in `NodeMessage`").into()),
259        }
260    }
261
262    async fn handle_actor_message(
263        &mut self,
264        message: message::ActorMessage,
265        ctx: &mut <Self as Actor>::Context,
266    ) -> Result<()> {
267        let message::ActorMessage {
268            actor_id,
269            message_id,
270            message,
271            tag,
272        } = message;
273
274        match tag {
275            Some(tag) => {
276                // send
277
278                let address = ctx.address();
279                let recipient = self.find_mailbox(actor_id);
280                let (tx, rx) = oneshot::channel();
281                let message = BinaryMessage::send(actor_id, message_id, message, tx)
282                    .with_encode_context(self.encode_context())
283                    .with_decode_context(self.decode_context());
284
285                // spawn a task to handle the potentially time consuming message handling process
286                tokio::spawn(
287                    async move {
288                        recipient?
289                            .do_send(message)
290                            .await
291                            .map_err(|e| SessionError::ForwardInboundMessageFailed(e.into()))?;
292
293                        let result = rx
294                            .await
295                            .map_err(|e| SessionError::HandleInboundMessageFailed(e.into()))?;
296
297                        Ok::<Bytes, SessionError>(result)
298                    }
299                    .then(move |result| async move {
300                        // send the result back to this actor with the MessageResponse message
301                        // the IpcConnection can not be cloned into the spawned task without a
302                        // Arc<Mutex<..>>, so we convert this into a sequential message handling
303                        // process
304                        address
305                            .do_send(MessageResponse {
306                                tag,
307                                result: result.map_err(|e| e.report()),
308                            })
309                            .await
310                    })
311                    .inspect_err(|e| {
312                        // we can not do much if sending the response back to this actor fails,
313                        // just log it
314                        warn!(
315                            "Could not send `ActorMessageResponse` to remote node: {}",
316                            e.report()
317                        );
318                    })
319                    .in_current_span(),
320                );
321
322                Ok(())
323            }
324
325            None => {
326                // do_send
327
328                // sender has explicitly indicated that it does not care about the result of this
329                // message, so we just return the error and the session's context will log it
330                self.find_mailbox(actor_id)?
331                    .do_send(
332                        BinaryMessage::do_send(actor_id, message_id, message)
333                            .with_decode_context(self.decode_context()),
334                    )
335                    .await
336                    .map_err(|e| SessionError::ForwardInboundMessageFailed(e.into()))
337            }
338        }
339    }
340
341    fn handle_message_response(
342        &mut self,
343        response: message::MessageResponse,
344        _ctx: &mut <Self as Actor>::Context,
345    ) -> Result<()> {
346        let tag = response.tag;
347
348        let (sender, _) = self
349            .message_res_tx_map
350            .remove(&tag)
351            // if the tag is not found in the map, we do not know who to send the result
352            // to, and we do not know who to report the error to either, so just return
353            // an error and the session's context will log it
354            .ok_or(SessionError::InvalidMessageResTxTag(tag))?;
355
356        // remote error and processing error should be reported to the original sender who is
357        // waiting for a `Result<M::Result, RecvError>`
358        let result: Result<_> = match response.response {
359            Some(message::ResponseType::Ok(bytes)) => Ok(bytes),
360            Some(message::ResponseType::Err(err)) => Err(SessionError::RemoteNodeError(err)),
361            None => Err(DecodeError::from("missing field `response` in `MessageResponse`").into()),
362        };
363
364        // we can not do much if sending the result back to the original sender fails, just return
365        // an error and the session's context will log it
366        match result {
367            Ok(bytes) => sender
368                .send(bytes)
369                .map_err(|_| SessionError::ForwardMessageResFailed),
370            Err(e) => sender
371                .send_err(e)
372                .map_err(|_| SessionError::ForwardMessageResFailed),
373        }
374    }
375
376    async fn handle_ipc_message(
377        &mut self,
378        message: Bytes,
379        ctx: &mut <Self as Actor>::Context,
380    ) -> Result<()> {
381        let ipc_message = message::IpcMessage::decode(message, None)?;
382
383        match ipc_message.message {
384            Some(message::IpcMessageType::NodeMessage(message)) => {
385                self.handle_node_message(message, ctx).await
386            }
387            Some(message::IpcMessageType::ActorMessage(message)) => {
388                self.handle_actor_message(message, ctx).await
389            }
390            Some(message::IpcMessageType::MessageResponse(response)) => {
391                self.handle_message_response(response, ctx)
392            }
393            _ => Err(DecodeError::from("missing field `message` in `IpcMessage`").into()),
394        }
395    }
396}
397
398impl Actor for Session {
399    type Context = SessionContext;
400    type Error = SessionError;
401
402    async fn post_start(&mut self, ctx: &mut Self::Context) -> Result<()> {
403        info!("Session {} is started", self.connection.peer_endpoint());
404
405        self.proxy = Some(Proxy::new(ctx.address(), self.registry.clone()));
406
407        Ok(())
408    }
409
410    async fn post_stop(&mut self, _ctx: &mut Self::Context) -> Result<()> {
411        self.connection
412            .close()
413            .await
414            .map_err(SessionError::IoError)?;
415
416        info!("Session {} is stopped", self.connection.peer_endpoint());
417
418        Ok(())
419    }
420}
421
422impl<A> Handler<command::RemoteCreateActor<A>> for Session
423where
424    A: Actor + RemoteSpawnable,
425{
426    type Result = FutureMessageResult<command::RemoteCreateActor<A>>;
427
428    async fn handle(
429        &mut self,
430        msg: command::RemoteCreateActor<A>,
431        _ctx: &mut <Self as Actor>::Context,
432    ) -> Self::Result {
433        debug_trace!("Handle command RemoteCreateActor<{}>", ShortName::of::<A>());
434
435        let command::RemoteCreateActor { label, config, .. } = msg;
436
437        let (tx, rx) = oneshot::channel();
438
439        let tag = self.next_tag();
440        let ipc_msg = message::IpcMessage::node_message(message::NodeMessage::create_actor(
441            A::TYPE_ID.as_u64(),
442            label,
443            config.unwrap_or_default(),
444            tag,
445        ));
446
447        if let Err(e) = self.send_ipc_message(ipc_msg).await {
448            warn!(
449                "Could not send `NodeMessage::CreateActor` to remote node: {}",
450                e.report()
451            );
452            // sends the error back to the original sender who is waiting for a
453            // `Result<Result<Address<A>, SessionError>, RecvError>`
454            if let Err(e) = tx.send_err(e) {
455                // we can not do much if sending the error back to the original sender fails, just
456                // log it
457                warn!(
458                    "Could not report the error in `Handler<RemoteCreateActor<{}>>` to the \
459                     original sender: {}",
460                    ShortName::of::<A>(),
461                    e.report()
462                );
463            }
464        } else {
465            self.message_res_tx_map.insert(tag, (tx, Instant::now()));
466        }
467
468        let decode_context = self.decode_context();
469
470        FutureMessageResult::new(async move {
471            let bytes = rx.await?;
472            let result = <proto_utils::ResultAddress as prost::Message>::decode(bytes)
473                .map_err(DecodeError::other)?;
474            match result.result {
475                Some(proto_utils::ResultAddressType::Ok(actor_id)) => {
476                    Address::new_with_decode_context(actor_id, decode_context.as_ref())
477                        .map_err(Into::into)
478                }
479                Some(proto_utils::ResultAddressType::Err(e)) => {
480                    Err(SessionError::RemoteNodeError(e))
481                }
482                _ => Err(DecodeError::other("missing field `result` in `ResultAddress`").into()),
483            }
484        })
485    }
486}
487
488impl<A> Handler<command::RemoteGetActor<A>> for Session
489where
490    A: Actor + RemoteAddressable,
491{
492    type Result = FutureMessageResult<command::RemoteGetActor<A>>;
493
494    async fn handle(
495        &mut self,
496        msg: command::RemoteGetActor<A>,
497        _ctx: &mut <Self as Actor>::Context,
498    ) -> Self::Result {
499        debug_trace!("Handle command RemoteGetActor<{}>", ShortName::of::<A>());
500
501        let command::RemoteGetActor { actor, .. } = msg;
502
503        let (tx, rx) = oneshot::channel();
504
505        let tag = self.next_tag();
506        let ipc_msg = match &actor {
507            ActorRef::Index(actor_id) => message::IpcMessage::node_message(
508                message::NodeMessage::get_actor_by_index(actor_id.as_local(), tag),
509            ),
510            ActorRef::Label(label) => message::IpcMessage::node_message(
511                message::NodeMessage::get_actor_by_label(label.clone(), tag),
512            ),
513        };
514
515        if let Err(e) = self.send_ipc_message(ipc_msg).await {
516            warn!(
517                "Could not send `NodeMessage::GetActor` to remote node: {}",
518                e.report()
519            );
520            // sends the error back to the original sender who is waiting for a
521            // `Result<Result<Address<A>, SessionError>, RecvError>`
522            if let Err(e) = tx.send_err(e) {
523                // we can not do much if sending the error back to the original sender fails, just
524                // log it
525                warn!(
526                    "Could not report the error in `Handler<GetRemoteActor>` to the original \
527                     sender: {}",
528                    e.report()
529                );
530            }
531        } else {
532            self.message_res_tx_map.insert(tag, (tx, Instant::now()));
533        }
534
535        let decode_context = self.decode_context();
536
537        FutureMessageResult::new(async move {
538            let bytes = rx.await?;
539            let result = <proto_utils::ResultAddress as prost::Message>::decode(bytes)
540                .map_err(DecodeError::other)?;
541            match result.result {
542                Some(proto_utils::ResultAddressType::Ok(actor_id)) => {
543                    Address::new_with_decode_context(actor_id, decode_context.as_ref())
544                        .map_err(Into::into)
545                }
546                Some(proto_utils::ResultAddressType::Err(e)) => {
547                    Err(SessionError::RemoteNodeError(e))
548                }
549                _ => Err(DecodeError::other("missing field `result` in `ResultAddress`").into()),
550            }
551        })
552    }
553}
554
555impl Handler<BinaryMessage> for Session {
556    type Result = ();
557
558    async fn handle(
559        &mut self,
560        msg: BinaryMessage,
561        _ctx: &mut <Self as Actor>::Context,
562    ) -> Self::Result {
563        debug_trace!("Handle command {:?}", msg);
564
565        let BinaryMessage {
566            actor_id,
567            message_id,
568            bytes: message,
569            result_tx,
570            ..
571        } = msg;
572
573        match result_tx {
574            Some(tx) => {
575                // send
576
577                let tag = self.next_tag();
578                let ipc_msg = message::IpcMessage::actor_message(message::ActorMessage::send(
579                    actor_id, message_id, message, tag,
580                ));
581
582                if let Err(e) = self.send_ipc_message(ipc_msg).await {
583                    warn!(
584                        "Could not send `ActorMessage` to remote node: {}",
585                        e.report()
586                    );
587                    // sends the error back to the original sender who is waiting for a
588                    // `Result<M::Result, RecvError>`
589                    if let Err(e) = tx.send_err(e) {
590                        // we can not do much if sending the error back to the original sender
591                        // fails, just log it
592                        warn!(
593                            "Could not report the error in `Handler<BinaryMessage>` to the \
594                             original sender: {}",
595                            e.report()
596                        );
597                    }
598
599                    return;
600                }
601
602                self.message_res_tx_map.insert(tag, (tx, Instant::now()));
603            }
604
605            None => {
606                // do_send
607
608                let ipc_msg = message::IpcMessage::actor_message(message::ActorMessage::do_send(
609                    actor_id, message_id, message,
610                ));
611
612                if let Err(e) = self.send_ipc_message(ipc_msg).await {
613                    // sender has explicitly indicated that it does not care about the result of
614                    // this message, so we just log the error
615                    warn!(
616                        "Could not do_send `ActorMessage` to remote node: {}",
617                        e.report()
618                    );
619                }
620            }
621        }
622    }
623}
624
625impl Handler<MessageResponse> for Session {
626    type Result = ();
627
628    async fn handle(
629        &mut self,
630        msg: MessageResponse,
631        _ctx: &mut <Self as Actor>::Context,
632    ) -> Self::Result {
633        debug_trace!("Handle command {:?}", msg);
634
635        let MessageResponse { tag, result } = msg;
636
637        let ipc_msg =
638            message::IpcMessage::message_response(message::MessageResponse::new(tag, result));
639
640        if let Err(e) = self.send_ipc_message(ipc_msg).await {
641            warn!(
642                "Could not send `MessageResponse` to remote node: {}",
643                e.report()
644            )
645        }
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use acktor::RecvError;
652    use anyhow::Result;
653    use pretty_assertions::assert_eq;
654    use tracing_test::traced_test;
655
656    use super::*;
657    use crate::actor_ref::ActorRef;
658    use crate::test_utils::{Echo, start_failing_session};
659
660    #[test]
661    fn test_debug_fmt() {
662        let ok = MessageResponse {
663            tag: 42,
664            result: Ok(Bytes::from_static(b"payload")),
665        };
666        assert_eq!(format!("{:?}", ok), "MessageResponse(42)");
667
668        let err = MessageResponse {
669            tag: 7,
670            result: Err("boom".to_string()),
671        };
672        assert_eq!(format!("{:?}", err), "MessageResponse(7)");
673    }
674
675    /// When the outbound IPC send fails for a `send` (response-expected) message, the handler must
676    /// log the failure and report the error back to the original sender instead of leaving it
677    /// hanging.
678    #[tokio::test]
679    #[traced_test]
680    async fn test_binary_message_send_reports_failure() -> Result<()> {
681        let session = start_failing_session();
682
683        let (tx, rx) = oneshot::channel::<Bytes>();
684
685        session
686            .do_send(BinaryMessage::send(1, 2, Bytes::new(), tx))
687            .await?;
688
689        let result = rx.await;
690        assert!(
691            matches!(result, Err(RecvError::Other(e)) if e.to_string() == "could not send the outbound remote message to the remote node")
692        );
693
694        assert!(logs_contain("Could not send `ActorMessage` to remote node"));
695
696        Ok(())
697    }
698
699    /// When the outbound IPC send fails for a `do_send` (fire-and-forget) message, the error is
700    /// only logged and the session must keep running. We prove it survived by following up with a
701    /// `send` message and still receiving the reported failure.
702    #[tokio::test]
703    #[traced_test]
704    async fn test_binary_message_do_send_survives_failure() -> Result<()> {
705        let session = start_failing_session();
706
707        // do_send variant: no result channel; failure is swallowed (logged) by the handler.
708        session
709            .do_send(BinaryMessage::do_send(1, 2, Bytes::new()))
710            .await?;
711
712        // The session is still alive and processes the next message.
713        let (tx, rx) = oneshot::channel::<Bytes>();
714        session
715            .do_send(BinaryMessage::send(3, 4, Bytes::new(), tx))
716            .await?;
717
718        assert!(matches!(rx.await, Err(RecvError::Other(_))));
719        assert!(logs_contain(
720            "Could not do_send `ActorMessage` to remote node"
721        ));
722
723        Ok(())
724    }
725
726    /// A failed outbound send while serving a `MessageResponse` (the relayed result of an inbound
727    /// request) is only logged; the session must keep running.
728    #[tokio::test]
729    #[traced_test]
730    async fn test_message_response_survives_failure() -> Result<()> {
731        let session = start_failing_session();
732
733        session
734            .do_send(MessageResponse {
735                tag: 1,
736                result: Ok(Bytes::from_static(b"payload")),
737            })
738            .await?;
739
740        // Still responsive afterwards.
741        let (tx, rx) = oneshot::channel::<Bytes>();
742        session
743            .do_send(BinaryMessage::send(2, 3, Bytes::new(), tx))
744            .await?;
745
746        assert!(matches!(rx.await, Err(RecvError::Other(_))));
747        assert!(logs_contain(
748            "Could not send `MessageResponse` to remote node"
749        ));
750
751        Ok(())
752    }
753
754    /// `RemoteCreateActor` must surface a failed outbound send to the caller as an error rather
755    /// than hanging on the response, and log the failure.
756    #[tokio::test]
757    #[traced_test]
758    async fn test_remote_create_actor_reports_failure() -> Result<()> {
759        let session = start_failing_session();
760
761        let result = session
762            .send(command::RemoteCreateActor::<Echo>::new("remote-echo", None))
763            .await?
764            .await?;
765
766        assert!(result.is_err());
767        assert!(logs_contain(
768            "Could not send `NodeMessage::CreateActor` to remote node"
769        ));
770
771        Ok(())
772    }
773
774    /// `RemoteGetActor` must likewise surface a failed outbound send to the caller as an error,
775    /// and log the failure.
776    #[tokio::test]
777    #[traced_test]
778    async fn test_remote_get_actor_reports_failure() -> Result<()> {
779        let session = start_failing_session();
780
781        let result = session
782            .send(command::RemoteGetActor::<Echo>::new(ActorRef::Label(
783                "remote-echo".to_string(),
784            )))
785            .await?
786            .await?;
787
788        assert!(result.is_err());
789        assert!(logs_contain(
790            "Could not send `NodeMessage::GetActor` to remote node"
791        ));
792
793        Ok(())
794    }
795}