exercises

Log | Files | Refs | README

main.rs (5563B)


      1 //! An exercise to create a simple actor
      2 //!
      3 //! What is an actor?
      4 //!
      5 //! The basic idea behind an actor is to spawn a self-contained task that performs
      6 //! some job independently of other parts of the program.
      7 //!
      8 //! Typically these actors communicate with the rest of the program through the
      9 //! use of message passing channels.
     10 //!
     11 //! Since each actor runs independently, programs designed using them are
     12 //! naturally parallel.
     13 //!
     14 //! Things to pay attention to:
     15 //! 1. Where to put the tokio::spawn call.
     16 //! 2. Struct with run method vs bare function.
     17 //! 3. andles to the actor.
     18 //! 4. Backpressure and bounded channels.
     19 //! 5. Graceful shutdown.
     20 
     21 use tokio::sync::{mpsc, oneshot};
     22 
     23 /// An actor is split into two parts: the task and the handle.
     24 ///
     25 /// The task is the independently spawned Tokio task that actually performs
     26 /// the duties of the actor, and the handle is a struct that allows you to
     27 /// communicate with the task.
     28 struct MyActor {
     29     receiver: mpsc::Receiver<ActorMessage>,
     30     next_id: u32,
     31 }
     32 
     33 /// The ActorMessage enum defines the kind of messages we can send to the actor.
     34 ///
     35 /// By using an enum, we can have many different message types, and each message
     36 /// type can have its own set of arguments. We return a value to the sender by
     37 /// using an oneshot channel, which is a message passing channel that allows
     38 /// sending exactly one message.
     39 enum ActorMessage {
     40     GetUniqueId { respond_to: oneshot::Sender<u32> },
     41 }
     42 
     43 impl MyActor {
     44     fn new(receiver: mpsc::Receiver<ActorMessage>) -> Self {
     45         MyActor {
     46             receiver,
     47             next_id: 0,
     48         }
     49     }
     50 
     51     fn handle_message(&mut self, msg: ActorMessage) {
     52         // We match on the enum inside a handle_message method on the actor struct,
     53         // but that isn't the only way to structure this. One could also match on
     54         // the enum in the run_my_actor function. Each branch in this match could
     55         // then call various methods such as get_unique_id on the actor object.
     56         match msg {
     57             ActorMessage::GetUniqueId { respond_to } => {
     58                 self.next_id += 1;
     59 
     60                 // The `let _ =` ignores any errors when sending.
     61                 //
     62                 // This can happen if the `select!` macro is used
     63                 // to cancel waiting for the response.
     64                 let _ = respond_to.send(self.next_id);
     65             }
     66         }
     67     }
     68 }
     69 
     70 async fn run_my_actor(mut actor: MyActor) {
     71     // We can detect when the actor should shut down by looking at failures to
     72     // receive messages. In our example, this happens in the following while loop:
     73     while let Some(msg) = actor.receiver.recv().await {
     74         actor.handle_message(msg);
     75     }
     76 }
     77 
     78 /// Now that we have the actor itself, we also need a handle to the actor.
     79 ///
     80 /// A handle is an object that other pieces of code can use to talk to the actor,
     81 /// and is also what keeps the actor alive.
     82 ///
     83 /// Derive Clone: Since the channel allows multiple producers, we can freely
     84 /// clone our handle to the actor, allowing us to talk to it from multiple places.
     85 #[derive(Clone)]
     86 pub struct MyActorHandle {
     87     sender: mpsc::Sender<ActorMessage>,
     88 }
     89 
     90 impl MyActorHandle {
     91     pub fn new() -> Self {
     92         let (sender, receiver) = mpsc::channel(8);
     93         let actor = MyActor::new(receiver);
     94         tokio::spawn(run_my_actor(actor));
     95 
     96         Self { sender }
     97     }
     98 
     99     pub async fn get_unique_id(&self) -> u32 {
    100         let (send, recv) = oneshot::channel();
    101         let msg = ActorMessage::GetUniqueId { respond_to: send };
    102 
    103         // Ignore send errors. If this send fails, so does the
    104         // recv.await below. There's no reason to check for the
    105         // same failure twice.
    106         let _ = self.sender.send(msg).await;
    107         recv.await.expect("Actor task has been killed")
    108     }
    109 }
    110 
    111 /// When you call MyActorHandle::new(), it already calls tokio::spawn(run_my_actor(actor))
    112 /// internally. So by the time new() returns, the actor task is live and waiting
    113 /// for messages on its mpsc::Receiver.
    114 ///
    115 /// You never call tokio::spawn in main — it's encapsulated inside MyActorHandle::new(),
    116 /// which is the idiomatic placement for this pattern. This keeps the spawning
    117 /// logic close to the actor itself.
    118 ///
    119 /// Graceful shutdown is automatic — when actor_handle (and all its clones) are
    120 /// dropped, the mpsc::Sender is dropped, causing actor.receiver.recv().await to
    121 /// return None, breaking the while let loop and ending the task.
    122 ///
    123 /// Cloning the handle is safe — MyActorHandle derives Clone, so multiple parts
    124 /// of your program can send messages to the same actor concurrently without any
    125 /// extra synchronization, since the actor processes them one at a time.
    126 ///
    127 /// Backpressure is built in — the channel is bounded (mpsc::channel(8)), so if
    128 /// the actor can't keep up, senders will .await until there's room, naturally
    129 /// throttling the workload.
    130 #[tokio::main]
    131 async fn main() {
    132     // 1. Creating the handle also spawns the actor task automatically (inside MyActorHandle::new)
    133     let actor_handle = MyActorHandle::new();
    134 
    135     // 2. Send a message to the actor and await the response
    136     let id1 = actor_handle.get_unique_id().await;
    137     println!("Got id: {}", id1); // prints 1
    138 
    139     let id2 = actor_handle.get_unique_id().await;
    140     println!("Got id: {}", id2); // prints 2
    141 
    142     // 3. Clone the handle to show multiple owners can talk to the same actor
    143     let handle2 = actor_handle.clone();
    144     let id3 = handle2.get_unique_id().await;
    145     println!("Got id from cloned handle: {}", id3); // prints 3
    146 }