├── .gitignore ├── doc ├── requirements.txt ├── index.rst ├── conf.py ├── glossary.rst ├── ecosystem.rst ├── state_machine.rst ├── loop_init.rst └── Makefile ├── Cargo.toml ├── src ├── future.rs ├── lib.rs ├── config.rs ├── loop_api.rs ├── notify.rs ├── compose.rs ├── error.rs ├── loop_time.rs ├── machine.rs ├── creator.rs ├── macros.rs ├── response.rs ├── handler.rs └── scope.rs ├── LICENSE ├── .travis.yml ├── vagga.yaml ├── README.rst └── examples ├── tcp_echo_server.rs └── telnet.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /run 3 | /.vagga 4 | /Cargo.lock 5 | /doc/_build 6 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinxcontrib-domaintools==0.1 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Welcome to Rotor's documentation! 3 | ================================= 4 | 5 | This is work in progress book about writing applications in "rotor". 6 | There are also `Api Docs`_. 7 | 8 | Contents: 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | loop_init 14 | state_machine 15 | ecosystem 16 | glossary 17 | 18 | .. _`Api Docs`: http://tailhook.github.com/rotor/ 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rotor" 3 | description = """ 4 | The mio-based framework for doing I/O in simple and composable way 5 | """ 6 | license = "MIT" 7 | readme = "README.rst" 8 | keywords = ["io", "loop", "state", "machine", "mio"] 9 | homepage = "http://github.com/tailhook/rotor" 10 | documentation = "http://tailhook.github.com/rotor/" 11 | version = "0.6.3" 12 | authors = ["paul@colomiets.name"] 13 | 14 | [dependencies] 15 | mio = "0.6.1" 16 | slab = "0.3" 17 | quick-error = "1.0.0" 18 | log = "0.3.1" 19 | void = "1.0.0" 20 | 21 | [dev-dependencies] 22 | argparse = "0.2.1" 23 | nix = "0.4.2" 24 | void = "1.0.0" 25 | 26 | [features] 27 | log_errors = [] 28 | 29 | [lib] 30 | name = "rotor" 31 | path = "src/lib.rs" 32 | 33 | -------------------------------------------------------------------------------- /src/future.rs: -------------------------------------------------------------------------------- 1 | use {Notify, Port, Future}; 2 | 3 | impl Port { 4 | /// Set the value of the future 5 | /// 6 | /// # Panics 7 | /// 8 | /// Panics when message to the target main loop can't be sent 9 | pub fn set(self, value: T) { 10 | *self.contents.lock() 11 | .expect("Lock of the future is poisoned") = Some(value); 12 | self.channel.send(Notify::Fsm(self.token)) 13 | .expect("Target channel for the future is full"); 14 | } 15 | } 16 | 17 | impl Future { 18 | /// Get the value consuming the future 19 | /// 20 | /// # Panics 21 | /// 22 | /// Panics when no value is set yet 23 | pub fn get(self) -> T { 24 | self.contents.lock().expect("Lock of the future is poisoned") 25 | .take().expect("Future is not resolved yet") 26 | } 27 | pub fn done(&self) -> bool { 28 | self.contents.lock() 29 | .expect("Lock of the future is poisoned").is_some() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Paul Colomiets 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | extensions = [] 4 | templates_path = ['_templates'] 5 | source_suffix = '.rst' 6 | master_doc = 'index' 7 | 8 | project = u'Rotor' 9 | copyright = u'2015, Paul Colomiets' 10 | 11 | version = '0.6' 12 | release = '0.6.3' 13 | exclude_patterns = ['_build'] 14 | pygments_style = 'sphinx' 15 | html_theme = 'default' 16 | html_static_path = ['_static'] 17 | htmlhelp_basename = 'Rotordoc' 18 | 19 | latex_elements = { } 20 | 21 | latex_documents = [ 22 | ('index', 'Rotor.tex', u'Rotor Documentation', 23 | u'Paul Colomiets', 'manual'), 24 | ] 25 | 26 | man_pages = [ 27 | ('index', 'rotor', u'Rotor Documentation', 28 | [u'Paul Colomiets'], 1) 29 | ] 30 | 31 | texinfo_documents = [ 32 | ('index', 'Rotor', u'Rotor Documentation', 33 | u'Paul Colomiets', 'Rotor', 'Asynchronous I/O for rust.', 34 | 'Miscellaneous'), 35 | ] 36 | 37 | # on_rtd is whether we are on readthedocs.org 38 | import os 39 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 40 | 41 | if not on_rtd: # only import and set the theme if we're building docs locally 42 | import sphinx_rtd_theme 43 | html_theme = 'sphinx_rtd_theme' 44 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 45 | 46 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 47 | -------------------------------------------------------------------------------- /doc/glossary.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Glossary 3 | ======== 4 | 5 | state machine 6 | You probably know `the theory`__. In this docs when we refer to 7 | state machine we refer to a type (most of the time the enum) that 8 | implements some trait designed according to the rules below. There is some 9 | `introductory article about why state machines are designed that 10 | way`__. 11 | 12 | State machine implements at least abstract ``rotor::Machine`` trait. 13 | But there are also more state machine traits that are more concrete. 14 | 15 | Rules: TBD 16 | 17 | .. __: https://en.wikipedia.org/wiki/State_machine 18 | .. __: https://medium.com/@paulcolomiets/asynchronous-io-in-rust-36b623e7b965 19 | 20 | .. _child state machine: 21 | 22 | child state machine 23 | Often one state machine calls an action from another state machine. The 24 | one that calls actions is a **parent**. The one that receives actions 25 | is called **child**. The parent state machine usually also owns the child 26 | state machine (means that when parent is shut down, the all the children 27 | too). 28 | 29 | There might be multiple child state machines when the protocol allows 30 | multiple underlying requests/substreams/whatever to be mixed and used 31 | simultaneously 32 | 33 | parent state machine 34 | See `child state machine`_ 35 | 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | env: 7 | secure: "B2ccsEzUfGU486O4YFv4dLr2oMeZJmBBv6cG1rdFiPkPccpbe0bSF9NMg2afIUZcOeOgzdF4V0JLyqf5ckx78VsbcWV1Di5DqJ8hnSdaw51+o5wu8YMjdx4eDDtLxVIWEnEus+11v9pJjoRxL/6+JzYaGD1vUZ2iu80mZiGCaAA2P1pXIdRZLWrt2S3sDRmkoMS0SIi5pEElFQ4RNiOWS6mcyNXUY34bAQv7F2J1J2l+N+ZyuuhaDdlx5xI2xDTdtVP1Jj1wf8e4SmVzqg9Ucovjfjpol9AP34r8dEU6A86LnVkNTmFj8Ts4Qy344+TlEGZ+37KaIqAGYQe35wawxYQizo2Y0IXfuH5NFJp514l4RxNeSs5MUamhIshxRljmD3DaNrKkyu7rCpB/z3dnFfJYLNOrpX9XBQTWtoWmW7RSHp6IDdvbLP/IyT8KNOiufED7RIgT4rbqK/wfunVHPVf4wsserTyBAvYd/wMwTMR+uwoSbfdiIY0FwmtWovBbJMRLwRDZV+8nQ2puT5FFuwleoOgPNaoc/tzGkS1ErCBQVaLo6zbKCKBLQiEBoFFn3YDUuA0Hay/Eu8l7etpKj92rnFn1FRTH+3h0+z6C2e+uN4qMg6W/tBc0jKP3WGrBqfj1hMNxEXYOjGWt24GmjeMsAjYqwjdfpjXTjuPgsjM=" 8 | script: 9 | - cargo build --verbose 10 | # don't run doc tests because they don't run well for macros 11 | - cargo test test --verbose 12 | after_success: | 13 | [ $TRAVIS_RUST_VERSION = stable ] && 14 | [ $TRAVIS_BRANCH = master ] && 15 | [ $TRAVIS_PULL_REQUEST = false ] && 16 | cargo doc && 17 | echo "" > target/doc/index.html && 18 | pip install ghp-import --user && 19 | ~/.local/bin/ghp-import -n target/doc && 20 | git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages 21 | -------------------------------------------------------------------------------- /vagga.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | 3 | make: !Command 4 | description: Build rotor library 5 | container: ubuntu 6 | run: [cargo, build] 7 | 8 | test: !Command 9 | description: Run unit tests 10 | container: ubuntu 11 | run: [cargo, test] 12 | 13 | cargo: !Command 14 | description: Run any cargo command 15 | container: ubuntu 16 | run: [cargo] 17 | 18 | doc: !Command 19 | description: Build sphinx documentation 20 | container: docs 21 | run: [make, html] 22 | work-dir: doc 23 | epilog: | 24 | -------------------------------------------------- 25 | Documentation is now at doc/_build/html/index.html 26 | 27 | doc-api: !Command 28 | description: Build API documentation (rustdoc) 29 | container: ubuntu 30 | run: [cargo, doc] 31 | epilog: | 32 | --------------------------------------------------- 33 | Documentation is now at target/doc/rotor/index.html 34 | 35 | containers: 36 | 37 | ubuntu: 38 | setup: 39 | - !Ubuntu trusty 40 | - !UbuntuUniverse ~ 41 | - !Install [make, checkinstall, wget, ca-certificates, 42 | libssl-dev, build-essential] 43 | 44 | - !TarInstall 45 | url: "http://static.rust-lang.org/dist/rust-1.8.0-x86_64-unknown-linux-gnu.tar.gz" 46 | script: "./install.sh --prefix=/usr \ 47 | --components=rustc,rust-std-x86_64-unknown-linux-gnu,cargo" 48 | 49 | environ: 50 | HOME: /work/target 51 | 52 | docs: 53 | setup: 54 | - !Alpine v3.2 55 | - !Install [alpine-base, py-sphinx, make] 56 | - !Py2Requirements doc/requirements.txt 57 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The mio-based framework for doing I/O in a simple and composable way. 2 | //! 3 | //! You may want to look at [the guide](http://rotor.readthedocs.org). 4 | 5 | #![crate_name="rotor"] 6 | 7 | pub extern crate void as void_original; 8 | pub extern crate mio as mio_original; 9 | pub extern crate slab; 10 | #[macro_use] extern crate log; 11 | #[macro_use] extern crate quick_error; 12 | 13 | mod handler; 14 | mod scope; 15 | mod loop_api; 16 | mod response; 17 | mod compose; 18 | mod macros; 19 | mod machine; 20 | mod notify; 21 | mod config; 22 | mod creator; 23 | mod error; 24 | mod loop_time; 25 | 26 | pub use machine::Machine; 27 | pub use scope::{Scope, EarlyScope, GenericScope}; 28 | pub use scope::{scope as _scope, early_scope as _early_scope}; 29 | pub use notify::{Notifier, WakeupError}; 30 | pub use config::Config; 31 | pub use creator::{LoopCreator as Loop, LoopInstance}; 32 | pub use error::SpawnError; 33 | pub use loop_time::Time; 34 | pub use handler::{Timeo as _Timeo, Notify as _Notify}; 35 | pub use loop_api::{LoopApi as _LoopApi}; 36 | 37 | pub use compose::{Compose2}; 38 | 39 | // Re-export mio types used in rotor 40 | pub use mio::{Ready as EventSet, Evented, PollOpt}; 41 | pub use mio::timer::{Timeout, TimerError}; 42 | pub use mio_original as mio; 43 | // Re-export void too 44 | pub use void::{Void}; 45 | pub use void_original as void; 46 | 47 | /// Slab 48 | pub type Slab = slab::Slab; 49 | 50 | /// The response of a state machine to the (mio) action 51 | /// 52 | /// This value is returned by many methods of the `Machine` trait. 53 | pub struct Response(response::ResponseImpl); 54 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::default::Default; 3 | 4 | use mio::deprecated::{EventLoop, EventLoopBuilder}; 5 | 6 | use handler::Handler; 7 | use {Machine, Slab}; 8 | 9 | 10 | /// Event loop configuration 11 | /// 12 | /// The structure currently embeds mio configuration too 13 | #[derive(Debug, Clone)] 14 | pub struct Config { 15 | mio: EventLoopBuilder, 16 | slab_capacity: usize, 17 | } 18 | 19 | impl Default for Config { 20 | fn default() -> Config { 21 | Config { 22 | mio: Default::default(), 23 | slab_capacity: 4096, 24 | } 25 | } 26 | } 27 | 28 | impl Config { 29 | /// Create new configuration with default options 30 | pub fn new() -> Config { 31 | Config { 32 | mio: EventLoopBuilder::new(), 33 | slab_capacity: 4096, 34 | } 35 | } 36 | /// A mutable reference for ``mio::EventLoopBuilder`` 37 | pub fn mio(&mut self) -> &mut EventLoopBuilder { 38 | &mut self.mio 39 | } 40 | /// A capacity of state machine slab 41 | /// 42 | /// This limits the number of state machines that application is able 43 | /// to create. Consequently this limits the number of connections that 44 | /// server is able to establish. 45 | pub fn slab_capacity(&mut self, capacity: usize) { 46 | self.slab_capacity = capacity; 47 | } 48 | } 49 | 50 | 51 | // The functions are probably don't belong here, but they accept otherwise 52 | // private parts of the config structure 53 | 54 | pub fn create_slab(cfg: &Config) -> Slab { 55 | Slab::with_capacity(cfg.slab_capacity) 56 | } 57 | 58 | pub fn create_loop(cfg: &Config) 59 | -> Result>, io::Error> 60 | { 61 | cfg.mio.clone().build() 62 | } 63 | -------------------------------------------------------------------------------- /src/loop_api.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::time::Duration; 3 | 4 | use mio::Token; 5 | use mio::deprecated::EventLoop; 6 | 7 | use handler::{Handler, Timeo}; 8 | use {Machine}; 9 | use {Evented, EventSet, PollOpt, Timeout, TimerError}; 10 | 11 | 12 | #[doc(hidden)] 13 | pub trait LoopApi { 14 | fn register(&mut self, io: &Evented, token: Token, 15 | interest: EventSet, opt: PollOpt) -> io::Result<()>; 16 | fn reregister(&mut self, io: &Evented, token: Token, 17 | interest: EventSet, opt: PollOpt) -> io::Result<()>; 18 | fn deregister(&mut self, io: &Evented) -> io::Result<()>; 19 | fn timeout_ms(&mut self, token: Token, delay: u64) 20 | -> Result; 21 | fn clear_timeout(&mut self, token: Timeout) -> bool; 22 | fn shutdown(&mut self); 23 | } 24 | 25 | impl<'a, M: Machine> LoopApi for EventLoop> 26 | { 27 | fn register(&mut self, io: &Evented, token: Token, 28 | interest: EventSet, opt: PollOpt) -> io::Result<()> 29 | { 30 | self.register(io, token, interest, opt) 31 | } 32 | 33 | fn reregister(&mut self, io: &Evented, token: Token, 34 | interest: EventSet, opt: PollOpt) -> io::Result<()> 35 | { 36 | self.reregister(io, token, interest, opt) 37 | } 38 | 39 | fn deregister(&mut self, io: &Evented) -> io::Result<()> 40 | { 41 | self.deregister(io) 42 | } 43 | 44 | fn timeout_ms(&mut self, token: Token, delay: u64) 45 | -> Result 46 | { 47 | self.timeout( Timeo::Fsm(token), Duration::from_millis(delay)) 48 | } 49 | fn clear_timeout(&mut self, token: Timeout) -> bool 50 | { 51 | self.clear_timeout(&token) 52 | } 53 | fn shutdown(&mut self) { 54 | self.shutdown() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /doc/ecosystem.rst: -------------------------------------------------------------------------------- 1 | .. _ecosystem: 2 | 3 | ========= 4 | Ecosystem 5 | ========= 6 | 7 | 8 | Libraries 9 | ========= 10 | 11 | * `rotor-tools `_ -- a collection of 12 | small convenience utilities 13 | * `rotor-test `_ -- a collection of 14 | utilities for writing unit tests 15 | * `rotor-stream `_ -- an abstraction for 16 | writing protocols which use TCP or Unix stream sockets 17 | * `rotor-carbon `_ -- implementation of 18 | the `carbon `_ protocol (more known as graphite) 19 | * `rotor-dns `_ -- DNS support for rotor 20 | * `rotor-http `_ -- HTTP server and client 21 | implementation 22 | * `rotor-redis `_ -- redis client 23 | implementation 24 | * `hyper `_ -- 25 | the implementation of HTTP protocol added to hyper itself 26 | * `rotor-capnp `_ -- implementation 27 | of Cap'n'Proto protocol 28 | 29 | 30 | Applications 31 | ============ 32 | 33 | * `Kinglet `_ -- a HTTP server 34 | * `basic-http-server `_ -- also a 35 | HTTP server 36 | 37 | 38 | Other 39 | ===== 40 | 41 | * `stator `_ -- a wrapper around foreign 42 | function interface (FFI) for various rotor libraries that allows 43 | dispatching them from scripting languages; thus offloading asynchronous 44 | and protocol parsing work to rotor that is put in separate thread; so 45 | rust code is running in parallel to the scripting language interpreter. 46 | -------------------------------------------------------------------------------- /src/notify.rs: -------------------------------------------------------------------------------- 1 | use mio::Token; 2 | use mio::deprecated::Sender; 3 | 4 | use handler::{Notify}; 5 | 6 | quick_error! { 7 | /// Error when waking up a connection 8 | /// 9 | /// In most cases it's okay to panic on this error 10 | #[derive(Debug)] 11 | pub enum WakeupError { 12 | /// I/O error when sending data to internal pipe 13 | /// 14 | /// We discard the io error as there no practical reason for this 15 | /// error to occur 16 | Io { 17 | description("I/O error happened when trying to wake up") 18 | } 19 | /// The pipe is full, the useful thing to do is configure longer 20 | /// queue in mio loop. Or alternatively, send less messages. 21 | Full { 22 | description("The notification pipe is full. \ 23 | You may want to increase it's size") 24 | } 25 | /// The notification queue is closed. Probably event loop is shut down 26 | Closed { 27 | description("Notification queue is close") 28 | } 29 | } 30 | } 31 | 32 | 33 | /// The object used to wakeup unrelated state machine 34 | /// 35 | /// You may use a notifiers between multiple threads 36 | #[derive(Clone, Debug)] 37 | pub struct Notifier { 38 | token: Token, 39 | channel: Sender, 40 | } 41 | 42 | pub fn create_notifier(token: Token, channel: &Sender) -> Notifier { 43 | Notifier { 44 | token: token, 45 | channel: channel.clone() 46 | } 47 | } 48 | 49 | impl Notifier { 50 | /// Wakeup a state machine 51 | /// 52 | /// 53 | pub fn wakeup(&self) -> Result<(), WakeupError> { 54 | use mio::deprecated::NotifyError::*; 55 | match self.channel.send(Notify::Fsm(self.token)) { 56 | Ok(()) => Ok(()), 57 | Err(Closed(_)) => Err(WakeupError::Closed), 58 | Err(Io(_)) => Err(WakeupError::Io), 59 | Err(Full(_)) => Err(WakeupError::Full), 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/compose.rs: -------------------------------------------------------------------------------- 1 | use mio::Ready; 2 | use void::{Void, unreachable}; 3 | 4 | use {Machine, Scope, Response}; 5 | 6 | 7 | /// Composes two state machines 8 | /// 9 | /// Used to "mount" two different application into single main loop, or to 10 | /// use multiple protocols simultaneously. Can be nested to any level. 11 | /// 12 | /// We will probably implement n > 2 composition later, for effeciency 13 | /// reasons. 14 | pub enum Compose2 { 15 | A(A), 16 | B(B), 17 | } 18 | 19 | pub enum Compose2Seed { 20 | As(A), 21 | Bs(B), 22 | } 23 | 24 | impl Machine for Compose2 25 | where AA: Machine, BB:Machine 26 | { 27 | type Context = X; 28 | type Seed = Compose2Seed; 29 | 30 | fn create(seed: Self::Seed, scope: &mut Scope) 31 | -> Response 32 | { 33 | use Compose2::*; 34 | use self::Compose2Seed::*; 35 | match seed { 36 | As(s) => AA::create(s, scope).map(A, |x| unreachable(x)), 37 | Bs(s) => BB::create(s, scope).map(B, |x| unreachable(x)), 38 | } 39 | } 40 | fn ready(self, events: Ready, scope: &mut Scope) 41 | -> Response 42 | { 43 | use Compose2::*; 44 | use self::Compose2Seed::*; 45 | match self { 46 | A(m) => { m.ready(events, scope).map(A, As) } 47 | B(m) => { m.ready(events, scope).map(B, Bs) } 48 | } 49 | } 50 | fn spawned(self, scope: &mut Scope) -> Response 51 | { 52 | use Compose2::*; 53 | use self::Compose2Seed::*; 54 | match self { 55 | A(m) => { m.spawned(scope).map(A, As) } 56 | B(m) => { m.spawned(scope).map(B, Bs) } 57 | } 58 | } 59 | fn timeout(self, scope: &mut Scope) -> Response { 60 | use Compose2::*; 61 | use self::Compose2Seed::*; 62 | match self { 63 | A(m) => { m.timeout(scope).map(A, As) } 64 | B(m) => { m.timeout(scope).map(B, Bs) } 65 | } 66 | } 67 | fn wakeup(self, scope: &mut Scope) -> Response { 68 | use Compose2::*; 69 | use self::Compose2Seed::*; 70 | match self { 71 | A(m) => { m.wakeup(scope).map(A, As) } 72 | B(m) => { m.wakeup(scope).map(B, Bs) } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::error::Error; 3 | 4 | 5 | /// Error when spawning a new state machine 6 | pub enum SpawnError { 7 | /// The State Machine Slab capacity is reached 8 | /// 9 | /// The capacity is configured in the `rotor::Config` and is used 10 | /// for creating `rotor::Loop`. 11 | /// 12 | /// The item in this struct is the Seed that send to create a machine 13 | NoSlabSpace(S), 14 | /// Error returned from `Machine::create` handler 15 | UserError(Box), 16 | } 17 | 18 | impl fmt::Display for SpawnError { 19 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 20 | use self::SpawnError::*; 21 | match *self { 22 | NoSlabSpace(_) => { 23 | write!(fmt, "state machine slab capacity limit is reached") 24 | } 25 | UserError(ref err) => { 26 | write!(fmt, "{}", err) 27 | } 28 | } 29 | } 30 | } 31 | 32 | impl SpawnError { 33 | pub fn description(&self) -> &str { 34 | use self::SpawnError::*; 35 | match self { 36 | &NoSlabSpace(_) => "state machine slab capacity limit is reached", 37 | &UserError(ref err) => err.description(), 38 | } 39 | } 40 | pub fn cause(&self) -> Option<&Error> { 41 | use self::SpawnError::*; 42 | match self { 43 | &NoSlabSpace(_) => None, 44 | &UserError(ref err) => Some(&**err), 45 | } 46 | } 47 | pub fn map T>(self, fun:F) -> SpawnError { 48 | use self::SpawnError::*; 49 | match self { 50 | NoSlabSpace(x) => NoSlabSpace(fun(x)), 51 | UserError(e) => UserError(e), 52 | } 53 | } 54 | } 55 | impl Error for SpawnError { 56 | fn description(&self) -> &str { 57 | self.description() 58 | } 59 | fn cause(&self) -> Option<&Error> { 60 | self.cause() 61 | } 62 | } 63 | 64 | impl From> for SpawnError { 65 | fn from(x: Box) -> SpawnError { 66 | SpawnError::UserError(x) 67 | } 68 | } 69 | 70 | impl fmt::Debug for SpawnError { 71 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 72 | use self::SpawnError::*; 73 | match *self { 74 | NoSlabSpace(..) => { 75 | write!(fmt, "NoSlabSpace()") 76 | } 77 | UserError(ref err) => { 78 | write!(fmt, "UserError({:?})", err) 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /doc/state_machine.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: rust 2 | 3 | =========================== 4 | Implementing State Machines 5 | =========================== 6 | 7 | This guide is for **authors** of the protocols, *not* for **users** of the 8 | protocols. Read it if you want to write an **new** protocol implementation on 9 | top of raw ``rotor::Machine``. Otherwise consult on your protocol documenation 10 | (probably good links are in :ref:`Ecosystem`) 11 | 12 | 13 | Boilerplate 14 | =========== 15 | 16 | This is just a blanket stub implementation I usually start with, filling 17 | in methods one by one:: 18 | 19 | extern crate rotor; 20 | 21 | use rotor::{Machine, EventSet, Scope, Response}; 22 | use rotor::void::{unreachable, Void}; 23 | 24 | impl Machine for Fsm { 25 | type Context = C; 26 | type Seed = Void; 27 | fn create(seed: Self::Seed, _scope: &mut Scope) 28 | -> Response 29 | { 30 | unreachable(seed) 31 | } 32 | fn ready(self, _events: EventSet, _scope: &mut Scope) 33 | -> Response 34 | { 35 | unimplemented!(); 36 | } 37 | fn spawned(self, _scope: &mut Scope) -> Response 38 | { 39 | unimplemented!(); 40 | } 41 | fn timeout(self, _scope: &mut Scope) -> Response 42 | { 43 | unimplemented!(); 44 | } 45 | fn wakeup(self, _scope: &mut Scope) -> Response 46 | { 47 | unimplemented!(); 48 | } 49 | } 50 | 51 | There are two intricate things here: 52 | 53 | 1. We use ``void`` crate and ``void::Void`` type to denote that seed can't be 54 | created so ``create`` method is never called 55 | 56 | Keep the type ``void`` unless your machine spawns new state machines. And 57 | in the latter case it's advised to use some abstraction for state machine 58 | spawning. There is an ``rotor_stream::Accept`` for accepting sockets, more 59 | to come. 60 | 61 | 2. Implementation should almost always use generic context (``impl``) as 62 | only end application should know the exact layout of a context. 63 | 64 | You may limit the generic with some traits (``impl``). 65 | 66 | Often, your state machine doesn't rely on context at all. Currently, this 67 | requires adding a ``PhantomData<*const C>`` marker to state machine. 68 | The marker_ is zero-sized, so it just a little bit of boring code. 69 | 70 | .. _marker:: http://doc.rust-lang.org/std/marker/struct.PhantomData.html 71 | 72 | -------------------------------------------------------------------------------- /src/loop_time.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::ops::Add; 3 | use std::time::{Duration, Instant, SystemTime}; 4 | 5 | /// The current time 6 | /// 7 | /// This value is similar to (and directly derived from) the 8 | /// `time::SteadyTime`. But it has three important properties: 9 | /// 10 | /// 1. It has a millisecond precision 11 | /// 2. It's size is 8 bytes (SteadyTime is 16 bytes) 12 | /// 3. It supports math with `std::time::Duration` (more future-proof) 13 | /// 14 | /// The size of the value is important because we are going to have a lot of 15 | /// timeouts and lots of timestamps stored inside the state machines. 16 | /// 17 | /// Precision of millisecond is good enough, and even better for our use case 18 | /// as it allows faster comparison and more frequent matches. 19 | /// 20 | /// Warning: when adding a duration that is not a multiple of a millisecond 21 | /// we truncate (i.e. floor) the duration value. We may change this in future. 22 | /// Note that for timeouts this works well enough, as mio already bumps the 23 | /// timeout to at least a millisecond ahead. 24 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 25 | pub struct Time(u64); 26 | 27 | 28 | fn millis(dur: Duration) -> u64 { 29 | dur.as_secs()*1000 + (dur.subsec_nanos()/1000000) as u64 30 | } 31 | 32 | impl Add for Time { 33 | type Output = Time; 34 | fn add(self, rhs: Duration) -> Time { 35 | Time(self.0 + millis(rhs)) 36 | } 37 | } 38 | 39 | impl Time { 40 | /// Zero time value, should be used only as a starting point for unit 41 | /// tests 42 | pub fn zero() -> Time { 43 | // In fact we don't care actual value, but the 1 allows us to 44 | // implement NonZero in the future 45 | Time(1) 46 | } 47 | } 48 | 49 | pub fn make_time(base: Instant, now: Instant) -> Time { 50 | Time(millis(now.duration_since(base)) 51 | // Time starts with 1 not with zero 52 | + 1) 53 | } 54 | 55 | pub fn mio_timeout_ms(now: Time, event: Time) -> u64 { 56 | if event.0 > now.0 { 57 | // We need +1 because we truncate both old and new timeouts to 58 | // millisecond precision, while mio calculates at the nanosecond 59 | // precision (but doesn't expose it). So wake up time may be up 60 | // to a millisecond smaller then expected 61 | event.0 - now.0 + 1 62 | } else { 63 | 0 64 | } 65 | } 66 | 67 | pub fn estimate_system_time(now: Time, value: Time) -> SystemTime { 68 | SystemTime::now() + Duration::from_millis(value.0 - now.0) 69 | } 70 | 71 | 72 | #[cfg(test)] 73 | mod test { 74 | use super::Time; 75 | use std::time::Duration; 76 | 77 | 78 | #[test] 79 | fn test_add_duration() { 80 | let tm = Time::zero(); 81 | assert_eq!(tm + Duration::new(10, 0), Time(10001)); 82 | assert_eq!(tm + Duration::from_millis(150), Time(151)); 83 | assert_eq!(tm + Duration::from_millis(12345), Time(12346)); 84 | assert_eq!(tm + Duration::new(5, 0) + Duration::from_millis(20), 85 | Time(5021)); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/machine.rs: -------------------------------------------------------------------------------- 1 | use void::Void; 2 | 3 | use {Response, Scope, EventSet, SpawnError}; 4 | 5 | 6 | /// A trait that every state machine in the loop must implement 7 | pub trait Machine: Sized { 8 | /// Context type for the state machine 9 | /// 10 | /// This is a container of the global state for the application 11 | type Context; 12 | /// Seed is piece of data that is needed to initialize the machine 13 | /// 14 | /// It needs Any because it's put into Box object when state machine 15 | /// is failed to create. Hopefully this is not huge limitation. 16 | /// 17 | /// Note: this is only used to create machines returned by this machine. 18 | /// So unless this machine processses accepting socket this should 19 | /// probably be Void. 20 | type Seed: Sized; 21 | 22 | /// Create a machine from some data 23 | /// 24 | /// The error should be rare enough so that Box overhead 25 | /// is negligible. Most errors here should be resource exhaustion, like 26 | /// there are no slots in Slab or system limit on epoll watches exceeded. 27 | /// 28 | /// Note: this method is used internally (by event loop) to create a 29 | /// socket from a Seed returned by this machine. This method should 30 | /// **not** be used to create machine by external code. Create a 31 | /// machine-specific `Type::new` method for the purpose. 32 | /// 33 | /// Note: we don't support spawning more state machines in create handler 34 | fn create(seed: Self::Seed, scope: &mut Scope) 35 | -> Response; 36 | 37 | /// Socket readiness notification 38 | fn ready(self, events: EventSet, scope: &mut Scope) 39 | -> Response; 40 | 41 | /// Called after spawn event 42 | /// 43 | /// This is mostly a continuation event. I.e. when you accept a socket 44 | /// and return a new state machine from `ready()`. You may wish to accept 45 | /// another socket right now. This is what `spawned` event is for. 46 | fn spawned(self, scope: &mut Scope) 47 | -> Response; 48 | 49 | /// Called instead of spawned, if there is no slab space 50 | /// 51 | /// For example, in `accept` handler you might want to put the thing 52 | /// into temporary storage, stop accepting and wait until slot is empty 53 | /// again. 54 | /// 55 | /// Note: it's useless to spawn from here if the failure was , it almost certainly will fail 56 | /// again, but may use a timeout 57 | fn spawn_error(self, _scope: &mut Scope, 58 | error: SpawnError) 59 | -> Response 60 | { 61 | panic!("Error spawning state machine: {}", error); 62 | } 63 | 64 | /// Timeout happened 65 | fn timeout(self, scope: &mut Scope) 66 | -> Response; 67 | 68 | /// Message received 69 | /// 70 | /// Note the spurious wakeups are possible, because messages are 71 | /// asynchronous, and state machine is identified by token. 72 | /// Tokens are reused quickly. 73 | /// 74 | /// So never make this `unreachable!()` or `unimplemented!()` 75 | fn wakeup(self, scope: &mut Scope) 76 | -> Response; 77 | } 78 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Rotor 3 | ===== 4 | 5 | :Status: Alpha 6 | :Examples: `TCP echo server`_, `TCP client (telnet)`_ 7 | :Ecosystem: `libraries and apps using rotor`_ 8 | :Documentation: http://tailhook.github.com/rotor/ 9 | 10 | .. _TCP echo server: https://github.com/tailhook/rotor/blob/master/examples/tcp_echo_server.rs 11 | .. _TCP client (telnet): https://github.com/tailhook/rotor/blob/master/examples/telnet.rs 12 | .. _libraries and apps using rotor: http://rotor.readthedocs.org/en/latest/ecosystem.html 13 | 14 | The mio-based framework for rust for doing I/O in simple and composable way. 15 | 16 | The rotor core (this crate) basically consists of: 17 | 18 | * An event loop handler (in terms of mio) which turns mio event into 19 | event to specific state machine 20 | * A Future type which allows communication between state machines in safe 21 | and efficient way 22 | * A simple way to combine multiple libraries (e.g. multiple protocol handlers) 23 | into single mio event loop 24 | 25 | At the end of the day, rotor is the minimalistic core for making composable 26 | libraries on top. It's less than 0.5KLoC. 27 | 28 | You are expected to use some higher level abstraction most of the time. 29 | For example, you should use stream abstraction (yet to be implemented) for 30 | making TCP protocol parser. 31 | 32 | 33 | Resources 34 | ========= 35 | 36 | **Both are rather historical at the moment** 37 | 38 | * `Asynchronous IO in Rust `_ 39 | (random design notes about this library) 40 | * `Asynchronous IO in Rust (part II) `_ 41 | * `Async IO in Rust (part III) `_ 42 | 43 | 44 | Benchmarks 45 | ========== 46 | 47 | These benchmarks are based on **old version of** `this example`_. Hopefully 48 | we will get updated benchmarks soon. 49 | 50 | .. _this example: https://github.com/tailhook/rotor-http/blob/master/examples/hello_world_server.rs 51 | 52 | Just few micro-benchmarks to show that framework has a decent peformance. 53 | 54 | The performance on the few years old laptop (i7-3517U CPU @ 1.90GHz):: 55 | 56 | > wrk -t2 -c 400 http://localhost:8888/ 57 | Running 10s test @ http://localhost:8888/ 58 | 2 threads and 400 connections 59 | Thread Stats Avg Stdev Max +/- Stdev 60 | Latency 11.19ms 18.03ms 627.44ms 99.54% 61 | Req/Sec 19.66k 1.76k 21.93k 81.00% 62 | 391170 requests in 10.01s, 32.83MB read 63 | Requests/sec: 39071.42 64 | Transfer/sec: 3.28MB 65 | 66 | Performance on newer desktop class CPU (i7-4790K CPU @ 4.00GHz):: 67 | 68 | > ./wrk -t 2 -c 400 http://127.0.0.1:8888 69 | Running 10s test @ http://127.0.0.1:8888 70 | 2 threads and 400 connections 71 | Thread Stats Avg Stdev Max +/- Stdev 72 | Latency 2.24ms 1.56ms 126.94ms 99.91% 73 | Req/Sec 91.35k 2.27k 93.76k 98.00% 74 | 1818133 requests in 10.00s, 152.58MB read 75 | Requests/sec: 181781.96 76 | Transfer/sec: 15.26MB 77 | 78 | Note: both benchmarks are run on **single threaded** server. 79 | 80 | The benchmarks are too early (not a full implementation of HTTP), so no 81 | comparison bencmarks listed here. 82 | 83 | 84 | -------------------------------------------------------------------------------- /doc/loop_init.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: rust 2 | 3 | =================== 4 | Loop Initialization 5 | =================== 6 | 7 | 8 | Overview 9 | ======== 10 | 11 | 12 | Loop initialization has two stages. First is created by:: 13 | 14 | let loop_creator = try!(rotor::Loop::new()); 15 | 16 | And the second is created by:: 17 | 18 | let loop_instance = loop_creator.instantiate(context) 19 | 20 | Then you can run the loop:: 21 | 22 | try!(loop_instance.run()); 23 | 24 | As you can see the ``loop_creator.instantiate(..)`` takes a context for the 25 | instantiation. This *is* the **key difference** between two stages. 26 | 27 | There is a shortcut if you want to skip second stage of initialization:: 28 | 29 | let loop_creator = try!(rotor::Loop::new()); 30 | try!(loop_creator.run(context)); 31 | 32 | 33 | Adding State Machines 34 | ===================== 35 | 36 | To have something useful of main loop you need to add a state machine to 37 | it. State machine initialization is done via ``add_machine_with`` method:: 38 | 39 | try!(loop_creator.add_machine_with(|scope| { 40 | Ok(Tcp::new(addr, scope)) 41 | })); 42 | 43 | And in loop instance there is similar method:: 44 | 45 | try!(loop_instance.add_machine_with(|scope| { 46 | Ok(Tcp::new(addr, scope)) 47 | })); 48 | 49 | The difference is in the signature of the function:: 50 | 51 | impl Loop { 52 | fn add_machine_with(&mut self, fun: F) 53 | -> Result<(), SpawnError<()>> 54 | where F: FnOnce(&mut EarlyScope) -> Result>; 55 | } 56 | impl LoopInstance { 57 | fn add_machine_with(&mut self, fun: F) 58 | -> Result<(), SpawnError<()>> 59 | where F: FnOnce(&mut Scope) -> Result>; 60 | } 61 | 62 | As you can see the only difference is that loop creator gets ``EarlyScope`` 63 | as an argument and latter gets ``Scope`` as an argument: 64 | 65 | 1. Both have ``GenericScope`` implementation, so you can have constructors 66 | generic over the scope type 67 | 2. ``Scope`` dereferences to the context while ``EarlyScope`` does not 68 | 69 | *Thats it*. But in reality it's important. For example, rotor-dns_ creates 70 | a pair: a state machine and a resolver object. State machine is just added 71 | to a loop, but you may want to put resolver object to a context. For example:: 72 | 73 | extern crate rotor_dns; 74 | 75 | let resolver_opt = None; 76 | try!(loop_creator.add_machine_with(|scope| { 77 | let (res, fsm) = try!(rotor_dns::create_resolver(scope, cfg)); 78 | resolver_opt = Some(res); 79 | Ok(fsm) 80 | })); 81 | let resolver = resolver_opt.unwrap(); 82 | let mut loop_instance = loop_creator.instantiate(Context { 83 | dns: resolver, 84 | }); 85 | loop_instance.add_machine_with(..) 86 | 87 | With rotor-tools_ the code is simplified to:: 88 | 89 | extern crate rotor_dns; 90 | extern crate rotor_tools; 91 | use rotor_tools::LoopExt; // The trait with helper functions 92 | 93 | let resolver = try!(loop_creator.add_and_fetch(|scope| { 94 | rotor_dns::create_resolver(scope, cfg) 95 | })); 96 | let mut loop_instance = loop_creator.instantiate(Context { 97 | dns: resolver, 98 | }); 99 | loop_instance.add_machine_with(..) 100 | 101 | .. _rotor-dns: http://github.com/tailhook/rotor-dns/ 102 | .. _rotor-tools: http://github.com/tailhook/rotor-tools/ 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /examples/tcp_echo_server.rs: -------------------------------------------------------------------------------- 1 | extern crate rotor; 2 | 3 | use std::io::{Write, stderr}; 4 | 5 | use rotor::{EventSet, PollOpt, Loop, Config, Void}; 6 | use rotor::mio::deprecated::{TryRead, TryWrite}; 7 | use rotor::mio::tcp::{TcpListener, TcpStream}; 8 | use rotor::{Machine, Response, EarlyScope, Scope}; 9 | 10 | 11 | struct Context; 12 | 13 | enum Echo { 14 | Server(TcpListener), 15 | Connection(TcpStream), 16 | } 17 | 18 | impl Echo { 19 | pub fn new(sock: TcpListener, scope: &mut EarlyScope) 20 | -> Response 21 | { 22 | scope.register(&sock, EventSet::readable(), PollOpt::edge()) 23 | .unwrap(); 24 | Response::ok(Echo::Server(sock)) 25 | } 26 | fn accept(self) -> Response { 27 | match self { 28 | Echo::Server(sock) => { 29 | match sock.accept() { 30 | Ok((conn, _)) => { 31 | Response::spawn(Echo::Server(sock), conn) 32 | } 33 | Err(e) => { 34 | writeln!(&mut stderr(), "Error: {}", e).ok(); 35 | Response::ok(Echo::Server(sock)) 36 | } 37 | } 38 | } 39 | _ => unreachable!(), 40 | } 41 | } 42 | } 43 | 44 | impl Machine for Echo { 45 | type Context = Context; 46 | type Seed = TcpStream; 47 | 48 | fn create(conn: TcpStream, scope: &mut Scope) 49 | -> Response 50 | { 51 | scope.register(&conn, EventSet::readable(), PollOpt::level()) 52 | .unwrap(); 53 | Response::ok(Echo::Connection(conn)) 54 | } 55 | 56 | fn ready(self, _events: EventSet, _scope: &mut Scope) 57 | -> Response 58 | { 59 | match self { 60 | me @ Echo::Server(..) => me.accept(), 61 | Echo::Connection(mut sock) => { 62 | let mut data = [0u8; 1024]; 63 | match sock.try_read(&mut data) { 64 | Err(e) => { 65 | writeln!(&mut stderr(), "read: {}", e).ok(); 66 | Response::done() 67 | } 68 | Ok(Some(0)) => { 69 | Response::done() 70 | } 71 | Ok(Some(x)) => { 72 | match sock.try_write(&data[..x]) { 73 | Ok(_) => { 74 | // this is example so we don't care if not all 75 | // (or none at all) bytes are written 76 | Response::ok(Echo::Connection(sock)) 77 | } 78 | Err(e) => { 79 | writeln!(&mut stderr(), "write: {}", e).ok(); 80 | Response::done() 81 | } 82 | } 83 | } 84 | Ok(None) => { 85 | Response::ok(Echo::Connection(sock)) 86 | } 87 | } 88 | } 89 | } 90 | } 91 | fn spawned(self, _scope: &mut Scope) -> Response 92 | { 93 | match self { 94 | me @ Echo::Server(..) => me.accept(), 95 | _ => unreachable!(), 96 | } 97 | } 98 | fn timeout(self, _scope: &mut Scope) 99 | -> Response 100 | { 101 | unreachable!(); 102 | } 103 | fn wakeup(self, _scope: &mut Scope) 104 | -> Response 105 | { 106 | unreachable!(); 107 | } 108 | } 109 | 110 | fn main() { 111 | let mut loop_creator = Loop::new(&Config::new()).unwrap(); 112 | let lst = TcpListener::bind(&"127.0.0.1:3000".parse().unwrap()).unwrap(); 113 | loop_creator.add_machine_with(|scope| { 114 | Echo::new(lst, scope) 115 | }).unwrap(); 116 | loop_creator.run(Context).unwrap(); 117 | } 118 | -------------------------------------------------------------------------------- /src/creator.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use mio::deprecated::EventLoop; 4 | use void::{Void, unreachable}; 5 | 6 | use config::{create_slab, create_loop}; 7 | use handler::{Handler, create_handler, set_timeout_opt}; 8 | use scope::{early_scope, EarlyScope, Scope}; 9 | use {Machine, Config, SpawnError, Timeout, Time, Response, Slab}; 10 | use SpawnError::NoSlabSpace; 11 | use response::decompose; 12 | 13 | 14 | /// An object that is used to construct a loop 15 | /// 16 | /// The purpose of the object is to shorten the boilerplate to create 17 | /// an event loop. 18 | /// 19 | /// The second purpose is to create the loop and state machines 20 | /// before Context is initialized. This is useful when you want to put 21 | /// `Notifier` objects of state machines into the context. 22 | /// 23 | /// You can create a loop either right away: 24 | /// 25 | /// ```ignore 26 | /// use rotor::{Loop, Config}; 27 | /// 28 | /// let mut lc = Loop::new(&Config::new()).unwrap(); 29 | /// loop_creator.add_machine_with(|scope| { 30 | /// // The scope here is the `EarlyScope` (no context) 31 | /// Ok(CreateMachine(x)) 32 | /// }).unwrap(); 33 | /// assert!(conn.is_ok()); 34 | /// lc.run(context).unwrap() 35 | /// ``` 36 | /// 37 | /// Or if you can create it in two stages: 38 | /// 39 | /// ```ignore 40 | /// let lc = Loop::new(&Config::new()).unwrap(); 41 | /// loop_creator.add_machine_with(|scope| { 42 | /// // The scope here is the `EarlyScope` 43 | /// Ok(StateMachine1(scope)) 44 | /// }).unwrap(); 45 | /// let mut inst = lc.instantiate(context); 46 | /// loop_creator.add_machine_with(|scope| { 47 | /// // The scope here is the real `Scope` 48 | /// Ok(StateMachine2(scope)) 49 | /// }).unwrap(); 50 | /// inst.run().unwrap() 51 | /// ``` 52 | /// 53 | /// The [the guide] for more information. 54 | /// 55 | /// [the guide]: http://rotor.readthedocs.org/en/latest/loop_init.html 56 | pub struct LoopCreator { 57 | slab: Slab<(Option<(Timeout, Time)>, M)>, 58 | mio: EventLoop>, 59 | } 60 | /// Second stage of loop creation 61 | /// 62 | /// See the docs of `LoopCreator` or [the guide] for more information. 63 | /// 64 | /// [the guide]: http://rotor.readthedocs.org/en/latest/loop_init.html 65 | pub struct LoopInstance { 66 | mio: EventLoop>, 67 | handler: Handler, 68 | } 69 | 70 | impl LoopCreator { 71 | pub fn new(cfg: &Config) -> Result, io::Error> { 72 | let slab = create_slab(&cfg); 73 | let eloop = try!(create_loop(&cfg)); 74 | Ok(LoopCreator { 75 | slab: slab, 76 | mio: eloop, 77 | }) 78 | } 79 | 80 | pub fn add_machine_with(&mut self, fun: F) -> Result<(), SpawnError<()>> 81 | where F: FnOnce(&mut EarlyScope) -> Response 82 | { 83 | let ref mut chan = self.mio.channel(); 84 | let ref mut mio = self.mio; 85 | let res = self.slab.vacant_entry().map(|entry| { 86 | let token = entry.index(); 87 | entry.insert({ 88 | let ref mut scope = early_scope(token, chan, mio); 89 | let (mach, void, timeout) = decompose(token, fun(scope)); 90 | void.map(|x| unreachable(x)); 91 | let m = mach.expect("You can't return Response::done() \ 92 | from Machine::create() until new release of slab crate. \ 93 | (requires insert_with_opt)"); 94 | let to = set_timeout_opt(timeout, scope); 95 | (to, m) 96 | }) 97 | }); 98 | if res.is_some() { 99 | Ok(()) 100 | } else { 101 | // TODO(tailhook) propagate error from state machine construtor 102 | Err(NoSlabSpace(())) 103 | } 104 | } 105 | 106 | pub fn instantiate(self, context: M::Context) -> LoopInstance { 107 | let LoopCreator { slab, mio } = self; 108 | let handler = create_handler(slab, context, mio.channel()); 109 | LoopInstance { mio: mio, handler: handler } 110 | } 111 | 112 | pub fn run(self, context: M::Context) -> Result<(), io::Error> { 113 | self.instantiate(context).run() 114 | } 115 | } 116 | 117 | impl LoopInstance { 118 | 119 | pub fn add_machine_with(&mut self, fun: F) -> Result<(), SpawnError<()>> 120 | where F: FnOnce(&mut Scope) -> Response 121 | { 122 | self.handler.add_machine_with(&mut self.mio, fun) 123 | } 124 | 125 | pub fn run(mut self) -> Result<(), io::Error> { 126 | let ref mut handler = self.handler; 127 | let ref mut mio = self.mio; 128 | mio.run(handler) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Compose multiple state machines into single type 2 | /// 3 | /// Composition requires two types: the state machine itself and Seed which 4 | /// is used to create children state machines. 5 | /// 6 | /// Mostly because of limitations of Rust macro system composition only works 7 | /// on concrete context type. 8 | /// 9 | /// # Example 10 | /// ```ignore 11 | /// rotor_compose!{ 12 | /// pub enum Fsm/Seed { 13 | /// Http(HttpMachine) 14 | /// Dns(DnsMachine) 15 | /// } 16 | /// } 17 | /// ``` 18 | /// 19 | /// This creates a an `Fsm` state machine type which is enum with two options. 20 | /// And `Seed` state machine type, which is also enum with same option names 21 | /// but uses `::Seed` for the wrapped type. 22 | #[macro_export] 23 | macro_rules! rotor_compose { 24 | /* TODO(tailhook) make and check generic combinators 25 | (pub enum $name:ident { $($x:ident ($y:ty),)* }) => { 26 | pub enum $name { $($x ($y),)* } 27 | rotor_compose!(@machine $name C [] $($x($y),)*); 28 | }; 29 | (enum $name:ident { $($x:ident ($y:ty),)* }) => { 30 | enum $name { $($x ($y),)* } 31 | rotor_compose!(@machine $name $($x($y),)*); 32 | }; 33 | */ 34 | (pub enum $name:ident/$cname:ident <$context_type:ident> 35 | { $($x:ident ($y:ty),)* }) 36 | => { 37 | pub enum $name { $($x ($y),)* } 38 | pub enum $cname { 39 | $( $x (<$y as $crate::Machine>::Seed), )* 40 | } 41 | rotor_compose!(@machine $name/$cname 42 | $context_type [] $($x($y),)*); 43 | }; 44 | (enum $name:ident/$cname:ident <$context_type:ident> 45 | { $($x:ident ($y:ty),)* }) 46 | => { 47 | enum $name { $($x ($y),)* } 48 | enum $cname { 49 | $( $x (<$y as $crate::Machine>::Seed), )* 50 | } 51 | rotor_compose!(@machine $name/$cname 52 | $context_type [] $($x($y),)*); 53 | }; 54 | (@machine $name:ident/$cname:ident $ctx_typ:ident 55 | [ $(<$ctx_name:ident $(: $ctx_bound:ident)*>)* ] 56 | $($iname:ident ($itype:ty),)*) 57 | => { 58 | impl $( <$ctx_name:$ctx_bound> )* $crate::Machine for $name { 59 | type Context = $ctx_typ; 60 | type Seed = $cname; 61 | fn create(seed: $cname, scope: &mut $crate::Scope<$ctx_typ>) 62 | -> $crate::Response 63 | { 64 | match seed { 65 | $( $cname::$iname (x) 66 | => $crate::Machine::create(x, scope) 67 | .map($name::$iname, 68 | |x| $crate::void::unreachable(x)), 69 | )* 70 | } 71 | } 72 | fn ready(self, events: $crate::EventSet, 73 | scope: &mut $crate::Scope<$ctx_typ>) 74 | -> $crate::Response 75 | { 76 | match self { 77 | $( 78 | $name::$iname(m) => { 79 | m.ready(events, scope) 80 | .map($name::$iname, $cname::$iname) 81 | } 82 | )* 83 | } 84 | } 85 | fn spawned(self, scope: &mut $crate::Scope<$ctx_typ>) 86 | -> $crate::Response 87 | { 88 | match self { 89 | $( 90 | $name::$iname(m) => { 91 | m.spawned(scope) 92 | .map($name::$iname, $cname::$iname) 93 | } 94 | )* 95 | } 96 | } 97 | fn timeout(self, scope: &mut $crate::Scope<$ctx_typ>) 98 | -> $crate::Response 99 | { 100 | match self { 101 | $( 102 | $name::$iname(m) => { 103 | m.timeout(scope) 104 | .map($name::$iname, $cname::$iname) 105 | } 106 | )* 107 | } 108 | } 109 | fn wakeup(self, scope: &mut $crate::Scope<$ctx_typ>) 110 | -> $crate::Response 111 | { 112 | match self { 113 | $( 114 | $name::$iname(m) => { 115 | m.wakeup(scope) 116 | .map($name::$iname, $cname::$iname) 117 | } 118 | )* 119 | } 120 | } 121 | } 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Rotor.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Rotor.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Rotor" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Rotor" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::error::Error; 3 | 4 | use mio::Token; 5 | 6 | use {Response, Time}; 7 | 8 | 9 | #[derive(Debug)] 10 | pub enum ResponseImpl { 11 | Normal(M), 12 | Deadline(M, Time), 13 | Spawn(M, N), 14 | Error(Box), 15 | Done, 16 | } 17 | 18 | impl Response { 19 | pub fn ok(machine: M) -> Response { 20 | Response(ResponseImpl::Normal(machine)) 21 | } 22 | pub fn spawn(machine: M, result: N) -> Response { 23 | Response(ResponseImpl::Spawn(machine, result)) 24 | } 25 | pub fn done() -> Response { 26 | Response::(ResponseImpl::Done) 27 | } 28 | 29 | /// Stop the state machine with an error. 30 | /// 31 | /// If `rotor` was compiled with the `log_errors` feature, the error will 32 | /// be logged on the warning level. 33 | 34 | // TODO add this documentation once mio has upgraded to slab 0.2.0 35 | // Additionally, if this response is returned from `Machine::create`, 36 | // the error is passed to `Machine::spawn_error`. 37 | pub fn error(e: Box) -> Response { 38 | Response::(ResponseImpl::Error(e)) 39 | } 40 | 41 | pub fn deadline(self, time: Time) -> Response { 42 | let imp = match self.0 { 43 | ResponseImpl::Normal(x) => ResponseImpl::Deadline(x, time), 44 | ResponseImpl::Deadline(x, _) => ResponseImpl::Deadline(x, time), 45 | ResponseImpl::Spawn(..) => { 46 | panic!("You can't attach a deadline/timeout to the \ 47 | Response::spawn(). The `spawn` action is synchronous \ 48 | you must set a deadline in the `spawned` handler."); } 49 | ResponseImpl::Done => { 50 | panic!("You can't attach a deadline/timeout to \ 51 | Response::done() as it's useless. \ 52 | Timeout will never happen"); 53 | } 54 | ResponseImpl::Error(_) => { 55 | panic!("You can't attach a deadline/timeout to \ 56 | Response::error(_) as it's useless. \ 57 | Timeout will never happen"); 58 | } 59 | }; 60 | Response(imp) 61 | } 62 | /// Maps state machine and/or spawned result with a function 63 | /// 64 | /// Usually it's okay to use constructor of wrapper state machine 65 | /// here as a mapper 66 | pub fn map(self, self_mapper: S, result_mapper: R) 67 | -> Response 68 | where S: FnOnce(M) -> T, 69 | R: FnOnce(N) -> U, 70 | { 71 | use self::ResponseImpl::*; 72 | let imp = match self.0 { 73 | Normal(m) => Normal(self_mapper(m)), 74 | Deadline(m, time) => Deadline(self_mapper(m), time), 75 | Spawn(m, n) => Spawn(self_mapper(m), result_mapper(n)), 76 | Done => Done, 77 | Error(e) => Error(e), 78 | }; 79 | Response(imp) 80 | } 81 | /// Similar to `map` but only maps state machine 82 | /// 83 | /// This is especially useful in state machine constructors, which 84 | /// have a Void child type. 85 | pub fn wrap(self, self_mapper: S) -> Response 86 | where S: FnOnce(M) -> T 87 | { 88 | use self::ResponseImpl::*; 89 | let imp = match self.0 { 90 | Normal(m) => Normal(self_mapper(m)), 91 | Deadline(m, time) => Deadline(self_mapper(m), time), 92 | Spawn(m, n) => Spawn(self_mapper(m), n), 93 | Done => Done, 94 | Error(e) => Error(e), 95 | }; 96 | Response(imp) 97 | } 98 | 99 | /// Returns true if state machine is stopped 100 | /// 101 | /// I.e. the method returns true if the `Response` was created either with 102 | /// `Response::done` or `Response::error` 103 | pub fn is_stopped(&self) -> bool { 104 | use self::ResponseImpl::*; 105 | match self.0 { 106 | Normal(..) => false, 107 | Deadline(..) => false, 108 | Spawn(..) => false, 109 | Done => true, 110 | Error(..) => true, 111 | } 112 | } 113 | 114 | /// Return a reference to an error passed to `Response::error` 115 | /// 116 | /// Returns None if any other constructor was used. 117 | /// 118 | /// This is mostly useful for printing the error. 119 | pub fn cause(&self) -> Option<&Error> { 120 | use self::ResponseImpl::*; 121 | match self.0 { 122 | Normal(..) => None, 123 | Deadline(..) => None, 124 | Spawn(..) => None, 125 | Done => None, 126 | Error(ref e) => Some(&**e), 127 | } 128 | } 129 | } 130 | 131 | impl Response { 132 | /// Return state machine if response created with `Response::ok(..)` 133 | /// 134 | /// *Use only for unit tests* 135 | /// 136 | /// If the response is not okay, the function panics. 137 | pub fn expect_machine(self) -> M { 138 | match self.0 { 139 | ResponseImpl::Normal(x) => x, 140 | ResponseImpl::Deadline(x, _) => x, 141 | me => panic!("expected machine (`Response::ok(x)`), \ 142 | got {:?} instead", me), 143 | } 144 | } 145 | /// Return a tuple if response created with `Response::spawn(..)` 146 | /// 147 | /// *Use only for unit tests* 148 | /// 149 | /// If the response is not `spawn`, the function panics. 150 | pub fn expect_spawn(self) -> (M, N) { 151 | match self.0 { 152 | ResponseImpl::Spawn(x, y) => (x, y), 153 | me => panic!("expected spawn (`Response::spawn(x)`), \ 154 | got {:?} instead", me), 155 | } 156 | } 157 | /// Returns if response created with `Response::done()` 158 | /// 159 | /// *Use only for unit tests* 160 | /// 161 | /// If the response is not done, the function panics. 162 | pub fn expect_done(self) { 163 | match self.0 { 164 | ResponseImpl::Done => {} 165 | me => panic!("expected done (`Response::done()`), \ 166 | got {:?} instead", me), 167 | } 168 | } 169 | /// Returns an error if response created with `Response::error(..)` 170 | /// 171 | /// *Use only for unit tests* 172 | /// 173 | /// If the response does not contain error, the function panics. 174 | pub fn expect_error(self) -> Box { 175 | match self.0 { 176 | ResponseImpl::Error(e) => e, 177 | me => panic!("expected error (`Response::error(e)`), \ 178 | got {:?} instead", me), 179 | } 180 | } 181 | } 182 | 183 | pub fn decompose(token: Token, res: Response) 184 | -> (Result>>, Option, Option