├── .gitignore ├── project-structure.png ├── project-structure-chapter-1.png ├── project-structure-chapter-2.png ├── project-structure-chapter-3.png ├── ractor-tutorial-Inquiry-PingPong-conversation-2.png ├── ractor-tutorial-interaction-Inquirer-Ping-Pong-1.png ├── README.md ├── LICENSE ├── README.chapter-2.md ├── README.chapter-3.md └── README.chapter-1.md /.gitignore: -------------------------------------------------------------------------------- 1 | *Tree.html 2 | -------------------------------------------------------------------------------- /project-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsengupta/ractor-tutorial/HEAD/project-structure.png -------------------------------------------------------------------------------- /project-structure-chapter-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsengupta/ractor-tutorial/HEAD/project-structure-chapter-1.png -------------------------------------------------------------------------------- /project-structure-chapter-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsengupta/ractor-tutorial/HEAD/project-structure-chapter-2.png -------------------------------------------------------------------------------- /project-structure-chapter-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsengupta/ractor-tutorial/HEAD/project-structure-chapter-3.png -------------------------------------------------------------------------------- /ractor-tutorial-Inquiry-PingPong-conversation-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsengupta/ractor-tutorial/HEAD/ractor-tutorial-Inquiry-PingPong-conversation-2.png -------------------------------------------------------------------------------- /ractor-tutorial-interaction-Inquirer-Ping-Pong-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsengupta/ractor-tutorial/HEAD/ractor-tutorial-interaction-Inquirer-Ping-Pong-1.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ractor-tutorial 2 | 3 | In the past, I have had the opportunity to learn and apply Actor-based design (using [#akka](https://www.linkedin.com/feed/hashtag/?keywords=akka&highlightedUpdateUrns=urn%3Ali%3Aactivity%3A7092388425978277888) ) in about 3 production software. I loved the way I could model the behaviour using Actors (and [#fsm](https://www.linkedin.com/feed/hashtag/?keywords=fsm&highlightedUpdateUrns=urn%3Ali%3Aactivity%3A7092388425978277888)) using [#scala](https://www.linkedin.com/feed/hashtag/?keywords=scala&highlightedUpdateUrns=urn%3Ali%3Aactivity%3A7092388425978277888) and [#java](https://www.linkedin.com/feed/hashtag/?keywords=java&highlightedUpdateUrns=urn%3Ali%3Aactivity%3A7092388425978277888). 4 | 5 | As a Rust (#rustlang) enthusiast, I was searching for Actor-based libraries and 6 | chanced upon **ractor** ([github](https://github.com/slawlor/ractor)) , which mirrored the behavior of Erlang's. 7 | 8 | I have captured my understanding during and after the exploration of 9 | `ractor` in the form of tutorials; in 3 parts, just so that reading each doesn't become 10 | too heavy. 11 | 12 | ------------------------------------------------- 13 | 14 | Important: this repository replaces the 3 individual repositories that I had 15 | created earlier. Each of those 3 repositories contained one tutorial. They 16 | were in a series named as `rust-ractor-tutorial-(1, 2, 3)`. This arrangement 17 | made maintenance of the tutorials easier but made reading quite cumbersome, 18 | what with all the jumping around repositories. Based upon suggestions 19 | received in LinkedIn's Rust Programming Language [forum](https://www.linkedin.com/groups/4973032/), I have combined those earlier 3 tutorials 20 | into 1, which is this repository. It walks one through the world 21 | of Actors as implemented by 'ractor' framework. 22 | 23 | I have retired the 3 older repositories. 24 | 25 | ------------------------------------------------- 26 | 27 | 28 | ![Alt text](./project-structure.png "Repo structure at a glance") 29 | 30 | The tutorials are divided into three parts for easier reading and these are in the following directories: 31 | 32 | * ractor-tutorial-chapter-1 ([README](./README.chapter-1.md)). 33 | * ractor-tutorial-chapter-2 ([README](./README.chapter-2.md)). 34 | * ractor-tutorial-chapter-3 ([README](./README.chapter-3.md)) 35 | 36 | Each of these tutorial-chapters has its `cargo.toml` and `src` directories. The accompanying blog is inside README, as well as in the directory `accompanying_contents`. 37 | 38 | In order to run the code, one needs to move to the corresponding directory and issue the command: `cargo run`. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.chapter-2.md: -------------------------------------------------------------------------------- 1 | # rust-ractor-tutorial-2 2 | 3 | Part 2 of a tutorial that aims to elaborate Actor-based programming using 4 | 'ractor' ( Rust library ) 5 | 6 | All the code is in './ractor-tutorial-chapter-2/src'. In order to run the 7 | code, move to the aforementioned directory ( `ractor-tutorial-chapter-2` ) 8 | and do a `cargo run`. 9 | 10 | Code structure: 11 | 12 | ![Alt text](./project-structure-chapter-2.png "Tutorial-2 structure at a 13 | glance") 14 | 15 | # Tutorial 16 | 17 | ### Prelude 18 | 19 | In the previous tutorial, we had gone through the basics of Actors in `ractor`. We saw the primary constituents of *Actorified* application objects and implementation of a conversation between two Actors. This conversation is built around a vocabulary, a collection of messages which both the Actors understand and can take action on. Additionally, each Actor can keep its own state: one or more pieces of data that held inside the Actor and is available for modification when the next message is processed. 20 | 21 | ### Inherently non-blocking communication 22 | 23 | One of the key aspects of communication between Actors is that it is non-blocking by design. A sender Actor sends a message and has to remain satisfied with the possibility that: 24 | 25 | * It may or may not have been received by the intended Actor. 26 | 27 | * It may or may not be processed by the intended Actor. 28 | 29 | In the absence of any guarantee that a message has indeed been seen and acted upon by the intended actor, the protocol of communication between Actors has to be designed with appropriate structures. 30 | 31 | ### Vocabulary 32 | 33 | If ActorA sends a message M1 to ActorB, it will not know if ActorB has processed it till it hears from ActorB, most possibly through message M2. Thus, messages M1 & M2 form the vocabulary of conversation between ActorA and ActorB. Both of them have to understand both the messages. Obviously, the number of such messages can be arbitrarily large and that can become unwieldy later. A manageable vocabulary should be comprised of messages which are necessary and sufficient. 34 | 35 | ### Protocol 36 | 37 | Vocabulary is not enough for actors to have a meaningful conversation. A protocol has to accompany it too. Referring to the Actors ( A, B ) and Messages ( M1, M2 ) above, it is important to ascertain, who starts the conversation and who replies. If M2 is passed across only after M1 is received by an Actor, then that is the basis of the protocol: if ActorA has sent a M1 to ActorB, then ActorA can expect an M2 from ActorB. 38 | 39 | From the previous tutiorial in the series, we know that `handle()` method takes action on a message that arrives. Drawing upon the structure of `handle()` method, the code for Actors A and B and messages M1 and M2 will look something like: 40 | 41 | ```rust 42 | #[derive(Debug, Clone)] 43 | pub enum PrivateVocabulary { 44 | M1, 45 | M2, 46 | } 47 | 48 | // .... 49 | 50 | // For Actor A 51 | async fn handle( 52 | // .. 53 | ) -> Result<(), ActorProcessingErr> { 54 | match message { 55 | PrivateVocabulary::M2 => // ... It understands M2 56 | _ => // .... 57 | } 58 | Ok(()) 59 | } 60 | 61 | // For Actor B 62 | async fn handle( 63 | // .. 64 | ) -> Result<(), ActorProcessingErr> { 65 | match message { 66 | PrivateVocabulary::M1 => // ... It understands M1 67 | _ => // .... 68 | } 69 | Ok(()) 70 | } 71 | ``` 72 | 73 | It is clear that when preparing the message-handling logic of an Actor ( the `handle()` method to be precise ), the messages that the Actor is supposed to understand and possibly take action on, are the primary considerations. Only when someone sends such a message, the Actor stirs into action! 74 | 75 | ### Who can send messages to an Actor 76 | 77 | Any part of the application, where an `ActorRef` is available, can send a message to the Actor it *refers* to. To put it more clearly, the following are sources of messages that an Actor can receive: 78 | 79 | - From other Actors (assuming that these 'other' Actors has the `ActorRef` of the recipient Actor) 80 | 81 | - From other methods of the application - including `main()` 82 | 83 | - From within itself (to itself, so as to say) 84 | 85 | - From the runtime of `ractor` ( messages that scheduled for a point of time in future) 86 | 87 | ### Order of arrival of messages 88 | 89 | If ActorB and ActorC send messages to ActorA, then order in which these messages reach ActorA can be random, but messages from any particular Actor will reach ActorA exactly in the order in which they are sent. To elaborate, say: 90 | 91 | ActorB sends messages m1B, m2B, m3B to ActorA, in that order 92 | 93 | ActorC sends messages m1C, m2C, m3C to ActorA, in that order 94 | 95 | Then, ActorA may receive messages in a mix of combined order, like: 96 | 97 | m1B; m1C, m2C, m2B, m3B, m3C OR 98 | 99 | m1B, m2B, m1C, m2C, m3C, m3B OR 100 | 101 | m1B, m2B, m3B, m1C, m2C, m3C OR 102 | 103 | any such mix of m[123]{B|C} but 104 | 105 | messages from ActorB will always be in the order of m1B,m2B,m3B. Message m3B will **never** reach ActorA, before m2B which in turn, will never reach ActorA before m1B. Ditto for messages from ActorC! 106 | 107 | This may seem obviousl but this guarantee has surprising and beneficial outcomes, as we will see. 108 | 109 | ### Messages are passed non-blockingly 110 | 111 | When an Actor sends a message to another actor, the semantics of sending is equivalent to non-blocking function calls. The act of send returns immediately (if the target Actor is non-existent, then an error is generated but that is not important here). and the caller doesn't know if and when, the target Actor will process the message. This looseness of guarantee calls for a very different approach towards weaving the logic of an application: usual ways to design the application's flow by calling method (or functions) and using their return values. 112 | 113 | This carries siginificant ramifications. For example: 114 | 115 | Say, ActorA asks ActorB and ActorC for a particular value. It will send a message to the latter two, non-blockingly. That means, once the send action succeeds (and *returns*) ActorA now has to find some means of knowing: 116 | 117 | * if both of them have received/understood/ignored/rejected/processed the messages 118 | 119 | * when both of them have responded to the message (with the value asked for) 120 | 121 | The problem is the first is impossible to verify till the second happens; but, then the second may happen, may happen but much later than expected or may never happen! 122 | 123 | In this section of the tutorial, we will try and model such situations. 124 | 125 | ### Actors participating in the tutorial 126 | 127 | We bring in two actors we had built in the previous tutorial: 128 | 129 | - A Pong-aware Actor: it understands Pong messages and responds with a Ping message 130 | 131 | - A Ping-aware Actor: it understands Ping messages and responds with a Pong message 132 | 133 | They interact between themselves by exchanging Ping and Pong messages a _fixed_ number of times and then stop themselves (and then destroyed). 134 | 135 | We introduce a 3rd Actor. Its job is to inquire the abovementioned two Actors - as if to say, 'are you fine and dandy?' - and the two respond with 'I am fine, thank you' along with a running serial number. This number helps to keep track of how many times, have these two been inquired. In the accompanying code, this 3rd "Inquirer" Actor is referred to as `CanInquire` : 136 | 137 | ```rust 138 | pub struct CanInquire; 139 | 140 | // the implementation of our actor's "logic". It is built to handle messages that deal with 141 | // inquiries only (refer to pingpong_vocabulary) 142 | #[async_trait::async_trait] 143 | impl Actor for CanInquire { 144 | type Msg = PingPongMessage; 145 | type State = Option; 146 | // No startup arguments for actor initialization, at the moment 147 | type Arguments = (); 148 | ``` 149 | 150 | To accommodate this additional interaction, the vocabulary has to change: 151 | 152 | ```rust 153 | pub enum PingPongMessage { 154 | Ping(ActorRef), 155 | Pong(ActorRef), 156 | Inquire(ActorRef), 157 | StartInquiry((u16,ActorRef,ActorRef)), 158 | InquiryReport(u16,ActorRef) 159 | } 160 | ``` 161 | 162 | The three new messages added to the vocabulary are: 163 | 164 | - `Inquire(ActorRef)` This message is sent by the Inquirer to "{ Ping | Pong } Aware" Actors. It carries the reference to the sender Actor (the _Inquirer_ ) itself. The target Ping|Pong Actors can use this for responding. 165 | 166 | - `InquiryReport(u16,ActorRef)` This message is sent in response to the message above by the Ping|Pong Actors, to the Inquirer. It carries a number that represents a running serial number of the responses and a reference to the sender. 167 | 168 | - `StartInquiry((u16,ActorRef,ActorRef))`: we will take a look at it later in this tutorial. 169 | 170 | The following sequence diagram makes it clearer: 171 | 172 | ![Alt text](./ractor-tutorial-interaction-Inquirer-Ping-Pong-1.png) 173 | 174 | It is important to remember that the interaction is completely non-blocking. There is **no guarantee** that PongAware will receive the message **later than** when PingAware will receive the message. Ditto for Inquirer receiving the responses: the order is completely random. 175 | 176 | The diagram above doesn't capture the whole set of interactions between the Actors. The Ping | Pong Actors also exchange Ping <--> Pong messages between themselves (and the Inquirer knows nothing about that). 177 | 178 | ![Alt text](./ractor-tutorial-Inquiry-PingPong-conversation-2.png) 179 | 180 | Again, the order of arrival is unknown, in all cases. 181 | 182 | Now, the Ping|Pong Actors have to be equipped with the logic to deal with either of the messages: ( a ) Inquiry and ( b ) Ping / Pong. 183 | 184 | For PongAware Actor: 185 | 186 | ```rust 187 | async fn handle( 188 | &self, 189 | myself: ActorRef, 190 | message: Self::Msg, 191 | counts: &mut Self::State, 192 | ) -> Result<(), ActorProcessingErr> { 193 | 194 | match message { 195 | PingPongMessage::Inquire(sender) => { 196 | // ... 197 | }, 198 | PingPongMessage::Pong(sender) => { // <--- Pong only 199 | // ... 200 | }, 201 | _ => println!("{} received Unknown message {:?}!", myself.get_name().unwrap(),message), 202 | } 203 | ``` 204 | 205 | For PingAware Actor: 206 | 207 | ```rust 208 | async fn handle( 209 | &self, 210 | myself: ActorRef, 211 | message: Self::Msg, 212 | counts: &mut Self::State, 213 | ) -> Result<(), ActorProcessingErr> { 214 | 215 | match message { 216 | PingPongMessage::Inquire(sender) => { 217 | // ... 218 | }, 219 | PingPongMessage::Ping(sender) => { // <-- Ping only 220 | // ... 221 | }, 222 | _ => println!("{} received Unknown message {:?}!", myself.get_name().unwrap(),message), 223 | 224 | } 225 | ``` 226 | 227 | The ways `PingPongMessage::Ping(sender)` and `PingPongMessage::Pong(sender)` are handled, ale almost exactly the same as what we had seen in the previous ([ here](./ractor-tutorial-chapter-1/README.chapter-1.md))) tutorial. In short, each of the Actors keeps a count of how many times the message has been received. If that number is less than a pre-defined limit, then it responds no more and stops itself; otherwise, it responds with a corresponding message (Ping | Pong, as applicable). 228 | 229 | ### Handling inquiries: how the Inquirer works 230 | 231 | - It waits for a `PingPongMessage::StartInquiry(((u16,ActorRef,ActorRef))`. The message is interpreted as: 232 | 233 | - u16 indicates the max number of inquiries to be made 234 | 235 | - first ActorRef is of PingAware Actor; it will be inquired 236 | 237 | - sscond ActorRef is of PongAware Actor; it will be inquired as well 238 | 239 | - At this point, the Inquirer does two things: 240 | 241 | - It sets up its own data structure, used for tracking the inquiries. This data structure is initialized as its own `State` and is used to track the number of inquiries which is limited by the maximum set in the parameter passed. 242 | 243 | - It sends the first inquiries to the Ping | Pong Actors. 244 | 245 | ```rust 246 | async fn handle( 247 | &self, 248 | myself: ActorRef, 249 | message: Self::Msg, 250 | state: &mut Self::State, 251 | ) -> Result<(), ActorProcessingErr> { 252 | 253 | match message { 254 | PingPongMessage::StartInquiry(inquiry_instructions) => { 255 | if let None = state { 256 | *state = Some(PingPongCountInquiry::new ( 257 | inquiry_instructions.max_inquiries_to_be_made, 258 | inquiry_instructions.ping_actor_to_inquire, 259 | inquiry_instructions.pong_actor_to_inquire 260 | )); 261 | 262 | }; 263 | 264 | // This actor understands Ping 265 | inquiry_instructions.ping_actor_to_inquire 266 |                     .send_message( 267 |                         PingPongMessage::Inquire (myself.clone() ) 268 |                     ) 269 |                     .unwrap(); 270 | 271 | // This actor understands Pong 272 | inquiry_instructions.ping_actor_to_inquire 273 |                     .send_message( 274 |                         PingPongMessage::Inquire (myself.clone() )) 275 |                     .unwrap(); 276 | }, 277 | ``` 278 | 279 | `StartInquiry` is a `PingPongMessage`: 280 | 281 | ```rust 282 | #[derive(Debug, Clone)] 283 | pub struct InquiryInstructions { 284 | pub max_inquiries_to_be_made: u16, 285 | pub ping_actor_to_inquire: ActorRef, 286 | pub pong_actor_to_inquire: ActorRef 287 | } 288 | 289 | // These constitute the vocabulary of interactions, using `PingPongMessage` 290 | #[derive(Debug, Clone)] 291 | pub enum PingPongMessage { 292 | Ping(ActorRef), 293 | Pong(ActorRef), 294 | Inquire(ActorRef), 295 | StartInquiry(InquiryInstructions), 296 | InquiryReport(u16,ActorRef) 297 | } 298 | ``` 299 | 300 | Using the contents of `StartInquiry`, the Inquirer Actor initializes its own state: 301 | 302 | ```rust 303 | if let None = state { 304 | *state = Some(PingPongCountInquiry::new ( 305 | inquiry_instructions.max_inquiries_to_be_made, 306 | inquiry_instructions.ping_actor_to_inquire, 307 | inquiry_instructions.pong_actor_to_inquire 308 | )); 309 | 310 | }; 311 | ``` 312 | 313 | The `state` is an Option: this is so, because the when the Inquirer Actor is initialized ( by `pre_start()` lifecycle method), the state is bereft of any known value. The first time it gets that is when `StartInquiry` is received. 314 | 315 | ```rust 316 | impl Actor for CanInquire { 317 | type Msg = PingPongMessage; 318 | type State = Option; 319 | // No startup arguments for actor initialization, at the moment 320 | type Arguments = (); 321 | 322 | async fn pre_start(&self, myself: ActorRef, _: ()) -> Result { 323 | Ok(None) // <-- Initialized with Some(..) later 324 | } 325 | // .... 326 | ``` 327 | 328 | Upon receiving the `Inquire(actor_ref)`, the Ping|Pong Actors are supposed to respnd with a `InquiryReport(u16,ActorRef)`. That `u16` carries a serial number, generated by the Ping | Pong Actors. The Inquirer Actor checks the maximum number of inquires have been made to the Ping | Pong Actors. If so, then the Inquirer Actor stops itself. 329 | 330 | ```rust 331 | PingPongMessage::InquiryReport(serial_received, from_actor) => { 332 | if let Some(state) = state { 333 | let sender_name = from_actor.get_name().unwrap(); 334 | let last_report = state.track_reports_of_inquiry(sender_name.as_str()); 335 | 336 | if state.are_we_done_inquiring() { 337 | myself.stop(None) // <-- stopping itself 338 | } 339 | else { 340 | from_actor 341 | .send_message( 342 | PingPongMessage::Inquire (myself.clone()) 343 | ) 344 | .unwrap(); 345 | state.track_reports_of_inquiry(sender_name.as_str()); 346 | } 347 | } 348 | }, 349 | ``` 350 | 351 | Let us explore the `state` of Inquirer Actor: 352 | 353 | ```rust 354 | impl Actor for CanInquire { 355 | type Msg = PingPongMessage; 356 | type State = Option; 357 | // No startup arguments for actor initialization, at the moment 358 | type Arguments = (); 359 | 360 | async fn pre_start(&self, myself: ActorRef, _: ()) -> Result { 361 | Ok(None) 362 | } 363 | ``` 364 | 365 | The `state` is an `Option`. 366 | 367 | ```rust 368 | #[derive(Debug)] 369 | pub struct InquiryReportTracker (ActorRef, u16); 370 | 371 | #[derive(Debug)] 372 | pub struct PingPongCountInquiry { 373 | max_inquiry_reports_expected: u16, 374 | actor_trackers: HashMap, 375 | } 376 | ``` 377 | 378 | It prepares a HashMap of Actors and the count of inquiries made so far. The Inquirer Actor uses this to check if maximum number of inquiries have been made to Ping | Pong Actors. 379 | 380 | ```rust 381 | pub fn track_reports_of_inquiry(&mut self, actor_name: &str) -> u16 { 382 | let may_be_an_actor = self.actor_trackers.get_mut(actor_name).unwrap(); 383 | may_be_an_actor.1 += 1u16; 384 | may_be_an_actor.1 385 | } 386 | 387 | pub fn are_we_done_inquiring(&self) -> bool { 388 | let mut combined_inquiry_completion = true; 389 | for (k,v) in self.actor_trackers.iter() { 390 | // even if inquiry reports are yet to be received 391 | // from 1 of the actors, we have to wait 392 | if v.1 < self.max_inquiry_reports_expected { 393 | // Admittedly, not very idiomatic, but gets the job done! 394 | combined_inquiry_completion = 395 | combined_inquiry_completion || false; 396 | } 397 | } 398 | return combined_inquiry_completion; 399 | } 400 | ``` 401 | 402 | ### Putting things together 403 | 404 | Referring to the sequence diagrams above, we see what the arrangement is. Ping and Pong Actors can keep exchaning messages between themselves, as long as the limiting condition is not reached. Meanwhile, the Inquirer Actor keeps on inquiring with these two, intermittently. They submit reports of inquiry, dutifully, while continuing with their own conversation. These two separate conversations continue till their terminating conditions have reached and the stop themselves. 405 | 406 | The relevant portion (below) in `main()` tells the story: 407 | 408 | ```rust 409 | // .... 410 | ping_actor_ref.send_message( 411 | PingPongMessage::Ping(pong_actor_ref.clone()) 412 | ).unwrap(); 413 | 414 | pong_actor_ref.send_message( 415 | PingPongMessage::Pong(ping_actor_ref.clone()) 416 | ).unwrap(); 417 | 418 | inquirer_actor_ref.send_message( 419 | PingPongMessage::StartInquiry( 420 | InquiryInstructions { 421 | max_inquiries_to_be_made: 5u16, 422 | ping_actor_to_inquire: ping_actor_ref.clone(), 423 | pong_actor_to_inquire: pong_actor_ref.clone() 424 | } 425 | )) 426 | .unwrap(); 427 | 428 | 429 | // .... 430 | ``` 431 | 432 | The Actors, after being created, are set on, by means of messages. Ping Actor receives a PING message and gets into action. It sends a PONG message to Pong Actor. Similarly, Pong Actor stirs into action when it receives the first PONG message. Inquirer Actor is told to `StartInquiry` and it begins to inquire, as described earlier in this article. In other words, `main()` starts the ball rolling by sending appropriate messages to the Actors. Thereafter, the Actors converse amongst themselves. 433 | 434 | ### Output 435 | 436 | If the program is run, the output will look something like this: 437 | 438 | ```bash 439 | PongAware-Actor <- Inquirer-Actor, inquiry 440 | Inquirer-Actor <-- PongAware-Actor, report serial[0], of total 1 so far! 441 | PingAware-Actor <- Inquirer-Actor, inquiry 442 | PongAware-Actor <- Inquirer-Actor, inquiry 443 | Inquirer-Actor <-- PingAware-Actor, report serial[0], of total 1 so far! 444 | Inquirer-Actor <-- PongAware-Actor, report serial[1], of total 2 so far! 445 | PingAware-Actor <- Inquirer-Actor, inquiry 446 | PongAware-Actor <- Inquirer-Actor, inquiry 447 | Inquirer-Actor <-- PingAware-Actor, report serial[1], of total 2 so far! 448 | Inquirer-Actor <-- PongAware-Actor, report serial[2], of total 3 so far! 449 | PingAware-Actor <- Inquirer-Actor, inquiry 450 | Inquirer-Actor <-- PingAware-Actor, report serial[2], of total 3 so far! 451 | # .... (more lines here) 452 | ``` 453 | 454 | However, the output is very likely to differ between runs. In some case, the program may seem to hang, even. Simply put, the program will not seem to run to completion, uniformly, every time! 455 | 456 | Why? 457 | 458 | ### Asynchronousness 459 | 460 | Because the order in which messages from other Actors, reach a particular target Actor, is unknown, exactly when the targer Actor processes a particular message is unpredictable. Additionally, the action that the target Actor takes on receipt of a message, may affect the way it processes the other messages. 461 | 462 | Such non-determinism in message processing is responsible for the variations in the output. But Actors are meant to process messages asynchronously and therefore, the non-determinism in processing is unavoidable. How shall make the application produce predictable output? 463 | 464 | That is the topic of the [next](./ractor-tutorial-chapter-3) tutorial. 465 | 466 | -------------------------------------------- 467 | -------------------------------------------------------------------------------- /README.chapter-3.md: -------------------------------------------------------------------------------- 1 | # rust-ractor-tutorial-3 2 | 3 | Part 3 of a tutorial that aims to elaborate Actor-based programming using 4 | 'ractor' ( Rust library ) 5 | 6 | All the code is in './ractor-tutorial-chapter-3/src'. In order to run the 7 | code, move to the aforementioned directory ( `ractor-tutorial-chapter-3` ) 8 | and do a `cargo run`. 9 | 10 | Code structure: 11 | 12 | ![Alt text](./project-structure-chapter-3.png "Tutorial-3 structure at a 13 | glance") 14 | 15 | # Tutorial 16 | 17 | ### Prelude 18 | 19 | In the previous tutorial, we had arranged for 3 Actors, namely *Inquirer*, *PingAware* and *PongAware*. Ping & Pong Aware Actors exchange messages between themselves for a predefined number of times. Meanwhile, the Inquirer, knocks at their doors asking for some information. They courteously respond, to the Inquirer. This exchange also happens for a certain predefined number of times. 20 | 21 | When we ran the application, the output were not the same every time. While that is not unexpected, we found that several times, the application didn't terminate gracefully; in fact, many times, it hanged. 22 | 23 | Why? 24 | 25 | ### That matter of asynchronous communication 26 | 27 | The key aspects of asynchronous message-passing between Actors are: 28 | 29 | - Any one can send a message to an (target) Actor. However, when that (target) Actor will be able to take a desired action on that message and how, are not known to the sender. 30 | 31 | - It is possible that the target Actor is busy servicing another message, sent by someone else, at that moment, or it is possible that the target Actor has indeed received the message alright but has decided not to respond to it due to some logic of its own. The sender Actor must be prepared to deal with this. 32 | 33 | - It is also possible that the act of 'sending' has been successful and the sender believes that the target Actor must be working on it soon. However, in the meantime, the target Actor has gone out of existence. The sender is never going to know. 34 | 35 | ### Revisiting the Actor trio in the tutorial 36 | 37 | The PingAware Actor: 38 | 39 | - Responds to `PingPongMessage::Inquire` message from the Inquirer Actor just after it receives it, with a sequentially increasing integer number. 40 | 41 | - Responds to `PingPongMessage::Ping` with a `PingPongMessage:Pong` but only if the number of responses so far has not exceeded a limit. If it has, then the Actor terminates itself. 42 | 43 | The PongAware Actor: 44 | 45 | - Responds to `PingPongMessage::Inquire` message from the Inquirer Actor just after it receives it, with a sequentially increasing integer number. 46 | 47 | - Responds to `PingPongMessage::Pong with a` PingPongMessage:Ping` but it doesn't keep track of number of such responses. In other words, it can keep responding as long as it receives a Pong message. It imposes no condition on itself to decide if and when to go out of existence. 48 | 49 | The Inquirer Actor: 50 | 51 | - Inquires both the Actors and keeps track of responses from both. If the number of responses from *both* Ping and Pon Aware Actors reach a pre-defined number, it terminates itself. 52 | 53 | Let's think about the implications of all these, a bit. 54 | 55 | For example, the Inquirer Actor doesn't know that Ping | Pong Aware Actors know one another and they are conversing too. That is alright because functionally, that is the correct depiction of what needs to happen. However, the PingAware Actor can go out of existence, if the aforesaid limit of Ping messages is reached, and the Inquirer Actor remains unaware of the disappearance of its inquiry's target. 56 | 57 | ### What happens first? 58 | 59 | Does the PingAware Actor hear from the Inquirer first or from the PongAware first? One doesn't know. 60 | 61 | Does the PingAware Actor receive *several* messages from PongAware Actor before it receives the *first* message from the Inquirer? Quite possible. 62 | 63 | Let's say that the PingAware Actor is codified to exchange messages with PongAware Actor 5 times max and after that, it terminates itself. Given this and extending the preceding point, can the PingAware finish responding to all 5 Ping messages and then die, and as a result, the Inquirer **never even gets a chance** to inquire it? Quite possible, again! 64 | 65 | A few other such possibilites exist in the application. 66 | 67 | The point is that **sequentiality of interactions** is not guaranteed. Put simply, if Actor A sends 2 messages, one each to Actor B and Actor C, and expects responses from both of them, then Actor A must be ready for response from either of them arriving in **any order**. Actor A must not assume that because it sends the message to Actor B first lexically, the response from Actor B will arrive first. Moreover, it may never arrive! 68 | 69 | ### Effect on the application's behaviour 70 | 71 | This is the partial output of 1 run: 72 | 73 | ```bash 74 | # ... truncated 75 | Inquirer-Actor, checking if all inquiries done: Actor PingAware-Actor, So far 1, max-allowed 5 76 | PongAware-Actor <- Inquirer-Actor, inquiry 77 | Inquirer-Actor, checking if all inquiries done: Actor PongAware-Actor, So far 0, max-allowed 5 78 | PongAware-Actor <- PingAware-Actor, a pong message 79 | Inquirer-Actor, checking if all inquiries done: Actor PingAware-Actor, So far 1, max-allowed 5 80 | Inquirer-Actor, checking if all inquiries done: Actor PongAware-Actor, So far 1, max-allowed 5 81 | PingAware-Actor <- PongAware-Actor, a ping message 82 | PingAware-Actor <- PongAware-Actor, a ping message 83 | PongAware-Actor <- PingAware-Actor, a pong message 84 | PingAware-Actor <- PongAware-Actor, a ping message 85 | PongAware-Actor <- PingAware-Actor, a pong message 86 | # .. more of these, total 10 87 | PingAware-Actor handled max Ping messages already. No further! # PingAware is terminated after this! 88 | Inquirer-Actor, checking if all inquiries done: Actor PingAware-Actor, So far 2, max-allowed 5 89 | Inquirer-Actor, checking if all inquiries done: Actor PongAware-Actor, So far 1, max-allowed 5 90 | thread 'tokio-runtime-worker' panicked at 'called `Result::unwrap()` on an `Err` value: SendErr', src/participants/object_can_inquire.rs:71:92 91 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 92 | # hangs ... 93 | ``` 94 | 95 | Because PingAware Actor has exchanged 10 messages with PongAware Actor, it **terminates** itself. At this point, PongAware Actor is living on, but is in a limbo because it never gets any further message from PingAware Actor. Inquirer Actor fails to send next inquiry to PingAware Actor because it doesn't exist any more and panics! 96 | 97 | Here's the partial output of another run: 98 | 99 | ```bash 100 | # .... truncated 101 | PingAware-Actor handled max Ping messages already. No further! 102 | Inquirer-Actor <-- PingAware-Actor, Inquiry report, total 3 so far! 103 | Inquirer-Actor, checking if all inquiries done: Actor PongAware-Actor, So far 2, max-allowed 5 104 | Inquirer-Actor, checking if all inquiries done: Actor PingAware-Actor, So far 3, max-allowed 5 105 | thread 'tokio-runtime-worker' panicked at 'called `Result::unwrap()` on an `Err` value: SendErr', src/participants/object_can_inquire.rs:71:92 106 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\ 107 | ``` 108 | 109 | PingAware Actor is terminating itself. However, Inquirer is processing PingAware Actor's last inquiry-report in its own time and then trying to send another inquiry to it. Obviously, it fails to do so and panics! 110 | 111 | Let us reorganize the code and prepare ourselves for such eventualities, so that the application behaves predictably. 112 | 113 | ### Reorganisation 114 | 115 | ##### Set limits at the start of the application 116 | 117 | There are two limits we are posing on the application: (a) maximum number of inquiry messages to be exchanged between Inquirer < -- > PingAware and Inquirer < -- > PongAware Actors and (b) maximum of number of Ping messages that the PingAware Actor responds to. Instead of hard-coding these values, we set them in the `main` at the beginning (ideally, we should pass in those values at the command-line but let's keep that for another time): 118 | 119 | ```rust 120 | let start_arguments = MaxLimitArguments { 121 | mx_inquiry_allowed: 5u16, // <-- how many inquiries 122 | mx_ping_messages_exchanged: 10u16 // <-- how many PING messages to respond to 123 | }; 124 | ``` 125 | 126 | ##### Use macro to send messages and return errors early 127 | 128 | ractor provides a macro called `cast` (more [here]([cast in ractor - Rust](https://docs.rs/ractor/latest/ractor/macro.cast.html))) to handle the act of sending a message. We will replace all explicit `send_message` call with this macro. Additionally, we will *return early (using '?' operator)* when an error occurs, in keeping with Rust's norm. 129 | 130 | ```rust 131 | myself.cast(PingPongMessage::CleanupIfAllDone)?; 132 | ``` 133 | 134 | ##### Give Actors their own names 135 | 136 | This sounds trivial but it is important to give user-defined and easily-readable names to each Actor that is created. This small step goes a long way in tracing and monitoring in non-trivial applications producing, working with and destroying large number of Actors: 137 | 138 | ```rust 139 | // PingAware Actor 140 | Actor::spawn( 141 | Some(String::from("PingAware-Actor")), // <-- Name! 142 | CanRecognizePing, 143 | PingAwareArguments(start_arguments.mx_ping_messages_exchanged) 144 | ).await 145 | .expect("Failed to start actor"); 146 | ``` 147 | 148 | ##### Initialize the Inquirer Actor with references to other two Actors 149 | 150 | As per the application's scope, the Inquirer Actor needs to know which other two Actors it has to interact with. Therefore, those two Actors (namely Ping | Pong Aware) have to pre-exist! So, it is alright to initialize the Inquirer Actor with the `ActorRef` of Ping | Pong Actors, instead passing these through messages (we did that in the previous tutorial): 151 | 152 | ```rust 153 | // In ./configuration/arguments.rs 154 | pub struct InquirerArguments { 155 | pub mx_inquiry_allowed: u16, 156 | pub ping_actor_to_inquire: ActorRef, 157 | pub pong_actor_to_inquire: ActorRef, 158 | } 159 | 160 | // ... 161 | 162 | // In ./participants/object_can_inquire.rs 163 | impl Actor for CanInquire { 164 | type Msg = PingPongMessage; 165 | type State = PingPongCountInquiry; 166 | // No startup arguments for actor initialization, at the moment 167 | type Arguments = InquirerArguments; 168 | 169 | async fn pre_start(&self, myself: ActorRef, start_arguments: InquirerArguments) -> Result { 170 | Ok(PingPongCountInquiry:: new ( 171 | start_arguments.mx_inquiry_allowed, 172 | start_arguments.ping_actor_to_inquire, 173 | start_arguments.pong_actor_to_inquire 174 | )) 175 | } 176 | // ... 177 | 178 | // In ./main.rs 179 | let (actor_ref_inquire, actor_handle_inquire) = 180 | Actor::spawn( 181 | Some(String::from("Inquirer-Actor")), 182 | CanInquire, 183 | InquirerArguments { 184 | mx_inquiry_allowed: start_arguments.mx_inquiry_allowed, 185 | ping_actor_to_inquire: ping_actor_ref.clone(), 186 | pong_actor_to_inquire: pong_actor_ref.clone() 187 | } 188 | ).await 189 | .expect("Failed to start actor"); 190 | // ... 191 | ``` 192 | 193 | ##### Initialize PingAware Actor with the max PING messsages to handle 194 | 195 | This is straightforward: 196 | 197 | ```rust 198 | // ./participants/object_can_understand_ping.rs 199 | pub struct ControlParameters { 200 | pub max_ping: u16, 201 | pub so_far_ping: u16, 202 | pub last_ping_serial: u16 203 | } 204 | 205 | // .. 206 | async fn pre_start(&self, myself: ActorRef, args: PingAwareArguments) 207 | -> Result { 208 | Ok(Parameters::ControlParameters { 209 | max_ping: args.0, 210 | so_far_ping: 0, 211 | last_ping_serial: 0 212 | }) 213 | } 214 | 215 | // .. 216 | ``` 217 | 218 | ##### Prevent overflowing when keeping count of responses to inquiries 219 | 220 | This is straightforward as well: 221 | 222 | ```rust 223 | // In pingpong_inquiry/inquiry_block.rs 224 | pub fn track_reports_of_inquiry(&mut self, actor_name: &str) -> u16 { 225 | let may_be_an_actor = self.actor_trackers.get_mut(actor_name).unwrap(); 226 | // Prevent unneccessary increment; no overflowing allowed 227 | if may_be_an_actor.1 < self.max_inquiry_reports_expected { 228 | may_be_an_actor.1 += 1u16; 229 | } 230 | may_be_an_actor.1 231 | } 232 | ``` 233 | 234 | ##### Give PongAware Actor a chance to stop itself 235 | 236 | PongAware Actor has a predicament. It responds to *Inquirer* Actor, with InquiryReport messages. It talks to PingAware Actor by sending *Ping* messages and receiving *Pong* messages. Now, either (or both) of Inquirer and PingAware may decide to stop themselves, and the PongAware Actor will not come to know of their disappearance, unless it tries to send another message to them. This means, it may keep waiting for the next message which never arrives! 237 | 238 | Essentially, there are 2 ways in which an Actor can be stopped gracefully: ( a ) send a special message to it or ( b ) let it have its own logic to decide. In this particular case, we will go for the first. 239 | 240 | -------------------------------------- 241 | 242 | An Actor can be _killed_ also, but that is not exactly a graceful stoppage. We are not considering that here. 243 | 244 | --------------------------------------------- 245 | 246 | PongAware has no information about the existence of the other two Actors. Till either of them sends a message, it sits idle. But, for how long? Not for an indefinite period. 247 | 248 | It can send itself a reminder though, after a certain predefined duration. When the reminder arrives, it assumes that other two Actors are unlikely to knock at its door and it decides to stop itself. 249 | 250 | How do we do this? 251 | 252 | An Actor can use `ractor`'s facilities to set a reminder for itself: 253 | 254 | ```rust 255 | let reminder_handle = 256 | ractor::time::send_after( 257 | Duration::from_millis(10), // <-- when 258 | myself.get_cell(), // <-- whom 259 | || { 260 | PingPongMessage::NoInquiryReportReceivedSince( 261 | Duration::from_millis(10) 262 | ) // <-- what 263 | } 264 | ); 265 | ``` 266 | 267 | `ractor` will send a `NoInquiryReportReceivedSince` message to the Actor after (*at least*, more accurately) 10 milliseconds. 268 | 269 | On receipt of this message, PongAware Actor decides that sufficient time has passed since it has heard from any of the other 2 Actors and that it is time to stop its own operation. 270 | 271 | What about cascaded reminders, when one reminder is set even though the previous reminder is still brewing? 272 | 273 | Well, that is troublesome because the PongAware Actor will not be able differentiate between two reminders. Therefore, the preceding reminder has to taken off and let the latest be the only one alive. 274 | 275 | When we initialize PongActor, we leave a place for an eventual reminder, in the `state`: 276 | 277 | ```rust 278 | async fn pre_start(&self, myself: ActorRef, _: ()) 279 | -> Result { 280 | // first: count of Pong messages; 281 | // second: running serial for inquiry reports; 282 | // third: placeholder for a reminder 283 | Ok((0u16, 0u16,None)) 284 | } 285 | ``` 286 | 287 | When we are about to set a reminder, we should check if one exists and if so, then remove it: 288 | 289 | ```rust 290 | // Remember, that we initialize this Actor with no reminder! 291 | // Extract it! 292 | let (_, _, may_be_handle) = counts; // the 'state' 293 | 294 | // It is possible that that a reminder set earlier is still alive. 295 | if let Some(existing_handle) = may_be_handle { 296 | existing_handle.abort(); // <-- remove 297 | } 298 | *counts = 299 | (counts.0,counts.1,Some(reminder_handle)); // <-- install afresh! 300 | ``` 301 | 302 | When the reminder arrives, the PongAware Actor stops itself: 303 | 304 | ```rust 305 | PingPongMessage::NoInquiryReportReceivedSince(duration) => { 306 | // .. 307 | myself.stop(None); // <-- stops itself! 308 | }, 309 | ``` 310 | 311 | #### The Inquirer needs to know if any further inquiry is of any use 312 | 313 | Here is the situation, explained earlier in section "What happens first", once more: 314 | 315 | The Inquirer Actor initiates a conversation: it sends an `Inquiry` to both of Ping | Pong Aware Actors and receives `InquiryReport` from them. However, these two targets of inqury also converse between themselves, and may decide to stop based on a condition that is unknown to the Inquirer. In other words, the Inquirer is likely to fail to send messages to them, because of a condition that is beyond its knowledge. This is a key observation. 316 | 317 | Till the time it tries to send a message to Ping | Pong Aware and fails, the Inquirer cannot decide if it makes sense to send further messages. Furthermore, recall that the PingAware Actor may have been gone (its own decision) but the PongAware may still be alive. The Inquirer has to deal with this partial availability too. 318 | 319 | The Inquirer tracks the number of responses from either of these two Actors, in a Map: 320 | 321 | ```rust 322 | // In src/conversation/pingpong_vocabulary.rs 323 | pub struct InquiryReportTracker (pub ActorRef, pub u16); 324 | 325 | // .. 326 | pub struct PingPongCountInquiry { 327 | max_inquiry_reports_expected: u16, 328 | actor_trackers: HashMap, 329 | } 330 | ``` 331 | 332 | and, makes use of some helper methods: 333 | 334 | ```rust 335 | // In src/conversations/pingpong_vocabulary.rs 336 | pub fn are_max_inquiries_done_with_target(&self, actor_name: &str) -> bool { 337 | self.actor_trackers.get(actor_name) 338 | .is_some_and(|may_be_an_actor| 339 | may_be_an_actor.1 == self.max_inquiry_reports_expected 340 | ) 341 | } 342 | 343 | pub fn track_reports_of_inquiry(&mut self, actor_name: &str) -> u16 { 344 | let may_be_an_actor = self.actor_trackers.get_mut(actor_name).unwrap(); 345 | // Prevent unneccessary increment; no overflowing allowed 346 | if may_be_an_actor.1 < self.max_inquiry_reports_expected { 347 | may_be_an_actor.1 += 1u16; 348 | } 349 | may_be_an_actor.1 350 | } 351 | 352 | pub fn remove_target_actor(&mut self, actor_name: &str) -> u16 { 353 | if let Some(may_be_existing_actor) = self.actor_trackers.remove(actor_name) { 354 | may_be_existing_actor.1 355 | } 356 | else { 357 | 0 as u16 358 | } 359 | } 360 | // .. some more 361 | ``` 362 | 363 | When an `InquiryReport`` arrives from either of the Actors, the Inquirer Actor checks the conditions: 364 | 365 | ```rust 366 | // In src/participants/objects_can_inquire.rs 367 | PingPongMessage::InquiryReport(serial_received, from_actor) => { 368 | let sender_name = from_actor.get_name().unwrap(); 369 | let last_report = state.track_reports_of_inquiry(sender_name.as_str()); 370 | 371 | if !state.are_max_inquiries_done_with_target(&sender_name) { // Cond[1] 372 | // failing to send the next message means that the target may 373 | // not be alive anymore 374 | if let Err(_) = // Cond[2] 375 | from_actor.cast(PingPongMessage::Inquire(myself.clone())) { 376 | // We are not interested to track this target anymore! 377 | state.remove_target_actor(&sender_name); 378 | myself.cast(PingPongMessage::CleanupIfAllDone)?; 379 | } 380 | else { // Cond [2] 381 | send_after( 382 | Duration::from_millis(10), 383 | myself.get_cell(), 384 | || { PingPongMessage::CheckAreTargetsAlive } 385 | ); 386 | } 387 | } 388 | else { // Cond[1] 389 | // Maximum inquiries are made. Now, the target can be dismissed. 390 | state.remove_target_actor(&sender_name); 391 | myself.cast(PingPongMessage::CleanupIfAllDone)?; 392 | } 393 | ``` 394 | 395 | - Cond[1] is straightforward. The Inquirer is checking if all inquires (pre-configured) made to this target Actor have been responded to. If those have been, then it is alright to dismiss that target Actor (Ping Aware or Pong Aware); the `else` block does the needful. 396 | 397 | - Cond[2] is to check if the Inquirer can send the next message to the (same) target. If it fails, then it is assumed that the target Actor is non-existent (e.g., the PingAware Actor has decided to stop itself, meanwhile). in that case, the target can be dismissed because there is no point in trying to contact it any further. 398 | 399 | The so-called _happy path_ can follow this. Remember though, that our intention is to make the application's behaviour predictable. Everytime we run it, we want it finish gracefully (no hanging etc.). 400 | 401 | #### Unpredictability of existence of target Actors 402 | 403 | Let us revisit the interaction between the Inquirer and Ping | Pong Aware Actors. Very importantly, the Ping | Pong Aware Actors **respond** to the Inquirer; by themselves, they send nothing to the Inquirer. Therefore, the only way for the Inquirer to know if they exist, is to try and send another `Inquiry` message to them and watch if that attempt, fails! 404 | 405 | **Possibility 1** 406 | 407 | Ping and Pong Aware Actors converse between themselves. Once it has responded to a pre-defined number of Ping messages, the Ping Aware Actor stops itself. This entire saga happens completely without Inquirer's knowledge! 408 | 409 | **Possibility 2** 410 | 411 | Pong Aware Actor sets up a timer for itself which it uses to guess if the Ping Aware Actor is not going to respond any more. Once the Pong Aware Actor infers the Ping Aware Actor's disappearance, it stops itself. Again, the Inquirer Actor never knows, when the Pong Aware goes out of existence. 412 | 413 | **Possibility 3** 414 | 415 | The Inquirer receives a few responses from Ping Aware Actor and suddenly. it receives nothing more. Because the Inquirer sends the next inquiry *only after* receving the previous response, it will not be in a position to send any further message to the Ping Aware Actor. It will be in a limbo: waiting for something which never comes! 416 | 417 | There exist a few other similar scenarios. In all of these, the Inquirer may remain undecided and non-moving for ever. The only way for it not to be in that stupor, is to send a message to itself through a timer! 418 | 419 | Message to self `CheckTargetsAreAlive` : 420 | 421 | On receipt of this message, the Inquirer Actor tries to ascertain if 2 target Actors are alive and if not, then it tries to wait for some more time and then, check again. 422 | 423 | ```rust 424 | // In src/participants/object_can_inquire.rs 425 | PingPongMessage::CheckAreTargetsAlive => { 426 | // get the remaining targets 427 | let remaining: Vec<_> = 428 | state 429 | .get_remaining_targets() 430 | .map(|next_t| &next_t.0) 431 | .cloned() 432 | .collect(); 433 | 434 | // Try to reach, each of the targets. On failure, take that off the 435 | // tracker. 436 | for next_target in remaining.iter() { 437 | if let Err(reason) = 438 | next_target.cast(PingPongMessage::Inquire(myself.clone())) 439 | { 440 | state.remove_target_actor( 441 | next_target.get_name().unwrap().as_str() 442 | ); 443 | } 444 | } 445 | 446 | // Send a message to itself, to clean itself 447 | myself.cast(PingPongMessage::CleanupIfAllDone)?; 448 | } 449 | ``` 450 | 451 | Message to self `CleanUpIfAllDone` : 452 | 453 | On receipt of this message, the Inquirer Actor confirms that none of the targets is alive any more and if so, stops itself. 454 | 455 | ```rust 456 | // In src/participants/object_can_inquire.rs 457 | PingPongMessage::CleanupIfAllDone => { 458 | if state.no_inquiry_target_still_remaining() { 459 | myself.stop(Some(String::from("All inquiries done!"))); 460 | } 461 | else { 462 | // At least one target still seems to exist. 463 | // Check again after some time. 464 | send_after( 465 | Duration::from_millis(20), 466 | myself.get_cell(), 467 | || { PingPongMessage::CheckAreTargetsAlive } 468 | ); 469 | } 470 | } 471 | ``` 472 | 473 | One message helps create a condition and another message decides on the condition! In message-driven design, this is a useful pattern. 474 | 475 | Because of such an arrangement, the Inquirer is guaranteed to be informed of non-existence of the two target ( Ping | Pong Aware ) Actors. It gets a chance to stop itself confidently and cleanly. 476 | 477 | The outputs of two successive runs may be very different but, the application will eventually terminate cleanly! Every time! 478 | 479 | #### Asynchronousness and determinable behaviour 480 | 481 | Let's refer to the section "That matter of asynchronous communication" at the beginning. Now that we have coded the behaviour, the implication of what that section says, is clearer. 482 | 483 | The asynchronous messaging leads to situations where the *send* action, the 484 | *processing* action and the *receive* action are handled at different times 485 | and most possibly by different threads (therefore, not in a predictable 486 | sequence). Moreover, an Actor can receive messages in random order. 487 | Crucially, its behaviour is dependent on the order of processing of the 488 | messages. 489 | 490 | But even then, the Actors must be designed in such a way, that their 491 | behaviour remains deterministic. The **design** is the operative word here. Building an Actor-based application is primarily about modeling the whole set of interaction between Actors. Because each Actor is a single-threaded structure, we can discount the whole aspect of thread-contention, thread-race and resource-locking and focus on **what must happen when a message arrives** ! 492 | 493 | ------------------------------------------------------------------------------------ 494 | -------------------------------------------------------------------------------- /README.chapter-1.md: -------------------------------------------------------------------------------- 1 | # rust-ractor-tutorial-1 2 | Part 1 of a tutorial that aims to elaborate Actor-based programming using 3 | 'ractor' ( Rust library ) 4 | 5 | All the code is in './ractor-tutorial-chapter-1/src'. In order to run the 6 | code, move to the aforementioned directory ( `ractor-tutorial-chapter-1` ) 7 | and do a `cargo run`. 8 | 9 | Code structure: 10 | 11 | ![Alt text](./project-structure-chapter-1.png "Tutorial-1 structure at a 12 | glance") 13 | 14 | ## Tutorial 15 | 16 | ### Prelude 17 | 18 | The idea of using Actors as the underpinning of a non-trivial application interests me a lot. Some years back, along with learning [Scala](https://www.scala-lang.org/) programming language, I also began to dabble with [Akka](https://akka.io/), the Actor-based library from [Lightbend](https://www.lightbend.com/). Thereafter, I had the opportunity to build production-level, applications using Scala (and Java) and Akka, a couple of times. Happily, they are still in use. :smiley: 19 | 20 | Since then, I have remained a very keen enthusiast to study and use the Actor-based approach while designing asynchronous yet performance applications, whenever possible. 21 | 22 | When I began to learn Rust - specifically its thread-model and its approach to asynchronous programming - I was curious to find out if Rustaceans had explored the world of Actors already and how. A search brought forth an endless list of crates - here is that famous compilation on [Reddit](https://www.reddit.com/r/rust/comments/n2cmvd/there_are_a_lot_of_actor_framework_projects_on/) - and I was at my wit's end! The sheer number was unsettling. 23 | 24 | All of a sudden, my eyes fell on a post on `ractor`, which said to have been inspired by `erlang` and its `gen_server`. I didn't know _erlang_ , the language but armed with the little knowledge collected from reading about its *processes* and *messages* - along with whatever I had learnt from Akka-days, I decided to find more. 25 | 26 | 27 | When I searched for any conversation on `ractor` on Stackoverflow, it reported **zero** entries!! 28 | 29 | I decided to go through their github repo and the companion website and try to understand the basic principles on which `ractor` was based. Whatever I understood, could be inputs to a tutorial, I thought. This blog series is a result of that attempt. 30 | 31 | ### Actors, a tl;dr 32 | 33 | This tutorial is not meant to expound on `Actors`. Numerous resources exist, including research papers, books, articles, deep and wide discussions, example and prod-ready implementations, Wikipedia entries, QnA etc. I am going to skip all of that (leaving to you, where to start and how far to go) and simply state, that : 34 | 35 | * An Actor is a typical Object (from the OO world) that doesn't expose public methods for the world to invoke. Instead, it advertises messages that it can *respond* to. 36 | * These messages are asynchronous; also called fire-and-forget! The sender Actor is not sure if the message is received by the target Actor and when. Because there is no public method, there is no return value. The target Actor, may send a message to the sender Actor, if that is the protocol, and that is the only way to have a result in return. 37 | * An Actor deals with messages, one at at time. No two messages are handled at the same time. Effectively, every Actor processes messages, in a single-threaded manner. 38 | 39 | ### Prepare to use `ractor` 40 | 41 | My *Cargo.toml* lists the following dependencies: 42 | 43 | ```toml 44 | ractor = "0.8.4" 45 | async-trait = "0.1" 46 | tokio = { version = "1", features = ["rt", "time", "sync", "macros", "rt-multi-thread", "signal"] } 47 | ``` 48 | 49 | 50 | ### How to make an Actor 51 | 52 | To begin with, we need an object of an user-defined type. This object, otherwise, would have one or more public(ly callable) methods. In Rust, we use a `struct` for this purpose. 53 | 54 | Because we want this object behave as an Actor, we *Actorify* it by implemnting an `ractor::Actor` trait for struct. For example: 55 | 56 | ```rust 57 | use ractor::{Actor, ActorRef}; 58 | struct SomethingThatCanBehaveAsAnActor ; 59 | 60 | impl Actor for SomethingThatCanBehaveAsAnActor { 61 | type Msg = Message; 62 | type State = u8; 63 | type Arguments = (); 64 | 65 | // ..... 66 | } 67 | ``` 68 | 69 | There are three *type aliase*s here! 70 | 71 | * `Msg` denotes the type of message(s) that this Actor understands and can take action on. Messages of any other tyoe, if sent to this actor, is simply ignored. 72 | * `State` denotes any kind of data that this actor wants to store between processing messages. 73 | * `Arguments` denotes values that should passed to the actor while it is being initialized. 74 | 75 | In order to understand how exactly are these to be used, let's expand the sample Pingpong Actor that is shared at the `ractor` github's README. 76 | 77 | ### Conversations between two Actors 78 | 79 | We intend to devise two Actors, who exchange messages between themselves. One Pings (sends a Ping message to the other) and then in response, another Pongs (sends a Pone message to the Ping-sender). The conversation is comprised of **only** these two messages. Neither of the actors understands any other message. 80 | 81 | ### Overview of structure of an Actor 82 | 83 | Any Object can decide to behave like an Actor. Thereafter, the object wears the capabilty of reacting to a message received.Why? Because, behaviorally, an Actor is equipped with the capabillity to respond to a message (one or more) that it is aware of. 84 | 85 | Almost always, we refer to the object itself as an **Actor**, interachangebly. However, it is important to distinguish between a regular object - having its own data and processing logic - and its 'wore-on' Actor-like behavior. 86 | 87 | Once it comes into being, such an *Actorified* object waits patiently till it receives such a message. If the message is unknown, the Actor is free not to take any action. 88 | 89 | Importantly, the Actor handles these messages, **one at a time**! Put it simply, even if a bunch of messages jostle its door, the Actor will process them **one at a time**. Obviously, while it is handling one message, other messages wait patiently. 90 | 91 | So, there are three main pieces that go together to bring an Actor to life: 92 | * A user-defined type (viz. `struct` ) that wants to behave like an Actor, in addition to its own behaviour, if any. 93 | * A trait named `Actor` (of type `ractor::actor::Actor` ) which needs specific implementation for the user-defined type. 94 | * A bunch of (1 or more) Messages that this Actor understands and can take action on. 95 | 96 | ### Build a Actor that can handle a _PING_ 97 | 98 | We want to build an Actor that can 99 | 1. understand a PING message, meaning PING is a part of its vocabulary. 100 | 2. respond to the sender with a PONG message, meaning PONG is a part of its vocabulary too. 101 | 102 | We bring in, an user-defined entity named `CanRecognizePing` , a zero-element `struct`. 103 | 104 | ```rust 105 | pub struct CanRecognizePing; 106 | ``` 107 | 108 | In order to behave as an Actor, this needs to implement the _Actor_ trait: `ractor::actor`. 109 | 110 | ```rust 111 | impl Actor for CanRecognizePing { 112 | // ... 113 | } 114 | ``` 115 | It claims to understand a PING message. Well, the following is that `Ping` message :smiley: : 116 | 117 | ```rust 118 | pub enum Message { 119 | Ping, 120 | } 121 | ``` 122 | We have the Actor and a Message. How does the Actor deal with the message? 123 | 124 | The `Actor` trait stiputales that `CanRecognizePing` must provide the logic to handle any message - *Ping* in this case - in the body of a method named `handle`: 125 | 126 | ```rust 127 | async fn handle( 128 | &self, 129 | myself: ActorRef, 130 | message: Self::Msg, 131 | state: &mut Self::State, 132 | ) -> Result<(), ActorProcessingErr> { 133 | 134 | match message { 135 | Message::Ping => println!("Received a ping message"), 136 | _ => println!("Unknown message {:?} received!", message), 137 | } 138 | 139 | Ok(()) 140 | } 141 | ``` 142 | Let us understand what the method expects and does. 143 | 144 | `&self` denotes the implementation itself - the CanRecognizePing. This is usual first parameter, in the signature of a *struct*'s method in Rust. 145 | 146 | `myself` denotes the Actor. At this point, it is sufficient for us to take this as the *self* of the Actor itself. The *struct* has an *actor* avatar, as it were, and this refers to that inner avatar. 147 | 148 | `message` denotes the type of messages that the Actor claims to understand. When it receives one, the Actor can handle that. 149 | 150 | `state` denotes the Actor's internal data structure and contents. We will skip it for the time being. 151 | 152 | 153 | Interestingly, the method can either return no value (absence of a value) or an `ActorProcessingError`. The significance of this will be clearer later. 154 | 155 | Inside the method, we are checking if the message that has been received, is some message that the Actor claims to understand. The logic of `CanRecognizePing` states that it indeed understands a PING message. On receipt of it, the Actor simply prints an acknowledgement and returns. For any other message, it prints a complaint! That is all about our `CanRecognizePing`. 156 | 157 | In order to see it in action, we have to ( a ) bring it into life and then ( b ) send a PING message to it. Let's do that. 158 | 159 | We create an Actor, using a special method called `Actor::spawn()`. 160 | 161 | ```rust 162 | let (ping_actor_ref, actor_handle) = Actor::spawn(None, CanRecognizePing, ()) 163 | .await 164 | .expect("Failed to start CanRecognizePing"); 165 | ``` 166 | 167 | The key parameter to the `spawn` funtion is `CanRecognizePing`. We are asking **ractor**'s runtime to create an Actor which is an incarnation of the `struct` named `CanRecognizePing`. The resultant pair, has two parts. The first is what we call a reference to the Actor of type `ActorRef`. For all interactions with the actor, this reference is to be used. We will ignore the second part, till later. 168 | 169 | Then, we send a PING message to the actor we have 170 | 171 | ```rust 172 | ping_actor_ref.send_message(PingPongMessage::Ping).unwrap(); 173 | ``` 174 | 175 | This is the complete `main` function: 176 | 177 | ```rust 178 | #[tokio::main] 179 | async fn main() { 180 | let (ping_actor_ref, actor_handle) = Actor::spawn(None, CanRecognizePing, ()).await.expect("Failed to start actor"); 181 | 182 | ping_actor_ref.send_message(PingPongMessage::Ping).unwrap(); 183 | 184 | actor_handle.await.expect("Actor failed to exit cleanly"); 185 | } 186 | ``` 187 | 188 | Ignore the last line, for the time being. 189 | 190 | On being run, the program's output is: 191 | 192 | ```bash 193 | CanRecognizePing has Received a ping message 194 | ^C 195 | ``` 196 | 197 | After printing that message, the program does nothing else. We stop it forcibly. 198 | 199 | ### Conversation between two actors 200 | 201 | Sending a ping message, to an actor only once, like we have done in the `main` function above, is of no use really. In order to receive as well as send messages, an Actor has to do more. In that case, two Actors can exchange messages and can have a meaningful conversation. 202 | 203 | We have a `CanRecognizePing` which can handle a PING message. Let's create its counterpart: `CanRecognizePong` which can handle a PONG message. 204 | 205 | Adding a PONG message to the vocabulary of the actors is easy. 206 | 207 | ```rust 208 | #[derive(Debug, Clone)] 209 | pub enum PingPongMessage { 210 | Ping, 211 | Pong, 212 | } 213 | ``` 214 | 215 | A `CanRecognizePong` is exactly like a `CanRecognizePing`; the name differs, that is all. 216 | 217 | ```rust 218 | pub struct CanRecognizePong; 219 | ``` 220 | 221 | The `handle` method is quite similar too. 222 | 223 | ```rust 224 | async fn handle( 225 | &self, 226 | myself: ActorRef, 227 | message: Self::Msg, 228 | state: &mut Self::State, 229 | ) -> Result<(), ActorProcessingErr> { 230 | 231 | match message { 232 | PingPongMessage::Pong => println!("CanRecognizePong has received a pong message"), 233 | _ => println!("Unknown message {:?} received!", message), 234 | } 235 | Ok(()) 236 | } 237 | ``` 238 | 239 | Let's create a `CanRecognizePong` and send a message to it. Thi is the modified `main` method: 240 | 241 | ```rust 242 | #[tokio::main] 243 | async fn main() { 244 | let (ping_actor_ref, actor_handle_ping) = Actor::spawn(None, CanRecognizePing, ()).await.expect("Failed to start actor"); 245 | 246 | let (pong_actor_ref, actor_handle_pong) = Actor::spawn(None, CanRecognizePong, ()).await.expect("Failed to start actor"); 247 | 248 | ping_actor_ref.send_message(PingPongMessage::Ping).unwrap(); 249 | pong_actor_ref.send_message(PingPongMessage::Pong).unwrap(); 250 | 251 | actor_handle_ping.await.expect("Actor failed to exit cleanly"); 252 | actor_handle_pong.await.expect("Actor failed to exit cleanly"); 253 | } 254 | ``` 255 | 256 | Nothing unexpected appears in the output: 257 | 258 | ```bash 259 | CanRecognizePing has Received a ping message 260 | CanRecognizePong has received a pong message 261 | ^C 262 | ``` 263 | 264 | On receipt of a message, each of them responds as expected. Yet, they are not *conversing* between themselves. 265 | 266 | In order to converse between themselves, an Actor has to know who has sent in a message, so that the former can respond to the latter. One way to send a message to Actor, is by using its `ActorRef`. If a message carries the `ActorRef` of the sender, it is simple for the receiver to send a message in return. To facilitate this, we need to modify the messages: 267 | 268 | ```rust 269 | pub enum PingPongMessage { 270 | Ping(ActorRef), 271 | Pong(ActorRef), 272 | } 273 | ``` 274 | 275 | Then, we need to modify the corresponding _handler_ s, too: 276 | 277 | ```rust 278 | // CanRecognizePing 279 | async fn handle( 280 | &self, 281 | myself: ActorRef, 282 | message: Self::Msg, 283 | state: &mut Self::State, 284 | ) -> Result<(), ActorProcessingErr> { 285 | 286 | match message { 287 | PingPongMessage::Ping(sender) => { 288 | println!("CanRecognizePing has Received a ping message"); 289 | sender.send_message(PingPongMessage::Pong(myself)).unwrap(); 290 | }, 291 | _ => println!("Unknown message {:?} received!", message), 292 | } 293 | Ok(()) 294 | } 295 | 296 | // CanRecognizePong 297 | async fn handle( 298 | &self, 299 | myself: ActorRef, 300 | message: Self::Msg, 301 | state: &mut Self::State, 302 | ) -> Result<(), ActorProcessingErr> { 303 | 304 | match message { 305 | PingPongMessage::Pong(sender) => { 306 | println!("CanRecognizePong has received a pong message"); 307 | sender.send_message(PingPongMessage::Ping(myself)).unwrap(); 308 | }, 309 | _ => println!("Unknown message {:?} received!", message), 310 | } 311 | Ok(()) 312 | } 313 | ``` 314 | 315 | Finally, the `main` is modified to begin the conversation: 316 | 317 | ```rust 318 | #[tokio::main] 319 | async fn main() { 320 | let (ping_actor_ref, actor_handle_ping) = 321 | Actor::spawn( 322 | None, 323 | CanRecognizePing, 324 | () 325 | ).await.expect("Failed to start actor"); 326 | 327 | let (pong_actor_ref, actor_handle_pong) = 328 | Actor::spawn( 329 | None, 330 | CanRecognizePong, () 331 | ).await.expect("Failed to start actor"); 332 | 333 | ping_actor_ref.send_message(PingPongMessage::Ping(pong_actor_ref.clone())).unwrap(); 334 | pong_actor_ref.send_message(PingPongMessage::Pong(ping_actor_ref.clone())).unwrap(); 335 | } 336 | ``` 337 | 338 | The output is an infinite series of messages: 339 | 340 | ```bash 341 | CanRecognizePing has Received a ping message 342 | CanRecognizePong has received a pong message 343 | CanRecognizePong has received a pong message 344 | CanRecognizePing has Received a ping message 345 | CanRecognizePing has Received a ping message 346 | CanRecognizePong has received a pong message 347 | # .... continues like this 348 | ``` 349 | 350 | What causes this output? To explain that, let's take a closer look at the `handler`s. 351 | 352 | The `CanRecognizePing` receives a Ping message. It uses the `ActorRef` of the sender ( the `CanRecognizePong` ) to send a Pong message to the sender. The `CanRecognizePong` receives a Pong message. It uses the `ActorRef` of the sender ( the `CanRecognizePing` ) to send a Ping message to the sender. The `CanRecognizePing` responds and then, the `CanRecognizePong` responds too. They continue doing this, *ad infinitum*. 353 | 354 | Can we restrict them, to a maximum number of mutual responses? 355 | 356 | If we can keep a count of the receipts at each end, we can then decide when to stop responding. How do we go about it? 357 | 358 | ### State of an Actor 359 | 360 | Let us start with the definition of `Actor` trait: 361 | 362 | ```rust 363 | pub trait Actor: Sized + Sync + Send + 'static { 364 | type Msg: Message; // <--- kind of message this Actor understands 365 | type State: State; // <--- data that the actor holds inside during its life 366 | type Arguments: State; // <--- data that the actor uses during its initialization 367 | //... 368 | } 369 | ``` 370 | 371 | While implementing the Actor for `CanRecognizePing`, we had ignored `State` and `Arguments`. We will make use of `State` this time. 372 | 373 | Because we want to keep count of how many times a Ping message has been received so far, we need a counter. When the Actor comes into being, it is initialized to zero. Thereafter, every time the Ping message is handled, the counter is incremented and checked if it has become a certain predefined value. If it has, then the Actor should stop handling any further messages. Straightforward! 374 | 375 | Let us say, that this counter is an `u16`. Then, `CanRecognizePing` implements an `Actor` this way: 376 | 377 | ```rust 378 | #[async_trait::async_trait] 379 | impl Actor for CanRecognizePing { 380 | // An actor has a message type 381 | type Msg = PingPongMessage; // As before 382 | type State = u16; // <-- The 'type` of counter 383 | type Arguments = (); // Ignore this, for the time being 384 | // ... 385 | } 386 | ``` 387 | 388 | Now, the question is where do we initialize the counter? 389 | 390 | Actors have a lifecycle: as they are created, used, stopped and destroyed, they go through stages of the lifecycle. We will explore this topic later; for the time being, important is a stage named `pre_start()` ([more here](https://docs.rs/ractor/latest/ractor/actor/trait.Actor.html#tymethod.pre_start)). This method is *guaranteed to be called* by `ractor`'s runtime, *just after* an Actor is *created* ( `spawned` to be precise ). 391 | 392 | When we implement trait `Actor` for `CanRecognizePing`, we are allowed to provide necessary initialization steps, the key being a value for the `State` type: 393 | 394 | ```rust 395 | #[async_trait::async_trait] 396 | impl Actor for CanRecognizePing { 397 | type Msg = PingPongMessage; 398 | type State = u16; 399 | type Arguments = (); 400 | 401 | async fn pre_start(&self, myself: ActorRef, _: ()) -> Result { 402 | // We initialize the counter here, but there is no variable named 'count'. 403 | Ok(0u16) 404 | } 405 | // ... 406 | } 407 | ``` 408 | 409 | Initialization is done. Now, the use: 410 | 411 | The `CanRecognizePing` is going to use this `State`, when it handles the incoming Ping messages. `ractor`'s Actor runtime provides the value initialized in `pre_start()` function, as a parameter to `handle()` function: 412 | 413 | ```rust 414 | async fn handle( 415 | &self, 416 | myself: ActorRef, 417 | message_known_to_me: Self::Msg, 418 | count: &mut Self::State, // <-- count is the `State`, and is initialized to 0! 419 | ) -> Result<(), ActorProcessingErr> { 420 | // ... 421 | 422 | } 423 | ``` 424 | 425 | Alright. The `CanRecognizePing` possesses tool to keep track of *count* of messages received so far. For every message received, it increments this count. If the we want the Actor to stop processing any further messages beyond a limit - say, 10 - then the logic specifies that: 426 | 427 | ```rust 428 | match message { 429 | 430 | PingPongMessage::Ping(sender) => { 431 | println!("CanRecognizePing has Received a ping message"); 432 | if *count == 10 { // 433 | println!("Max Ping messages already handled. No further!"); 434 | } 435 | else { 436 | *count += 1; 437 | sender.send_message(PingPongMessage::Pong(myself)).unwrap(); 438 | } 439 | }, 440 | 441 | _ => println!("Unknown message {:?} received!", message), 442 | 443 | } 444 | ``` 445 | 446 | The output says that `CanRecognizePing` is indeed quite self-restricted now: 447 | 448 | ```rust 449 | # ... similar lines printed earlier 450 | CanRecognizePing has Received a ping message 451 | CanRecognizePong has received a pong message 452 | CanRecognizePing has Received a ping message 453 | CanRecognizePong has received a pong message 454 | CanRecognizePing has Received a ping message 455 | Max Ping messages already handled. No further! 456 | CanRecognizePing has Received a ping message 457 | Max Ping messages already handled. No further! 458 | ^C 459 | ``` 460 | 461 | Once the `CanRecognizePing` stops further processing, it doesn't send Pong messages to `CanRecognizePong` any more. As a result, the `CanRecognizePong` becomes silent as well. So, the conversation stops. 462 | 463 | The point to note is the storage of `State`: the `count`. It is not returned from the `handle()` method. Instead, the Actor itself remembers the changes made to the count, its state. This modified value is put to use when the next message arrives. This arrangement has implications, which will be clearer as we move forward. 464 | 465 | Let us look at the output above, again. The Actors stop conversing but they seem to live on, expecting another message to arrive,some time in the future; which of course, never comes! They have to be told to stop waiting forever. How? 466 | 467 | ### Starting and Stopping an Actor 468 | 469 | We have briefly referred to an Actor's life-cycle, earlier in this article. An Actor is brought into being, is let to process messages and then is ended (destroyed) releasing all the comouting resources it has been holding so far. 470 | 471 | There exists only one way to create an Actor. We have seen this earlier of course: 472 | 473 | ```rust 474 | let (ping_actor_ref, actor_handle_ping) = 475 | Actor::spawn( // <--- Creating a new Actor 476 | None, 477 | CanRecognizePing, 478 | () 479 | ) 480 | .await 481 | .expect("Failed to start actor"); 482 | ``` 483 | The first element of the tuple above, is an `ActorRef`. From this point onwards, It represents the 'Actor' object that has been created. All interactions with this object is done through this `ActorRef`. 484 | 485 | Once created, the Actor gets busy, waiting for a message to reach it, through its `handle()` function. On receipt of a message, it tries to identify it (through a `match` expression) and takes action accordingly. After that, it again begins its wait for the next message. This eternal wait for the next message comes to an end, when it is terminated and then, desytroyed. That is the end of its life cycle. 486 | 487 | How do we terminate the actor? 488 | 489 | By asking the Actor to **stop**! 490 | 491 | In order to stop an Actor, we need to call its `.stop()` method. Inside the `handle()` function, the Actor has access to its own `ActorRef`. It can use this `ActorRef`, to stop itself. 492 | 493 | ```rust 494 | // CanRecognizePing... 495 | match message { 496 | PingPongMessage::Ping(sender) => { 497 | println!("CanRecognizePing has Received a ping message"); 498 | if *count == 10 { // 499 | println!("Max Ping messages already handled. No further!"); 500 | // This actor decides to stop itself. 501 | myself.stop(Some(String::from("maximum number of ping messages received and processed"))); 502 | } 503 | else { 504 | *count += 1; 505 | sender.send_message(PingPongMessage::Pong(myself)).unwrap(); 506 | println!("Count {}, sending Pong in response once more", *count); 507 | } 508 | }, 509 | // .. 510 | } 511 | ``` 512 | 513 | Once it stops, the `CanRecognizePing` cases to exist. However, the `CanRecognizePong` is not aware of disappearance of its partner in conversation. Thus, it may still try to send another Ping message to the now-deceased `CanRecognizePing` and it will fail. 514 | 515 | ```rust 516 | // CanRecognizePong 517 | match message { 518 | 519 | PingPongMessage::Pong(sender) => { 520 | println!("CanRecognizePong has received a pong message"); 521 | sender.send_message(PingPongMessage::Ping(myself)).unwrap(); // <-- This may fail 522 | }, 523 | _ => println!("Unknown message {:?} received!", message), 524 | } 525 | ``` 526 | 527 | The output proves that `CanRecognizePong` panics, at that `unwrap()`: 528 | 529 | ```bash 530 | # more messages before this 531 | Count 10, sending Pong in response once more 532 | CanRecognizePing has Received a ping message 533 | Max Ping messages already handled. No further! 534 | CanRecognizePong has received a pong message 535 | thread 'tokio-runtime-worker' panicked at 'called `Result::unwrap()` on an `Err` value: SendErr', src/participants/pong_capable_actor.rs:44:68 536 | note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 537 | ``` 538 | 539 | We can of course, relieve it from its trouble, by dealing with the outcome of `send_message()`: 540 | 541 | ```rust 542 | // CanRecognizePong 543 | match message { 544 | 545 | PingPongMessage::Pong(sender) => { 546 | println!("CanRecognizePong has received a pong message"); 547 | if let Err(reason) = sender.send_message(PingPongMessage::Ping(myself.clone())) { 548 | println!("Error in CanRecognizePong while sending Ping message {:?}", reason); 549 | myself.stop(Some(String::from("No receiver. Stopping CanRecognizePong."))); 550 | } 551 | }, 552 | _ => println!("Unknown message {:?} received!", message), 553 | } 554 | ``` 555 | 556 | This time, the output is more conclusive: 557 | 558 | ```bash 559 | # more outut before this .... 560 | Count 10, sending Pong in response once more 561 | CanRecognizePing has Received a ping message 562 | Max Ping messages already handled. No further! 563 | CanRecognizePong has received a pong message 564 | Error in CanRecognizePong while sending Ping message SendErr 565 | ``` 566 | 567 | Both the Actors stop themselves, when conditions specific to them, are met. The program then terminates as expected. 568 | 569 | The code is in the `./src` directory: 570 | 571 | ![Alt text](./project-structure-chapter-1.png "Tutorial-1 structure at a 572 | glance") 573 | 574 | ------------------------------------- 575 | 576 | The order in which the messages are printed on the console ( *stdout* ) will almost certainly differ from what I have shown here. Even multiple executions on my machine don't produce them in the same order, every time. This has to do with the **asynchronous nature of intracommunication of Actors**. This is a key concept, required to structure an application around Actors. We will explore it in future tutorials. 577 | 578 | -------------------------------------- 579 | 580 | ### Final Note 581 | 582 | We have seen 583 | 584 | * What are the main components of an Actor-based system, namely 585 | - A User-defined compound type - a `struct` - which wants to behave like an Actor 586 | - A trait called `ractor::Actor` which infuses the `struct` above, with the characteristics an Actor 587 | - One or more messages which form the vocabulary of the Actors created 588 | * How to create Actors using `Ractor`'s facilities 589 | * How to provide implementation of Actor-like behavior to the `struct`-s mentioned above 590 | * How to let two Actors communicate with one another using the vocabulary stipulated 591 | * How to bring the live Actors to an end, gracefully. 592 | 593 | This is very simple set up, and this paves the way for the next tutorial ([here](./README.chapter-2.md)). 594 | 595 | ------------------------------------------------------------------------------ 596 | --------------------------------------------------------------------------------