Skip to main content

guinea_core/actor/
ctx.rs

1use crate::actor::addr::{Addr, registered};
2use crate::actor::cancel::Cancel;
3use crate::actor::event_bus::{EventBus, GlobalEventBus};
4use crate::actor::event_bus::subscribe::Event;
5use crate::actor::traits::Handler;
6use crate::actor::{AllowedSignal, ManagedActor, invoke_on_ui, short_type_name};
7use crate::trace::{self, Cause, Point};
8use futures_core::Stream;
9use std::marker::PhantomData;
10use std::time::Instant;
11
12/// What a handler may do besides change its actor: send on, publish, start
13/// background work.
14///
15/// Typed by the message being handled, not holding it: the message is the
16/// handler's own argument, and `M` is what `actor!`'s flow checks - a handler
17/// of `Query` declared `Query => { bg Found }` may spawn work that answers
18/// `Found`, and nothing else.
19pub struct Cx<A: 'static, M = ()> {
20    pub(super) addr: Addr<A>,
21    handling: PhantomData<fn() -> M>,
22}
23
24impl<A: 'static, M> Clone for Cx<A, M> {
25    fn clone(&self) -> Self {
26        Self::new(self.addr.clone())
27    }
28}
29
30/// One background task as the trace sees it: whose it is, what it owes them,
31/// and when it started.
32///
33/// The actor's id is what ties the task to a segment: a task belongs where
34/// its actor does, and the snapshot already says where that is.
35#[derive(Clone, Copy)]
36struct Task {
37    actor: &'static str,
38    actor_id: u64,
39    output: &'static str,
40    started: Instant,
41}
42
43impl Task {
44    fn new<A: 'static>(actor_id: usize, output: &'static str) -> Self {
45        Self {
46            actor: short_type_name::<A>(),
47            actor_id: actor_id as u64,
48            output,
49            started: Instant::now(),
50        }
51    }
52
53    fn spawn(&self) -> Point {
54        Point::Spawn {
55            actor: self.actor,
56            actor_id: self.actor_id,
57            output: self.output,
58        }
59    }
60
61    fn settled(&self) -> Point {
62        Point::Settled {
63            actor: self.actor,
64            actor_id: self.actor_id,
65            output: self.output,
66            took_us: self.took_us(),
67        }
68    }
69
70    fn cancelled(&self) -> Point {
71        Point::Cancelled {
72            actor: self.actor,
73            actor_id: self.actor_id,
74            output: self.output,
75            took_us: self.took_us(),
76        }
77    }
78
79    fn took_us(&self) -> u64 {
80        self.started.elapsed().as_micros() as u64
81    }
82
83    /// Records the end of the task under its spawn, from whichever thread
84    /// polled it last: [`crate::observability::mark_anywhere`] carries it to
85    /// the thread being watched.
86    fn ended(self, spawned: Cause, point: Point) {
87        let _resumed = trace::resume(Some(spawned));
88
89        crate::observability::mark_anywhere(move || point);
90    }
91}
92
93/// One source as the trace sees it: whose it is, what its items come as, and
94/// when it opened.
95#[derive(Clone, Copy)]
96struct Feed {
97    actor: &'static str,
98    actor_id: u64,
99    output: &'static str,
100    started: Instant,
101}
102
103impl Feed {
104    fn new<A: 'static>(actor_id: usize, output: &'static str) -> Self {
105        Self {
106            actor: short_type_name::<A>(),
107            actor_id: actor_id as u64,
108            output,
109            started: Instant::now(),
110        }
111    }
112
113    fn opened(&self) -> Point {
114        Point::Source {
115            actor: self.actor,
116            actor_id: self.actor_id,
117            output: self.output,
118        }
119    }
120
121    fn arrived(&self, source: Cause) -> Point {
122        Point::Arrived {
123            actor: self.actor,
124            actor_id: self.actor_id,
125            output: self.output,
126            source: source.get(),
127        }
128    }
129
130    fn pulled(&self, source: Cause) -> Point {
131        Point::Pull {
132            actor: self.actor,
133            actor_id: self.actor_id,
134            output: self.output,
135            source: source.get(),
136        }
137    }
138
139    /// Opens a pull of the source: what the stream does until it has its next
140    /// item, runs dry or is dropped is recorded under it.
141    fn pull(self, opened: Cause) -> Pulling {
142        let id = trace::reserve();
143        crate::observability::begin_anywhere(id, None, move || self.pulled(opened));
144
145        Pulling {
146            id,
147            started: Instant::now(),
148        }
149    }
150
151    fn closed(self, opened: Cause, gone: bool) {
152        let point = Point::Closed {
153            actor: self.actor,
154            actor_id: self.actor_id,
155            output: self.output,
156            took_us: self.started.elapsed().as_micros() as u64,
157            gone,
158        };
159        let _resumed = trace::resume(Some(opened));
160
161        crate::observability::mark_anywhere(move || point);
162    }
163}
164
165/// A pull that is open, closed when it is dropped: when the item comes, or
166/// when the source is dropped waiting for it.
167struct Pulling {
168    id: Cause,
169    started: Instant,
170}
171
172impl Drop for Pulling {
173    fn drop(&mut self) {
174        crate::observability::end_anywhere(self.id, self.started.elapsed());
175    }
176}
177
178impl<A: 'static, M> Cx<A, M> {
179    pub(crate) fn new(addr: Addr<A>) -> Self {
180        Self {
181            addr,
182            handling: PhantomData,
183        }
184    }
185
186    /// The same actor, as the handler of another message.
187    pub(crate) fn handling<N>(self) -> Cx<A, N> {
188        Cx::new(self.addr)
189    }
190
191    pub fn addr(&self) -> Addr<A> {
192        self.addr.clone()
193    }
194
195    /// The same actor, handling nothing in particular.
196    pub fn detach(&self) -> Cx<A, ()> {
197        Cx::new(self.addr.clone())
198    }
199
200    /// Sends to this actor's own queue; drained by the same `process_queue`.
201    pub fn send<Out>(&self, msg: Out)
202    where
203        Out: 'static,
204        A: Handler<Out> + ManagedActor,
205        A::Flow: crate::actor::flow::Allows<M, Out>,
206    {
207        self.addr.send(msg);
208    }
209
210    pub fn publish<E>(&self, msg: E)
211    where
212        A: ManagedActor,
213        E: Event,
214        A::Signals: AllowedSignal<E>,
215    {
216        GlobalEventBus::instance().publish(msg);
217    }
218
219    pub fn publish_local<E>(&self, bus: &EventBus, msg: E)
220    where
221        A: ManagedActor,
222        E: Event,
223        A::Signals: AllowedSignal<E>,
224    {
225        bus.publish(msg);
226    }
227
228    /// The actor's cancellation token, for work that has to end itself rather
229    /// than be dropped at an await - closing a file, telling the other end.
230    ///
231    /// [`Cx::spawn_bg`] already guards what it spawns with it.
232    pub fn cancellation(&self) -> Cancel {
233        self.addr.cancellation()
234    }
235
236    /// Runs `fut` off the UI thread and sends what it returns back to this
237    /// actor.
238    ///
239    /// The task lives as long as the actor: once the actor is disposed, `fut`
240    /// is dropped where it last awaited and nothing is sent.
241    pub fn spawn_bg<Out, Fut>(&self, fut: Fut)
242    where
243        Out: Send + 'static,
244        A: Handler<Out> + ManagedActor,
245        A::Flow: crate::actor::flow::Allows<M, Out>,
246        Fut: Future<Output = Out> + 'static + Send,
247    {
248        self.bg(fut, false);
249    }
250
251    /// [`Cx::spawn_bg`], with `listens` for work that was handed the
252    /// token and winds itself down: it is left to finish instead of being
253    /// dropped at its next await. Either way nothing is sent to an actor
254    /// that is gone.
255    fn bg<Out, Fut>(&self, fut: Fut, listens: bool)
256    where
257        Out: Send + 'static,
258        A: Handler<Out> + ManagedActor,
259        A::Flow: crate::actor::flow::Allows<M, Out>,
260        Fut: Future<Output = Out> + 'static + Send,
261    {
262        let id = self.addr.id;
263        let cancel = self.addr.cancellation();
264        let task = Task::new::<A>(id, short_type_name::<Out>());
265        let spawned = trace::mark(|| task.spawn());
266
267        #[cfg(feature = "test-utils")]
268        let counted = crate::actor::event_bus::Counted::new();
269
270        crate::executor::spawn(async move {
271            let running = trace::within(Some(spawned), fut);
272            let result = match listens {
273                true => Some(running.await),
274                false => cancel.guard(running).await,
275            };
276
277            let Some(result) = result.filter(|_| !cancel.is_cancelled()) else {
278                task.ended(spawned, task.cancelled());
279                return;
280            };
281
282            let return_task = move || {
283                #[cfg(feature = "test-utils")]
284                let _counted = counted;
285
286                let settled = trace::mark_under(Some(spawned), || task.settled());
287
288                if let Some(addr) = registered::<A>(id) {
289                    addr.send_under(result, Some(settled));
290                }
291            };
292
293            invoke_on_ui(return_task);
294        });
295    }
296
297    /// [`Cx::spawn_bg`] for work that wants to hear the cancellation
298    /// rather than be dropped by it: a loop that checks between rounds, a
299    /// connection that says goodbye, a `select!` arm.
300    ///
301    /// ```ignore
302    /// cx.spawn_bg_with::<Tick, _, _>(|gone| async move {
303    ///     while !gone.is_cancelled() {
304    ///         poll().await;
305    ///     }
306    ///     Tick
307    /// });
308    /// ```
309    pub fn spawn_bg_with<Out, Fut, F>(&self, work: F)
310    where
311        Out: Send + 'static,
312        A: Handler<Out> + ManagedActor,
313        A::Flow: crate::actor::flow::Allows<M, Out>,
314        Fut: Future<Output = Out> + 'static + Send,
315        F: FnOnce(Cancel) -> Fut,
316    {
317        self.bg(work(self.addr.cancellation()), true);
318    }
319
320    /// [`Cx::spawn_bg`] for work that answers no one; cancelled with the
321    /// actor just the same.
322    pub fn spawn_bg_detached<Fut>(&self, fut: Fut)
323    where
324        Fut: Future<Output = ()> + 'static + Send,
325    {
326        self.bg_detached(fut, false);
327    }
328
329    /// [`Cx::spawn_bg_with`] for work that answers no one.
330    pub fn spawn_bg_detached_with<Fut, F>(&self, work: F)
331    where
332        Fut: Future<Output = ()> + 'static + Send,
333        F: FnOnce(Cancel) -> Fut,
334    {
335        self.bg_detached(work(self.addr.cancellation()), true);
336    }
337
338    fn bg_detached<Fut>(&self, fut: Fut, listens: bool)
339    where
340        Fut: Future<Output = ()> + 'static + Send,
341    {
342        let cancel = self.addr.cancellation();
343        let task = Task::new::<A>(self.addr.id, "()");
344        let spawned = trace::mark(|| task.spawn());
345
346        #[cfg(feature = "test-utils")]
347        let counted = crate::actor::event_bus::Counted::new();
348
349        crate::executor::spawn(async move {
350            #[cfg(feature = "test-utils")]
351            let _counted = counted;
352
353            let running = trace::within(Some(spawned), fut);
354            let ran = if listens {
355                running.await;
356                true
357            } else {
358                cancel.guard(running).await.is_some()
359            };
360
361            let ended = match ran && !cancel.is_cancelled() {
362                true => task.settled(),
363                false => task.cancelled(),
364            };
365
366            task.ended(spawned, ended);
367        });
368    }
369
370    /// Feeds this actor what `source` yields, each item as `into(item)`, for
371    /// as long as the actor lives: a watch, a subscription, a pipe that
372    /// pushes.
373    ///
374    /// A source is not work of what opened it, as a timer is not work of
375    /// what started it. The handler that opens it is done once it is open,
376    /// and each item arrives as a root of its own - so an action that opens
377    /// a watch is finished when the watch is open, not when it runs dry.
378    /// What the stream does to make an item - a publish from inside its poll,
379    /// say - is recorded under a [`Point::Pull`] of the source, a root as
380    /// well.
381    ///
382    /// Declared as `bg` in `actor!`, like any other work that answers later.
383    ///
384    /// ```ignore
385    /// cx.spawn_source(changes, Changed);
386    /// ```
387    pub fn spawn_source<S, Out, F>(&self, source: S, into: F)
388    where
389        S: Stream + Send + 'static,
390        Out: Send + 'static,
391        F: FnMut(S::Item) -> Out + Send + 'static,
392        A: Handler<Out> + ManagedActor,
393        A::Flow: crate::actor::flow::Allows<M, Out>,
394    {
395        let id = self.addr.id;
396        let cancel = self.addr.cancellation();
397        let feed = Feed::new::<A>(id, short_type_name::<Out>());
398        let opened = trace::mark(|| feed.opened());
399
400        crate::executor::spawn(async move {
401            let pouring = trace::within(None, pour::<A, _, _, _>(id, source, into, feed, opened));
402            let ran_dry = cancel.guard(pouring).await.is_some();
403
404            feed.closed(opened, !ran_dry || cancel.is_cancelled());
405        });
406    }
407}
408
409/// Hands each item of `source` to the UI thread, as a root of its own, until
410/// the source runs dry. Gives way after each item, so a source that is always
411/// ready still notices its actor going.
412async fn pour<A, S, Out, F>(id: usize, source: S, mut into: F, feed: Feed, opened: Cause)
413where
414    A: Handler<Out> + 'static,
415    S: Stream,
416    Out: Send + 'static,
417    F: FnMut(S::Item) -> Out,
418{
419    let mut source = std::pin::pin!(source);
420
421    loop {
422        let next = async {
423            let pulling = feed.pull(opened);
424
425            std::future::poll_fn(|cx| {
426                let _pulling = trace::resume(Some(pulling.id));
427                source.as_mut().poll_next(cx)
428            })
429            .await
430        };
431        let Some(item) = next.await else {
432            break;
433        };
434        let message = into(item);
435
436        invoke_on_ui(move || {
437            let _root = trace::resume(None);
438            let _arrived = trace::enter(|| feed.arrived(opened));
439
440            if let Some(addr) = registered::<A>(id) {
441                addr.send(message);
442            }
443        });
444
445        crate::executor::yield_now().await;
446    }
447}
448
449pub struct AsyncContext<A: 'static> {
450    actor_id: usize,
451    cancel: Cancel,
452    _phantom: PhantomData<A>,
453}
454
455impl<A: 'static> Clone for AsyncContext<A> {
456    fn clone(&self) -> Self {
457        Self {
458            actor_id: self.actor_id,
459            cancel: self.cancel.clone(),
460            _phantom: PhantomData,
461        }
462    }
463}
464
465unsafe impl<A: 'static> Send for AsyncContext<A> {}
466unsafe impl<A: 'static> Sync for AsyncContext<A> {}
467
468impl<A: 'static> AsyncContext<A> {
469    pub(crate) fn new(actor_id: usize, cancel: Cancel) -> Self {
470        Self {
471            actor_id,
472            cancel,
473            _phantom: PhantomData,
474        }
475    }
476
477    /// The actor's cancellation token, to hand to something that takes one.
478    pub fn cancellation(&self) -> Cancel {
479        self.cancel.clone()
480    }
481
482    /// Whether the actor is gone. Worth asking between steps of long work:
483    /// what comes after is for nobody.
484    pub fn is_cancelled(&self) -> bool {
485        self.cancel.is_cancelled()
486    }
487
488    /// Waits until the actor is gone - one arm of a `select!`, for work that
489    /// has to end itself rather than be dropped at an await.
490    pub async fn cancelled(&self) {
491        self.cancel.cancelled().await;
492    }
493
494    /// `fut`'s output, or `None` if the actor went away first.
495    pub async fn until_gone<F: Future>(&self, fut: F) -> Option<F::Output> {
496        self.cancel.guard(fut).await
497    }
498
499    /// Publishes `msg` on the global bus, from whichever thread this runs on:
500    /// the bus lives on the UI thread, and the event goes there with its
501    /// cause.
502    pub fn publish<M>(&self, msg: M)
503    where
504        A: ManagedActor,
505        M: Event,
506        A::Signals: AllowedSignal<M>,
507    {
508        GlobalEventBus::publish(msg);
509    }
510
511    pub fn publish_local<M>(&self, bus: &EventBus, msg: M)
512    where
513        A: ManagedActor,
514        M: Event,
515        A::Signals: AllowedSignal<M>,
516    {
517        bus.publish(msg);
518    }
519
520    pub fn send<M>(&self, msg: M)
521    where
522        M: Send + 'static,
523        A: Handler<M>,
524    {
525        let id = self.actor_id;
526        let cause = trace::current();
527
528        invoke_on_ui(move || {
529            if let Some(addr) = registered::<A>(id) {
530                addr.send_under(msg, cause);
531            }
532        });
533    }
534}
535
536impl<A: 'static, M> Cx<A, M> {
537    pub fn async_ctx(&self) -> AsyncContext<A> {
538        AsyncContext::new(self.addr.id, self.addr.cancellation())
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use crate::actor::UiThreadToken;
546    use std::cell::RefCell;
547    use std::rc::Rc;
548    use std::sync::Arc;
549    use std::sync::atomic::{AtomicBool, Ordering};
550
551    struct First;
552    struct Second;
553
554    struct Chain {
555        log: Rc<RefCell<Vec<&'static str>>>,
556    }
557
558    guinea_macros::actor! {
559        Chain {
560            handlers {
561                First => { send Second, bg Second }
562                Second
563            }
564        }
565    }
566
567    impl Handler<First> for Chain {
568        fn handle(&mut self, _: First, cx: Cx<Self, First>) {
569            self.log.borrow_mut().push("first");
570            cx.send(Second);
571        }
572    }
573
574    impl Handler<Second> for Chain {
575        fn handle(&mut self, _: Second, _cx: Cx<Self, Second>) {
576            self.log.borrow_mut().push("second");
577        }
578    }
579
580    #[test]
581    fn send_from_a_handler_is_drained_by_the_same_queue() {
582        let log = Rc::new(RefCell::new(Vec::new()));
583        let addr = Addr::new(
584            Chain { log: log.clone() },
585            UiThreadToken::dangerously_create_token_unchecked(),
586        );
587
588        addr.send(First);
589
590        assert_eq!(&*log.borrow(), &["first", "second"]);
591    }
592
593    #[test]
594    fn a_watcher_hears_which_actor_handled_a_message() {
595        use crate::observability::changes::{self, Change};
596
597        let seen = Rc::new(RefCell::new(Vec::new()));
598        let sink = seen.clone();
599        changes::watch(move |change| {
600            if let Change::ActorHandled { id } = change {
601                sink.borrow_mut().push(*id);
602            }
603        });
604
605        let addr = Addr::new(
606            Chain {
607                log: Rc::new(RefCell::new(Vec::new())),
608            },
609            UiThreadToken::dangerously_create_token_unchecked(),
610        );
611        addr.send(First);
612        changes::stop_watching();
613
614        let id = addr.id();
615        assert_eq!(*seen.borrow(), [id, id], "First, then the Second it sent");
616    }
617
618    #[test]
619    fn a_chain_of_sends_is_traced_back_to_the_action_that_started_it() {
620        use crate::trace::{Cause, Record, Trace};
621
622        let seen = Rc::new(RefCell::new(Vec::<Record>::new()));
623        let sink = seen.clone();
624        trace::observe(move |trace| {
625            if let Trace::Begin(record) | Trace::Mark(record) = trace {
626                sink.borrow_mut().push(record.clone());
627            }
628        });
629
630        let addr = Addr::new(
631            Chain {
632                log: Rc::new(RefCell::new(Vec::new())),
633            },
634            UiThreadToken::dangerously_create_token_unchecked(),
635        );
636        {
637            let _action = trace::enter(|| Point::Action { message: "First" });
638            addr.send(First);
639        }
640        trace::stop_observing();
641
642        let seen = seen.borrow();
643        let find = |wanted: &dyn Fn(&Point) -> bool| -> &Record {
644            seen.iter().find(|record| wanted(&record.point)).expect("recorded")
645        };
646        let parent = |record: &Record| -> Option<Cause> { record.parent };
647
648        let action = find(&|p| matches!(p, Point::Action { .. }));
649        let send_first = find(&|p| matches!(p, Point::Send { message, .. } if message.ends_with("First")));
650        let handle_first =
651            find(&|p| matches!(p, Point::Handle { message, .. } if message.ends_with("First")));
652        let send_second =
653            find(&|p| matches!(p, Point::Send { message, .. } if message.ends_with("Second")));
654        let handle_second =
655            find(&|p| matches!(p, Point::Handle { message, .. } if message.ends_with("Second")));
656
657        assert_eq!(parent(send_first), Some(action.id));
658        assert_eq!(parent(handle_first), Some(send_first.id));
659        assert_eq!(parent(send_second), Some(handle_first.id));
660        assert_eq!(parent(handle_second), Some(send_second.id));
661    }
662
663    #[test]
664    fn disposing_an_actor_cancels_what_it_spawned() {
665        let addr = Addr::new(
666            Chain {
667                log: Rc::new(RefCell::new(Vec::new())),
668            },
669            UiThreadToken::dangerously_create_token_unchecked(),
670        );
671
672        let cx = Cx::<_, First>::new(addr.clone());
673        let cancel = cx.cancellation();
674        assert!(!cancel.is_cancelled());
675
676        addr.dispose();
677
678        assert!(cancel.is_cancelled(), "teardown has to reach the tasks too");
679    }
680
681    #[tokio::test]
682    async fn a_task_cancelled_with_its_actor_says_so_and_answers_nobody() {
683        let seen = Rc::new(RefCell::new(Vec::new()));
684        let sink = seen.clone();
685        trace::observe(move |trace| {
686            if let crate::trace::Trace::Mark(record) = trace {
687                sink.borrow_mut().push(record.point.kind());
688            }
689        });
690
691        let log = Rc::new(RefCell::new(Vec::new()));
692        let addr = Addr::new_managed(
693            Chain { log: log.clone() },
694            UiThreadToken::dangerously_create_token_unchecked(),
695        );
696
697        let cx = Cx::<_, First>::new(addr.clone());
698        cx.spawn_bg::<Second, _>(async {
699            std::future::pending::<()>().await;
700            Second
701        });
702
703        addr.dispose();
704        for _ in 0..8 {
705            tokio::task::yield_now().await;
706        }
707        trace::stop_observing();
708
709        let seen = seen.borrow();
710        assert_eq!(*seen, ["spawn", "cancelled"], "the task's whole life");
711        assert!(log.borrow().is_empty(), "nothing came back to be handled");
712    }
713
714    #[tokio::test]
715    async fn work_that_listens_for_the_token_is_left_to_wind_itself_down() {
716        let addr = Addr::new_managed(
717            Chain {
718                log: Rc::new(RefCell::new(Vec::new())),
719            },
720            UiThreadToken::dangerously_create_token_unchecked(),
721        );
722
723        let wound_down = Arc::new(AtomicBool::new(false));
724        let noted = wound_down.clone();
725
726        let cx = Cx::<_, First>::new(addr.clone());
727        cx.spawn_bg_with::<Second, _, _>(|gone| async move {
728            gone.cancelled().await;
729            tokio::task::yield_now().await;
730            noted.store(true, Ordering::SeqCst);
731            Second
732        });
733
734        addr.dispose();
735        for _ in 0..8 {
736            tokio::task::yield_now().await;
737        }
738
739        assert!(
740            wound_down.load(Ordering::SeqCst),
741            "the await after the cancellation still ran"
742        );
743    }
744
745    #[test]
746    fn detach_keeps_the_address() {
747        let log = Rc::new(RefCell::new(Vec::new()));
748        let addr = Addr::new(
749            Chain { log },
750            UiThreadToken::dangerously_create_token_unchecked(),
751        );
752
753        let cx = Cx::<_, First>::new(addr.clone());
754        let bare = cx.detach();
755
756        assert_eq!(bare.addr().id(), addr.id());
757    }
758}