├── .github └── workflows │ └── test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── benchmarks ├── Cargo.toml └── benches │ └── actors.rs ├── core ├── Cargo.toml └── src │ ├── channel.rs │ └── lib.rs ├── executors ├── Cargo.toml └── src │ └── lib.rs └── tokio ├── Cargo.toml └── src └── lib.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/cache@v2 20 | with: 21 | path: | 22 | ~/.cargo/registry 23 | ~/.cargo/git 24 | target 25 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: stable 29 | override: true 30 | components: rustfmt, clippy 31 | 32 | - name: Lint 33 | run: cargo clippy 34 | 35 | - name: Format 36 | run: cargo fmt -- --check 37 | 38 | - name: Test 39 | run: cargo test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .DS_Store -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "benchmarks", 4 | "core", 5 | "executors", 6 | "tokio", 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | \* \* \* EXPERIMENTAL \* \* \* 2 | 3 | # Stage 4 | [![Test](https://github.com/titanclass/stage/actions/workflows/test.yml/badge.svg)](https://github.com/titanclass/stage/actions/workflows/test.yml) 5 | 6 | A minimal [actor model](https://en.wikipedia.org/wiki/Actor_model) library targeting `nostd` [Rust](https://www.rust-lang.org/) 7 | consisting of just two traits and two types, and designed to run with any executor. 8 | 9 | ## Why are actors useful? 10 | 11 | Actors provide a stateful programming convenience for concurrent computations. Actors can only receive messages, send more messages, 12 | and create more actors. Actors are guaranteed to only ever receive one message at a time and can maintain their own state 13 | without the concern of locks. Given actor references, location transparency is also attainable where the sender of a message 14 | has no knowledge of where the actor's execution takes place (current thread, another thread, another core, another machine...). 15 | 16 | Actors are particularly good at hosting [Finite State Machines](https://en.wikipedia.org/wiki/Finite-state_machine), particularly 17 | [event-driven ones](http://christopherhunt-software.blogspot.com/2021/02/event-driven-finite-state-machines.html). 18 | 19 | ## Why Stage? 20 | 21 | Stage's core library `stage_core` provides a minimal set of types and traits required 22 | to sufficiently express an actor model, and no more. The resulting actors should then 23 | be able to run on [the popular async runtimes](https://rust-lang.github.io/async-book/08_ecosystem/00_chapter.html#popular-async-runtimes) available, including tokio and async-std. 24 | 25 | ## Inspiration 26 | 27 | We wish to acknowledge the [Akka](https://akka.io/) project as a great source of inspiration, with the authors of Stage 28 | having applied Akka over the years. Akka's goal is to, "Build powerful reactive, concurrent, and distributed applications more easily". 29 | We hope that Stage can be composed with other projects to achieve the same goals while using Rust. 30 | 31 | ## In a nutshell 32 | 33 | The essential traits and types are `Actor`, `ActorRef` and `ActorContext`. Actors need to run on something, and they are dispatched 34 | to whatever that is via a `Dispatcher`. Other crates such as `stage_dispatch_crossbeam_executors` are available to provide an 35 | execution environment. 36 | 37 | Actor declarations look like this: 38 | 39 | ```rust 40 | struct Greet { 41 | whom: String, 42 | } 43 | 44 | struct HelloWorld {} 45 | 46 | impl Actor for HelloWorld { 47 | fn receive(&mut self, context: &mut ActorContext, message: &Greet) { 48 | println!("Hello {}!", message.whom); 49 | } 50 | } 51 | ``` 52 | 53 | For Tokio, dispatcher and mailbox setup looks like the following. We use Tokio's preferred bounded channels being 54 | set to 10 pending system messages (unbounded channels are also available): 55 | 56 | ```rust 57 | let (command_tx, command_rx) = channel(10); 58 | let dispatcher = Arc::new(TokioDispatcher { command_tx }); 59 | ``` 60 | 61 | Each actor has its own queue of messages, named a "mailbox", and a channel is supplied by a `mailbox_fn` factory function. 62 | Again, we can use bounded or unbounded channels, and we use a bounded one with 100 pending actor-destined messages. This 63 | bound is generally higher for the actor than the dispatcher as the dispatcher tends to do less. 64 | 65 | ```rust 66 | let mailbox_fn = Arc::new(mailbox_fn(100)); 67 | ``` 68 | 69 | As an alternative to Tokio, we can use the `stage_dispatch_crossbeam_executors` work-stealing pool for 4 processors 70 | along with an unbounded channel for communicating with it and for each actor: 71 | 72 | ```rust 73 | let pool = crossbeam_workstealing_pool::small_pool(4); 74 | let (command_tx, command_rx) = unbounded(); 75 | let dispatcher = Arc::new(WorkStealingPoolDispatcher { pool, command_tx }); 76 | 77 | let mailbox_fn = Arc::new(unbounded_mailbox_fn()); 78 | ``` 79 | 80 | No matter what dispatcher is used, the rest of the code is the same. Sending a message to an actor looks like this: 81 | 82 | ```rust 83 | let system = ActorContext::::new( 84 | || Box::new(HelloWorld {}), 85 | dispatcher, 86 | mailbox_fn, 87 | ); 88 | 89 | system.actor_ref.tell(SayHello { 90 | name: "Stage".to_string(), 91 | }); 92 | ``` 93 | 94 | We are also able to perform request/reply scenarios using `ask`. For example, using tokio as a runtime: 95 | 96 | ```rust 97 | actor_ref.ask( 98 | &|reply_to| Request { 99 | reply_to: reply_to.to_owned(), 100 | }, 101 | Duration::from_secs(1), 102 | ) 103 | ``` 104 | 105 | The `ask` method of an actor reference takes a function that is responsible for producing a request 106 | with a `reply_to` actor reference. Asks always take a timeout parameter which, in the case of Tokio, 107 | may return an `Elapsed` error. 108 | 109 | For complete examples, please consult the tests associated with each dispatcher library. 110 | 111 | ## What about... 112 | 113 | ### Channels 114 | 115 | Actors here build on channels and associate state with the receiver. The type system is used 116 | to enforce actor semantics; in particular, requiring a single receiver so that an actor's 117 | state can be mutated without contention. 118 | 119 | ### async/await within an actor 120 | 121 | Using async/await (Futures) within an actor's `receive` method would permit calling out to async 122 | functions of other libraries. However, a danger here is that these async functions may block 123 | indefinitely as there is no contractual obligation to ever return (an issue for discussing the 124 | contractual obligations of async functions has been 125 | [raised on the Rust internals forum](https://internals.rust-lang.org/t/future-and-its-assurance-of-completion/14542)). 126 | Blocking would prevent an actor from processing other messages in its mailbox. 127 | 128 | Another argument here is that actors can be considered orthoganal to async/await. Actors make 129 | great state machines, and receiving commands, including ones to stop the state machine, should not 130 | be blocked from processing. 131 | 132 | Finally, async functions can call into actors by using the `ActorRef.ask` async method call. 133 | Therefore, async functions and actors are able to co-exist and potentially serve distinct use-cases. 134 | 135 | ### What about actor supervision? 136 | 137 | Actor model libraries often include supervisory functions, although this is not a requirement 138 | of the actor model per se. 139 | 140 | We believe that supervisory concerns should be external to Stage. However, we may need 141 | to provide the ability to watch actors so that supervisor implementations can be 142 | achieved. That said, we have found that trying to recover from unforeseen events may 143 | point to a design concern. In many cases, panicking and having the process die (and then 144 | restart given OS level supervision), or restarting an embedded device, will be a better 145 | course of action. 146 | 147 | ### What about actor naming? 148 | 149 | Many libraries permit their actors to be named. We see the naming of actors as an external 150 | concern e.g. maintaining a hash map of names to actor refs. 151 | 152 | ### What about distributing actors across a network? 153 | 154 | Stage does not concern itself directly with networking. A custom dispatcher should be able 155 | to be written that dispatches work across a network. 156 | 157 | ## Contribution policy 158 | 159 | Contributions via GitHub pull requests are gladly accepted from their original author. Along with any pull requests, please state that the contribution is your original work and that you license the work to the project under the project's open source license. Whether or not you state this explicitly, by submitting any copyrighted material via pull request, email, or other means you agree to license the material under the project's open source license and warrant that you have the legal authority to do so. 160 | 161 | ## License 162 | 163 | This code is open source software licensed under the [Apache-2.0 license](./LICENSE). 164 | 165 | © Copyright [Titan Class P/L](https://www.titanclass.com.au/), 2020 166 | -------------------------------------------------------------------------------- /benchmarks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmarks" 3 | version = "0.1.0" 4 | authors = ["Titan Class P/L"] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Benchmarking for Stage" 8 | homepage = "https://www.titanclass.com.au/" 9 | repository = "https://github.com/titanclass/stage" 10 | readme = "README.md" 11 | publish = false 12 | 13 | [dev-dependencies] 14 | criterion = {version = "0.3", features = ["async_tokio", "html_reports"]} 15 | crossbeam-channel = "0.5" 16 | executors = "0.8.0" 17 | stage_core = { path = "../core" } 18 | stage_dispatch_crossbeam_executors = { path = "../executors" } 19 | stage_dispatch_tokio = { path = "../tokio" } 20 | tokio = { version = "1", features = ["rt-multi-thread", "time"] } 21 | 22 | [[bench]] 23 | name = "actors" 24 | harness = false 25 | -------------------------------------------------------------------------------- /benchmarks/benches/actors.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, thread, time::Duration}; 2 | 3 | use criterion::{criterion_group, criterion_main, Criterion}; 4 | use crossbeam_channel::{bounded as cb_bounded, unbounded as cb_unbounded}; 5 | use executors::crossbeam_workstealing_pool; 6 | use stage_core::{Actor, ActorContext, ActorRef}; 7 | use stage_dispatch_crossbeam_executors::{ 8 | bounded_mailbox_fn as cbe_bounded_mailbox_fn, unbounded_mailbox_fn as cbe_unbounded_mailbox_fn, 9 | WorkStealingPoolDispatcher, 10 | }; 11 | use stage_dispatch_tokio::{ 12 | mailbox_fn as tk_mailbox_fn, unbounded_mailbox_fn as tk_unbounded_mailbox_fn, TokioDispatcher, 13 | TokioUnboundedDispatcher, 14 | }; 15 | use tokio::sync::mpsc::{channel as tk_channel, unbounded_channel as tk_unbounded_channel}; 16 | 17 | // Fixtures 18 | 19 | struct Request { 20 | reply_to: ActorRef, 21 | } 22 | struct Reply {} 23 | struct MyActor {} 24 | impl Actor for MyActor { 25 | fn receive(&mut self, _context: &mut ActorContext, message: &Request) { 26 | message.reply_to.tell(Reply {}) 27 | } 28 | } 29 | 30 | // Benchmarks 31 | 32 | fn send_messages_to_other_actors_bounded_cbe(c: &mut Criterion) { 33 | let pool = crossbeam_workstealing_pool::small_pool(4); 34 | let (command_tx, command_rx) = cb_bounded(1); 35 | let dispatcher = Arc::new(WorkStealingPoolDispatcher { pool, command_tx }); 36 | 37 | let mailbox_fn = Arc::new(cbe_bounded_mailbox_fn(1)); 38 | 39 | let dispatcher_thread_dispatcher = dispatcher.to_owned(); 40 | let _ = thread::spawn(move || dispatcher_thread_dispatcher.start(command_rx)); 41 | 42 | let my_actor = ActorContext::::new( 43 | || Box::new(MyActor {}), 44 | dispatcher.to_owned(), 45 | mailbox_fn.to_owned(), 46 | ); 47 | 48 | use stage_dispatch_crossbeam_executors::Ask; 49 | 50 | c.bench_function( 51 | "send messages to other actors using Crossbeam and Executors with bounded channels", 52 | |b| { 53 | b.iter(|| { 54 | my_actor.actor_ref.ask( 55 | &|reply_to| Request { 56 | reply_to: reply_to.to_owned(), 57 | }, 58 | Duration::from_secs(1), 59 | ) 60 | }); 61 | }, 62 | ); 63 | } 64 | 65 | fn send_messages_to_other_actors_unbounded_cbe(c: &mut Criterion) { 66 | let pool = crossbeam_workstealing_pool::small_pool(4); 67 | let (command_tx, command_rx) = cb_unbounded(); 68 | let dispatcher = Arc::new(WorkStealingPoolDispatcher { pool, command_tx }); 69 | 70 | let mailbox_fn = Arc::new(cbe_unbounded_mailbox_fn()); 71 | 72 | let dispatcher_thread_dispatcher = dispatcher.to_owned(); 73 | let _ = thread::spawn(move || dispatcher_thread_dispatcher.start(command_rx)); 74 | 75 | let my_actor = ActorContext::::new( 76 | || Box::new(MyActor {}), 77 | dispatcher.to_owned(), 78 | mailbox_fn.to_owned(), 79 | ); 80 | 81 | use stage_dispatch_crossbeam_executors::Ask; 82 | 83 | c.bench_function( 84 | "send messages to other actors using Crossbeam and Executors with unbounded channels", 85 | |b| { 86 | b.iter(|| { 87 | my_actor.actor_ref.ask( 88 | &|reply_to| Request { 89 | reply_to: reply_to.to_owned(), 90 | }, 91 | Duration::from_secs(1), 92 | ) 93 | }); 94 | }, 95 | ); 96 | } 97 | 98 | fn send_messages_to_other_actors_bounded_tk(c: &mut Criterion) { 99 | let rt = tokio::runtime::Runtime::new().unwrap(); 100 | let (command_tx, command_rx) = tk_channel(1); 101 | let dispatcher = Arc::new(TokioDispatcher { command_tx }); 102 | let mailbox_fn = Arc::new(tk_mailbox_fn(1)); 103 | 104 | let my_actor = ActorContext::::new( 105 | || Box::new(MyActor {}), 106 | dispatcher.to_owned(), 107 | mailbox_fn.to_owned(), 108 | ); 109 | 110 | let dispatcher_task_dispatcher = dispatcher.to_owned(); 111 | let _ = rt.spawn(async move { dispatcher_task_dispatcher.start(command_rx).await }); 112 | 113 | use stage_dispatch_tokio::Ask; 114 | 115 | c.bench_function( 116 | "send messages to other actors using Tokio with bounded channels", 117 | |b| { 118 | b.to_async(&rt).iter(|| async { 119 | my_actor.actor_ref.ask( 120 | &|reply_to| Request { 121 | reply_to: reply_to.to_owned(), 122 | }, 123 | Duration::from_secs(1), 124 | ) 125 | }); 126 | }, 127 | ); 128 | } 129 | 130 | fn send_messages_to_other_actors_unbounded_tk(c: &mut Criterion) { 131 | let rt = tokio::runtime::Runtime::new().unwrap(); 132 | let (command_tx, command_rx) = tk_unbounded_channel(); 133 | let dispatcher = Arc::new(TokioUnboundedDispatcher { command_tx }); 134 | let mailbox_fn = Arc::new(tk_unbounded_mailbox_fn()); 135 | 136 | let my_actor = ActorContext::::new( 137 | || Box::new(MyActor {}), 138 | dispatcher.to_owned(), 139 | mailbox_fn.to_owned(), 140 | ); 141 | 142 | let dispatcher_task_dispatcher = dispatcher.to_owned(); 143 | let _ = rt.spawn(async move { dispatcher_task_dispatcher.start(command_rx).await }); 144 | 145 | use stage_dispatch_tokio::Ask; 146 | 147 | c.bench_function( 148 | "send messages to other actors using Tokio with unbounded channels", 149 | |b| { 150 | b.to_async(&rt).iter(|| async { 151 | my_actor.actor_ref.ask( 152 | &|reply_to| Request { 153 | reply_to: reply_to.to_owned(), 154 | }, 155 | Duration::from_secs(1), 156 | ) 157 | }); 158 | }, 159 | ); 160 | } 161 | 162 | fn create_new_actors_unbounded_cbe(c: &mut Criterion) { 163 | let pool = crossbeam_workstealing_pool::small_pool(4); 164 | let (command_tx, command_rx) = cb_unbounded(); 165 | let dispatcher = Arc::new(WorkStealingPoolDispatcher { pool, command_tx }); 166 | 167 | let mailbox_fn = Arc::new(cbe_unbounded_mailbox_fn()); 168 | 169 | let dispatcher_thread_dispatcher = dispatcher.to_owned(); 170 | let _ = thread::spawn(move || dispatcher_thread_dispatcher.start(command_rx)); 171 | 172 | c.bench_function( 173 | "create new actors using Crossbeam and Executors with unbounded channels", 174 | |b| { 175 | b.iter(|| { 176 | let _ = ActorContext::::new( 177 | || Box::new(MyActor {}), 178 | dispatcher.to_owned(), 179 | mailbox_fn.to_owned(), 180 | ); 181 | }); 182 | }, 183 | ); 184 | } 185 | 186 | fn create_new_actors_unbounded_tk(c: &mut Criterion) { 187 | let rt = tokio::runtime::Runtime::new().unwrap(); 188 | let (command_tx, command_rx) = tk_unbounded_channel(); 189 | let dispatcher = Arc::new(TokioUnboundedDispatcher { command_tx }); 190 | let mailbox_fn = Arc::new(tk_unbounded_mailbox_fn()); 191 | 192 | let dispatcher_task_dispatcher = dispatcher.to_owned(); 193 | let _ = rt.spawn(async move { dispatcher_task_dispatcher.start(command_rx).await }); 194 | 195 | c.bench_function( 196 | "create new actors using Tokio with unbounded channels", 197 | |b| { 198 | b.to_async(&rt).iter(|| async { 199 | let _ = ActorContext::::new( 200 | || Box::new(MyActor {}), 201 | dispatcher.to_owned(), 202 | mailbox_fn.to_owned(), 203 | ); 204 | }); 205 | }, 206 | ); 207 | } 208 | 209 | criterion_group!( 210 | benches, 211 | send_messages_to_other_actors_bounded_cbe, 212 | send_messages_to_other_actors_unbounded_cbe, 213 | send_messages_to_other_actors_bounded_tk, 214 | send_messages_to_other_actors_unbounded_tk, 215 | create_new_actors_unbounded_cbe, 216 | create_new_actors_unbounded_tk, 217 | ); 218 | criterion_main!(benches); 219 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stage_core" 3 | version = "0.1.0" 4 | authors = ["Titan Class P/L"] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "A minimal actor model library using nostd and designed to run with any executor" 8 | homepage = "https://www.titanclass.com.au/" 9 | repository = "https://github.com/titanclass/stage" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | log = "0.4" 14 | 15 | [dev-dependencies] 16 | env_logger = "0.8.3" 17 | -------------------------------------------------------------------------------- /core/src/channel.rs: -------------------------------------------------------------------------------- 1 | use alloc::boxed::Box; 2 | use core::any::Any; 3 | use core::fmt; 4 | 5 | /// Provides an abstraction over any type of channel so that the core actor library 6 | /// can function. 7 | /// 8 | /// The primary declarations and their doc were copied from Crossbeam but appear 9 | /// to apply across popular runtimes including Tokio and async-std. 10 | 11 | /// The receiving side of a channel. 12 | pub struct Receiver { 13 | pub receiver_impl: Box + Send>, 14 | } 15 | 16 | impl fmt::Debug for Receiver { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | f.pad("Receiver { .. }") 19 | } 20 | } 21 | 22 | /// Required to be implemented by the provider of a channel 23 | pub trait ReceiverImpl { 24 | type Item; 25 | /// Return self as an Any so that it can be downcast 26 | fn as_any(&mut self) -> &mut (dyn Any + Send); 27 | } 28 | 29 | /// The sending side of a channel. 30 | pub struct Sender { 31 | pub sender_impl: Box + Send + Sync>, 32 | } 33 | 34 | impl Sender { 35 | /// Attempts to send a message into the channel without blocking. 36 | /// 37 | /// This method will either send a message into the channel immediately or return an error if 38 | /// the channel is full or disconnected. The returned error contains the original message. 39 | /// 40 | /// If called on a zero-capacity channel, this method will send the message only if there 41 | /// happens to be a receive operation on the other side of the channel at the same time. 42 | pub fn try_send(&self, msg: T) -> Result<(), TrySendError> { 43 | self.sender_impl.try_send(msg) 44 | } 45 | } 46 | 47 | impl Clone for Sender { 48 | fn clone(&self) -> Self { 49 | Sender { 50 | sender_impl: self.sender_impl.clone(), 51 | } 52 | } 53 | } 54 | 55 | impl fmt::Debug for Sender { 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | f.pad("Sender { .. }") 58 | } 59 | } 60 | 61 | /// Required to be implemented by the provider of a channel 62 | pub trait SenderImpl { 63 | type Item; 64 | // Provide a cloning function 65 | fn clone(&self) -> Box + Send + Sync>; 66 | // Provide a try_send function 67 | fn try_send(&self, msg: Self::Item) -> Result<(), TrySendError>; 68 | } 69 | 70 | /// An error returned from the [`try_recv`] method. 71 | /// 72 | /// [`try_recv`]: super::Receiver::try_recv 73 | #[derive(PartialEq, Eq, Clone, Copy, Debug)] 74 | pub enum TryRecvError { 75 | /// A message could not be received because the channel is empty. 76 | /// 77 | /// If this is a zero-capacity channel, then the error indicates that there was no sender 78 | /// available to send a message at the time. 79 | Empty, 80 | 81 | /// The message could not be received because the channel is empty and disconnected. 82 | Disconnected, 83 | } 84 | 85 | impl fmt::Display for TryRecvError { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | match *self { 88 | TryRecvError::Empty => "receiving on an empty channel".fmt(f), 89 | TryRecvError::Disconnected => "receiving on an empty and disconnected channel".fmt(f), 90 | } 91 | } 92 | } 93 | 94 | /// An error returned from the [`try_send`] method. 95 | /// 96 | /// The error contains the message being sent so it can be recovered. 97 | /// 98 | /// [`try_send`]: super::Sender::try_send 99 | #[derive(PartialEq, Eq, Clone, Copy)] 100 | pub enum TrySendError { 101 | /// The message could not be sent because the channel is full. 102 | /// 103 | /// If this is a zero-capacity channel, then the error indicates that there was no receiver 104 | /// available to receive the message at the time. 105 | Full(T), 106 | 107 | /// The message could not be sent because the channel is disconnected. 108 | Disconnected(T), 109 | } 110 | 111 | impl fmt::Display for TrySendError { 112 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 113 | match *self { 114 | TrySendError::Full(..) => "sending on a full channel".fmt(f), 115 | TrySendError::Disconnected(..) => "sending on a disconnected channel".fmt(f), 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(test), no_std)] 2 | 3 | use core::{any::Any, fmt::Debug, marker::PhantomData}; 4 | 5 | extern crate alloc; 6 | use crate::alloc::borrow::ToOwned; 7 | use crate::alloc::{boxed::Box, sync::Arc}; 8 | 9 | pub mod channel; 10 | use channel::{Receiver, Sender, TrySendError}; 11 | 12 | use log::{debug, warn}; 13 | 14 | /// Any message that can be sent and received by an actor. Used within dispatchers. 15 | pub type AnyMessage = Box; 16 | 17 | /// An actor is a computational entity that, in response to a message it receives, can concurrently: 18 | /// * send messages to other actors 19 | /// * create new actors 20 | /// * designate the behavior to be used for the next message it receives 21 | /// (from https://en.wikipedia.org/wiki/Actor_model#Fundamental_concepts) 22 | pub trait Actor { 23 | /// Receive a message while being able to mutate the actor state safely and 24 | /// without requiring a locking mechanism. An actor context is the meta state 25 | /// that enables actor creation and the stopping of this actor. 26 | fn receive(&mut self, context: &mut ActorContext, message: &M); 27 | } 28 | 29 | /// This is the type used to represent its corresponding enum as enum variants cannot 30 | /// be used as types in Rust. 31 | pub struct SelectWithAction { 32 | pub receiver: Receiver, 33 | pub action: Box bool + Send + Sync>, 34 | } 35 | 36 | /// Dispatchers can be sent commands on a control channel as well as being able to 37 | /// dispatch messages to the actors they are responsible to execute. 38 | pub enum DispatcherCommand { 39 | /// Tells the dispatcher to select on a receiver of messages by providing 40 | /// the receiver. If selection signals activity on the receiver then 41 | /// a function should be performed to process it. 42 | SelectWithAction { underlying: SelectWithAction }, 43 | 44 | /// Tells the dispatcher to finish up. The thread on which the dispatcher run 45 | /// function is running can then be joined. 46 | Stop, 47 | } 48 | 49 | /// A dispatcher composes a executor to call upon the actor's message queue, ultimately calling 50 | /// upon the actor's receive method. 51 | pub trait Dispatcher { 52 | /// Enqueue a command to the channel being selected on. 53 | fn send(&self, command: DispatcherCommand) -> Result<(), TrySendError>; 54 | 55 | /// Stop the current dispatcher 56 | fn stop(&self) { 57 | let _ = self.send(DispatcherCommand::Stop); 58 | } 59 | } 60 | 61 | /// An actor context provides state that all actors need to be able to operate. 62 | /// These contexts are used mainly to obtain actor references to themselves. 63 | pub struct ActorContext { 64 | active: bool, 65 | pub actor_ref: ActorRef, 66 | pub dispatcher: Arc, 67 | pub mailbox_fn: Arc (Sender, Receiver) + Send + Sync>, 68 | } 69 | 70 | impl ActorContext { 71 | /// Create a new actor context and associate it with a dispatcher. 72 | pub fn new( 73 | new_actor_fn: FA, 74 | dispatcher: Arc, 75 | mailbox_fn: Arc (Sender, Receiver) + Send + Sync>, 76 | ) -> ActorContext 77 | where 78 | FA: FnOnce() -> Box + Send + Sync>, 79 | M: Send + Sync + 'static, 80 | { 81 | let (tx, rx) = mailbox_fn(); 82 | let actor_ref = ActorRef { 83 | phantom_marker: PhantomData, 84 | sender: tx, 85 | }; 86 | let context = ActorContext { 87 | active: true, 88 | actor_ref: actor_ref.to_owned(), 89 | dispatcher: dispatcher.to_owned(), 90 | mailbox_fn, 91 | }; 92 | let mut actor = new_actor_fn(); 93 | 94 | let mut dispatcher_context = context.to_owned(); 95 | 96 | if let Err(e) = dispatcher.send(DispatcherCommand::SelectWithAction { 97 | underlying: SelectWithAction { 98 | receiver: rx, 99 | action: Box::new(move |message| { 100 | if dispatcher_context.active { 101 | match message.downcast::() { 102 | Ok(ref m) => { 103 | actor.receive(&mut dispatcher_context, m); 104 | } 105 | Err(m) => warn!( 106 | "Unexpected message in {:?}: type_id: {:?}", 107 | dispatcher_context.actor_ref, 108 | m.type_id() 109 | ), 110 | } 111 | } 112 | dispatcher_context.active 113 | }), 114 | }, 115 | }) { 116 | debug!("Error received establishing {:?}: {}", actor_ref, e); 117 | } 118 | 119 | context 120 | } 121 | 122 | /// Create a new actor as a child to this one. The child actor will receive 123 | /// the same dispatcher as the current one. 124 | pub fn spawn(&mut self, new_actor_fn: FA) -> ActorRef 125 | where 126 | FA: FnOnce() -> Box + Send + Sync>, 127 | M2: Send + Sync + 'static, 128 | { 129 | let context = ActorContext::::new( 130 | new_actor_fn, 131 | self.dispatcher.to_owned(), 132 | self.mailbox_fn.to_owned(), 133 | ); 134 | context.actor_ref 135 | } 136 | 137 | /// Stop this actor immediately. 138 | pub fn stop(&mut self) { 139 | self.active = true; 140 | } 141 | } 142 | 143 | impl Clone for ActorContext { 144 | fn clone(&self) -> ActorContext { 145 | ActorContext { 146 | active: self.active, 147 | actor_ref: self.actor_ref.to_owned(), 148 | dispatcher: self.dispatcher.to_owned(), 149 | mailbox_fn: self.mailbox_fn.to_owned(), 150 | } 151 | } 152 | } 153 | 154 | /// An actor ref provides a means by which to communicate 155 | /// with an actor; in fact it is the only means to send 156 | /// a message to an actor. Any associated actor may no 157 | /// longer exist, in which case messages will be delivered 158 | /// to a dead letter channel. 159 | pub struct ActorRef { 160 | pub phantom_marker: PhantomData, 161 | pub sender: Sender, 162 | } 163 | 164 | impl ActorRef { 165 | /// Best effort send a message to the associated actor 166 | pub fn tell(&self, message: M) { 167 | let _ = self.sender.try_send(Box::new(message)); 168 | } 169 | } 170 | 171 | impl Clone for ActorRef { 172 | fn clone(&self) -> ActorRef { 173 | ActorRef { 174 | phantom_marker: PhantomData, 175 | sender: self.sender.to_owned(), 176 | } 177 | } 178 | } 179 | 180 | impl Debug for ActorRef { 181 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 182 | self.sender.fmt(f) 183 | } 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use channel::{ReceiverImpl, SenderImpl}; 189 | 190 | use super::*; 191 | 192 | use std::sync::mpsc::{sync_channel, Receiver as SyncReceiver, SyncSender}; 193 | use std::sync::mpsc::{ 194 | RecvError as SyncRecvError, RecvTimeoutError as SyncRecvTimeoutError, 195 | TrySendError as SyncTrySendError, 196 | }; 197 | use std::time::Duration; 198 | 199 | // This test provides reasonable coverage across the core APIs. A dispatcher is 200 | // set up to process command messages and actor messages synchronously to reason 201 | // the tests more easily. 202 | #[test] 203 | fn test_ask() { 204 | // Declare our actor to test - a simply recipient of a request and a reply 205 | 206 | struct Request { 207 | reply_to: ActorRef, 208 | reply_with: u32, 209 | } 210 | #[derive(Debug, PartialEq)] 211 | struct Reply { 212 | value: u32, 213 | } 214 | struct MyActor {} 215 | impl Actor for MyActor { 216 | fn receive(&mut self, _context: &mut ActorContext, message: &Request) { 217 | message.reply_to.tell(Reply { 218 | value: message.reply_with, 219 | }) 220 | } 221 | } 222 | 223 | // Declares a dispatcher to process commands and actor messages synchronously 224 | 225 | struct MyDispatcher { 226 | command_tx: SyncSender, 227 | } 228 | impl MyDispatcher { 229 | pub fn receive_command( 230 | &self, 231 | command_rx: &SyncReceiver, 232 | ) -> Result, SyncRecvError> { 233 | match command_rx.recv() { 234 | Ok(message) => Ok(match message.downcast::() { 235 | Ok(dispatcher_command) => match *dispatcher_command { 236 | DispatcherCommand::SelectWithAction { underlying } => Some(underlying), 237 | DispatcherCommand::Stop => None, 238 | }, 239 | Err(e) => { 240 | warn!( 241 | "Error received when expecting a dispatcher command: {:?}", 242 | e 243 | ); 244 | None 245 | } 246 | }), 247 | Err(e) => Err(e), 248 | } 249 | } 250 | pub fn receive_messages(&self, selection: &mut SelectWithAction) { 251 | let receiver = selection 252 | .receiver 253 | .receiver_impl 254 | .as_any() 255 | .downcast_ref::>() 256 | .unwrap(); 257 | while let Ok(m) = receiver.try_recv() { 258 | let active = (selection.action)(m); 259 | if !active { 260 | break; 261 | } 262 | } 263 | } 264 | } 265 | impl Dispatcher for MyDispatcher { 266 | fn send(&self, command: DispatcherCommand) -> Result<(), TrySendError> { 267 | self.command_tx 268 | .try_send(Box::new(command)) 269 | .map_err(|e| match e { 270 | SyncTrySendError::Disconnected(e) => TrySendError::Disconnected(e), 271 | SyncTrySendError::Full(e) => TrySendError::Full(e), 272 | }) 273 | } 274 | } 275 | struct SyncReceiverImpl { 276 | receiver: SyncReceiver, 277 | } 278 | impl ReceiverImpl for SyncReceiverImpl { 279 | type Item = AnyMessage; 280 | 281 | fn as_any(&mut self) -> &mut (dyn Any + Send) { 282 | &mut self.receiver 283 | } 284 | } 285 | struct SyncSenderImpl { 286 | sender: SyncSender, 287 | } 288 | impl SenderImpl for SyncSenderImpl { 289 | type Item = AnyMessage; 290 | 291 | fn clone(&self) -> Box + Send + Sync> { 292 | Box::new(SyncSenderImpl { 293 | sender: self.sender.to_owned(), 294 | }) 295 | } 296 | 297 | fn try_send(&self, msg: AnyMessage) -> Result<(), TrySendError> { 298 | match self.sender.try_send(msg) { 299 | Ok(_) => Ok(()), 300 | Err(SyncTrySendError::Disconnected(e)) => Err(TrySendError::Disconnected(e)), 301 | Err(SyncTrySendError::Full(e)) => Err(TrySendError::Full(e)), 302 | } 303 | } 304 | } 305 | pub fn sync_mailbox_fn( 306 | buffer: usize, 307 | ) -> Box (Sender, Receiver) + Send + Sync> { 308 | Box::new(move || { 309 | let (mailbox_tx, mailbox_rx) = sync_channel(buffer); 310 | ( 311 | Sender { 312 | sender_impl: Box::new(SyncSenderImpl { 313 | sender: mailbox_tx.to_owned(), 314 | }), 315 | }, 316 | Receiver { 317 | receiver_impl: Box::new(SyncReceiverImpl { 318 | receiver: mailbox_rx, 319 | }), 320 | }, 321 | ) 322 | }) 323 | } 324 | type AskResult = Result, SyncRecvTimeoutError>; 325 | pub trait Ask { 326 | fn request(&self, request_fn: &dyn Fn(&ActorRef) -> M) -> SyncReceiver; 327 | fn reply( 328 | &self, 329 | reply_rx: &SyncReceiver, 330 | recv_timeout: Duration, 331 | ) -> AskResult; 332 | } 333 | impl Ask for ActorRef 334 | where 335 | M: Send + 'static, 336 | M2: Send + 'static, 337 | { 338 | fn request(&self, request_fn: &dyn Fn(&ActorRef) -> M) -> SyncReceiver { 339 | let (reply_tx, reply_rx) = sync_channel::(1); 340 | let _ = self.sender.try_send(Box::new(request_fn(&ActorRef { 341 | phantom_marker: PhantomData::, 342 | sender: Sender { 343 | sender_impl: Box::new(SyncSenderImpl { sender: reply_tx }), 344 | }, 345 | }))); 346 | reply_rx 347 | } 348 | fn reply( 349 | &self, 350 | reply_rx: &SyncReceiver, 351 | recv_timeout: Duration, 352 | ) -> AskResult { 353 | reply_rx 354 | .recv_timeout(recv_timeout) 355 | .map(|message| { 356 | message 357 | .downcast::() 358 | .map_err(|_| SyncRecvTimeoutError::Timeout) 359 | }) 360 | .and_then(|v| v) 361 | } 362 | } 363 | 364 | // Establish the dispatcher and mailbox 365 | 366 | let (command_tx, command_rx) = sync_channel::(1); 367 | let dispatcher = Arc::new(MyDispatcher { command_tx }); 368 | 369 | let mailbox_fn = Arc::new(sync_mailbox_fn(1)); 370 | 371 | // Establish our root actor and get a handle to the actor's receiver 372 | 373 | let my_actor = ActorContext::::new( 374 | || Box::new(MyActor {}), 375 | dispatcher.to_owned(), 376 | mailbox_fn.to_owned(), 377 | ); 378 | let selection = dispatcher.receive_command(&command_rx); 379 | 380 | // Send an ask request to the actor and have the actor process its 381 | // messages 382 | 383 | let expected_value = 10; 384 | 385 | let reply_rx = my_actor.actor_ref.request(&|reply_to| Request { 386 | reply_to: reply_to.to_owned(), 387 | reply_with: expected_value, 388 | }); 389 | dispatcher.receive_messages(&mut selection.unwrap().unwrap()); 390 | let result = my_actor.actor_ref.reply(&reply_rx, Duration::from_secs(1)); 391 | 392 | // Test the ask reply 393 | 394 | assert_eq!( 395 | result, 396 | Ok(Box::new(Reply { 397 | value: expected_value 398 | })) 399 | ) 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /executors/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stage_dispatch_crossbeam_executors" 3 | version = "0.1.0" 4 | authors = ["Titan Class P/L"] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "A Crossbeam/executors flavoured actor dispatcher library for core_stage" 8 | homepage = "https://www.titanclass.com.au/" 9 | repository = "https://github.com/titanclass/stage" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | crossbeam-channel = "0.5" 14 | executors = "0.8.0" 15 | log = "0.4" 16 | stage_core = { path = "../core" } 17 | 18 | [dev-dependencies] 19 | env_logger = "0.8.3" -------------------------------------------------------------------------------- /executors/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, marker::PhantomData, time::Duration}; 2 | 3 | use crossbeam_channel::{ 4 | bounded, unbounded, Receiver as CbReceiver, RecvError as CbRecvError, RecvTimeoutError, Select, 5 | Sender as CbSender, TryRecvError as CbTryRecvError, TrySendError as CbTrySendError, 6 | }; 7 | use executors::*; 8 | use executors::{crossbeam_workstealing_pool, parker::Parker}; 9 | use stage_core::{ 10 | channel::{Receiver, ReceiverImpl, Sender, SenderImpl, TrySendError}, 11 | ActorRef, 12 | }; 13 | 14 | use log::{debug, warn}; 15 | use stage_core::{AnyMessage, Dispatcher, DispatcherCommand, SelectWithAction}; 16 | 17 | /// Provides an executor based on the executors package implementation of 18 | /// crossbeam_workstealing_pool. In addition, the channels available for use 19 | /// with mailbox and the command channel of a dispatcher are those of Crossbeam. 20 | 21 | struct CbReceiverImpl { 22 | receiver: CbReceiver, 23 | } 24 | 25 | impl ReceiverImpl for CbReceiverImpl { 26 | type Item = AnyMessage; 27 | 28 | fn as_any(&mut self) -> &mut (dyn Any + Send) { 29 | &mut self.receiver 30 | } 31 | } 32 | 33 | // A convenience for extracting a Crossbeam Receiver from a Stage Receiver type. 34 | macro_rules! receiver { 35 | ($receiver:expr) => { 36 | $receiver 37 | .receiver_impl 38 | .as_any() 39 | .downcast_ref::>() 40 | .unwrap() 41 | }; 42 | } 43 | 44 | struct CbSenderImpl { 45 | sender: CbSender, 46 | } 47 | 48 | impl SenderImpl for CbSenderImpl { 49 | type Item = AnyMessage; 50 | 51 | fn clone(&self) -> Box + Send + Sync> { 52 | Box::new(CbSenderImpl { 53 | sender: self.sender.to_owned(), 54 | }) 55 | } 56 | 57 | fn try_send(&self, msg: AnyMessage) -> Result<(), TrySendError> { 58 | match self.sender.try_send(msg) { 59 | Ok(_) => Ok(()), 60 | Err(CbTrySendError::Disconnected(e)) => Err(TrySendError::Disconnected(e)), 61 | Err(CbTrySendError::Full(e)) => Err(TrySendError::Full(e)), 62 | } 63 | } 64 | } 65 | 66 | /// Creates a Crossbeam-based bounded mailbox for communicating with an actor. 67 | pub fn bounded_mailbox_fn( 68 | cap: usize, 69 | ) -> Box (Sender, Receiver) + Send + Sync> { 70 | Box::new(move || { 71 | let (mailbox_tx, mailbox_rx) = bounded::(cap); 72 | ( 73 | Sender { 74 | sender_impl: Box::new(CbSenderImpl { 75 | sender: mailbox_tx.to_owned(), 76 | }), 77 | }, 78 | Receiver { 79 | receiver_impl: Box::new(CbReceiverImpl { 80 | receiver: mailbox_rx, 81 | }), 82 | }, 83 | ) 84 | }) 85 | } 86 | 87 | /// Creates a Crossbeam-based bounded mailbox for communicating with an actor. 88 | pub fn unbounded_mailbox_fn( 89 | ) -> Box (Sender, Receiver) + Send + Sync> { 90 | Box::new(|| { 91 | let (mailbox_tx, mailbox_rx) = unbounded::(); 92 | ( 93 | Sender { 94 | sender_impl: Box::new(CbSenderImpl { 95 | sender: mailbox_tx.to_owned(), 96 | }), 97 | }, 98 | Receiver { 99 | receiver_impl: Box::new(CbReceiverImpl { 100 | receiver: mailbox_rx, 101 | }), 102 | }, 103 | ) 104 | }) 105 | } 106 | 107 | type AskResult = Result, RecvTimeoutError>; 108 | 109 | /// Implements the Ask pattern where a request message is sent 110 | /// and a reply is expected. 111 | pub trait Ask { 112 | /// Perform a one-shot ask operation while passing in a function 113 | /// to construct a message that accepts a reply_to sender. 114 | /// All asks have a timeout. Note also that if an unexpected 115 | /// reply is received then a timeout error will be indicated. 116 | /// Note that this method will block due to the lack of 117 | /// async/await support: https://github.com/crossbeam-rs/crossbeam/issues/501 118 | fn ask(&self, request_fn: &dyn Fn(&ActorRef) -> M, recv_timeout: Duration) 119 | -> AskResult; 120 | } 121 | 122 | impl Ask for ActorRef 123 | where 124 | M: Send + 'static, 125 | M2: Send + 'static, 126 | { 127 | fn ask( 128 | &self, 129 | request_fn: &dyn Fn(&ActorRef) -> M, 130 | recv_timeout: Duration, 131 | ) -> AskResult { 132 | let (reply_tx, reply_rx) = bounded::(1); 133 | let _ = self.sender.try_send(Box::new(request_fn(&ActorRef { 134 | phantom_marker: PhantomData::, 135 | sender: Sender { 136 | sender_impl: Box::new(CbSenderImpl { sender: reply_tx }), 137 | }, 138 | }))); 139 | reply_rx 140 | .recv_timeout(recv_timeout) 141 | .map(|message| { 142 | message 143 | .downcast::() 144 | .map_err(|_| RecvTimeoutError::Timeout) 145 | }) 146 | .and_then(|v| v) 147 | } 148 | } 149 | 150 | /// A Dispatcher for Stage that leverages the Executors crate's work-stealing ThreadPool. 151 | /// 152 | /// The following code declares a dispatcher that work-steals across 4 cores. An unbounded 153 | /// channel is established for internal communication with the dispatcher. Consideration 154 | /// should be given to bounded channels upon its domain being understood and throughput 155 | /// having been measured. 156 | /// ``` 157 | /// use std::sync::Arc; 158 | /// use crossbeam_channel::unbounded; 159 | /// use executors::crossbeam_workstealing_pool; 160 | /// use stage_dispatch_crossbeam_executors::WorkStealingPoolDispatcher; 161 | /// 162 | /// let pool = crossbeam_workstealing_pool::small_pool(4); 163 | /// let (command_tx, command_rx) = unbounded(); 164 | /// let dispatcher = Arc::new(WorkStealingPoolDispatcher { pool, command_tx }); 165 | /// // command_rx is then used later when using [start]. 166 | /// ``` 167 | 168 | pub struct WorkStealingPoolDispatcher

