Skip to main content

informalsystems_malachitebft_engine/util/
output_port.rs

1// Copyright (c) Sean Lawlor
2//
3// This source code is licensed under both the MIT license found in the
4// LICENSE-MIT file in the root directory of this source tree.
5
6//! Output ports for publish-subscribe notifications between actors
7//!
8//! This notion extends beyond traditional actors in this that is a publish-subscribe
9//! mechanism we've added in `ractor`. Output ports are ports which can have messages published
10//! to them which are automatically forwarded to downstream actors waiting for inputs. They optionally
11//! have a message transformer attached to them to convert them to the appropriate message type
12
13use std::sync::RwLock;
14
15use ractor::concurrency::JoinHandle;
16use ractor::{ActorRef, Message};
17use tokio::sync::broadcast as pubsub;
18
19/// Output messages, since they need to be replicated, require [Clone] in addition
20/// to the base [Message] constraints
21pub trait OutputMessage: Message + Clone {}
22impl<T: Message + Clone> OutputMessage for T {}
23
24/// An [OutputPort] is a publish-subscribe mechanism for connecting actors together.
25/// It allows actors to emit messages without knowing which downstream actors are subscribed.
26///
27/// You can subscribe to the output port with an [ActorRef] and a message converter from the output
28/// type to the actor's expected input type. If the actor is dropped or stops, the subscription will
29/// be dropped and if the output port is dropped, then the subscription will also be dropped
30/// automatically.
31pub struct OutputPort<TMsg>
32where
33    TMsg: OutputMessage,
34{
35    tx: pubsub::Sender<Option<TMsg>>,
36    subscriptions: RwLock<Vec<OutputPortSubscription>>,
37}
38
39impl<TMsg> Default for OutputPort<TMsg>
40where
41    TMsg: OutputMessage,
42{
43    fn default() -> Self {
44        // We only need enough buffer for the subscription task to forward to the input port
45        // of the receiving actor. Hence 10 should be plenty.
46        Self::with_capacity(10)
47    }
48}
49
50impl<TMsg> OutputPort<TMsg>
51where
52    TMsg: OutputMessage,
53{
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    pub fn with_capacity(capacity: usize) -> Self {
59        let (tx, _rx) = pubsub::channel(capacity);
60        Self {
61            tx,
62            subscriptions: RwLock::new(vec![]),
63        }
64    }
65
66    /// Subscribe to the output port, passing in a converter to convert to the input message
67    /// of another actor
68    ///
69    /// * `receiver` - The reference to the actor which will receive forwarded messages
70    /// * `converter` - The converter which will convert the output message type to the
71    ///   receiver's input type and return [Some(_)] if the message should be forwarded, [None]
72    ///   if the message should be skipped.
73    pub fn subscribe<TReceiverMsg, F>(&self, receiver: ActorRef<TReceiverMsg>, converter: F)
74    where
75        F: Fn(TMsg) -> Option<TReceiverMsg> + Send + 'static,
76        TReceiverMsg: Message,
77    {
78        let mut subs = self.subscriptions.write().unwrap();
79
80        // filter out dead subscriptions, since they're no longer valid
81        subs.retain(|sub| !sub.is_dead());
82
83        let sub = OutputPortSubscription::new::<TMsg, F, TReceiverMsg>(
84            self.tx.subscribe(),
85            converter,
86            receiver,
87        );
88        subs.push(sub);
89    }
90
91    /// Send a message on the output port
92    ///
93    /// * `msg`: The message to send
94    pub fn send(&self, msg: TMsg) {
95        if self.tx.receiver_count() > 0 {
96            let _ = self.tx.send(Some(msg));
97        }
98    }
99}
100
101impl<TMsg> Drop for OutputPort<TMsg>
102where
103    TMsg: OutputMessage,
104{
105    fn drop(&mut self) {
106        let mut subs = self.subscriptions.write().unwrap();
107        for sub in subs.iter_mut() {
108            sub.stop();
109        }
110        subs.clear();
111    }
112}
113
114// ============== Subscription implementation ============== //
115
116/// The output port's subscription handle. It holds a handle to a [JoinHandle]
117/// which listens to the [pubsub::Receiver] to see if there's a new message, and if there is
118/// forwards it to the [ActorRef] asynchronously using the specified converter.
119struct OutputPortSubscription {
120    handle: JoinHandle<()>,
121}
122
123impl OutputPortSubscription {
124    /// Determine if the subscription is dead
125    pub fn is_dead(&self) -> bool {
126        self.handle.is_finished()
127    }
128
129    /// Stop the subscription, by aborting the underlying [JoinHandle]
130    pub fn stop(&mut self) {
131        self.handle.abort();
132    }
133
134    /// Create a new subscription
135    pub fn new<TMsg, F, TReceiverMsg>(
136        mut port: pubsub::Receiver<Option<TMsg>>,
137        converter: F,
138        receiver: ActorRef<TReceiverMsg>,
139    ) -> Self
140    where
141        TMsg: OutputMessage,
142        F: Fn(TMsg) -> Option<TReceiverMsg> + Send + 'static,
143        TReceiverMsg: Message,
144    {
145        let handle = ractor::concurrency::spawn(async move {
146            loop {
147                match port.recv().await {
148                    Err(tokio::sync::broadcast::error::RecvError::Lagged(l)) => {
149                        tracing::warn!("Output port is lagging, we've dropped {l} messages!");
150                    }
151                    Ok(Some(msg)) => {
152                        if let Some(new_msg) = converter(msg) {
153                            if receiver.cast(new_msg).is_err() {
154                                // kill the subscription process, as the forwarding agent is stopped
155                                return;
156                            }
157                        }
158                    }
159                    Ok(None) => {
160                        // skip this message
161                    }
162                    Err(tokio::sync::broadcast::error::RecvError::Closed) => {
163                        tracing::warn!("Subscription is dying due to closed channel!");
164                        return;
165                    }
166                }
167            }
168        });
169
170        Self { handle }
171    }
172}
173
174/// Represents a boxed `ActorRef` subscriber capable of handling messages from a
175/// publisher via an `OutputPort`, employing a publish-subscribe pattern to
176/// decouple message broadcasting from handling. For a subscriber `ActorRef` to
177/// function as an `OutputPortSubscriber<T>`, its message type must implement
178/// `From<T>` to convert the published message type to its own message format.
179///
180/// # Example
181/// ```
182/// // First, define the publisher's message types, including a variant for
183/// // subscribing `OutputPortSubscriber`s and another for publishing messages:
184/// use ractor::{
185///     cast,
186///     port::{OutputPort, OutputPortSubscriber},
187///     Actor, ActorProcessingErr, ActorRef, Message,
188/// };
189///
190/// enum PublisherMessage {
191///     Publish(u8),                         // Message type for publishing
192///     Subscribe(OutputPortSubscriber<u8>), // Message type for subscribing an actor to the output port
193/// }
194///
195/// #[cfg(feature = "cluster")]
196/// impl Message for PublisherMessage {
197///     fn serializable() -> bool {
198///         false
199///     }
200/// }
201///
202/// // In the publisher actor's `handle` function, handle subscription requests and
203/// // publish messages accordingly:
204///
205/// struct Publisher;
206/// struct State {
207///     output_port: OutputPort<u8>,
208/// }
209///
210/// #[cfg_attr(feature = "async-trait", ractor::async_trait)]
211/// impl Actor for Publisher {
212///     type State = State;
213///     type Msg = PublisherMessage;
214///     type Arguments = ();
215///
216///     async fn pre_start(
217///         &self,
218///         _myself: ActorRef<Self::Msg>,
219///         _: (),
220///     ) -> Result<Self::State, ActorProcessingErr> {
221///         Ok(State {
222///             output_port: OutputPort::default(),
223///         })
224///     }
225///
226///     async fn handle(
227///         &self,
228///         _myself: ActorRef<Self::Msg>,
229///         message: Self::Msg,
230///         state: &mut Self::State,
231///     ) -> Result<(), ActorProcessingErr> {
232///         match message {
233///             PublisherMessage::Subscribe(subscriber) => {
234///                 // Subscribes the `OutputPortSubscriber` wrapped actor to the `OutputPort`
235///                 subscriber.subscribe_to_port(&state.output_port);
236///             }
237///             PublisherMessage::Publish(value) => {
238///                 // Broadcasts the `u8` value to all subscribed actors, which will handle the type conversion
239///                 state.output_port.send(value);
240///             }
241///         }
242///         Ok(())
243///     }
244/// }
245///
246/// // The subscriber's message type demonstrates how to transform the publisher's
247/// // message type by implementing `From<T>`:
248///
249/// #[derive(Debug)]
250/// enum SubscriberMessage {
251///     Handle(String), // Subscriber's intent for message handling
252/// }
253///
254/// #[cfg(feature = "cluster")]
255/// impl Message for SubscriberMessage {
256///     fn serializable() -> bool {
257///         false
258///     }
259/// }
260///
261/// impl From<u8> for SubscriberMessage {
262///     fn from(value: u8) -> Self {
263///         SubscriberMessage::Handle(value.to_string()) // Converts u8 to String
264///     }
265/// }
266///
267/// // To subscribe a subscriber actor to the publisher and broadcast a message:
268/// struct Subscriber;
269/// #[cfg_attr(feature = "async-trait", ractor::async_trait)]
270/// impl Actor for Subscriber {
271///     type State = ();
272///     type Msg = SubscriberMessage;
273///     type Arguments = ();
274///
275///     async fn pre_start(
276///         &self,
277///         _myself: ActorRef<Self::Msg>,
278///         _: (),
279///     ) -> Result<Self::State, ActorProcessingErr> {
280///         Ok(())
281///     }
282///
283///     async fn handle(
284///         &self,
285///         _myself: ActorRef<Self::Msg>,
286///         message: Self::Msg,
287///         _state: &mut Self::State,
288///     ) -> Result<(), ActorProcessingErr> {
289///         Ok(())
290///     }
291/// }
292/// async fn example() {
293///     let (publisher_actor_ref, publisher_actor_handle) =
294///         Actor::spawn(None, Publisher, ()).await.unwrap();
295///     let (subscriber_actor_ref, subscriber_actor_handle) =
296///         Actor::spawn(None, Subscriber, ()).await.unwrap();
297///
298///     publisher_actor_ref
299///         .send_message(PublisherMessage::Subscribe(Box::new(subscriber_actor_ref)))
300///         .unwrap();
301///
302///     // Broadcasting a message to all subscribers
303///     publisher_actor_ref
304///         .send_message(PublisherMessage::Publish(123))
305///         .unwrap();
306///
307///     publisher_actor_handle.await.unwrap();
308///     subscriber_actor_handle.await.unwrap();
309/// }
310/// ```
311pub type OutputPortSubscriber<InputMessage> = Box<dyn OutputPortSubscriberTrait<InputMessage>>;
312
313/// A trait for subscribing to an [OutputPort]
314pub trait OutputPortSubscriberTrait<I>: Send
315where
316    I: Message + Clone,
317{
318    /// Subscribe to the output port
319    fn subscribe_to_port(&self, port: &OutputPort<I>);
320}
321
322impl<I, O> OutputPortSubscriberTrait<I> for ActorRef<O>
323where
324    I: Message + Clone,
325    O: Message + From<I>,
326{
327    fn subscribe_to_port(&self, port: &OutputPort<I>) {
328        port.subscribe(self.clone(), |msg| Some(O::from(msg)));
329    }
330}