Skip to main content

guinea_core/actor/event_bus/
rpc.rs

1use crate::actor::event_bus::{Answering, Event, GlobalEventBus};
2use crate::actor::traits::Handler;
3use crate::actor::{Cx, invoke_on_ui};
4use crate::trace;
5use once_cell::sync::Lazy;
6use parking_lot::RwLock;
7use std::any::{Any, TypeId};
8use std::collections::HashMap;
9use std::future::Future;
10use std::time::Duration;
11use tokio::sync::oneshot;
12use uuid::Uuid;
13
14tokio::task_local! {
15    /// The chain of `RpcCall` types whose replies are currently pending
16    /// somewhere up this task's ancestry, oldest first. Set by
17    /// `AsyncBus::spawn_reply` around a handler body when it starts running,
18    /// so that any `AsyncBus::request` called from within that body - even
19    /// transitively, through further `spawn_reply`d handlers on other
20    /// actors - sees it. See `AsyncBus::request`'s cycle check below for why
21    /// this exists.
22    static RPC_CHAIN: Vec<TypeId>;
23}
24
25fn current_rpc_chain() -> Vec<TypeId> {
26    RPC_CHAIN.try_with(|c| c.clone()).unwrap_or_default()
27}
28
29#[derive(Clone)]
30pub struct RpcRequest<T> {
31    pub correlation_id: Uuid,
32    pub payload: T,
33    /// See `RPC_CHAIN`. Only meaningful to `AsyncBus::spawn_reply` - a
34    /// hand-written `Handler<RpcRequest<Req>>` that spawns its own reply
35    /// task without going through `spawn_reply` simply won't get cycle
36    /// detection for requests made from inside it.
37    pub chain: Vec<TypeId>,
38}
39
40impl<T: RpcCall> Event for RpcRequest<T> {
41    /// Without the address the reply goes to: a listener hears the request,
42    /// and a reply from it lands nowhere.
43    fn overheard(self) -> Self {
44        Self {
45            correlation_id: Uuid::nil(),
46            ..self
47        }
48    }
49}
50
51/// Why a request was not published: nothing could have answered it.
52struct Unanswered(String);
53
54#[derive(Clone)]
55pub struct RpcResponse<T> {
56    pub correlation_id: Uuid,
57    pub payload: T,
58}
59
60impl<T: Clone + Send + 'static> Event for RpcResponse<T> {}
61
62/// A request on the global bus, and what answers it. Written with
63/// `#[derive(guinea::Request)]` and `#[request(reply = Type)]`.
64///
65/// One subscriber answers it - a handler that returns `Response`, or
66/// [`EventBus::answer_fn`](super::EventBus::answer_fn). A handler that
67/// returns nothing only hears it; a `Handler<RpcRequest<Req>>` written by hand
68/// answers only if it says `const ANSWERS: bool = true`.
69pub trait RpcCall: Clone + Send + 'static {
70    type Response: Clone + Send + 'static;
71}
72
73impl<Req: RpcCall> RpcRequest<Req> {
74    pub fn reply(self, response: Req::Response) {
75        AsyncBus::reply(self.correlation_id, response);
76    }
77}
78
79/// What a request is answered with: a value now, or the value work in the
80/// background comes to.
81///
82/// A handler that answers later reads what it needs from its actor first,
83/// on the UI thread, and hands the rest to the background:
84///
85/// ```ignore
86/// #[handler]
87/// fn act(this: &mut Actions, Act(action): Act) -> Reply<Outcome> {
88///     let Some(transport) = this.transport.clone() else {
89///         return Reply::now(Outcome::NotConnected);
90///     };
91///     Reply::later(async move { transport.act(action).await })
92/// }
93/// ```
94///
95/// A handler that returns the reply itself answers now; `Reply` is for
96/// the one that may not.
97pub struct Reply<T>(Answer<T>);
98
99enum Answer<T> {
100    Now(T),
101    Later(std::pin::Pin<Box<dyn Future<Output = T> + Send>>),
102}
103
104impl<T> Reply<T> {
105    pub fn now(value: T) -> Self {
106        Self(Answer::Now(value))
107    }
108
109    /// Answers with what `work` comes to, run in the background. It is not
110    /// cut short when the actor goes: somebody is waiting for the value.
111    pub fn later(work: impl Future<Output = T> + Send + 'static) -> Self {
112        Self(Answer::Later(Box::pin(work)))
113    }
114}
115
116impl<T> From<T> for Reply<T> {
117    fn from(value: T) -> Self {
118        Self::now(value)
119    }
120}
121
122impl<T: Clone + Send + 'static> Reply<T> {
123    fn send(self, correlation_id: Uuid, chain: Vec<TypeId>) {
124        match self.0 {
125            Answer::Now(value) => AsyncBus::reply(correlation_id, value),
126            Answer::Later(work) => AsyncBus::spawn_reply(correlation_id, chain, work),
127        }
128    }
129}
130
131/// The type-level half of the request/response contract: `handle_rpc`
132/// returns the reply instead of taking a `RpcRequest<Req>` and being
133/// trusted to call `.reply(...)` somewhere inside its body. There is no way
134/// to compile a `RpcHandler` impl that forgets to reply, replies twice, or
135/// replies with the wrong type - the blanket `Handler<RpcRequest<Req>>`
136/// impl below is the only thing that replies, exactly once, with what
137/// `handle_rpc` returned: at once, or when its [`Reply::later`] is done.
138pub trait RpcHandler<Req: RpcCall>: 'static {
139    /// Where the handler was written; `#[handler]` fills it in.
140    const DECLARED: Option<crate::actor::shape::Declared> = None;
141
142    fn handle_rpc(&mut self, req: Req, cx: Cx<Self, Req>) -> Reply<Req::Response>
143    where
144        Self: Sized;
145}
146
147impl<A, Req> Handler<RpcRequest<Req>> for A
148where
149    A: RpcHandler<Req> + 'static,
150    Req: RpcCall,
151{
152    const DECLARED: Option<crate::actor::shape::Declared> = <A as RpcHandler<Req>>::DECLARED;
153
154    const ANSWERS: bool = true;
155
156    fn handle(&mut self, msg: RpcRequest<Req>, cx: Cx<Self, RpcRequest<Req>>) {
157        self.handle_rpc(msg.payload, cx.handling())
158            .send(msg.correlation_id, msg.chain);
159    }
160}
161
162static PENDING_REQUESTS: Lazy<RwLock<HashMap<Uuid, oneshot::Sender<Box<dyn Any + Send>>>>> =
163    Lazy::new(|| RwLock::new(HashMap::new()));
164
165pub struct AsyncBus;
166
167impl AsyncBus {
168    pub async fn request<Req>(payload: Req, timeout: Duration) -> anyhow::Result<Req::Response>
169    where
170        Req: RpcCall,
171    {
172        let req_type = TypeId::of::<Req>();
173        let chain = current_rpc_chain();
174
175        // A cycle here means some handler up this call chain is - directly
176        // or transitively, via other actors - awaiting a reply that can
177        // only ever be produced after *this* request resolves. That can
178        // never happen; every hop is stuck waiting on the next one forever.
179        // Letting it run to `timeout` would just make the failure slow and
180        // its cause invisible (a generic "RPC request timed out" many
181        // layers away from the actual cycle) - panicking immediately, with
182        // the chain that proves it, turns a silent multi-actor wedge into
183        // an obvious bug report at the exact call site that closed the loop.
184        if chain.contains(&req_type) {
185            panic!(
186                "AsyncBus: RPC cycle detected requesting {} - it (or a request that led back \
187                 to it) is already awaiting its own reply {} level(s) up this call chain. This \
188                 can never resolve: each hop is waiting on the next, all the way back to itself.",
189                std::any::type_name::<Req>(),
190                chain.len(),
191            );
192        }
193
194        let mut next_chain = chain;
195        next_chain.push(req_type);
196
197        let correlation_id = Uuid::new_v4();
198        let envelope = RpcRequest {
199            correlation_id,
200            payload,
201            chain: next_chain,
202        };
203
204        let (tx, rx) = oneshot::channel();
205        PENDING_REQUESTS.write().insert(correlation_id, tx);
206
207        // Must go through the UI-thread dispatcher, not
208        // `GlobalEventBus::instance().publish(...)` directly - the bus is
209        // thread_local, and `request` is typically awaited from a
210        // `spawn_bg` future running on a background tokio thread. Publishing
211        // there would hit an empty, subscriber-less bus instance and always
212        // time out.
213        let cause = trace::current();
214        invoke_on_ui(move || {
215            let _resumed = trace::resume(cause);
216            let bus = GlobalEventBus::instance();
217
218            let unanswered = match bus.answering::<RpcRequest<Req>>() {
219                Answering::Awake => return bus.publish(envelope),
220                Answering::Nobody => format!("nobody answers {}", std::any::type_name::<Req>()),
221                Answering::Asleep(answerer) => format!(
222                    "{answerer}, which answers {}, is asleep",
223                    std::any::type_name::<Req>()
224                ),
225            };
226
227            if let Some(tx) = PENDING_REQUESTS.write().remove(&correlation_id) {
228                let _ = tx.send(Box::new(Unanswered(unanswered)));
229            }
230        });
231
232        match tokio::time::timeout(timeout, rx).await {
233            Ok(Ok(any_res)) => match any_res.downcast::<RpcResponse<Req::Response>>() {
234                Ok(res) => Ok(res.payload),
235                Err(other) => match other.downcast::<Unanswered>() {
236                    Ok(unanswered) => Err(anyhow::anyhow!(unanswered.0)),
237                    Err(_) => Err(anyhow::anyhow!("Type mismatch in async response")),
238                },
239            },
240            Ok(Err(_)) => Err(anyhow::anyhow!("Response channel closed")),
241            Err(_) => {
242                PENDING_REQUESTS.write().remove(&correlation_id);
243                Err(anyhow::anyhow!("RPC request timed out"))
244            }
245        }
246    }
247
248    pub fn reply<Res>(correlation_id: Uuid, payload: Res)
249    where
250        Res: Clone + Send + 'static,
251    {
252        if correlation_id.is_nil() {
253            tracing::warn!(
254                reply = std::any::type_name::<Res>(),
255                "a reply from something that only hears the request went nowhere"
256            );
257            return;
258        }
259
260        let envelope = RpcResponse {
261            correlation_id,
262            payload,
263        };
264
265        if let Some(tx) = PENDING_REQUESTS.write().remove(&correlation_id) {
266            let _ = tx.send(Box::new(envelope.clone()));
267        }
268
269        // Same reasoning as in `request`: route through the UI-thread
270        // dispatcher rather than the calling thread's own bus instance.
271        GlobalEventBus::publish(envelope);
272    }
273
274    /// Runs `fut` to completion in a spawned task and replies to
275    /// `correlation_id` with whatever it produces, with `fut` running
276    /// inside `chain`'s `RPC_CHAIN` scope - so any `AsyncBus::request` made
277    /// from within `fut` (even transitively, on some other actor `fut`
278    /// itself makes a request to) is correctly attributed to this call
279    /// chain for cycle detection.
280    ///
281    /// This is the async-RPC-handler counterpart to `RpcHandler`'s blanket
282    /// sync impl: the only thing that calls `reply` here is this function,
283    /// after `fut` resolves, so it still holds exactly once. It exists as a
284    /// standalone entry point (rather than folded into a trait like
285    /// `RpcHandler`) because `#[handler]`'s generated async-RPC glue and any
286    /// hand-written `Handler<RpcRequest<Req>>` both need to call it the same
287    /// way, and a trait can't express "produces a value after an await" any
288    /// more precisely than `Future<Output = Req::Response>` already does.
289    /// Unlike [`Cx::spawn_bg`], this is not cut short when the actor
290    /// that answers is disposed: somebody is waiting for a value, and the
291    /// only thing a dropped future would leave them is the request's
292    /// timeout. A handler that would rather stop early has the token through
293    /// its `AsyncContext` - but it still owes an answer.
294    ///
295    /// [`Cx::spawn_bg`]: crate::actor::Cx::spawn_bg
296    pub fn spawn_reply<Res, Fut>(correlation_id: Uuid, chain: Vec<TypeId>, fut: Fut)
297    where
298        Res: Clone + Send + 'static,
299        Fut: Future<Output = Res> + Send + 'static,
300    {
301        #[cfg(feature = "test-utils")]
302        let counted = crate::actor::event_bus::Counted::new();
303
304        crate::executor::spawn(RPC_CHAIN.scope(chain, async move {
305            #[cfg(feature = "test-utils")]
306            let _counted = counted;
307
308            let response = fut.await;
309            AsyncBus::reply(correlation_id, response);
310        }));
311    }
312}
313
314#[cfg(all(test, feature = "test-utils"))]
315mod tests {
316    use super::*;
317    use crate::actor::event_bus::EventBus;
318    use std::rc::Rc;
319    use std::sync::Mutex;
320    use std::sync::mpsc as std_mpsc;
321    use std::time::Duration as StdDuration;
322
323    /// Every test here drives the bus by hand via `EventBus::process_queue()`,
324    /// and under `test-utils` `invoke_on_ui` funnels all work into one
325    /// process-wide `TEST_TASK_QUEUE`. A drain is therefore not scoped to the
326    /// test that issued it: run two of these concurrently (cargo's default)
327    /// and one test consumes the other's queued `RpcRequest`/`RpcResponse`
328    /// deliveries, so the rightful owner never sees its reply and dies on
329    /// `RPC request timed out`. `PENDING_REQUESTS` is global for the same
330    /// reason. Serializing the module with a plain `Mutex` held for each
331    /// test's duration is enough; `into_inner` on poisoning keeps a single
332    /// failing test from cascading into bogus failures everywhere else.
333    static TEST_LOCK: Mutex<()> = Mutex::new(());
334
335    #[tokio::test]
336    async fn request_reply_round_trip_same_thread() {
337        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
338        #[derive(Clone, Debug, guinea_macros::Request)]
339        #[request(reply = Pong)]
340        struct Ping;
341        #[derive(Clone, Debug)]
342        struct Pong;
343
344        let _sub = GlobalEventBus::answer_fn(|_: Ping| Pong);
345
346        let handle = tokio::spawn(AsyncBus::request::<Ping>(Ping, StdDuration::from_secs(1)));
347        // Let the spawned task run up to its `rx.await` - `request` queues
348        // the publish synchronously before that point, so one yield is
349        // enough for the queued task to exist by the time we drain it.
350        tokio::task::yield_now().await;
351        EventBus::process_queue();
352        // `reply()` also queues an `RpcResponse` broadcast - drain it too so
353        // it doesn't linger in the global queue for a later test.
354        EventBus::process_queue();
355
356        assert!(handle.await.unwrap().is_ok());
357    }
358
359    #[tokio::test]
360    async fn rpc_handler_replies_exactly_once_via_the_blanket_impl() {
361        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
362        use crate::actor::{Addr, UiThreadToken};
363
364        #[derive(Clone, Debug, guinea_macros::Request)]
365        #[request(reply = Echoed)]
366        struct Echo(u32);
367        #[derive(Clone, Debug)]
368        struct Echoed(u32);
369
370        struct EchoActor;
371        impl RpcHandler<Echo> for EchoActor {
372            fn handle_rpc(&mut self, Echo(n): Echo, _cx: Cx<Self, Echo>) -> Reply<Echoed> {
373                Reply::now(Echoed(n * 2))
374            }
375        }
376
377        let addr = Addr::new(EchoActor, UiThreadToken::dangerously_create_token_unchecked());
378        let _sub = GlobalEventBus::instance().subscribe::<EchoActor, RpcRequest<Echo>>(addr);
379
380        let handle = tokio::spawn(AsyncBus::request::<Echo>(Echo(21), StdDuration::from_secs(1)));
381        tokio::task::yield_now().await;
382        // One drain delivers `RpcRequest<Echo>` to the actor (which replies
383        // synchronously inside `handle`), a second drains the `RpcResponse`
384        // broadcast that reply also queues.
385        EventBus::process_queue();
386        EventBus::process_queue();
387
388        let response = handle.await.unwrap().expect("rpc handler should have replied");
389        assert_eq!(response.0, 42);
390    }
391
392    #[tokio::test]
393    async fn a_request_nobody_answers_fails_at_once() {
394        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
395        #[derive(Clone, Debug, guinea_macros::Request)]
396        #[request(reply = NeverReplied)]
397        struct Unasked;
398        #[derive(Clone, Debug)]
399        struct NeverReplied;
400
401        let _hears = GlobalEventBus::instance().subscribe_fn(|_: RpcRequest<Unasked>| {});
402
403        let handle = tokio::spawn(AsyncBus::request::<Unasked>(Unasked, StdDuration::from_secs(60)));
404        tokio::task::yield_now().await;
405        EventBus::process_queue();
406
407        let error = tokio::time::timeout(StdDuration::from_secs(5), handle)
408            .await
409            .expect("it waited for a reply nothing could give")
410            .unwrap()
411            .expect_err("nothing answers it");
412        assert!(error.to_string().contains("nobody answers"), "{error}");
413    }
414
415    #[test]
416    #[should_panic(expected = "a request has exactly one answerer")]
417    fn a_second_answerer_is_refused_when_it_subscribes() {
418        #[derive(Clone, Debug, guinea_macros::Request)]
419        #[request(reply = Answer)]
420        struct Question;
421        #[derive(Clone, Debug)]
422        struct Answer;
423
424        let bus = Rc::new(EventBus::new());
425        let _first = bus.answer_fn(|_: Question| Answer);
426        let _second = bus.answer_fn(|_: Question| Answer);
427    }
428
429    #[test]
430    #[should_panic(expected = "a request has exactly one answerer")]
431    fn two_actors_returning_the_reply_cannot_both_answer() {
432        use crate::actor::{Addr, UiThreadToken};
433
434        #[derive(Clone, Debug, guinea_macros::Request)]
435        #[request(reply = Answer)]
436        struct Question;
437        #[derive(Clone, Debug)]
438        struct Answer;
439
440        struct Service;
441        struct Monitor;
442        impl RpcHandler<Question> for Service {
443            fn handle_rpc(&mut self, _: Question, _cx: Cx<Self, Question>) -> Reply<Answer> {
444                Reply::now(Answer)
445            }
446        }
447        impl RpcHandler<Question> for Monitor {
448            fn handle_rpc(&mut self, _: Question, _cx: Cx<Self, Question>) -> Reply<Answer> {
449                Reply::now(Answer)
450            }
451        }
452
453        let token = UiThreadToken::dangerously_create_token_unchecked();
454        let bus = Rc::new(EventBus::new());
455        let _service =
456            bus.subscribe::<Service, RpcRequest<Question>>(Addr::new(Service, token.clone()));
457        let _monitor =
458            bus.subscribe::<Monitor, RpcRequest<Question>>(Addr::new(Monitor, token));
459    }
460
461    #[test]
462    fn the_answerer_s_place_is_free_again_once_it_goes() {
463        #[derive(Clone, Debug, guinea_macros::Request)]
464        #[request(reply = Answer)]
465        struct Question;
466        #[derive(Clone, Debug)]
467        struct Answer;
468
469        let bus = Rc::new(EventBus::new());
470        drop(bus.answer_fn(|_: Question| Answer));
471        let _next = bus.answer_fn(|_: Question| Answer);
472    }
473
474    #[tokio::test]
475    async fn a_listener_hears_the_request_and_its_reply_goes_nowhere() {
476        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
477        use std::sync::Arc;
478        use std::sync::atomic::{AtomicBool, Ordering};
479
480        #[derive(Clone, Debug, guinea_macros::Request)]
481        #[request(reply = Count)]
482        struct HowMany;
483        #[derive(Clone, Debug, PartialEq)]
484        struct Count(u32);
485
486        let heard = Arc::new(AtomicBool::new(false));
487        let hearing = heard.clone();
488        let _listener = GlobalEventBus::subscribe_fn(move |request: RpcRequest<HowMany>| {
489            hearing.store(true, Ordering::SeqCst);
490            request.reply(Count(0));
491        });
492        let _answerer = GlobalEventBus::answer_fn(|_: HowMany| Count(7));
493
494        let handle = tokio::spawn(AsyncBus::request::<HowMany>(HowMany, StdDuration::from_secs(1)));
495        tokio::task::yield_now().await;
496        EventBus::process_queue();
497        EventBus::process_queue();
498
499        assert_eq!(handle.await.unwrap().unwrap(), Count(7));
500        assert!(heard.load(Ordering::SeqCst), "the listener was not told");
501    }
502
503    #[tokio::test]
504    async fn a_request_whose_answerer_sleeps_fails_at_once() {
505        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
506        use crate::actor::{Addr, UiThreadToken};
507        use crate::scope::ScopeTree;
508
509        #[derive(Clone, Debug, guinea_macros::Request)]
510        #[request(reply = Done)]
511        struct Work;
512        #[derive(Clone, Debug)]
513        struct Done;
514
515        struct Worker;
516        impl RpcHandler<Work> for Worker {
517            fn handle_rpc(&mut self, _: Work, _cx: Cx<Self, Work>) -> Reply<Done> {
518                Reply::now(Done)
519            }
520        }
521
522        let scope = ScopeTree::new();
523        let addr = Addr::new(Worker, UiThreadToken::dangerously_create_token_unchecked());
524        addr.live_in(scope.scope(), Some(&Rc::new(EventBus::new())));
525        let _sub = GlobalEventBus::instance().subscribe::<Worker, RpcRequest<Work>>(addr);
526        scope.sleep();
527
528        let handle = tokio::spawn(AsyncBus::request::<Work>(Work, StdDuration::from_secs(60)));
529        tokio::task::yield_now().await;
530        EventBus::process_queue();
531
532        let error = tokio::time::timeout(StdDuration::from_secs(5), handle)
533            .await
534            .expect("it waited for an answerer that sleeps")
535            .unwrap()
536            .expect_err("its answerer sleeps");
537        assert!(error.to_string().contains("is asleep"), "{error}");
538    }
539
540    /// Regression test for the bug fixed alongside this: `request`/`reply`
541    /// used to call `GlobalEventBus::instance().publish(...)` directly,
542    /// which resolves the thread_local bus of whichever OS thread happens
543    /// to run that code. A requester awaiting from one OS thread and a
544    /// subscriber registered on another (the normal shape - subscriber on
545    /// the UI thread, requester on a `spawn_bg` worker) would silently miss
546    /// each other and every request would time out. Going through
547    /// `GlobalEventBus::publish`, which routes through the shared
548    /// dispatcher queue, fixes that regardless of which thread each side
549    /// runs on.
550    #[test]
551    fn request_resolves_when_subscriber_is_on_a_different_os_thread() {
552        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
553        #[derive(Clone, Debug, guinea_macros::Request)]
554        #[request(reply = Pong)]
555        struct Ping;
556        #[derive(Clone, Debug)]
557        struct Pong;
558
559        let (ready_tx, ready_rx) = std_mpsc::channel::<()>();
560        let (stop_tx, stop_rx) = std_mpsc::channel::<()>();
561
562        // Stands in for the real UI thread: owns its own thread_local
563        // `EventBus` instance, subscribes there, and pumps the shared
564        // dispatcher queue on an interval - the same shape a real
565        // `UiDispatcher` runs in production.
566        let ui_thread = std::thread::spawn(move || {
567            let _sub = GlobalEventBus::answer_fn(|_: Ping| Pong);
568            ready_tx.send(()).unwrap();
569            while stop_rx.try_recv().is_err() {
570                EventBus::process_queue();
571                std::thread::sleep(StdDuration::from_millis(5));
572            }
573        });
574
575        ready_rx.recv().unwrap();
576
577        // Deliberately a different OS thread than `ui_thread` above.
578        let rt = tokio::runtime::Builder::new_current_thread()
579            .enable_time()
580            .build()
581            .unwrap();
582        let result =
583            rt.block_on(AsyncBus::request::<Ping>(Ping, StdDuration::from_secs(2)));
584
585        stop_tx.send(()).unwrap();
586        ui_thread.join().unwrap();
587
588        assert!(
589            result.is_ok(),
590            "expected a reply delivered from another OS thread, got {result:?}"
591        );
592    }
593
594    /// Exercises the `#[handler]` macro's RPC heuristic end to end for both
595    /// its sync and async branches (see guinea-macros/src/handler.rs) - not
596    /// just the hand-written `RpcHandler` impl the other tests use.
597    #[tokio::test]
598    async fn handler_macro_rpc_heuristic_sync_and_async() {
599        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
600        use crate::actor::{Addr, AsyncContext, UiThreadToken};
601        use guinea_macros::handler;
602
603        #[derive(Clone, Debug, guinea_macros::Request)]
604        #[request(reply = Doubled)]
605        struct Double(u32);
606        #[derive(Clone, Debug)]
607        struct Doubled(u32);
608
609        #[derive(Clone, Debug, guinea_macros::Request)]
610        #[request(reply = Sum)]
611        struct DelayedAdd(u32, u32);
612        #[derive(Clone, Debug)]
613        struct Sum(u32);
614
615        struct MathActor;
616
617        // Sync branch: a plain return type, no `.reply()` anywhere in sight.
618        #[handler]
619        fn double(this: &mut MathActor, Double(n): Double) -> Doubled {
620            let _ = this;
621            Doubled(n * 2)
622        }
623
624        // Async branch: `.await`s before producing the value the macro
625        // replies with.
626        #[handler]
627        async fn delayed_add(_ctx: AsyncContext<MathActor>, req: DelayedAdd) -> Sum {
628            tokio::time::sleep(StdDuration::from_millis(1)).await;
629            Sum(req.0 + req.1)
630        }
631
632        let addr =
633            Addr::new(MathActor, UiThreadToken::dangerously_create_token_unchecked());
634        let _sub_double =
635            GlobalEventBus::instance().subscribe::<MathActor, RpcRequest<Double>>(addr.clone());
636        let _sub_add = GlobalEventBus::instance()
637            .subscribe::<MathActor, RpcRequest<DelayedAdd>>(addr.clone());
638
639        let double_handle =
640            tokio::spawn(AsyncBus::request::<Double>(Double(21), StdDuration::from_secs(1)));
641        tokio::task::yield_now().await;
642        EventBus::process_queue(); // deliver RpcRequest<Double> -> reply queued
643        EventBus::process_queue(); // drain the RpcResponse broadcast
644        assert_eq!(double_handle.await.unwrap().unwrap().0, 42);
645
646        let add_handle = tokio::spawn(AsyncBus::request::<DelayedAdd>(
647            DelayedAdd(2, 3),
648            StdDuration::from_secs(1),
649        ));
650        tokio::task::yield_now().await;
651        EventBus::process_queue(); // deliver RpcRequest<DelayedAdd> -> spawns the async body
652        // The async body needs actual wall-clock time (`tokio::time::sleep`)
653        // before it replies, so poll the queue a few times instead of
654        // draining once.
655        for _ in 0..20 {
656            tokio::time::sleep(StdDuration::from_millis(5)).await;
657            EventBus::process_queue();
658        }
659        assert_eq!(add_handle.await.unwrap().unwrap().0, 5);
660    }
661
662    /// The scenario the cycle check exists for: actor A's async RPC handler
663    /// calls out to actor B, and actor B's handler calls back into A for
664    /// the *same request type* A is still waiting on - a genuine cross-actor
665    /// deadlock (A can't reply until B replies, B can't reply until A's
666    /// original request resolves). Without the `RPC_CHAIN` check in
667    /// `AsyncBus::request`, this would just sit until both requests' 5s
668    /// timeouts expired. With it, `AsyncBus::request::<ReqA>` inside B's
669    /// handler must panic immediately, because `spawn_reply` carried the
670    /// in-flight chain `[ReqA, ReqB]` across the `tokio::spawn` boundary
671    /// into B's handler body.
672    ///
673    /// Panics inside a `tokio::spawn`ed task don't propagate to this test
674    /// function's own thread - tokio catches them into a `JoinError` on a
675    /// `JoinHandle` nobody here holds. A process-wide panic hook is the only
676    /// way to observe that the panic happened at all, which makes this test
677    /// unsafe to run concurrently with anything else that installs its own
678    /// hook - run this module with `--test-threads=1` (already required by
679    /// the rest of it, which shares `GlobalEventBus`/`PENDING_REQUESTS`).
680    #[tokio::test]
681    async fn cross_actor_rpc_cycle_is_detected_immediately() {
682        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
683        use crate::actor::{Addr, AsyncContext, UiThreadToken};
684        use guinea_macros::handler;
685        use std::panic;
686        use std::sync::{Arc, Mutex};
687
688        #[derive(Clone, Debug, guinea_macros::Request)]
689        #[request(reply = RespA)]
690        struct ReqA(u32);
691        #[derive(Clone, Debug)]
692        struct RespA(u32);
693
694        #[derive(Clone, Debug, guinea_macros::Request)]
695        #[request(reply = RespB)]
696        struct ReqB(u32);
697        #[derive(Clone, Debug)]
698        struct RespB(u32);
699
700        struct ActorA;
701        struct ActorB;
702
703        #[handler]
704        async fn handle_req_a(_ctx: AsyncContext<ActorA>, req: ReqA) -> RespA {
705            let RespB(n) = AsyncBus::request::<ReqB>(ReqB(req.0), StdDuration::from_secs(5))
706                .await
707                .expect("should never resolve normally - the cycle panics first");
708            RespA(n)
709        }
710
711        #[handler]
712        async fn handle_req_b(_ctx: AsyncContext<ActorB>, req: ReqB) -> RespB {
713            // Closes the loop: same `ReqA` type is already on the chain.
714            let RespA(n) = AsyncBus::request::<ReqA>(ReqA(req.0), StdDuration::from_secs(5))
715                .await
716                .expect("should never resolve normally - the cycle panics first");
717            RespB(n)
718        }
719
720        // Every panic, not the first. The hook is process-wide and the test
721        // binary is one process: another test panicking on purpose - and one
722        // does, to poison a lock - would otherwise take this slot and this
723        // test would fail reporting someone else's message.
724        let panics: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
725        let hook_slot = panics.clone();
726        let prev_hook = panic::take_hook();
727        panic::set_hook(Box::new(move |info| {
728            hook_slot.lock().unwrap().push(info.to_string());
729        }));
730
731        let seen_the_cycle = || {
732            panics
733                .lock()
734                .unwrap()
735                .iter()
736                .any(|message| message.contains("RPC cycle detected"))
737        };
738
739        let addr_a = Addr::new(ActorA, UiThreadToken::dangerously_create_token_unchecked());
740        let addr_b = Addr::new(ActorB, UiThreadToken::dangerously_create_token_unchecked());
741        let _sub_a = GlobalEventBus::instance().subscribe::<ActorA, RpcRequest<ReqA>>(addr_a);
742        let _sub_b = GlobalEventBus::instance().subscribe::<ActorB, RpcRequest<ReqB>>(addr_b);
743
744        let handle = tokio::spawn(AsyncBus::request::<ReqA>(ReqA(1), StdDuration::from_secs(5)));
745
746        // Bounded, not open-ended - a regression where the cycle silently
747        // stops being detected must fail this test quickly, not hang it.
748        // Bounded by wall clock rather than by a count of scheduler turns:
749        // 200 turns is a budget that shrinks with machine load, and this test
750        // shares a process (and the task queue) with every other one.
751        let deadline = std::time::Instant::now() + StdDuration::from_secs(5);
752        while std::time::Instant::now() < deadline {
753            tokio::task::yield_now().await;
754            EventBus::process_queue();
755            if seen_the_cycle() {
756                break;
757            }
758            tokio::time::sleep(StdDuration::from_millis(1)).await;
759        }
760
761        panic::set_hook(prev_hook);
762        handle.abort(); // the top-level request never gets a reply; don't wait out its timeout
763
764        assert!(
765            seen_the_cycle(),
766            "expected AsyncBus's cycle check to panic inside actor B's handler; \
767             the panics seen were: {:?}",
768            panics.lock().unwrap()
769        );
770    }
771
772    /// Stress test guarding against a self-deadlock: fires many concurrent
773    /// `AsyncBus::request` calls, from several OS threads at once, against a
774    /// single `RpcHandler` actor pumped from yet another OS thread. Nothing
775    /// here holds a lock across an `.await` (`PENDING_REQUESTS.write()` is
776    /// always a temporary, dropped before either `request` or `reply`
777    /// yields), so this should always complete - but that invariant is easy
778    /// to break by accident in a future edit, and a broken invariant here
779    /// means the whole app wedges the next time two RPCs race. If it ever
780    /// does deadlock, the test must not just hang forever (which would burn
781    /// CI time until an external timeout kills the runner with no useful
782    /// message) - the watchdog thread below turns that hang into a fast,
783    /// loud, obviously-a-deadlock failure instead.
784    #[test]
785    fn many_concurrent_requests_do_not_deadlock() {
786        let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
787        use crate::actor::{Addr, UiThreadToken};
788        use std::sync::Arc;
789        use std::sync::atomic::{AtomicBool, Ordering};
790
791        #[derive(Clone, Debug, guinea_macros::Request)]
792        #[request(reply = Sum)]
793        struct Add(u32, u32);
794        #[derive(Clone, Debug)]
795        struct Sum(u32);
796
797        struct AddActor;
798        impl RpcHandler<Add> for AddActor {
799            fn handle_rpc(&mut self, Add(a, b): Add, _cx: Cx<Self, Add>) -> Reply<Sum> {
800                Reply::now(Sum(a + b))
801            }
802        }
803
804        let done = Arc::new(AtomicBool::new(false));
805        let watchdog_done = done.clone();
806        let watchdog = std::thread::spawn(move || {
807            for _ in 0..100 {
808                if watchdog_done.load(Ordering::SeqCst) {
809                    return;
810                }
811                std::thread::sleep(StdDuration::from_millis(50));
812            }
813            eprintln!(
814                "many_concurrent_requests_do_not_deadlock: deadline exceeded \
815                 without completing - treating this as a deadlock and \
816                 aborting instead of hanging"
817            );
818            std::process::abort();
819        });
820
821        let (ready_tx, ready_rx) = std_mpsc::channel::<()>();
822        let (stop_tx, stop_rx) = std_mpsc::channel::<()>();
823
824        // Stands in for the UI thread, same shape as the cross-thread test
825        // above: owns the actor, subscribes it, and pumps the dispatcher
826        // queue on an interval.
827        let ui_thread = std::thread::spawn(move || {
828            let addr =
829                Addr::new(AddActor, UiThreadToken::dangerously_create_token_unchecked());
830            let _sub = GlobalEventBus::instance().subscribe::<AddActor, RpcRequest<Add>>(addr);
831            ready_tx.send(()).unwrap();
832            while stop_rx.try_recv().is_err() {
833                EventBus::process_queue();
834                std::thread::sleep(StdDuration::from_millis(2));
835            }
836        });
837
838        ready_rx.recv().unwrap();
839
840        const REQUESTER_THREADS: u32 = 8;
841        const REQUESTS_PER_THREAD: u32 = 25;
842
843        let requesters: Vec<_> = (0..REQUESTER_THREADS)
844            .map(|t| {
845                std::thread::spawn(move || {
846                    let rt = tokio::runtime::Builder::new_current_thread()
847                        .enable_time()
848                        .build()
849                        .unwrap();
850                    rt.block_on(async {
851                        for i in 0..REQUESTS_PER_THREAD {
852                            let result =
853                                AsyncBus::request::<Add>(Add(t, i), StdDuration::from_secs(5))
854                                    .await
855                                    .unwrap_or_else(|e| panic!("request {t}/{i} failed: {e}"));
856                            assert_eq!(result.0, t + i);
857                        }
858                    });
859                })
860            })
861            .collect();
862
863        for r in requesters {
864            r.join().unwrap();
865        }
866
867        stop_tx.send(()).unwrap();
868        ui_thread.join().unwrap();
869
870        // Only reached if every request/reply round trip above actually
871        // completed - tell the watchdog it can stand down.
872        done.store(true, Ordering::SeqCst);
873        watchdog.join().unwrap();
874    }
875}
876
877#[deprecated(
878    since = "0.18.6",
879    note = "write the reply on the request: `#[derive(guinea::Request)] #[request(reply = Res)]`"
880)]
881#[macro_export]
882macro_rules! rpc_bind {
883    ($( $req:ident => $res:ident );* $(;)?) => {
884        $(
885            impl $crate::actor::event_bus::rpc::RpcCall for $req {
886                type Response = $res;
887            }
888        )*
889    };
890}