169 | where 170 | P: Parker + Clone + 'static, 171 | { 172 | pub pool: crossbeam_workstealing_pool::ThreadPool

, 173 | pub command_tx: CbSender, 174 | } 175 | 176 | impl

WorkStealingPoolDispatcher

177 | where 178 | P: Parker + Clone + 'static, 179 | { 180 | /// Start this dispatcher. Starting this dispatcher selects all receivers and dispatch their actions. 181 | /// On dispatching on action, their selection should becomes ineligible so that they cannot be selected 182 | /// on another message until they have completed their processing. Once complete, the action is be followed by 183 | /// an enqueuing of their selection once more by calling upon the send function. 184 | /// Note that this function is blocking. 185 | pub fn start(&self, command_rx: CbReceiver) -> Result { 186 | let mut actionable_receivers: Vec<(CbReceiver, Box)> = vec![]; 187 | loop { 188 | let mut sel = Select::new(); 189 | sel.recv(&command_rx); // The first one added is always our control channel for receiving commands 190 | actionable_receivers.iter().for_each(|command| { 191 | sel.recv(&command.0); 192 | }); 193 | let oper = sel.select(); 194 | 195 | let index = oper.index(); 196 | let receiver = match index { 197 | 0 => &command_rx, 198 | _ => &actionable_receivers[index - 1].0, 199 | }; 200 | let res = oper.recv(receiver); 201 | 202 | if index > 0 { 203 | // Handle a message destined for an actor - this is the common case. 204 | let mut current_select_command = actionable_receivers.swap_remove(index - 1); 205 | match res { 206 | Ok(message) => { 207 | let tx = self.command_tx.to_owned(); 208 | self.pool.execute(move || { 209 | let receiver = current_select_command.0; 210 | let mut active = (current_select_command.1.action)(message); 211 | while active { 212 | match receiver.try_recv() { 213 | Ok(next_message) => { 214 | active = (current_select_command.1.action)(next_message) 215 | } 216 | Err(e) if e == CbTryRecvError::Empty => break, 217 | Err(e) => { 218 | debug!("Error received for actor {}", e); 219 | break; 220 | } 221 | } 222 | } 223 | if active { 224 | let _ = tx.send(current_select_command.1); 225 | } else { 226 | debug!("Actor has shutdown: {:?} - treating as a dead letter", tx); 227 | } 228 | }); 229 | } 230 | Err(e) => { 231 | debug!( 232 | "Cannot receive on an actor channel: {} - treating as a dead letter", 233 | e 234 | ); 235 | } 236 | } 237 | } else { 238 | // Dispatcher message handling is prioritised to process SelectWithAction as we will 239 | // receive these ones mostly. 240 | match res { 241 | Ok(message) => { 242 | match message.downcast::() { 243 | Ok(mut select_with_action) => actionable_receivers.push(( 244 | receiver!(select_with_action.receiver).to_owned(), 245 | select_with_action, 246 | )), 247 | Err(other_message_type) => { 248 | match other_message_type.downcast::() { 249 | Ok(dispatcher_command) => match *dispatcher_command { 250 | DispatcherCommand::SelectWithAction { mut underlying } => { 251 | actionable_receivers.push(( 252 | receiver!(underlying.receiver).to_owned(), 253 | Box::new(underlying), 254 | )); 255 | } 256 | DispatcherCommand::Stop => { 257 | self.pool.shutdown_async(); 258 | return Ok(Box::new(DispatcherCommand::Stop)); 259 | } 260 | }, 261 | Err(e) => { 262 | warn!("Error received when expecting a dispatcher command: {:?}", e) 263 | } 264 | } 265 | } 266 | } 267 | } 268 | e @ Err(_) => { 269 | return e; 270 | } 271 | } 272 | } 273 | } 274 | } 275 | } 276 | 277 | impl

Dispatcher for WorkStealingPoolDispatcher

278 | where 279 | P: Parker + Clone + 'static, 280 | { 281 | fn send(&self, command: DispatcherCommand) -> Result<(), TrySendError> { 282 | self.command_tx 283 | .try_send(Box::new(command)) 284 | .map_err(|e| match e { 285 | CbTrySendError::Disconnected(e) => TrySendError::Disconnected(e), 286 | CbTrySendError::Full(e) => TrySendError::Full(e), 287 | }) 288 | } 289 | } 290 | 291 | impl

Drop for WorkStealingPoolDispatcher

292 | where 293 | P: Parker + Clone + 'static, 294 | { 295 | fn drop(&mut self) { 296 | self.stop() 297 | } 298 | } 299 | 300 | #[cfg(test)] 301 | mod tests { 302 | use std::{sync::Arc, thread, time::Duration}; 303 | 304 | use crossbeam_channel::unbounded; 305 | 306 | use executors::crossbeam_workstealing_pool; 307 | use stage_core::{Actor, ActorContext, ActorRef}; 308 | 309 | use super::*; 310 | 311 | fn init_logging() { 312 | let _ = env_logger::builder().is_test(true).try_init(); 313 | } 314 | 315 | #[test] 316 | fn test_greeting() { 317 | init_logging(); 318 | 319 | // Re-creates https://doc.akka.io/docs/akka/current/typed/actors.html#first-example 320 | 321 | // The messages 322 | 323 | struct Greet { 324 | whom: String, 325 | reply_to: ActorRef, 326 | } 327 | 328 | struct Greeted { 329 | whom: String, 330 | from: ActorRef, 331 | } 332 | 333 | struct SayHello { 334 | name: String, 335 | } 336 | 337 | // The HelloWorld actor 338 | 339 | struct HelloWorld {} 340 | 341 | impl Actor for HelloWorld { 342 | fn receive(&mut self, context: &mut ActorContext, message: &Greet) { 343 | println!("Hello {}!", message.whom); 344 | message.reply_to.tell(Greeted { 345 | whom: message.whom.to_owned(), 346 | from: context.actor_ref.to_owned(), 347 | }); 348 | } 349 | } 350 | 351 | // The HelloWorldBot actor 352 | 353 | struct HelloWorldBot { 354 | greeting_counter: u32, 355 | max: u32, 356 | } 357 | 358 | impl Actor for HelloWorldBot { 359 | fn receive(&mut self, context: &mut ActorContext, message: &Greeted) { 360 | let n = self.greeting_counter + 1; 361 | println!("Greeting {} for {}", n, message.whom); 362 | if n == self.max { 363 | context.stop(); 364 | } else { 365 | message.from.tell(Greet { 366 | whom: message.whom.to_owned(), 367 | reply_to: context.actor_ref.to_owned(), 368 | }); 369 | self.greeting_counter = n; 370 | } 371 | } 372 | } 373 | 374 | // The root actor 375 | 376 | struct HelloWorldMain { 377 | greeter: Option>, 378 | } 379 | 380 | impl Actor for HelloWorldMain { 381 | fn receive(&mut self, context: &mut ActorContext, message: &SayHello) { 382 | let greeter = match &self.greeter { 383 | None => { 384 | let greeter = context.spawn(|| Box::new(HelloWorld {})); 385 | self.greeter = Some(greeter.to_owned()); 386 | greeter 387 | } 388 | Some(greeter) => greeter.to_owned(), 389 | }; 390 | 391 | let reply_to = context.spawn(|| { 392 | Box::new(HelloWorldBot { 393 | greeting_counter: 0, 394 | max: 3, 395 | }) 396 | }); 397 | greeter.tell(Greet { 398 | whom: message.name.to_owned(), 399 | reply_to, 400 | }); 401 | } 402 | } 403 | 404 | // Establish our dispatcher. 405 | 406 | let pool = crossbeam_workstealing_pool::small_pool(4); 407 | let (command_tx, command_rx) = unbounded(); 408 | let dispatcher = Arc::new(WorkStealingPoolDispatcher { pool, command_tx }); 409 | 410 | // Establish a function to produce mailboxes 411 | 412 | let mailbox_fn = Arc::new(unbounded_mailbox_fn()); 413 | 414 | // Create a root context, which is essentiallly the actor system. We 415 | // also send a couple of messages for our demo. 416 | 417 | let system = ActorContext::::new( 418 | || Box::new(HelloWorldMain { greeter: None }), 419 | dispatcher.to_owned(), 420 | mailbox_fn, 421 | ); 422 | 423 | system.actor_ref.tell(SayHello { 424 | name: "World".to_string(), 425 | }); 426 | 427 | system.actor_ref.tell(SayHello { 428 | name: "Stage".to_string(), 429 | }); 430 | 431 | // Run the dispatcher on its own thread. We wait for the run 432 | // function to finish, which will be when will we tell the "system" 433 | // (the actor context above) to stop, it is stops itself. 434 | 435 | let dispatcher_thread_dispatcher = dispatcher.to_owned(); 436 | let dispatcher_thread = 437 | thread::spawn(move || dispatcher_thread_dispatcher.start(command_rx)); 438 | 439 | thread::sleep(Duration::from_millis(500)); 440 | 441 | dispatcher.stop(); 442 | 443 | assert!(dispatcher_thread.join().is_ok()); 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /tokio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stage_dispatch_tokio" 3 | version = "0.1.0" 4 | authors = ["Titan Class P/L"] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "A Tokio flavoured actor dispatcher library for core_stage" 8 | homepage = "https://www.titanclass.com.au/" 9 | repository = "https://github.com/titanclass/stage" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | tokio = { version = "1", features = ["sync"] } 14 | log = "0.4" 15 | stage_core = { path = "../core" } 16 | 17 | [dev-dependencies] 18 | env_logger = "0.8.3" 19 | tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } 20 | -------------------------------------------------------------------------------- /tokio/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, future::Future, marker::PhantomData, pin::Pin, time::Duration}; 2 | 3 | use log::warn; 4 | use stage_core::{ 5 | channel::{Receiver, ReceiverImpl, Sender, SenderImpl, TrySendError}, 6 | ActorRef, 7 | }; 8 | use tokio::{ 9 | sync::mpsc::error::{SendError as TkSendError, TrySendError as TkTrySendError}, 10 | task::JoinHandle, 11 | time::error::Elapsed, 12 | }; 13 | use tokio::{ 14 | sync::mpsc::{ 15 | channel, unbounded_channel, Receiver as TkReceiver, Sender as TkSender, 16 | UnboundedReceiver as TkUnboundedReceiver, UnboundedSender as TkUnboundedSender, 17 | }, 18 | time::timeout, 19 | }; 20 | 21 | use stage_core::{AnyMessage, Dispatcher, DispatcherCommand}; 22 | 23 | /// Provides an Tokio based dispatcher. 24 | 25 | struct TkReceiverImpl { 26 | receiver: TkReceiver, 27 | } 28 | 29 | impl ReceiverImpl for TkReceiverImpl { 30 | type Item = AnyMessage; 31 | 32 | fn as_any(&mut self) -> &mut (dyn Any + Send) { 33 | &mut self.receiver 34 | } 35 | } 36 | 37 | struct TkSenderImpl { 38 | sender: TkSender, 39 | } 40 | 41 | impl SenderImpl for TkSenderImpl { 42 | type Item = AnyMessage; 43 | 44 | fn clone(&self) -> Box + Send + Sync> { 45 | Box::new(TkSenderImpl { 46 | sender: self.sender.to_owned(), 47 | }) 48 | } 49 | 50 | fn try_send(&self, msg: AnyMessage) -> Result<(), TrySendError> { 51 | match self.sender.try_send(msg) { 52 | Ok(_) => Ok(()), 53 | Err(TkTrySendError::Closed(e)) => Err(TrySendError::Disconnected(e)), 54 | Err(TkTrySendError::Full(e)) => Err(TrySendError::Full(e)), 55 | } 56 | } 57 | } 58 | 59 | /// Creates a Tokio bounded mailbox for communicating with an actor. 60 | pub fn mailbox_fn( 61 | buffer: usize, 62 | ) -> Box (Sender, Receiver) + Send + Sync> { 63 | Box::new(move || { 64 | let (mailbox_tx, mailbox_rx) = channel(buffer); 65 | ( 66 | Sender { 67 | sender_impl: Box::new(TkSenderImpl { 68 | sender: mailbox_tx.to_owned(), 69 | }), 70 | }, 71 | Receiver { 72 | receiver_impl: Box::new(TkReceiverImpl { 73 | receiver: mailbox_rx, 74 | }), 75 | }, 76 | ) 77 | }) 78 | } 79 | 80 | struct TkUnboundedReceiverImpl { 81 | receiver: TkUnboundedReceiver, 82 | } 83 | 84 | impl ReceiverImpl for TkUnboundedReceiverImpl { 85 | type Item = AnyMessage; 86 | 87 | fn as_any(&mut self) -> &mut (dyn Any + Send) { 88 | &mut self.receiver 89 | } 90 | } 91 | 92 | struct TkUnboundedSenderImpl { 93 | sender: TkUnboundedSender, 94 | } 95 | 96 | impl SenderImpl for TkUnboundedSenderImpl { 97 | type Item = AnyMessage; 98 | 99 | fn clone(&self) -> Box + Send + Sync> { 100 | Box::new(TkUnboundedSenderImpl { 101 | sender: self.sender.to_owned(), 102 | }) 103 | } 104 | 105 | fn try_send(&self, msg: AnyMessage) -> Result<(), TrySendError> { 106 | match self.sender.send(msg) { 107 | Ok(_) => Ok(()), 108 | Err(TkSendError(e)) => Err(TrySendError::Disconnected(e)), 109 | } 110 | } 111 | } 112 | 113 | /// Creates a Tokio unbounded mailbox for communicating with an actor. 114 | pub fn unbounded_mailbox_fn( 115 | ) -> Box (Sender, Receiver) + Send + Sync> { 116 | Box::new(|| { 117 | let (mailbox_tx, mailbox_rx) = unbounded_channel(); 118 | ( 119 | Sender { 120 | sender_impl: Box::new(TkUnboundedSenderImpl { 121 | sender: mailbox_tx.to_owned(), 122 | }), 123 | }, 124 | Receiver { 125 | receiver_impl: Box::new(TkUnboundedReceiverImpl { 126 | receiver: mailbox_rx, 127 | }), 128 | }, 129 | ) 130 | }) 131 | } 132 | 133 | type AskResult = Result>, Elapsed>; 134 | 135 | /// Implements the Ask pattern where a request message is sent 136 | /// and a reply is expected. 137 | pub trait Ask { 138 | /// Perform a one-shot ask operation while passing in a function 139 | /// to construct a message that accepts a reply_to sender. 140 | /// All asks have a timeout. Note also that if an unexpected 141 | /// reply is received then a None value will be returned. 142 | fn ask( 143 | &self, 144 | request_fn: &dyn Fn(&ActorRef) -> M, 145 | recv_timeout: Duration, 146 | ) -> Pin>>>; 147 | } 148 | 149 | impl Ask for ActorRef 150 | where 151 | M: Send + 'static, 152 | M2: Send + 'static, 153 | { 154 | fn ask( 155 | &self, 156 | request_fn: &dyn Fn(&ActorRef) -> M, 157 | recv_timeout: Duration, 158 | ) -> Pin>>> { 159 | let (reply_tx, mut reply_rx) = channel::(1); 160 | let _ = self.sender.try_send(Box::new(request_fn(&ActorRef { 161 | phantom_marker: PhantomData::, 162 | sender: Sender { 163 | sender_impl: Box::new(TkSenderImpl { sender: reply_tx }), 164 | }, 165 | }))); 166 | 167 | Box::pin(async move { 168 | match timeout(recv_timeout, reply_rx.recv()).await { 169 | Ok(Some(a)) => Ok(a.downcast::().ok()), 170 | Ok(None) => Ok(None), 171 | Err(e) => Err(e), 172 | } 173 | }) 174 | } 175 | } 176 | 177 | struct TaskStopped { 178 | id: usize, 179 | } 180 | 181 | // A macro describing the dispatching logic for both bounded and unbounded receivers 182 | macro_rules! start { 183 | ($receiver_type:ty, $command_tx:expr, $command_rx:expr) => {{ 184 | let mut spawned_tasks: Vec<(usize, JoinHandle<()>)> = vec![]; 185 | while let Some(message) = $command_rx.recv().await { 186 | match message.downcast::() { 187 | Ok(dispatcher_command) => match *dispatcher_command { 188 | DispatcherCommand::SelectWithAction { mut underlying } => { 189 | let id = spawned_tasks.len(); 190 | let tx = $command_tx.to_owned(); 191 | let join_handle = tokio::spawn(async move { 192 | let receiver = underlying 193 | .receiver 194 | .receiver_impl 195 | .as_any() 196 | .downcast_mut::<$receiver_type>() 197 | .unwrap(); 198 | while let Some(m) = receiver.recv().await { 199 | let active = (underlying.action)(m); 200 | if !active { 201 | break; 202 | } 203 | } 204 | let _ = tx.send(Box::new(TaskStopped { id })); 205 | }); 206 | spawned_tasks.push((id, join_handle)); 207 | } 208 | DispatcherCommand::Stop => { 209 | break; 210 | } 211 | }, 212 | Err(other_message_type) => match other_message_type.downcast::() { 213 | Ok(task_stopped) => { 214 | let _ = spawned_tasks.swap_remove(task_stopped.id); 215 | } 216 | Err(e) => { 217 | warn!( 218 | "Error received when expecting a dispatcher command: {:?}", 219 | e 220 | ) 221 | } 222 | }, 223 | } 224 | } 225 | }}; 226 | } 227 | 228 | /// A Dispatcher for Stage that leverages the Tokio runtime and a bounded command channel. 229 | 230 | pub struct TokioDispatcher { 231 | pub command_tx: TkSender, 232 | } 233 | 234 | impl TokioDispatcher { 235 | pub async fn start(&self, mut command_rx: TkReceiver) { 236 | start!(TkReceiver, self.command_tx, command_rx) 237 | } 238 | } 239 | 240 | impl Dispatcher for TokioDispatcher { 241 | fn send(&self, command: DispatcherCommand) -> Result<(), TrySendError> { 242 | self.command_tx 243 | .try_send(Box::new(command)) 244 | .map_err(|e| match e { 245 | TkTrySendError::Closed(e) => TrySendError::Disconnected(e), 246 | TkTrySendError::Full(e) => TrySendError::Full(e), 247 | }) 248 | } 249 | } 250 | 251 | impl Drop for TokioDispatcher { 252 | fn drop(&mut self) { 253 | self.stop() 254 | } 255 | } 256 | 257 | /// A Dispatcher for Stage that leverages the Tokio runtime and an unbounded command channel. 258 | 259 | pub struct TokioUnboundedDispatcher { 260 | pub command_tx: TkUnboundedSender, 261 | } 262 | 263 | impl TokioUnboundedDispatcher { 264 | pub async fn start(&self, mut command_rx: TkUnboundedReceiver) { 265 | start!(TkUnboundedReceiver, self.command_tx, command_rx) 266 | } 267 | } 268 | 269 | impl Drop for TokioUnboundedDispatcher { 270 | fn drop(&mut self) { 271 | self.stop() 272 | } 273 | } 274 | 275 | impl Dispatcher for TokioUnboundedDispatcher { 276 | fn send(&self, command: DispatcherCommand) -> Result<(), TrySendError> { 277 | self.command_tx 278 | .send(Box::new(command)) 279 | .map_err(|e| match e { 280 | TkSendError(e) => TrySendError::Disconnected(e), 281 | }) 282 | } 283 | } 284 | 285 | #[cfg(test)] 286 | mod tests { 287 | use std::{sync::Arc, time::Duration}; 288 | 289 | use tokio::{sync::mpsc::unbounded_channel, time::sleep}; 290 | 291 | use stage_core::{Actor, ActorContext, ActorRef}; 292 | 293 | use super::*; 294 | 295 | fn init_logging() { 296 | let _ = env_logger::builder().is_test(true).try_init(); 297 | } 298 | 299 | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] 300 | async fn test_greeting() { 301 | init_logging(); 302 | 303 | // Re-creates https://doc.akka.io/docs/akka/current/typed/actors.html#first-example 304 | 305 | // The messages 306 | 307 | struct Greet { 308 | whom: String, 309 | reply_to: ActorRef, 310 | } 311 | 312 | struct Greeted { 313 | whom: String, 314 | from: ActorRef, 315 | } 316 | 317 | struct SayHello { 318 | name: String, 319 | } 320 | 321 | // The HelloWorld actor 322 | 323 | struct HelloWorld {} 324 | 325 | impl Actor for HelloWorld { 326 | fn receive(&mut self, context: &mut ActorContext, message: &Greet) { 327 | println!("Hello {}!", message.whom); 328 | message.reply_to.tell(Greeted { 329 | whom: message.whom.to_owned(), 330 | from: context.actor_ref.to_owned(), 331 | }); 332 | } 333 | } 334 | 335 | // The HelloWorldBot actor 336 | 337 | struct HelloWorldBot { 338 | greeting_counter: u32, 339 | max: u32, 340 | } 341 | 342 | impl Actor for HelloWorldBot { 343 | fn receive(&mut self, context: &mut ActorContext, message: &Greeted) { 344 | let n = self.greeting_counter + 1; 345 | println!("Greeting {} for {}", n, message.whom); 346 | if n == self.max { 347 | context.stop(); 348 | } else { 349 | message.from.tell(Greet { 350 | whom: message.whom.to_owned(), 351 | reply_to: context.actor_ref.to_owned(), 352 | }); 353 | self.greeting_counter = n; 354 | } 355 | } 356 | } 357 | 358 | // The root actor 359 | 360 | struct HelloWorldMain { 361 | greeter: Option>, 362 | } 363 | 364 | impl Actor for HelloWorldMain { 365 | fn receive(&mut self, context: &mut ActorContext, message: &SayHello) { 366 | let greeter = match &self.greeter { 367 | None => { 368 | let greeter = context.spawn(|| Box::new(HelloWorld {})); 369 | self.greeter = Some(greeter.to_owned()); 370 | greeter 371 | } 372 | Some(greeter) => greeter.to_owned(), 373 | }; 374 | 375 | let reply_to = context.spawn(|| { 376 | Box::new(HelloWorldBot { 377 | greeting_counter: 0, 378 | max: 3, 379 | }) 380 | }); 381 | greeter.tell(Greet { 382 | whom: message.name.to_owned(), 383 | reply_to, 384 | }); 385 | } 386 | } 387 | 388 | // Establish our dispatcher. 389 | 390 | let (command_tx, command_rx) = unbounded_channel(); 391 | let dispatcher = Arc::new(TokioUnboundedDispatcher { command_tx }); 392 | 393 | // Establish a function to produce mailboxes 394 | 395 | let mailbox_fn = Arc::new(unbounded_mailbox_fn()); 396 | 397 | // Create a root context, which is essentiallly the actor system. We 398 | // also send a couple of messages for our demo. 399 | 400 | let system = ActorContext::::new( 401 | || Box::new(HelloWorldMain { greeter: None }), 402 | dispatcher.to_owned(), 403 | mailbox_fn, 404 | ); 405 | 406 | system.actor_ref.tell(SayHello { 407 | name: "World".to_string(), 408 | }); 409 | 410 | system.actor_ref.tell(SayHello { 411 | name: "Stage".to_string(), 412 | }); 413 | 414 | // Run the dispatcher select function on its own thread. We wait 415 | // for the select function to finish, which will be when will 416 | // tell the "system" (the actor context above) to stop, it is stops 417 | // itself. 418 | 419 | let dispatcher_task_dispatcher = dispatcher.to_owned(); 420 | let dispatcher_task = 421 | tokio::spawn(async move { dispatcher_task_dispatcher.start(command_rx).await }); 422 | 423 | let _ = sleep(Duration::from_millis(500)).await; 424 | 425 | dispatcher.stop(); 426 | 427 | assert!(dispatcher_task.await.is_ok()) 428 | } 429 | } 430 | --------------------------------------------------------------------------------