diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml deleted file mode 100644 index 60f4323f..00000000 --- a/.github/workflows/clippy.yml +++ /dev/null @@ -1,16 +0,0 @@ -on: pull_request - -name: Clippy Check -jobs: - clippy_check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - components: clippy - override: true - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index a700d30d..9bb85e89 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -14,7 +14,7 @@ jobs: - name: Install uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2020-06-22 + toolchain: nightly-2021-03-20 override: true - uses: davidB/rust-cargo-make@v1 with: @@ -24,13 +24,4 @@ jobs: RUST_BACKTRACE: full RUST_LOG: 'trace' run: | - rustup component add miri - cargo miri setup - cargo clean - # Do some Bastion shake - cd src/bastion && \ - cargo miri test --features lever/nightly -- -Zmiri-disable-isolation -Zmiri-ignore-leaks -- dispatcher && \ - cargo miri test --features lever/nightly -- -Zmiri-disable-isolation -Zmiri-ignore-leaks -- path && \ - cargo miri test --features lever/nightly -- -Zmiri-disable-isolation -Zmiri-ignore-leaks -- broadcast && \ - cargo miri test --features lever/nightly -- -Zmiri-disable-isolation -Zmiri-ignore-leaks -- children_ref && \ - cd - + tools/miri.sh diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index 553f631d..0578794d 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -15,7 +15,7 @@ jobs: - name: Install uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2020-03-08 + toolchain: nightly-2021-03-20 override: true - uses: davidB/rust-cargo-make@v1 with: diff --git a/README.md b/README.md index e465c6ad..ad1df3ff 100644 --- a/README.md +++ b/README.md @@ -2,82 +2,73 @@ <img src="https://github.com/bastion-rs/bastion/blob/master/img/bastion.png"><br> </div> ------------------ - -<h1 align="center">Highly-available Distributed Fault-tolerant Runtime</h1> +--- -<table align=left style='float: left; margin: 4px 10px 0px 0px; border: 1px solid #000000;'> -<tr> - <td>Latest Release</td> - <td> - <a href="https://crates.io/crates/bastion"> - <img alt="Crates.io" src="https://img.shields.io/crates/v/bastion.svg?style=popout-square"> - </a> - </td> -</tr> -<tr> - <td></td> -</tr> -<tr> - <td>License</td> - <td> - <a href="https://github.com/bastion-rs/bastion/blob/master/LICENSE"> - <img alt="Crates.io" src="https://img.shields.io/crates/l/bastion.svg?style=popout-square"> - </a> -</td> -</tr> -<tr> - <td>Doc [Bastion]</td> - <td> - <a href="https://docs.rs/bastion"> - <img alt="Documentation (Bastion)" src="https://img.shields.io/badge/rustdoc-bastion-blue.svg" /> - </a> - </td> -</tr> -<tr> +<table> + <tr> + <td>Latest Release</td> + <td> + <a href="https://crates.io/crates/bastion"> + <img alt="Crates.io" src="https://img.shields.io/crates/v/bastion.svg?style=popout-square"> + </a> + </td> + <td>License</td> + <td> + <a href="https://github.com/bastion-rs/bastion/blob/master/LICENSE"> + <img alt="Crates.io" src="https://img.shields.io/crates/l/bastion.svg?style=popout-square"> + </a> + </td> + </tr> + <tr> + <td>Doc [Bastion]</td> + <td> + <a href="https://docs.rs/bastion"> + <img alt="Documentation (Bastion)" src="https://img.shields.io/badge/rustdoc-bastion-blue.svg" /> + </a> + </td> + <td>Downloads</td> + <td> + <a href="https://crates.io/crates/bastion"> + <img alt="Crates.io" src="https://img.shields.io/crates/d/bastion.svg?style=popout-square"> + </a> + </td> + </tr> + <tr> <td>Doc [Bastion Executor]</td> - <td> - <a href="https://docs.rs/bastion-executor"> - <img alt="Documentation (Bastion Executor)" src="https://img.shields.io/badge/rustdoc-bastion_executor-blue.svg" /> - </a> - </td> -</tr> -<tr> - <td>Doc [LightProc]</td> - <td> - <a href="https://docs.rs/lightproc"> - <img alt="Documentation (LightProc)" src="https://img.shields.io/badge/rustdoc-lightproc-blue.svg" /> - </a> - </td> -</tr> -<tr> - <td>Build Status</td> - <td> - <a href="https://github.com/bastion-rs/bastion/actions"> - <img alt="Build Status" src="https://github.com/bastion-rs/bastion/workflows/CI/badge.svg" /> - </a> - </td> -</tr> -<tr> - <td>Downloads</td> - <td> - <a href="https://crates.io/crates/bastion"> - <img alt="Crates.io" src="https://img.shields.io/crates/d/bastion.svg?style=popout-square"> - </a> - </td> -</tr> -<tr> - <td>Discord</td> - <td> - <a href="https://discord.gg/DqRqtRT"> - <img src="https://img.shields.io/discord/628383521450360842.svg?logo=discord" /> - </a> - </td> -</tr> + <td> + <a href="https://docs.rs/bastion-executor"> + <img alt="Documentation (Bastion Executor)" src="https://img.shields.io/badge/rustdoc-bastion_executor-blue.svg" /> + </a> + </td> + <td>Discord</td> + <td> + <a href="https://discord.gg/DqRqtRT"> + <img src="https://img.shields.io/discord/628383521450360842.svg?logo=discord" /> + </a> + </td> + </tr> + <tr> + </tr> + <tr> + <td>Doc [LightProc]</td> + <td> + <a href="https://docs.rs/lightproc"> + <img alt="Documentation (LightProc)" src="https://img.shields.io/badge/rustdoc-lightproc-blue.svg" /> + </a> + </td> + <td>Build Status</td> + <td> + <a href="https://github.com/bastion-rs/bastion/actions"> + <img alt="Build Status" src="https://github.com/bastion-rs/bastion/workflows/CI/badge.svg" /> + </a> + </td> + </tr> </table> --- +<h1 align="center">Highly-available Distributed Fault-tolerant Runtime</h1> + Bastion is a highly-available, fault-tolerant runtime system with dynamic, dispatch-oriented, lightweight process model. It supplies actor-model-like concurrency with a lightweight process implementation and utilizes all of the system resources efficiently guaranteeing of at-most-once message delivery. --- @@ -87,6 +78,23 @@ Bastion is a highly-available, fault-tolerant runtime system with dynamic, dispa Bastion comes with a default one-for-one strategy root supervisor. You can use this to launch automatically supervised tasks. +## Get Started + +Include bastion to your project with: +```toml +bastion = "0.4" +``` + +### Documentation + +Official documentation is hosted on [docs.rs](https://docs.rs/bastion). + +### Examples + +Check the [getting started example](https://github.com/bastion-rs/bastion/blob/master/src/bastion/examples/getting_started.rs) in <code>bastion/examples</code> + +[Examples](https://github.com/bastion-rs/bastion/blob/master/src/bastion/examples) cover possible use cases of the crate. + ## Features * Message-based communication makes this project a lean mesh of actor system. * Without web servers, weird shenanigans, forced trait implementations, and static dispatch. @@ -144,43 +152,23 @@ It's independent of it's framework implementation. It uses lightproc to encapsul ### [Agnostik](https://github.com/bastion-rs/agnostik) Agnostik is a layer between your application and the executor for your async stuff. It lets you switch the executors smooth and easy without having to change your applications code. Valid features are `runtime_bastion` (default), `runtime_tokio`, `runtime_asyncstd` and `runtime_nostd` (coming soon). -## Get Started -Check the [getting started example](https://github.com/bastion-rs/bastion/blob/master/src/bastion/examples/getting_started.rs) in <code>bastion/examples</code> - -[Examples](https://github.com/bastion-rs/bastion/blob/master/src/bastion/examples) cover possible use cases of the crate. - -Include bastion to your project with: -```toml -bastion = "0.4" -``` - -For more information please check [Bastion Documentation](https://docs.rs/bastion) - ## Architecture of the Runtime Runtime is structured by the user. Only root supervision comes in batteries-included fashion. Worker code, worker group redundancy, supervisors and their supervision strategies are defined by the user. -## License - -Licensed under either of - - * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -## Documentation +Supervision strategies define how child actor failures are handled, how often a child can fail, and how long to wait before a child actor is recreated. As the name suggests, One-For-One strategy means the supervision strategy is applied only to the failed child. All-For-One strategy means that the supervision strategy is applied to all the actor siblings as well. One-for-one supervision is used at the root supervisor, while child groups may have different strategies like rest-for-one or one-for-all. -Official documentation is hosted on [docs.rs](https://docs.rs/bastion). + -## Getting Help +## Community +### Getting Help Please head to our [Discord](https://discord.gg/DqRqtRT) or use [StackOverflow](https://stackoverflow.com/questions/tagged/bastion) -## Discussion and Development +### Discussion and Development We use [Discord](https://discord.gg/DqRqtRT) for development discussions. Also please don't hesitate to open issues on GitHub ask for features, report bugs, comment on design and more! More interaction and more ideas are better! -## Contributing to Bastion [](https://www.codetriage.com/bastion-rs/bastion) +### Contributing to Bastion [](https://www.codetriage.com/bastion-rs/bastion) All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. @@ -188,6 +176,13 @@ A detailed overview on how to contribute can be found in the [CONTRIBUTING guid ## License +Licensed under either of + + * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + [](https://app.fossa.io/projects/git%2Bgithub.com%2Fbastion-rs%2Fbastion?ref=badge_large) [](https://app.fossa.io/projects/git%2Bgithub.com%2Fbastion-rs%2Fbastion?ref=badge_shield) diff --git a/img/bastion-architecture.png b/img/bastion-architecture.png new file mode 100644 index 00000000..84ad4b0c Binary files /dev/null and b/img/bastion-architecture.png differ diff --git a/src/bastion-executor/Cargo.toml b/src/bastion-executor/Cargo.toml index af659711..4e1c6143 100644 --- a/src/bastion-executor/Cargo.toml +++ b/src/bastion-executor/Cargo.toml @@ -5,7 +5,7 @@ name = "bastion-executor" # - Update CHANGELOG.md. # - npm install -g auto-changelog && auto-changelog at the root # - Create "v0.x.y" git tag at the root of the project. -version = "0.3.7-alpha.0" +version = "0.4.1" description = "Cache affine NUMA-aware executor for Rust" authors = ["Mahmut Bulut <vertexclique@gmail.com>"] keywords = ["fault-tolerant", "runtime", "actor", "system"] @@ -25,34 +25,41 @@ travis-ci = { repository = "bastion-rs/bastion", branch = "master" } maintenance = { status = "actively-developed" } [features] -unstable = ["numanji", "allocator-suite", "jemallocator"] +unstable = [] +tokio-runtime = ["tokio"] [dependencies] -lightproc = "0.3.5" bastion-utils = "0.3.2" +# lightproc = "0.3" +lightproc = { git = "https://github.com/bastion-rs/bastion.git" } # lightproc = { path = "../lightproc" } # bastion-utils = { path = "../bastion-utils" } -crossbeam-utils = "0.7" -crossbeam-channel = "0.4" -crossbeam-epoch = "0.8" +crossbeam-utils = "0.8" +crossbeam-channel = "0.5" +crossbeam-epoch = "0.9" lazy_static = "1.4" libc = "0.2" num_cpus = "1.13" pin-utils = "0.1.0" # Allocator -numanji = { version = "^0.1", optional = true, default-features = false } -allocator-suite = { version = "^0.1", optional = true, default-features = false } -arrayvec = { version = "0.5.1", features = ["array-sizes-129-255"]} +arrayvec = { version = "0.7.0" } futures-timer = "3.0.2" +once_cell = "1.4.0" +lever = "0.1" +tracing = "0.1.19" +crossbeam-queue = "0.3.0" -[target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] -jemallocator = { version = "^0.3", optional = true, default-features = false } +# Feature tokio +tokio = {version = "1.1", features = ["rt", "rt-multi-thread"], optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "^0.3.8", features = ["basetsd"] } [dev-dependencies] -proptest = "^0.10" +tokio = {version = "1.1", features = ["rt", "rt-multi-thread", "macros"] } +tokio-test = "0.4.0" +proptest = "^1.0" futures = "0.3.5" +tracing-subscriber = "0.2.11" diff --git a/src/bastion-executor/benches/blocking.rs b/src/bastion-executor/benches/blocking.rs index df01bda4..6c5a6ffd 100644 --- a/src/bastion-executor/benches/blocking.rs +++ b/src/bastion-executor/benches/blocking.rs @@ -3,19 +3,43 @@ extern crate test; use bastion_executor::blocking; -use bastion_executor::run::run; -use futures::future::join_all; use lightproc::proc_stack::ProcStack; -use lightproc::recoverable_handle::RecoverableHandle; use std::thread; use std::time::Duration; use test::Bencher; +#[cfg(feature = "tokio-runtime")] +mod tokio_benchs { + use super::*; + #[bench] + fn blocking(b: &mut Bencher) { + tokio_test::block_on(async { _blocking(b) }); + } + #[bench] + fn blocking_single(b: &mut Bencher) { + tokio_test::block_on(async { + _blocking_single(b); + }); + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod no_tokio_benchs { + use super::*; + #[bench] + fn blocking(b: &mut Bencher) { + _blocking(b); + } + #[bench] + fn blocking_single(b: &mut Bencher) { + _blocking_single(b); + } +} + // Benchmark for a 10K burst task spawn -#[bench] -fn blocking(b: &mut Bencher) { +fn _blocking(b: &mut Bencher) { b.iter(|| { - let handles = (0..10_000) + (0..10_000) .map(|_| { blocking::spawn_blocking( async { @@ -25,15 +49,12 @@ fn blocking(b: &mut Bencher) { ProcStack::default(), ) }) - .collect::<Vec<RecoverableHandle<()>>>(); - - run(join_all(handles), ProcStack::default()); + .collect::<Vec<_>>() }); } // Benchmark for a single blocking task spawn -#[bench] -fn blocking_single(b: &mut Bencher) { +fn _blocking_single(b: &mut Bencher) { b.iter(|| { blocking::spawn_blocking( async { diff --git a/src/bastion-executor/benches/run_blocking.rs b/src/bastion-executor/benches/run_blocking.rs new file mode 100644 index 00000000..43de4400 --- /dev/null +++ b/src/bastion-executor/benches/run_blocking.rs @@ -0,0 +1,69 @@ +#![feature(test)] + +extern crate test; + +use bastion_executor::blocking; +use bastion_executor::run::run; +use futures::future::join_all; +use lightproc::proc_stack::ProcStack; +use std::thread; +use std::time::Duration; +use test::Bencher; + +#[cfg(feature = "tokio-runtime")] +mod tokio_benchs { + use super::*; + #[bench] + fn blocking(b: &mut Bencher) { + tokio_test::block_on(async { _blocking(b) }); + } + #[bench] + fn blocking_single(b: &mut Bencher) { + tokio_test::block_on(async { + _blocking_single(b); + }); + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod no_tokio_benchs { + use super::*; + #[bench] + fn blocking(b: &mut Bencher) { + _blocking(b); + } + #[bench] + fn blocking_single(b: &mut Bencher) { + _blocking_single(b); + } +} + +// Benchmark for a 10K burst task spawn +fn _blocking(b: &mut Bencher) { + b.iter(|| { + (0..10_000) + .map(|_| { + blocking::spawn_blocking( + async { + let duration = Duration::from_millis(1); + thread::sleep(duration); + }, + ProcStack::default(), + ) + }) + .collect::<Vec<_>>() + }); +} + +// Benchmark for a single blocking task spawn +fn _blocking_single(b: &mut Bencher) { + b.iter(|| { + blocking::spawn_blocking( + async { + let duration = Duration::from_millis(1); + thread::sleep(duration); + }, + ProcStack::default(), + ) + }); +} diff --git a/src/bastion-executor/benches/spawn.rs b/src/bastion-executor/benches/spawn.rs index f9d7d273..02b896bf 100644 --- a/src/bastion-executor/benches/spawn.rs +++ b/src/bastion-executor/benches/spawn.rs @@ -2,54 +2,69 @@ extern crate test; +use bastion_executor::load_balancer; use bastion_executor::prelude::spawn; -use bastion_executor::run::run; -use futures::future::join_all; use futures_timer::Delay; use lightproc::proc_stack::ProcStack; -use lightproc::recoverable_handle::RecoverableHandle; use std::time::Duration; use test::Bencher; +#[cfg(feature = "tokio-runtime")] +mod tokio_benchs { + use super::*; + #[bench] + fn spawn_lot(b: &mut Bencher) { + tokio_test::block_on(async { _spawn_lot(b) }); + } + #[bench] + fn spawn_single(b: &mut Bencher) { + tokio_test::block_on(async { + _spawn_single(b); + }); + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod no_tokio_benchs { + use super::*; + #[bench] + fn spawn_lot(b: &mut Bencher) { + _spawn_lot(b); + } + #[bench] + fn spawn_single(b: &mut Bencher) { + _spawn_single(b); + } +} + // Benchmark for a 10K burst task spawn -#[bench] -fn spawn_lot(b: &mut Bencher) { +fn _spawn_lot(b: &mut Bencher) { + let proc_stack = ProcStack::default(); b.iter(|| { - let proc_stack = ProcStack::default(); - let handles = (0..10_000) + let _ = (0..10_000) .map(|_| { spawn( async { - let duration = Duration::from_millis(0); + let duration = Duration::from_millis(1); Delay::new(duration).await; }, proc_stack.clone(), ) }) - .collect::<Vec<RecoverableHandle<()>>>(); - - run(join_all(handles), proc_stack); + .collect::<Vec<_>>(); }); } -// Benchmark for a single blocking task spawn -#[bench] -fn spawn_single(b: &mut Bencher) { +// Benchmark for a single task spawn +fn _spawn_single(b: &mut Bencher) { + let proc_stack = ProcStack::default(); b.iter(|| { - let proc_stack = ProcStack::default(); - - let handle = spawn( + spawn( async { - let duration = Duration::from_millis(0); + let duration = Duration::from_millis(1); Delay::new(duration).await; }, proc_stack.clone(), ); - run( - async { - handle.await; - }, - proc_stack, - ) }); } diff --git a/src/bastion-executor/benches/stats.rs b/src/bastion-executor/benches/stats.rs index c6cb5f3f..684e7cb1 100644 --- a/src/bastion-executor/benches/stats.rs +++ b/src/bastion-executor/benches/stats.rs @@ -1,16 +1,16 @@ #![feature(test)] extern crate test; -use bastion_executor::load_balancer::{stats, SmpStats}; +use bastion_executor::load_balancer::{core_count, get_cores, stats, SmpStats}; use bastion_executor::placement; use std::thread; +use test::Bencher; fn stress_stats<S: SmpStats + Sync + Send>(stats: &'static S) { - let cores = placement::get_core_ids().expect("Core mapping couldn't be fetched"); - let mut handles = Vec::new(); - for core in cores { + let mut handles = Vec::with_capacity(*core_count()); + for core in get_cores() { let handle = thread::spawn(move || { - placement::set_for_current(core); + placement::set_for_current(*core); for i in 0..100 { stats.store_load(core.id, 10); if i % 3 == 0 { @@ -25,7 +25,6 @@ fn stress_stats<S: SmpStats + Sync + Send>(stats: &'static S) { handle.join().unwrap(); } } -use test::Bencher; // previous lock based stats benchmark 1,352,791 ns/iter (+/- 2,682,013) @@ -36,3 +35,37 @@ fn lockless_stats_bench(b: &mut Bencher) { stress_stats(stats()); }); } + +#[bench] +fn lockless_stats_bad_load(b: &mut Bencher) { + let stats = stats(); + const MAX_CORE: usize = 256; + for i in 0..MAX_CORE { + // Generating the worst possible mergesort scenario + // [0,2,4,6,8,10,1,3,5,7,9]... + if i <= MAX_CORE / 2 { + stats.store_load(i, i * 2); + } else { + stats.store_load(i, i - 1 - MAX_CORE / 2); + } + } + + b.iter(|| { + let _sorted_load = stats.get_sorted_load(); + }); +} + +#[bench] +fn lockless_stats_good_load(b: &mut Bencher) { + let stats = stats(); + const MAX_CORE: usize = 256; + for i in 0..MAX_CORE { + // Generating the best possible mergesort scenario + // [0,1,2,3,4,5,6,7,8,9]... + stats.store_load(i, i); + } + + b.iter(|| { + let _sorted_load = stats.get_sorted_load(); + }); +} diff --git a/src/bastion-executor/src/allocator.rs b/src/bastion-executor/src/allocator.rs deleted file mode 100644 index 74d55955..00000000 --- a/src/bastion-executor/src/allocator.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! -//! NUMA-aware locality enabled allocator with optional fallback. -//! -//! Currently this API marked as `unstable` and can only be used with `unstable` feature. -//! -//! This allocator checks for NUMA-aware locality, and if it suitable it can start -//! this allocator with local allocation policy [MPOL_LOCAL]. -//! In other cases otherwise it tries to use jemalloc. -//! -//! This allocator is an allocator called [Numanji]. -//! -//! [Numanji]: https://docs.rs/numanji -//! [MPOL_LOCAL]: http://man7.org/linux/man-pages/man2/set_mempolicy.2.html -//! -unstable_api! { - // Allocation selector import - use numanji::*; - - // Drive selection of allocator here - autoselect!(); -} diff --git a/src/bastion-executor/src/blocking.rs b/src/bastion-executor/src/blocking.rs index 2fe269a5..5f595ebb 100644 --- a/src/bastion-executor/src/blocking.rs +++ b/src/bastion-executor/src/blocking.rs @@ -1,333 +1,151 @@ -//! A thread pool for running blocking functions asynchronously. //! -//! Blocking thread pool consists of four elements: -//! * Frequency Detector -//! * Trend Estimator -//! * Predictive Upscaler -//! * Time-based Downscaler +//! Pool of threads to run heavy processes //! -//! ## Frequency Detector -//! Detects how many tasks are submitted from scheduler to thread pool in a given time frame. -//! Pool manager thread does this sampling every 200 milliseconds. -//! This value is going to be used for trend estimation phase. +//! We spawn futures onto the pool with [`spawn_blocking`] method of global run queue or +//! with corresponding [`Worker`]'s spawn method. //! -//! ## Trend Estimator -//! Hold up to the given number of frequencies to create an estimation. -//! Trend estimator holds 10 frequencies at a time. -//! This value is stored as constant in [FREQUENCY_QUEUE_SIZE](constant.FREQUENCY_QUEUE_SIZE.html). -//! Estimation algorithm and prediction uses Exponentially Weighted Moving Average algorithm. -//! -//! This algorithm is adapted from [A Novel Predictive and Self–Adaptive Dynamic Thread Pool Management](https://doi.org/10.1109/ISPA.2011.61) -//! and altered to: -//! * use instead of heavy calculation of trend, utilize thread redundancy which is the sum of the differences between the predicted and observed value. -//! * use instead of linear trend estimation, it uses exponential trend estimation where formula is: -//! ```text -//! LOW_WATERMARK * (predicted - observed) + LOW_WATERMARK -//! ``` -//! *NOTE:* If this algorithm wants to be tweaked increasing [LOW_WATERMARK](constant.LOW_WATERMARK.html) will automatically adapt the additional dynamic thread spawn count -//! * operate without watermarking by timestamps (in paper which is used to measure algorithms own performance during the execution) -//! * operate extensive subsampling. Extensive subsampling congests the pool manager thread. -//! * operate without keeping track of idle time of threads or job out queue like TEMA and FOPS implementations. -//! -//! ## Predictive Upscaler -//! Upscaler has three cases (also can be seen in paper): -//! * The rate slightly increases and there are many idle threads. -//! * The number of worker threads tends to be reduced since the workload of the system is descending. -//! * The system has no request or stalled. (Our case here is when the current tasks block further tasks from being processed – throughput hogs) -//! -//! For the first two EMA calculation and exponential trend estimation gives good performance. -//! For the last case, upscaler selects upscaling amount by amount of tasks mapped when throughput hogs happen. -//! -//! **example scenario:** Let's say we have 10_000 tasks where every one of them is blocking for 1 second. Scheduler will map plenty of tasks but will got rejected. -//! This makes estimation calculation nearly 0 for both entering and exiting parts. When this happens and we still see tasks mapped from scheduler. -//! We start to slowly increase threads by amount of frequency linearly. High increase of this value either make us hit to the thread threshold on -//! some OS or make congestion on the other thread utilizations of the program, because of context switch. -//! -//! Throughput hogs determined by a combination of job in / job out frequency and current scheduler task assignment frequency. -//! Threshold of EMA difference is eluded by machine epsilon for floating point arithmetic errors. -//! -//! ## Time-based Downscaler -//! When threads becomes idle, they will not shut down immediately. -//! Instead, they wait a random amount between 1 and 11 seconds -//! to even out the load. +//! [`Worker`]: crate::run_queue::Worker -use std::collections::VecDeque; -use std::future::Future; -use std::io::ErrorKind; -use std::iter::Iterator; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Mutex; -use std::time::Duration; -use std::{env, thread}; - -use crossbeam_channel::{bounded, Receiver, Sender}; - -use bastion_utils::math; +use crate::thread_manager::{DynamicPoolManager, DynamicRunner}; +use crossbeam_channel::{unbounded, Receiver, Sender}; use lazy_static::lazy_static; use lightproc::lightproc::LightProc; use lightproc::proc_stack::ProcStack; use lightproc::recoverable_handle::RecoverableHandle; - -use crate::placement::CoreId; -use crate::{load_balancer, placement}; +use once_cell::sync::{Lazy, OnceCell}; +use std::future::Future; +use std::iter::Iterator; +use std::sync::Arc; +use std::time::Duration; +use std::{env, thread}; +use tracing::trace; /// If low watermark isn't configured this is the default scaler value. /// This value is used for the heuristics of the scaler const DEFAULT_LOW_WATERMARK: u64 = 2; -/// Pool managers interval time (milliseconds). -/// This is the actual interval which makes adaptation calculation. -const MANAGER_POLL_INTERVAL: u64 = 200; - -/// Frequency histogram's sliding window size. -/// Defines how many frequencies will be considered for adaptation. -const FREQUENCY_QUEUE_SIZE: usize = 10; - -/// Exponential moving average smoothing coefficient for limited window. -/// Smoothing factor is estimated with: 2 / (N + 1) where N is sample size. -const EMA_COEFFICIENT: f64 = 2_f64 / (FREQUENCY_QUEUE_SIZE as f64 + 1_f64); - -/// Pool task frequency variable. -/// Holds scheduled tasks onto the thread pool for the calculation time window. -static FREQUENCY: AtomicU64 = AtomicU64::new(0); - -/// Possible max threads (without OS contract). -static MAX_THREADS: AtomicU64 = AtomicU64::new(10_000); - -/// Pool interface between the scheduler and thread pool -struct Pool { - sender: Sender<LightProc>, - receiver: Receiver<LightProc>, -} - -lazy_static! { - /// Blocking pool with static starting thread count. - static ref POOL: Pool = { - for _ in 0..*low_watermark() { - thread::Builder::new() - .name("bastion-blocking-driver".to_string()) - .spawn(|| { - self::affinity_pinner(); - - for task in &POOL.receiver { - task.run(); - } - }) - .expect("cannot start a thread driving blocking tasks"); - } - - // Pool manager to check frequency of task rates - // and take action by scaling the pool accordingly. - thread::Builder::new() - .name("bastion-pool-manager".to_string()) - .spawn(|| { - let poll_interval = Duration::from_millis(MANAGER_POLL_INTERVAL); - loop { - scale_pool(); - thread::sleep(poll_interval); - } - }) - .expect("thread pool manager cannot be started"); - - // We want to use an unbuffered channel here to help - // us drive our dynamic control. In effect, the - // kernel's scheduler becomes the queue, reducing - // the number of buffers that work must flow through - // before being acted on by a core. This helps keep - // latency snappy in the overall async system by - // reducing bufferbloat. - let (sender, receiver) = bounded(0); - Pool { sender, receiver } - }; +const THREAD_RECV_TIMEOUT: Duration = Duration::from_millis(100); - static ref ROUND_ROBIN_PIN: Mutex<CoreId> = Mutex::new(CoreId { id: 0 }); - - /// Sliding window for pool task frequency calculation - static ref FREQ_QUEUE: Mutex<VecDeque<u64>> = { - Mutex::new(VecDeque::with_capacity(FREQUENCY_QUEUE_SIZE.saturating_add(1))) - }; - - /// Dynamic pool thread count variable - static ref POOL_SIZE: Mutex<u64> = Mutex::new(*low_watermark()); +/// Spawns a blocking task. +/// +/// The task will be spawned onto a thread pool specifically dedicated to blocking tasks. +pub fn spawn_blocking<F, R>(future: F, stack: ProcStack) -> RecoverableHandle<R> +where + F: Future<Output = R> + Send + 'static, + R: Send + 'static, +{ + let (task, handle) = LightProc::recoverable(future, schedule, stack); + task.schedule(); + handle } -/// Exponentially Weighted Moving Average calculation -/// -/// This allows us to find the EMA value. -/// This value represents the trend of tasks mapped onto the thread pool. -/// Calculation is following: -/// ```text -/// +--------+-----------------+----------------------------------+ -/// | Symbol | Identifier | Explanation | -/// +--------+-----------------+----------------------------------+ -/// | α | EMA_COEFFICIENT | smoothing factor between 0 and 1 | -/// | Yt | freq | frequency sample at time t | -/// | St | acc | EMA at time t | -/// +--------+-----------------+----------------------------------+ -/// ``` -/// Under these definitions formula is following: -/// ```text -/// EMA = α * [ Yt + (1 - α)*Yt-1 + ((1 - α)^2)*Yt-2 + ((1 - α)^3)*Yt-3 ... ] + St -/// ``` -/// # Arguments -/// -/// * `freq_queue` - Sliding window of frequency samples -#[inline] -fn calculate_ema(freq_queue: &VecDeque<u64>) -> f64 { - freq_queue.iter().enumerate().fold(0_f64, |acc, (i, freq)| { - acc + ((*freq as f64) * ((1_f64 - EMA_COEFFICIENT).powf(i as f64) as f64)) - }) * EMA_COEFFICIENT as f64 +struct BlockingRunner { + // We keep a handle to the tokio runtime here to make sure + // it will never be dropped while the DynamicPoolManager is alive, + // In case we need to spin up some threads. + #[cfg(feature = "tokio-runtime")] + runtime_handle: tokio::runtime::Handle, } -/// Adaptive pool scaling function -/// -/// This allows to spawn new threads to make room for incoming task pressure. -/// Works in the background detached from the pool system and scales up the pool based -/// on the request rate. -/// -/// It uses frequency based calculation to define work. Utilizing average processing rate. -fn scale_pool() { - // Fetch current frequency, it does matter that operations are ordered in this approach. - let current_frequency = FREQUENCY.swap(0, Ordering::SeqCst); - let mut freq_queue = FREQ_QUEUE.lock().unwrap(); +impl DynamicRunner for BlockingRunner { + fn run_static(&self, park_timeout: Duration) -> ! { + loop { + while let Ok(task) = POOL.receiver.recv_timeout(THREAD_RECV_TIMEOUT) { + trace!("static thread: running task"); + self.run(task); + } - // Make it safe to start for calculations by adding initial frequency scale - if freq_queue.len() == 0 { - freq_queue.push_back(0); + trace!("static: empty queue, parking with timeout"); + thread::park_timeout(park_timeout); + } } + fn run_dynamic(&self, parker: &dyn Fn()) -> ! { + loop { + while let Ok(task) = POOL.receiver.recv_timeout(THREAD_RECV_TIMEOUT) { + trace!("dynamic thread: running task"); + self.run(task); + } + trace!( + "dynamic thread: parking - {:?}", + std::thread::current().id() + ); + parker(); + } + } + fn run_standalone(&self) { + while let Ok(task) = POOL.receiver.recv_timeout(THREAD_RECV_TIMEOUT) { + self.run(task); + } + trace!("standalone thread: quitting."); + } +} - // Calculate message rate for the given time window - let frequency = (current_frequency as f64 / MANAGER_POLL_INTERVAL as f64) as u64; - - // Calculates current time window's EMA value (including last sample) - let prev_ema_frequency = calculate_ema(&freq_queue); - - // Add seen frequency data to the frequency histogram. - freq_queue.push_back(frequency); - if freq_queue.len() == FREQUENCY_QUEUE_SIZE.saturating_add(1) { - freq_queue.pop_front(); +impl BlockingRunner { + fn run(&self, task: LightProc) { + #[cfg(feature = "tokio-runtime")] + { + self.runtime_handle.spawn_blocking(|| task.run()); + } + #[cfg(not(feature = "tokio-runtime"))] + { + task.run(); + } } +} - // Calculates current time window's EMA value (including last sample) - let curr_ema_frequency = calculate_ema(&freq_queue); +/// Pool interface between the scheduler and thread pool +struct Pool { + sender: Sender<LightProc>, + receiver: Receiver<LightProc>, +} - // Adapts the thread count of pool - // - // Sliding window of frequencies visited by the pool manager. - // Pool manager creates EMA value for previous window and current window. - // Compare them to determine scaling amount based on the trends. - // If current EMA value is bigger, we will scale up. - if curr_ema_frequency > prev_ema_frequency { - // "Scale by" amount can be seen as "how much load is coming". - // "Scale" amount is "how many threads we should spawn". - let scale_by: f64 = curr_ema_frequency - prev_ema_frequency; - let scale = num_cpus::get().min( - ((DEFAULT_LOW_WATERMARK as f64 * scale_by) + DEFAULT_LOW_WATERMARK as f64) as usize, - ); +static DYNAMIC_POOL_MANAGER: OnceCell<DynamicPoolManager> = OnceCell::new(); - // It is time to scale the pool! - (0..scale).for_each(|_| { - create_blocking_thread(); - }); - } else if (curr_ema_frequency - prev_ema_frequency).abs() < std::f64::EPSILON - && current_frequency != 0 +static POOL: Lazy<Pool> = Lazy::new(|| { + #[cfg(feature = "tokio-runtime")] { - // Throughput is low. Allocate more threads to unblock flow. - // If we fall to this case, scheduler is congested by longhauling tasks. - // For unblock the flow we should add up some threads to the pool, but not that many to - // stagger the program's operation. - (0..DEFAULT_LOW_WATERMARK).for_each(|_| { - create_blocking_thread(); + let runner = Arc::new(BlockingRunner { + // We use current() here instead of try_current() + // because we want bastion to crash as soon as possible + // if there is no available runtime. + runtime_handle: tokio::runtime::Handle::current(), }); - } -} -/// Creates blocking thread to receive tasks -/// Dynamic threads will terminate themselves if they don't -/// receive any work after between one and ten seconds. -fn create_blocking_thread() { - // Check that thread is spawnable. - // If it hits to the OS limits don't spawn it. + DYNAMIC_POOL_MANAGER + .set(DynamicPoolManager::new(*low_watermark() as usize, runner)) + .expect("couldn't create dynamic pool manager"); + } + #[cfg(not(feature = "tokio-runtime"))] { - let pool_size = *POOL_SIZE.lock().unwrap(); - if pool_size >= MAX_THREADS.load(Ordering::SeqCst) { - MAX_THREADS.store(10_000, Ordering::SeqCst); - return; - } + let runner = Arc::new(BlockingRunner {}); + + DYNAMIC_POOL_MANAGER + .set(DynamicPoolManager::new(*low_watermark() as usize, runner)) + .expect("couldn't create dynamic pool manager"); } - // We want to avoid having all threads terminate at - // exactly the same time, causing thundering herd - // effects. We want to stagger their destruction over - // 10 seconds or so to make the costs fade into - // background noise. - // - // Generate a simple random number of milliseconds - let rand_sleep_ms = 1000_u64 - .checked_add(u64::from(math::random(10_000))) - .expect("shouldn't overflow"); - let _ = thread::Builder::new() - .name("bastion-blocking-driver-dynamic".to_string()) - .spawn(move || { - self::affinity_pinner(); + DYNAMIC_POOL_MANAGER + .get() + .expect("couldn't get static pool manager") + .initialize(); - let wait_limit = Duration::from_millis(rand_sleep_ms); - - // Adjust the pool size counter before and after spawn - *POOL_SIZE.lock().unwrap() += 1; - while let Ok(task) = POOL.receiver.recv_timeout(wait_limit) { - task.run(); - } - *POOL_SIZE.lock().unwrap() -= 1; - }) - .map_err(|err| { - match err.kind() { - ErrorKind::WouldBlock => { - // Maximum allowed threads per process is varying from system to system. - // Also, some systems have it(like macOS), and some don't(Linux). - // This case expected not to happen. - // But when happened this shouldn't throw a panic. - let guarded_count = POOL_SIZE - .lock() - .unwrap() - .checked_sub(1) - .expect("shouldn't underflow"); - MAX_THREADS.store(guarded_count, Ordering::SeqCst); - } - _ => eprintln!( - "cannot start a dynamic thread driving blocking tasks: {}", - err - ), - } - }); -} + let (sender, receiver) = unbounded(); + Pool { sender, receiver } +}); /// Enqueues work, attempting to send to the thread pool in a /// nonblocking way and spinning up needed amount of threads /// based on the previous statistics without relying on /// if there is not a thread ready to accept the work or not. fn schedule(t: LightProc) { - // Add up for every incoming scheduled task - FREQUENCY.fetch_add(1, Ordering::Acquire); - if let Err(err) = POOL.sender.try_send(t) { // We were not able to send to the channel without // blocking. POOL.sender.send(err.into_inner()).unwrap(); } -} -/// Spawns a blocking task. -/// -/// The task will be spawned onto a thread pool specifically dedicated to blocking tasks. -pub fn spawn_blocking<F, R>(future: F, stack: ProcStack) -> RecoverableHandle<R> -where - F: Future<Output = R> + Send + 'static, - R: Send + 'static, -{ - let (task, handle) = LightProc::recoverable(future, schedule, stack); - task.schedule(); - handle + // Add up for every incoming scheduled task + DYNAMIC_POOL_MANAGER.get().unwrap().increment_frequency(); } /// @@ -335,7 +153,7 @@ where /// Spawns initial thread set. /// Can be configurable with env var `BASTION_BLOCKING_THREADS` at runtime. #[inline] -pub fn low_watermark() -> &'static u64 { +fn low_watermark() -> &'static u64 { lazy_static! { static ref LOW_WATERMARK: u64 = { env::var_os("BASTION_BLOCKING_THREADS") @@ -346,15 +164,3 @@ pub fn low_watermark() -> &'static u64 { &*LOW_WATERMARK } - -/// -/// Affinity pinner for blocking pool -/// Pinning isn't going to be enabled for single core systems. -#[inline] -pub fn affinity_pinner() { - if 1 != *load_balancer::core_retrieval() { - let mut core = ROUND_ROBIN_PIN.lock().unwrap(); - placement::set_for_current(*core); - core.id = (core.id + 1) % *load_balancer::core_retrieval(); - } -} diff --git a/src/bastion-executor/src/distributor.rs b/src/bastion-executor/src/distributor.rs deleted file mode 100644 index 13548304..00000000 --- a/src/bastion-executor/src/distributor.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! -//! Cache affine thread pool distributor -//! -//! Distributor provides a fair distribution of threads and pinning them to cores for fair execution. -//! It assigns threads in round-robin fashion to all cores. -use crate::placement::{self, CoreId}; -use crate::run_queue::{Stealer, Worker}; -use crate::worker; -use lightproc::prelude::*; -use std::thread; - -pub(crate) struct Distributor { - pub(crate) cores: Vec<CoreId>, -} - -impl Distributor { - pub(crate) fn new() -> Self { - Distributor { - cores: placement::get_core_ids().expect("Core mapping couldn't be fetched"), - } - } - - pub(crate) fn assign(self) -> Vec<Stealer<LightProc>> { - let mut stealers = Vec::<Stealer<LightProc>>::new(); - - for core in self.cores { - let wrk = Worker::new_fifo(); - stealers.push(wrk.stealer()); - - thread::Builder::new() - .name("bastion-async-thread".to_string()) - .spawn(move || { - // affinity assignment - placement::set_for_current(core); - - // run initial stats generation for cores - worker::stats_generator(core.id, &wrk); - // actual execution - worker::main_loop(core.id, wrk); - }) - .expect("cannot start the thread for running proc"); - } - - stealers - } -} diff --git a/src/bastion-executor/src/lib.rs b/src/bastion-executor/src/lib.rs index aa885077..145614c2 100644 --- a/src/bastion-executor/src/lib.rs +++ b/src/bastion-executor/src/lib.rs @@ -28,26 +28,15 @@ // Force missing implementations #![warn(missing_docs)] #![warn(missing_debug_implementations)] -#![cfg_attr( - any(feature = "numanji", feature = "allocator-suite"), - feature(allocator_api) -)] -#![cfg_attr( - any(feature = "numanji", feature = "allocator-suite"), - feature(nonnull_slice_from_raw_parts) -)] -#[macro_use] -mod macros; -pub mod allocator; pub mod blocking; -pub mod distributor; pub mod load_balancer; pub mod placement; pub mod pool; pub mod run; pub mod run_queue; pub mod sleepers; +mod thread_manager; pub mod worker; /// diff --git a/src/bastion-executor/src/load_balancer.rs b/src/bastion-executor/src/load_balancer.rs index 2ee4a1c8..9b253f6c 100644 --- a/src/bastion-executor/src/load_balancer.rs +++ b/src/bastion-executor/src/load_balancer.rs @@ -7,49 +7,102 @@ use crate::load_balancer; use crate::placement; use arrayvec::ArrayVec; +use fmt::{Debug, Formatter}; use lazy_static::*; +use once_cell::sync::Lazy; +use placement::CoreId; use std::mem::MaybeUninit; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::thread; -use std::time::Duration; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::RwLock; +use std::time::{Duration, Instant}; use std::{fmt, usize}; +use tracing::{debug, error}; + +const MEAN_UPDATE_TRESHOLD: Duration = Duration::from_millis(200); /// Stats of all the smp queues. pub trait SmpStats { /// Stores the load of the given queue. fn store_load(&self, affinity: usize, load: usize); - /// returns tuple of queue id and load in an sorted order. - fn get_sorted_load(&self) -> ArrayVec<[(usize, usize); MAX_CORE]>; + /// returns tuple of queue id and load ordered from highest load to lowest. + fn get_sorted_load(&self) -> ArrayVec<(usize, usize), MAX_CORE>; /// mean of the all smp queue load. fn mean(&self) -> usize; /// update the smp mean. fn update_mean(&self); } -/// -/// Load-balancer struct which is just a convenience wrapper over the statistics calculations. -#[derive(Debug)] -pub struct LoadBalancer; + +static LOAD_BALANCER: Lazy<LoadBalancer> = Lazy::new(|| { + let lb = LoadBalancer::new(placement::get_core_ids().unwrap()); + debug!("Instantiated load_balancer: {:?}", lb); + lb +}); + +/// Load-balancer struct which allows us to update the mean load +pub struct LoadBalancer { + /// The number of cores + /// available for this program + pub num_cores: usize, + /// The core Ids available for this program + /// This doesn't take affinity into account + pub cores: Vec<CoreId>, + mean_last_updated_at: RwLock<Instant>, +} impl LoadBalancer { - /// - /// AMQL sampling thread for run queue load balancing. - pub fn amql_generation() { - thread::Builder::new() - .name("bastion-load-balancer-thread".to_string()) - .spawn(move || { - loop { - load_balancer::stats().update_mean(); - // We don't have β-reduction here… Life is unfair. Life is cruel. - // - // Try sleeping for a while to wait - // Should be smaller time slice than 4 times per second to not miss - thread::sleep(Duration::from_millis(245)); - // Yield immediately back to os so we can advance in workers - thread::yield_now(); - } + /// Creates a new LoadBalancer. + /// if you're looking for `num_cores` and `cores` + /// Have a look at `load_balancer::core_count()` + /// and `load_balancer::get_cores()` respectively. + pub fn new(cores: Vec<CoreId>) -> Self { + Self { + num_cores: cores.len(), + cores, + mean_last_updated_at: RwLock::new(Instant::now()), + } + } +} + +impl Debug for LoadBalancer { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt.debug_struct("LoadBalancer") + .field("num_cores", &self.num_cores) + .field("cores", &self.cores) + .field("mean_last_updated_at", &self.mean_last_updated_at) + .finish() + } +} + +impl LoadBalancer { + /// Iterates the statistics to get the mean load across the cores + pub fn update_load_mean(&self) { + // Check if update should occur + if !self.should_update() { + return; + } + self.mean_last_updated_at + .write() + .map(|mut last_updated_at| { + *last_updated_at = Instant::now(); }) - .expect("load-balancer couldn't start"); + .unwrap_or_else(|e| error!("couldn't update mean timestamp - {}", e)); + + load_balancer::stats().update_mean(); } + + fn should_update(&self) -> bool { + // If we couldn't acquire a lock on the mean last_updated_at, + // There is probably someone else updating already + self.mean_last_updated_at + .try_read() + .map(|last_updated_at| last_updated_at.elapsed() > MEAN_UPDATE_TRESHOLD) + .unwrap_or(false) + } +} + +/// Update the mean load on the singleton +pub fn update() { + LOAD_BALANCER.update_load_mean() } /// Maximum number of core supported by modern computers. @@ -64,6 +117,7 @@ const MAX_CORE: usize = 256; pub struct Stats { smp_load: [AtomicUsize; MAX_CORE], mean_level: AtomicUsize, + updating_mean: AtomicBool, } impl fmt::Debug for Stats { @@ -71,6 +125,7 @@ impl fmt::Debug for Stats { fmt.debug_struct("Stats") .field("smp_load", &&self.smp_load[..]) .field("mean_level", &self.mean_level) + .field("updating_mean", &self.updating_mean) .finish() } } @@ -81,26 +136,24 @@ impl Stats { let smp_load: [AtomicUsize; MAX_CORE] = { let mut data: [MaybeUninit<AtomicUsize>; MAX_CORE] = unsafe { MaybeUninit::uninit().assume_init() }; - let mut i = 0; - while i < MAX_CORE { - if i < num_cores { - unsafe { - std::ptr::write(data[i].as_mut_ptr(), AtomicUsize::new(0)); - } - i += 1; - continue; + + for core_data in data.iter_mut().take(num_cores) { + unsafe { + std::ptr::write(core_data.as_mut_ptr(), AtomicUsize::new(0)); } - // MAX is for unused slot. + } + for core_data in data.iter_mut().take(MAX_CORE).skip(num_cores) { unsafe { - std::ptr::write(data[i].as_mut_ptr(), AtomicUsize::new(usize::MAX)); + std::ptr::write(core_data.as_mut_ptr(), AtomicUsize::new(usize::MAX)); } - i += 1; } + unsafe { std::mem::transmute::<_, [AtomicUsize; MAX_CORE]>(data) } }; Stats { smp_load, mean_level: AtomicUsize::new(0), + updating_mean: AtomicBool::new(false), } } } @@ -113,41 +166,46 @@ impl SmpStats for Stats { self.smp_load[affinity].store(load, Ordering::SeqCst); } - fn get_sorted_load(&self) -> ArrayVec<[(usize, usize); MAX_CORE]> { - let mut sorted_load = ArrayVec::<[(usize, usize); MAX_CORE]>::new(); + fn get_sorted_load(&self) -> ArrayVec<(usize, usize), MAX_CORE> { + let mut sorted_load = ArrayVec::new(); - for (i, item) in self.smp_load.iter().enumerate() { - let load = item.load(Ordering::SeqCst); + for (core, load) in self.smp_load.iter().enumerate() { + let load = load.load(Ordering::SeqCst); // load till maximum core. if load == usize::MAX { break; } // unsafe is ok here because self.smp_load.len() is MAX_CORE - unsafe { sorted_load.push_unchecked((i, load)) }; + unsafe { sorted_load.push_unchecked((core, load)) }; } sorted_load.sort_by(|x, y| y.1.cmp(&x.1)); sorted_load } fn mean(&self) -> usize { - self.mean_level.load(Ordering::SeqCst) + self.mean_level.load(Ordering::Acquire) } fn update_mean(&self) { + // Don't update if it's updating already + if self.updating_mean.load(Ordering::Acquire) { + return; + } + + self.updating_mean.store(true, Ordering::Release); let mut sum: usize = 0; + let num_cores = LOAD_BALANCER.num_cores; - for item in self.smp_load.iter() { - let load = item.load(Ordering::SeqCst); - if let Some(tmp) = sum.checked_add(load) { + for item in self.smp_load.iter().take(num_cores) { + if let Some(tmp) = sum.checked_add(item.load(Ordering::Acquire)) { sum = tmp; - continue; } - break; } - self.mean_level.store( - sum.wrapping_div(placement::get_core_ids().unwrap().len()), - Ordering::SeqCst, - ); + + self.mean_level + .store(sum.wrapping_div(num_cores), Ordering::Release); + + self.updating_mean.store(false, Ordering::Release); } } @@ -156,7 +214,7 @@ impl SmpStats for Stats { #[inline] pub fn stats() -> &'static Stats { lazy_static! { - static ref LOCKLESS_STATS: Stats = Stats::new(*core_retrieval()); + static ref LOCKLESS_STATS: Stats = Stats::new(*core_count()); } &*LOCKLESS_STATS } @@ -164,10 +222,13 @@ pub fn stats() -> &'static Stats { /// /// Retrieve core count for the runtime scheduling purposes #[inline] -pub fn core_retrieval() -> &'static usize { - lazy_static! { - static ref CORE_COUNT: usize = placement::get_core_ids().unwrap().len(); - } +pub fn core_count() -> &'static usize { + &LOAD_BALANCER.num_cores +} - &*CORE_COUNT +/// +/// Retrieve cores for the runtime scheduling purposes +#[inline] +pub fn get_cores() -> &'static [CoreId] { + &*LOAD_BALANCER.cores } diff --git a/src/bastion-executor/src/macros.rs b/src/bastion-executor/src/macros.rs deleted file mode 100644 index 56407401..00000000 --- a/src/bastion-executor/src/macros.rs +++ /dev/null @@ -1,11 +0,0 @@ -/// -/// Marker of unstable API. -#[doc(hidden)] -macro_rules! unstable_api { - ($($block:item)*) => { - $( - #[cfg(feature = "unstable")] - $block - )* - } -} diff --git a/src/bastion-executor/src/placement.rs b/src/bastion-executor/src/placement.rs index 27a368c3..1d3e0a76 100644 --- a/src/bastion-executor/src/placement.rs +++ b/src/bastion-executor/src/placement.rs @@ -10,9 +10,15 @@ pub fn get_core_ids() -> Option<Vec<CoreId>> { get_core_ids_helper() } +/// This function tries to retrieve +/// the number of active "cores" on the system. +pub fn get_num_cores() -> Option<usize> { + get_core_ids().map(|ids| ids.len()) +} /// /// Sets the current threads affinity pub fn set_for_current(core_id: CoreId) { + tracing::trace!("Executor: placement: set affinity on core {}", core_id.id); set_for_current_helper(core_id); } diff --git a/src/bastion-executor/src/pool.rs b/src/bastion-executor/src/pool.rs index b8c919a9..191e0bb8 100644 --- a/src/bastion-executor/src/pool.rs +++ b/src/bastion-executor/src/pool.rs @@ -1,16 +1,26 @@ //! //! Pool of threads to run lightweight processes //! -//! Pool management and tracking belongs here. -//! We spawn futures onto the pool with [spawn] method of global run queue or -//! with corresponding [Worker]'s spawn method. -use crate::distributor::Distributor; -use crate::run_queue::{Injector, Stealer}; -use crate::sleepers::Sleepers; +//! We spawn futures onto the pool with [`spawn`] method of global run queue or +//! with corresponding [`Worker`]'s spawn method. +//! +//! [`spawn`]: crate::pool::spawn +//! [`Worker`]: crate::run_queue::Worker + +use crate::thread_manager::{DynamicPoolManager, DynamicRunner}; use crate::worker; +use crossbeam_channel::{unbounded, Receiver, Sender}; use lazy_static::lazy_static; -use lightproc::prelude::*; +use lightproc::lightproc::LightProc; +use lightproc::proc_stack::ProcStack; +use lightproc::recoverable_handle::RecoverableHandle; +use once_cell::sync::{Lazy, OnceCell}; use std::future::Future; +use std::iter::Iterator; +use std::sync::Arc; +use std::time::Duration; +use std::{env, thread}; +use tracing::trace; /// /// Spawn a process (which contains future + process stack) onto the executor from the global level. @@ -20,6 +30,18 @@ use std::future::Future; /// use bastion_executor::prelude::*; /// use lightproc::prelude::*; /// +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # start(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # start(); +/// # } +/// # +/// # fn start() { /// let pid = 1; /// let stack = ProcStack::default().with_pid(pid); /// @@ -36,28 +58,36 @@ use std::future::Future; /// }, /// stack.clone(), /// ); +/// # } /// ``` pub fn spawn<F, T>(future: F, stack: ProcStack) -> RecoverableHandle<T> where F: Future<Output = T> + Send + 'static, T: Send + 'static, { - self::get().spawn(future, stack) + let (task, handle) = LightProc::recoverable(future, worker::schedule, stack); + task.schedule(); + handle } +/// Spawns a blocking task. /// -/// Pool that global run queue, stealers of the workers, and parked threads. -#[derive(Debug)] -pub struct Pool { - /// - /// Global run queue implementation - pub(crate) injector: Injector<LightProc>, - /// - /// Stealers of the workers - pub(crate) stealers: Vec<Stealer<LightProc>>, - /// - /// Container of parked threads - pub(crate) sleepers: Sleepers, +/// The task will be spawned onto a thread pool specifically dedicated to blocking tasks. +pub fn spawn_blocking<F, R>(future: F, stack: ProcStack) -> RecoverableHandle<R> +where + F: Future<Output = R> + Send + 'static, + R: Send + 'static, +{ + let (task, handle) = LightProc::recoverable(future, schedule, stack); + task.schedule(); + handle +} + +/// +/// Acquire the static Pool reference +#[inline] +pub fn get() -> &'static Pool { + &*POOL } impl Pool { @@ -78,21 +108,132 @@ impl Pool { } } +/// Enqueues work, attempting to send to the thread pool in a +/// nonblocking way and spinning up needed amount of threads +/// based on the previous statistics without relying on +/// if there is not a thread ready to accept the work or not. +pub(crate) fn schedule(t: LightProc) { + if let Err(err) = POOL.sender.try_send(t) { + // We were not able to send to the channel without + // blocking. + POOL.sender.send(err.into_inner()).unwrap(); + } + // Add up for every incoming scheduled task + DYNAMIC_POOL_MANAGER.get().unwrap().increment_frequency(); +} + /// -/// Acquire the static Pool reference +/// Low watermark value, defines the bare minimum of the pool. +/// Spawns initial thread set. +/// Can be configurable with env var `BASTION_BLOCKING_THREADS` at runtime. #[inline] -pub fn get() -> &'static Pool { +fn low_watermark() -> &'static u64 { lazy_static! { - static ref POOL: Pool = { - let distributor = Distributor::new(); - let stealers = distributor.assign(); - - Pool { - injector: Injector::new(), - stealers, - sleepers: Sleepers::new(), - } + static ref LOW_WATERMARK: u64 = { + env::var_os("BASTION_BLOCKING_THREADS") + .map(|x| x.to_str().unwrap().parse::<u64>().unwrap()) + .unwrap_or(DEFAULT_LOW_WATERMARK) }; } - &*POOL + + &*LOW_WATERMARK +} + +/// If low watermark isn't configured this is the default scaler value. +/// This value is used for the heuristics of the scaler +const DEFAULT_LOW_WATERMARK: u64 = 2; + +/// Pool interface between the scheduler and thread pool +#[derive(Debug)] +pub struct Pool { + sender: Sender<LightProc>, + receiver: Receiver<LightProc>, +} + +struct AsyncRunner { + // We keep a handle to the tokio runtime here to make sure + // it will never be dropped while the DynamicPoolManager is alive, + // In case we need to spin up some threads. + #[cfg(feature = "tokio-runtime")] + runtime_handle: tokio::runtime::Handle, +} + +impl DynamicRunner for AsyncRunner { + fn run_static(&self, park_timeout: Duration) -> ! { + loop { + for task in &POOL.receiver { + trace!("static: running task"); + self.run(task); + } + + trace!("static: empty queue, parking with timeout"); + thread::park_timeout(park_timeout); + } + } + fn run_dynamic(&self, parker: &dyn Fn()) -> ! { + loop { + while let Ok(task) = POOL.receiver.try_recv() { + trace!("dynamic thread: running task"); + self.run(task); + } + trace!( + "dynamic thread: parking - {:?}", + std::thread::current().id() + ); + parker(); + } + } + fn run_standalone(&self) { + while let Ok(task) = POOL.receiver.try_recv() { + self.run(task); + } + trace!("standalone thread: quitting."); + } } + +impl AsyncRunner { + fn run(&self, task: LightProc) { + #[cfg(feature = "tokio-runtime")] + { + self.runtime_handle.spawn_blocking(|| task.run()); + } + #[cfg(not(feature = "tokio-runtime"))] + { + task.run(); + } + } +} + +static DYNAMIC_POOL_MANAGER: OnceCell<DynamicPoolManager> = OnceCell::new(); + +static POOL: Lazy<Pool> = Lazy::new(|| { + #[cfg(feature = "tokio-runtime")] + { + let runner = Arc::new(AsyncRunner { + // We use current() here instead of try_current() + // because we want bastion to crash as soon as possible + // if there is no available runtime. + runtime_handle: tokio::runtime::Handle::current(), + }); + + DYNAMIC_POOL_MANAGER + .set(DynamicPoolManager::new(*low_watermark() as usize, runner)) + .expect("couldn't create dynamic pool manager"); + } + #[cfg(not(feature = "tokio-runtime"))] + { + let runner = Arc::new(AsyncRunner {}); + + DYNAMIC_POOL_MANAGER + .set(DynamicPoolManager::new(*low_watermark() as usize, runner)) + .expect("couldn't create dynamic pool manager"); + } + + DYNAMIC_POOL_MANAGER + .get() + .expect("couldn't get static pool manager") + .initialize(); + + let (sender, receiver) = unbounded(); + Pool { sender, receiver } +}); diff --git a/src/bastion-executor/src/run.rs b/src/bastion-executor/src/run.rs index 4a3d5757..9eb62f64 100644 --- a/src/bastion-executor/src/run.rs +++ b/src/bastion-executor/src/run.rs @@ -7,11 +7,11 @@ use crossbeam_utils::sync::Parker; use lightproc::proc_stack::ProcStack; use std::cell::{Cell, UnsafeCell}; use std::future::Future; +use std::mem; use std::mem::ManuallyDrop; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; -use std::{mem, panic}; /// /// This method blocks the current thread until passed future is resolved with an output (including the panic). diff --git a/src/bastion-executor/src/run_queue.rs b/src/bastion-executor/src/run_queue.rs index a494ad11..ae196c17 100644 --- a/src/bastion-executor/src/run_queue.rs +++ b/src/bastion-executor/src/run_queue.rs @@ -18,39 +18,34 @@ //! //! [`Worker`] has two constructors: //! -//! * [`new_fifo()`] - Creates a FIFO queue, in which tasks are pushed and popped from opposite +//! * [`new_fifo`] - Creates a FIFO queue, in which tasks are pushed and popped from opposite //! ends. -//! * [`new_lifo()`] - Creates a LIFO queue, in which tasks are pushed and popped from the same +//! * [`new_lifo`] - Creates a LIFO queue, in which tasks are pushed and popped from the same //! end. //! //! Each [`Worker`] is owned by a single thread and supports only push and pop operations. //! -//! Method [`stealer()`] creates a [`Stealer`] that may be shared among threads and can only steal +//! Method [`stealer`] creates a [`Stealer`] that may be shared among threads and can only steal //! tasks from its [`Worker`]. Tasks are stolen from the end opposite to where they get pushed. //! //! # Stealing //! //! Steal operations come in three flavors: //! -//! 1. [`steal()`] - Steals one task. -//! 2. [`steal_batch()`] - Steals a batch of tasks and moves them into another worker. -//! 3. [`steal_batch_and_pop()`] - Steals a batch of tasks, moves them into another queue, and pops +//! 1. [`steal`] - Steals one task. +//! 2. [`steal_batch`] - Steals a batch of tasks and moves them into another worker. +//! 3. [`steal_batch_and_pop`] - Steals a batch of tasks, moves them into another queue, and pops //! one task from that worker. //! //! In contrast to push and pop operations, stealing can spuriously fail with [`Steal::Retry`], in //! which case the steal operation needs to be retried. //! -//! -//! [`Worker`]: struct.Worker.html -//! [`Stealer`]: struct.Stealer.html -//! [`Injector`]: struct.Injector.html -//! [`Steal::Retry`]: enum.Steal.html#variant.Retry -//! [`new_fifo()`]: struct.Worker.html#method.new_fifo -//! [`new_lifo()`]: struct.Worker.html#method.new_lifo -//! [`stealer()`]: struct.Worker.html#method.stealer -//! [`steal()`]: struct.Stealer.html#method.steal -//! [`steal_batch()`]: struct.Stealer.html#method.steal_batch -//! [`steal_batch_and_pop()`]: struct.Stealer.html#method.steal_batch_and_pop +//! [`new_fifo`]: Worker::new_fifo +//! [`new_lifo`]: Worker::new_lifo +//! [`stealer`]: Worker::stealer +//! [`steal`]: Stealer::steal +//! [`steal_batch`]: Stealer::steal_batch +//! [`steal_batch_and_pop`]: Stealer::steal_batch_and_pop use crossbeam_epoch::{self as epoch, Atomic, Owned}; use crossbeam_utils::{Backoff, CachePadded}; use std::cell::{Cell, UnsafeCell}; @@ -1727,26 +1722,17 @@ pub enum Steal<T> { impl<T> Steal<T> { /// Returns `true` if the queue was empty at the time of stealing. pub fn is_empty(&self) -> bool { - match self { - Steal::Empty => true, - _ => false, - } + matches!(self, Steal::Empty) } /// Returns `true` if at least one task was stolen. pub fn is_success(&self) -> bool { - match self { - Steal::Success(_) => true, - _ => false, - } + matches!(self, Steal::Success(_)) } /// Returns `true` if the steal operation needs to be retried. pub fn is_retry(&self) -> bool { - match self { - Steal::Retry => true, - _ => false, - } + matches!(self, Steal::Retry) } /// Returns the result of the operation, if successful. @@ -1793,10 +1779,14 @@ impl<T> fmt::Debug for Steal<T> { } impl<T> FromIterator<Steal<T>> for Steal<T> { - /// Consumes items until a `Success` is found and returns it. + /// Consumes items until a [`Success`] is found and returns it. + /// + /// If no [`Success`] was found, but there was at least one [`Retry`], then returns [`Retry`]. + /// Otherwise, [`Empty`] is returned. /// - /// If no `Success` was found, but there was at least one `Retry`, then returns `Retry`. - /// Otherwise, `Empty` is returned. + /// [`Success`]: Steal::Success + /// [`Retry`]: Steal::Retry + /// [`Empty`]: Steal::Empty fn from_iter<I>(iter: I) -> Steal<T> where I: IntoIterator<Item = Steal<T>>, diff --git a/src/bastion-executor/src/thread_manager.rs b/src/bastion-executor/src/thread_manager.rs new file mode 100644 index 00000000..4f89dc08 --- /dev/null +++ b/src/bastion-executor/src/thread_manager.rs @@ -0,0 +1,394 @@ +//! A thread manager to predict how many threads should be spawned to handle the upcoming load. +//! +//! The thread manager consists of three elements: +//! * Frequency Detector +//! * Trend Estimator +//! * Predictive Upscaler +//! +//! ## Frequency Detector +//! Detects how many tasks are submitted from scheduler to thread pool in a given time frame. +//! Pool manager thread does this sampling every 90 milliseconds. +//! This value is going to be used for trend estimation phase. +//! +//! ## Trend Estimator +//! Hold up to the given number of frequencies to create an estimation. +//! Trend estimator holds 10 frequencies at a time. +//! This value is stored as constant in [FREQUENCY_QUEUE_SIZE](constant.FREQUENCY_QUEUE_SIZE.html). +//! Estimation algorithm and prediction uses Exponentially Weighted Moving Average algorithm. +//! +//! This algorithm is adapted from [A Novel Predictive and Self–Adaptive Dynamic Thread Pool Management](https://doi.org/10.1109/ISPA.2011.61) +//! and altered to: +//! * use instead of heavy calculation of trend, utilize thread redundancy which is the sum of the differences between the predicted and observed value. +//! * use instead of linear trend estimation, it uses exponential trend estimation where formula is: +//! ```text +//! LOW_WATERMARK * (predicted - observed) + LOW_WATERMARK +//! ``` +//! *NOTE:* If this algorithm wants to be tweaked increasing [LOW_WATERMARK](constant.LOW_WATERMARK.html) will automatically adapt the additional dynamic thread spawn count +//! * operate without watermarking by timestamps (in paper which is used to measure algorithms own performance during the execution) +//! * operate extensive subsampling. Extensive subsampling congests the pool manager thread. +//! * operate without keeping track of idle time of threads or job out queue like TEMA and FOPS implementations. +//! +//! ## Predictive Upscaler +//! Upscaler has three cases (also can be seen in paper): +//! * The rate slightly increases and there are many idle threads. +//! * The number of worker threads tends to be reduced since the workload of the system is descending. +//! * The system has no request or stalled. (Our case here is when the current tasks block further tasks from being processed – throughput hogs) +//! +//! For the first two EMA calculation and exponential trend estimation gives good performance. +//! For the last case, upscaler selects upscaling amount by amount of tasks mapped when throughput hogs happen. +//! +//! **example scenario:** Let's say we have 10_000 tasks where every one of them is blocking for 1 second. Scheduler will map plenty of tasks but will get rejected. +//! This makes estimation calculation nearly 0 for both entering and exiting parts. When this happens and we still see tasks mapped from scheduler. +//! We start to slowly increase threads by amount of frequency linearly. High increase of this value either make us hit to the thread threshold on +//! some OS or make congestion on the other thread utilizations of the program, because of context switch. +//! +//! Throughput hogs determined by a combination of job in / job out frequency and current scheduler task assignment frequency. +//! Threshold of EMA difference is eluded by machine epsilon for floating point arithmetic errors. + +use crate::{load_balancer, placement}; +use core::fmt; +use crossbeam_queue::ArrayQueue; +use fmt::{Debug, Formatter}; +use lazy_static::lazy_static; +use lever::prelude::TTas; +use placement::CoreId; +use std::collections::VecDeque; +use std::time::Duration; +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, Mutex, + }, + thread::{self, Thread}, +}; +use tracing::{debug, trace}; + +/// The default thread park timeout before checking for new tasks. +const THREAD_PARK_TIMEOUT: Duration = Duration::from_millis(1); + +/// Frequency histogram's sliding window size. +/// Defines how many frequencies will be considered for adaptation. +const FREQUENCY_QUEUE_SIZE: usize = 10; + +/// If low watermark isn't configured this is the default scaler value. +/// This value is used for the heuristics of the scaler +const DEFAULT_LOW_WATERMARK: u64 = 2; + +/// Pool scaler interval time (milliseconds). +/// This is the actual interval which makes adaptation calculation. +const SCALER_POLL_INTERVAL: u64 = 90; + +/// Exponential moving average smoothing coefficient for limited window. +/// Smoothing factor is estimated with: 2 / (N + 1) where N is sample size. +const EMA_COEFFICIENT: f64 = 2_f64 / (FREQUENCY_QUEUE_SIZE as f64 + 1_f64); + +lazy_static! { + static ref ROUND_ROBIN_PIN: Mutex<CoreId> = Mutex::new(CoreId { id: 0 }); +} + +/// The `DynamicRunner` is piloted by `DynamicPoolManager`. +/// Upon request it needs to be able to provide runner routines for: +/// * Static threads. +/// * Dynamic threads. +/// * Standalone threads. +/// +/// Your implementation of `DynamicRunner` +/// will allow you to define what tasks must be accomplished. +/// +/// Run static threads: +/// +/// run_static should never return, and park for park_timeout instead. +/// +/// Run dynamic threads: +/// run_dynamic should never return, and call `parker()` when it has no more tasks to process. +/// It will be unparked automatically by the `DynamicPoolManager` if needs be. +/// +/// Run standalone threads: +/// run_standalone should return once it has no more tasks to process. +/// The `DynamicPoolManager` will spawn other standalone threads if needs be. +pub trait DynamicRunner { + fn run_static(&self, park_timeout: Duration) -> !; + fn run_dynamic(&self, parker: &dyn Fn()) -> !; + fn run_standalone(&self); +} + +/// The `DynamicPoolManager` is responsible for +/// growing and shrinking a pool according to EMA rules. +/// +/// It needs to be passed a structure that implements `DynamicRunner`, +/// That will be responsible for actually spawning threads. +/// +/// The `DynamicPoolManager` keeps track of the number +/// of required number of threads to process load correctly. +/// and depending on the current state it will case it will: +/// - Spawn a lot of threads (we're predicting a load spike, and we need to prepare for it) +/// - Spawn few threads (there's a constant load, and throughput is low because the current resources are busy) +/// - Do nothing (the load is shrinking, threads will automatically stop once they're done). +/// +/// Kinds of threads: +/// +/// ## Static threads: +/// Defined in the constructor, they will always be available. They park for `THREAD_PARK_TIMEOUT` on idle. +/// +/// ## Dynamic threads: +/// Created during `DynamicPoolManager` initialization, they will park on idle. +/// The `DynamicPoolManager` grows the number of Dynamic threads +/// so the total number of Static threads + Dynamic threads +/// is the number of available cores on the machine. (`num_cpus::get()`) +/// +/// ## Standalone threads: +/// They are created when there aren't enough static and dynamic threads to process the expected load. +/// They will be destroyed on idle. +/// +/// ## Spawn order: +/// In order to handle a growing load, the pool manager will ask to: +/// - Use Static threads +/// - Unpark Dynamic threads +/// - Spawn Standalone threads +/// +/// The pool manager is not responsible for the tasks to be performed by the threads, it's handled by the `DynamicRunner` +/// +/// If you use tracing, you can have a look at the trace! logs generated by the structure. +/// +pub struct DynamicPoolManager { + static_threads: usize, + dynamic_threads: usize, + parked_threads: ArrayQueue<Thread>, + runner: Arc<dyn DynamicRunner + Send + Sync>, + last_frequency: AtomicU64, + frequencies: TTas<VecDeque<u64>>, +} + +impl Debug for DynamicPoolManager { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt.debug_struct("DynamicPoolManager") + .field("static_threads", &self.static_threads) + .field("dynamic_threads", &self.dynamic_threads) + .field("parked_threads", &self.parked_threads.len()) + .field("parked_threads", &self.parked_threads.len()) + .field("last_frequency", &self.last_frequency) + .field("frequencies", &self.frequencies.try_lock()) + .finish() + } +} + +impl DynamicPoolManager { + pub fn new(static_threads: usize, runner: Arc<dyn DynamicRunner + Send + Sync>) -> Self { + let dynamic_threads = 1.max(num_cpus::get().checked_sub(static_threads).unwrap_or(0)); + Self { + static_threads, + dynamic_threads, + parked_threads: ArrayQueue::new(dynamic_threads), + runner, + last_frequency: AtomicU64::new(0), + frequencies: TTas::new(VecDeque::with_capacity( + FREQUENCY_QUEUE_SIZE.saturating_add(1), + )), + } + } + + pub fn increment_frequency(&self) { + self.last_frequency.fetch_add(1, Ordering::Acquire); + } + + /// Initialize the dynamic pool + /// That will be scaled + pub fn initialize(&'static self) { + // Static thread manager that will always be available + trace!("setting up the static thread manager"); + (0..self.static_threads).for_each(|_| { + let clone = Arc::clone(&self.runner); + thread::Builder::new() + .name("bastion-driver-static".to_string()) + .spawn(move || { + Self::affinity_pinner(); + clone.run_static(THREAD_PARK_TIMEOUT); + }) + .expect("couldn't spawn static thread"); + }); + + // Dynamic thread manager that will allow us to unpark threads + // According to the needs + trace!("setting up the dynamic thread manager"); + (0..self.dynamic_threads).for_each(|_| { + let clone = Arc::clone(&self.runner); + thread::Builder::new() + .name("bastion-driver-dynamic".to_string()) + .spawn(move || { + Self::affinity_pinner(); + clone.run_dynamic(&|| self.park_thread()); + }) + .expect("cannot start dynamic thread"); + }); + + // Pool manager to check frequency of task rates + // and take action by scaling the pool accordingly. + thread::Builder::new() + .name("bastion-pool-manager".to_string()) + .spawn(move || { + let poll_interval = Duration::from_millis(SCALER_POLL_INTERVAL); + trace!("setting up the pool manager"); + loop { + self.scale_pool(); + thread::park_timeout(poll_interval); + } + }) + .expect("thread pool manager cannot be started"); + } + + /// Provision threads takes a number of threads that need to be made available. + /// It will try to unpark threads from the dynamic pool, and spawn more threads if needs be. + pub fn provision_threads(&'static self, n: usize) { + for i in 0..n { + if !self.unpark_thread() { + let new_threads = n - i; + trace!( + "no more threads to unpark, spawning {} new threads", + new_threads + ); + return self.spawn_threads(new_threads); + } + } + } + + fn spawn_threads(&'static self, n: usize) { + (0..n).for_each(|_| { + let clone = Arc::clone(&self.runner); + thread::Builder::new() + .name("bastion-blocking-driver-standalone".to_string()) + .spawn(move || { + Self::affinity_pinner(); + clone.run_standalone(); + }) + .unwrap(); + }) + } + + /// Parks a thread until unpark_thread unparks it + pub fn park_thread(&self) { + let _ = self + .parked_threads + .push(std::thread::current()) + .map(|_| { + trace!("parking thread {:?}", std::thread::current().id()); + std::thread::park(); + }) + .map_err(|t| { + debug!("couldn't park thread {:?}", t.id(),); + }); + } + + /// Pops a thread from the parked_threads queue and unparks it. + /// returns true on success. + fn unpark_thread(&self) -> bool { + trace!("parked_threads: len is {}", self.parked_threads.len()); + if let Some(thread) = self.parked_threads.pop() { + debug!("Executor: unpark_thread: unparking {:?}", thread.id()); + thread.unpark(); + true + } else { + false + } + } + + /// + /// Affinity pinner for blocking pool + /// Pinning isn't going to be enabled for single core systems. + #[inline] + fn affinity_pinner() { + if 1 != *load_balancer::core_count() { + let mut core = ROUND_ROBIN_PIN.lock().unwrap(); + placement::set_for_current(*core); + core.id = (core.id + 1) % *load_balancer::core_count(); + } + } + + /// Exponentially Weighted Moving Average calculation + /// + /// This allows us to find the EMA value. + /// This value represents the trend of tasks mapped onto the thread pool. + /// Calculation is following: + /// ```text + /// +--------+-----------------+----------------------------------+ + /// | Symbol | Identifier | Explanation | + /// +--------+-----------------+----------------------------------+ + /// | α | EMA_COEFFICIENT | smoothing factor between 0 and 1 | + /// | Yt | freq | frequency sample at time t | + /// | St | acc | EMA at time t | + /// +--------+-----------------+----------------------------------+ + /// ``` + /// Under these definitions formula is following: + /// ```text + /// EMA = α * [ Yt + (1 - α)*Yt-1 + ((1 - α)^2)*Yt-2 + ((1 - α)^3)*Yt-3 ... ] + St + /// ``` + /// # Arguments + /// + /// * `freq_queue` - Sliding window of frequency samples + #[inline] + fn calculate_ema(freq_queue: &VecDeque<u64>) -> f64 { + freq_queue.iter().enumerate().fold(0_f64, |acc, (i, freq)| { + acc + ((*freq as f64) * ((1_f64 - EMA_COEFFICIENT).powf(i as f64) as f64)) + }) * EMA_COEFFICIENT as f64 + } + + /// Adaptive pool scaling function + /// + /// This allows to spawn new threads to make room for incoming task pressure. + /// Works in the background detached from the pool system and scales up the pool based + /// on the request rate. + /// + /// It uses frequency based calculation to define work. Utilizing average processing rate. + fn scale_pool(&'static self) { + // Fetch current frequency, it does matter that operations are ordered in this approach. + let current_frequency = self.last_frequency.swap(0, Ordering::SeqCst); + let mut freq_queue = self.frequencies.lock(); + + // Make it safe to start for calculations by adding initial frequency scale + if freq_queue.len() == 0 { + freq_queue.push_back(0); + } + + // Calculate message rate for the given time window + let frequency = (current_frequency as f64 / SCALER_POLL_INTERVAL as f64) as u64; + + // Calculates current time window's EMA value (including last sample) + let prev_ema_frequency = Self::calculate_ema(&freq_queue); + + // Add seen frequency data to the frequency histogram. + freq_queue.push_back(frequency); + if freq_queue.len() == FREQUENCY_QUEUE_SIZE.saturating_add(1) { + freq_queue.pop_front(); + } + + // Calculates current time window's EMA value (including last sample) + let curr_ema_frequency = Self::calculate_ema(&freq_queue); + + // Adapts the thread count of pool + // + // Sliding window of frequencies visited by the pool manager. + // Pool manager creates EMA value for previous window and current window. + // Compare them to determine scaling amount based on the trends. + // If current EMA value is bigger, we will scale up. + if curr_ema_frequency > prev_ema_frequency { + // "Scale by" amount can be seen as "how much load is coming". + // "Scale" amount is "how many threads we should spawn". + let scale_by: f64 = curr_ema_frequency - prev_ema_frequency; + let scale = num_cpus::get().min( + ((DEFAULT_LOW_WATERMARK as f64 * scale_by) + DEFAULT_LOW_WATERMARK as f64) as usize, + ); + trace!("unparking {} threads", scale); + + // It is time to scale the pool! + self.provision_threads(scale); + } else if (curr_ema_frequency - prev_ema_frequency).abs() < std::f64::EPSILON + && current_frequency != 0 + { + // Throughput is low. Allocate more threads to unblock flow. + // If we fall to this case, scheduler is congested by longhauling tasks. + // For unblock the flow we should add up some threads to the pool, but not that many to + // stagger the program's operation. + trace!("unparking {} threads", DEFAULT_LOW_WATERMARK); + self.provision_threads(DEFAULT_LOW_WATERMARK as usize); + } + } +} diff --git a/src/bastion-executor/src/worker.rs b/src/bastion-executor/src/worker.rs index eb1a9b73..d97e260a 100644 --- a/src/bastion-executor/src/worker.rs +++ b/src/bastion-executor/src/worker.rs @@ -3,13 +3,17 @@ //! //! This worker implementation relies on worker run queue statistics which are hold in the pinned global memory //! where workload distribution calculated and amended to their own local queues. -use crate::load_balancer; -use crate::pool::{self, Pool}; -use crate::run_queue::{Steal, Worker}; + +use crate::pool; + use lightproc::prelude::*; -use load_balancer::SmpStats; -use std::cell::{Cell, UnsafeCell}; -use std::{iter, ptr}; +use std::cell::Cell; +use std::ptr; +use std::time::Duration; + +/// The timeout we'll use when parking before an other Steal attempt +pub const THREAD_PARK_TIMEOUT: Duration = Duration::from_millis(1); + /// /// Get the current process's stack pub fn current() -> ProcStack { @@ -55,97 +59,6 @@ where } } -thread_local! { - static QUEUE: UnsafeCell<Option<Worker<LightProc>>> = UnsafeCell::new(None); -} - pub(crate) fn schedule(proc: LightProc) { - QUEUE.with(|queue| { - let local = unsafe { (*queue.get()).as_ref() }; - - match local { - None => pool::get().injector.push(proc), - Some(q) => q.push(proc), - } - }); - - pool::get().sleepers.notify_one(); -} - -/// -/// Fetch the process from the run queue. -/// Does the work of work-stealing if process doesn't exist in the local run queue. -pub fn fetch_proc(affinity: usize) -> Option<LightProc> { - let pool = pool::get(); - - QUEUE.with(|queue| { - let local = unsafe { (*queue.get()).as_ref().unwrap() }; - local.pop().or_else(|| affine_steal(pool, local, affinity)) - }) -} - -fn affine_steal(pool: &Pool, local: &Worker<LightProc>, affinity: usize) -> Option<LightProc> { - let load_mean = load_balancer::stats().mean(); - // Pop a task from the local queue, if not empty. - local.pop().or_else(|| { - // Otherwise, we need to look for a task elsewhere. - iter::repeat_with(|| { - let core_vec = load_balancer::stats().get_sorted_load(); - - // First try to get procs from global queue - pool.injector.steal_batch_and_pop(&local).or_else(|| { - match core_vec.get(0) { - Some((core, _)) => { - // If affinity is the one with the highest let other's do the stealing - if *core == affinity { - Steal::Retry - } else { - // Try iterating through biggest to smallest - core_vec - .iter() - .map(|s| { - // Steal the mean amount to balance all queues considering incoming workloads - // Otherwise do an ignorant steal (which is going to be useless) - if load_mean > 0 { - pool.stealers - .get(s.0) - .unwrap() - .steal_batch_and_pop_with_amount(&local, load_mean) - } else { - pool.stealers.get(s.0).unwrap().steal_batch_and_pop(&local) - // TODO: Set evacuation flag in thread_local - } - }) - .collect() - } - } - _ => Steal::Retry, - } - }) - }) - // Loop while no task was stolen and any steal operation needs to be retried. - .find(|s| !s.is_retry()) - // Extract the stolen task, if there is one. - .and_then(|s| s.success()) - }) -} - -pub(crate) fn stats_generator(affinity: usize, local: &Worker<LightProc>) { - load_balancer::stats().store_load(affinity, local.worker_run_queue_size()); -} - -pub(crate) fn main_loop(affinity: usize, local: Worker<LightProc>) { - QUEUE.with(|queue| unsafe { *queue.get() = Some(local) }); - - loop { - QUEUE.with(|queue| { - let local = unsafe { (*queue.get()).as_ref().unwrap() }; - stats_generator(affinity, local); - }); - - match fetch_proc(affinity) { - Some(proc) => set_stack(proc.stack(), || proc.run()), - None => pool::get().sleepers.wait(), - } - } + pool::schedule(proc) } diff --git a/src/bastion-executor/tests/lib.rs b/src/bastion-executor/tests/lib.rs index 2a3c9f8b..416c571c 100644 --- a/src/bastion-executor/tests/lib.rs +++ b/src/bastion-executor/tests/lib.rs @@ -8,8 +8,19 @@ mod tests { dbg!(core_ids); } - #[test] - fn pool_check() { - pool::get(); + #[cfg(feature = "tokio-runtime")] + mod tokio_tests { + #[tokio::test] + async fn pool_check() { + super::pool::get(); + } + } + + #[cfg(not(feature = "tokio-runtime"))] + mod no_tokio_tests { + #[test] + fn pool_check() { + super::pool::get(); + } } } diff --git a/src/bastion-executor/tests/run_blocking.rs b/src/bastion-executor/tests/run_blocking.rs new file mode 100644 index 00000000..6f792957 --- /dev/null +++ b/src/bastion-executor/tests/run_blocking.rs @@ -0,0 +1,38 @@ +use bastion_executor::blocking; +use bastion_executor::run::run; +use lightproc::proc_stack::ProcStack; +use std::thread; +use std::time::Duration; + +#[cfg(feature = "tokio-runtime")] +mod tokio_tests { + #[tokio::test] + async fn test_run_blocking() { + super::run_test() + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod no_tokio_tests { + #[test] + fn test_run_blocking() { + super::run_test() + } +} + +fn run_test() { + let output = run( + blocking::spawn_blocking( + async { + let duration = Duration::from_millis(1); + thread::sleep(duration); + 42 + }, + ProcStack::default(), + ), + ProcStack::default(), + ) + .unwrap(); + + assert_eq!(42, output); +} diff --git a/src/bastion/Cargo.toml b/src/bastion/Cargo.toml index 429dd26d..b0fbb65e 100644 --- a/src/bastion/Cargo.toml +++ b/src/bastion/Cargo.toml @@ -5,7 +5,7 @@ name = "bastion" # - Update CHANGELOG.md. # - npm install -g auto-changelog && auto-changelog at the root # - Create "v0.x.y" git tag at the root of the project. -version = "0.4.3-alpha.0" +version = "0.4.5-alpha.0" description = "Fault-tolerant Runtime for Rust applications" authors = ["Mahmut Bulut <vertexclique@gmail.com>"] keywords = ["fault-tolerant", "runtime", "actor", "system"] @@ -43,19 +43,20 @@ distributed = [ ] scaling = [] docs = ["distributed", "scaling", "default"] - +tokio-runtime = ["bastion-executor/tokio-runtime"] [package.metadata.docs.rs] features = ["docs"] rustdoc-args = ["--cfg", "feature=\"docs\""] [dependencies] -bastion-executor = "0.3.6" -lightproc = "0.3.5" -# bastion-executor = { version = "= 0.3.7-alpha.0", path = "../bastion-executor" } -# lightproc = { version = "= 0.3.6-alpha.0", path = "../lightproc" } +bastion-executor = { git = "https://github.com/bastion-rs/bastion.git" } +# bastion-executor = { path = "../bastion-executor" } +lightproc = { git = "https://github.com/bastion-rs/bastion.git" } +# lightproc = "0.3" +# lightproc = { path = "../lightproc" } -lever = "0.1.1-alpha.11" +lever = "0.1" futures = "0.3.5" futures-timer = "3.0.2" fxhash = "0.2" @@ -64,7 +65,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" pin-utils = "0.1" -async-mutex = "1.1" +async-mutex = "1.4.0" uuid = { version = "0.8", features = ["v4"] } # Distributed @@ -73,17 +74,32 @@ artillery-core = { version = "0.1.2-alpha.3", optional = true } # Log crates tracing-subscriber = "0.2.6" tracing = "0.1.15" -anyhow = "1.0.31" +anyhow = "1.0" +crossbeam-queue = "0.3.0" +log = "0.4.14" +lasso = {version = "0.5", features = ["multi-threaded"] } +once_cell = "1.7.2" +thiserror = "1.0.24" +async-channel = "1.6.1" +regex = "1.4.5" +async-trait = "0.1.48" +crossbeam = "0.8.0" [target.'cfg(not(windows))'.dependencies] -nuclei = "0.1.2-alpha.1" +nuclei = "0.1" [dev-dependencies] -env_logger = "0.7" -proptest = "0.10" +env_logger = "0.8" +proptest = "1.0" snap = "1.0" # prime_numbers example bastion-utils = { version = "0.3.2", path = "../bastion-utils" } -rand = "0.7.3" +rand = "0.8" rayon = "1.3.1" num_cpus = "1.13.0" +# hello_tokio example +tokio = { version="1.1", features = ["time", "macros"] } +# bastion-executor = { path = "../bastion-executor" } +bastion-executor = { git = "https://github.com/bastion-rs/bastion.git" } +once_cell = "1.5.2" +tokio-test = "0.4.0" diff --git a/src/bastion/examples/broadcast_message.rs b/src/bastion/examples/broadcast_message.rs index 51094574..35898942 100644 --- a/src/bastion/examples/broadcast_message.rs +++ b/src/bastion/examples/broadcast_message.rs @@ -14,12 +14,12 @@ use tracing::Level; /// broadcasting messages features. /// /// The pipeline in this example can be described in the following way: -/// 1. The Input group contains the only one actor that stars the processing with +/// 1. The Input group contains the only one actor that starts the processing with /// sending messages through a dispatcher to actors in the Map group. /// 2. Each actor of the Process group does some useful work and passes a result /// to the next stage with the similar call to the Reduce group. /// 3. The actor from the Response group retrieves the data from the actors of the -/// Reduce group, combines the results and prints its when everything is done. +/// Reduce group, combines the results and prints them when everything is done. /// fn main() { let subscriber = tracing_subscriber::fmt() @@ -32,16 +32,16 @@ fn main() { Bastion::init(); - Bastion::supervisor(input_supervisor) + Bastion::supervisor(response_supervisor) .and_then(|_| Bastion::supervisor(map_supervisor)) - .and_then(|_| Bastion::supervisor(response_supervisor)) + .and_then(|_| Bastion::supervisor(input_supervisor)) .expect("Couldn't create supervisor chain."); Bastion::start(); Bastion::block_until_stopped(); } -// Supervisor that tracking only the single actor with input data +// Supervisor which tracks only the single actor with input data fn input_supervisor(supervisor: Supervisor) -> Supervisor { supervisor.children(input_group) } @@ -59,7 +59,7 @@ fn response_supervisor(supervisor: Supervisor) -> Supervisor { fn input_group(children: Children) -> Children { children.with_name("input").with_redundancy(1).with_exec( move |ctx: BastionContext| async move { - println!("[Input] Worker started!"); + tracing::info!("[Input] Worker started!"); let data = vec!["A B C", "A C C", "B C C"]; let group_name = "Processing".to_string(); @@ -83,11 +83,11 @@ fn process_group(children: Children) -> Children { // the namespace with the "Map" name and removed after being stopped or killed // automatically. // - // If needed to use more than one groups, then do more `with_dispatcher` calls + // If needed to use more than one group, then do more `with_dispatcher` calls Dispatcher::with_type(DispatcherType::Named("Processing".to_string())), ) .with_exec(move |ctx: BastionContext| async move { - println!("[Processing] Worker started!"); + tracing::info!("[Processing] Worker started!"); msg! { ctx.recv().await?, // We received the message from other actor wrapped in Arc<T> @@ -128,14 +128,14 @@ fn response_group(children: Children) -> Children { .with_redundancy(1) .with_dispatcher( // We will re-use the dispatcher to make the example easier to understand - // and flexibility in code. + // and increase flexibility in code. // - // The single difference is only the name for Dispatcher for our actor's group. + // The single difference is only the name for Dispatcher for our actors group. Dispatcher::with_type(DispatcherType::Named("Response".to_string())), ) .with_exec(move |ctx: BastionContext| { async move { - println!("[Response] Worker started!"); + tracing::info!("[Response] Worker started!"); let mut received_messages = 0; let expected_messages = 3; diff --git a/src/bastion/examples/distributed-fwrite.rs b/src/bastion/examples/distributed-fwrite.rs index 76ec7e77..f5c2786b 100644 --- a/src/bastion/examples/distributed-fwrite.rs +++ b/src/bastion/examples/distributed-fwrite.rs @@ -1,11 +1,16 @@ -use bastion::prelude::*; -use futures::*; -use std::fs::{File, OpenOptions}; +#[cfg(not(target_os = "windows"))] +use std::fs::File; +use std::fs::OpenOptions; #[cfg(target_os = "windows")] use std::io::Write; use std::path::PathBuf; use std::sync::Arc; +#[cfg(not(target_os = "windows"))] +use futures::*; + +use bastion::prelude::*; + /// /// Parallel (MapReduce) job which async writes results to a single output file /// @@ -48,7 +53,6 @@ fn main() { // Get a shadowed sharable reference of workers. let workers = Arc::new(workers); - // // Mapper that generates work. Bastion::children(|children: Children| { children.with_exec(move |ctx: BastionContext| { @@ -60,6 +64,11 @@ fn main() { path.push("data"); path.push("distwrite"); + if !path.exists() || !path.is_file() { + Bastion::stop(); + panic!("The file could not be opened."); + } + let fo = OpenOptions::new() .read(true) .write(true) diff --git a/src/bastion/examples/distributor.rs b/src/bastion/examples/distributor.rs new file mode 100644 index 00000000..5ebf7b4b --- /dev/null +++ b/src/bastion/examples/distributor.rs @@ -0,0 +1,267 @@ +///! Create a conference. +///! +///! 1st Group +///! Staff (5) - Going to organize the event // OK +///! +///! 2nd Group +///! Enthusiasts (50) - interested in participating to the conference (haven't registered yet) // OK +///! +///! 3rd Group +///! Attendees (empty for now) - Participate +///! +///! Enthusiast -> Ask one of the staff members "when is the conference going to happen ?" // OK +///! Broadcast / Question => Answer 0 or 1 Staff members are going to reply eventually? // OK +///! +///! Staff -> Send a Leaflet to all of the enthusiasts, letting them know that they can register. // OK +///! +///! "hey conference <awesomeconference> is going to happen. will you be there?" +///! Broadcast / Question -> if people reply with YES => fill the 3rd group +///! some enthusiasts are now attendees +///! +///! Staff -> send the actual schedule and misc infos to Attendees +///! Broadcast / Statement (Attendees) +///! +///! An attendee sends a thank you note to one staff member (and not bother everyone) +///! One message / Statement (Staff) // OK +///! +///! ```rust +///! let staff = Distributor::named("staff"); +///! let enthusiasts = Distributor::named("enthusiasts"); +///! let attendees = Disitributor::named("attendees"); +///! // Enthusiast -> Ask the whole staff "when is the conference going to happen ?" +///! ask_one(Message + Clone) -> Result<impl Future<Output = Reply>, CouldNotSendError> +///! // await_one // await_all +///! // first ? means "have we been able to send the question?" +///! // it s in a month +///! let replies = staff.ask_one("when is the conference going to happen ?")?.await?; +///! ask_everyone(Message + Clone) -> Result<impl Stream<Item = Reply>, CouldNotSendError> +///! let participants = enthusiasts.ask_everyone("here's our super nice conference, it s happening people!").await?; +///! for participant in participants { +///! // grab the sender and add it to the attendee recipient group +///! } +///! // send the schedule +///! tell_everyone(Message + Clone) -> Result<(), CouldNotSendError> +///! attendees.tell_everyone("hey there, conf is in a week, here s where and how it s going to happen")?; +///! // send a thank you note +///! tell(Message) -> Result<(), CouldNotSendError> +///! staff.tell_one("thank's it was amazing")?; +///! children +///! .with_redundancy(10) +///! .with_distributor(Distributor::named("staff")) +///! // We create the function to exec when each children is called +///! .with_exec(move |ctx: BastionContext| async move { /* ... */ }) +///! children +///! .with_redundancy(100) +///! .with_distributor(Distributor::named("enthusiasts")) +///! // We create the function to exec when each children is called +///! .with_exec(move |ctx: BastionContext| async move { /* ... */ }) +///! children +///! .with_redundancy(0) +///! .with_distributor(Distributor::named("attendees")) +///! // We create the function to exec when each children is called +///! .with_exec(move |ctx: BastionContext| async move { /* ... */ }) +///! ``` +use anyhow::{anyhow, Context, Result as AnyResult}; +use bastion::distributor::*; +use bastion::prelude::*; +use tracing::Level; + +// true if the person attends the conference +#[derive(Debug)] +struct RSVP { + attends: bool, + child_ref: ChildRef, +} + +#[derive(Debug, Clone)] +struct ConferenceSchedule { + start: std::time::Duration, + end: std::time::Duration, + misc: String, +} + +/// cargo r --features=tokio-runtime --example distributor +#[cfg(feature = "tokio-runtime")] +#[tokio::main] +async fn main() -> AnyResult<()> { + run() +} + +#[cfg(not(feature = "tokio-runtime"))] +fn main() -> AnyResult<()> { + run() +} + +fn run() -> AnyResult<()> { + let subscriber = tracing_subscriber::fmt() + .with_max_level(Level::INFO) + .finish(); + tracing::subscriber::set_global_default(subscriber).unwrap(); + + // Initialize bastion + Bastion::init(); + + // 1st Group + Bastion::supervisor(|supervisor| { + supervisor.children(|children| { + // Iniit staff + // Staff (5 members) - Going to organize the event + children + .with_redundancy(5) + .with_distributor(Distributor::named("staff")) + .with_exec(organize_the_event) + }) + }) + // 2nd Group + .and_then(|_| { + Bastion::supervisor(|supervisor| { + supervisor.children(|children| { + // Enthusiasts (50) - interested in participating to the conference (haven't registered yet) + children + .with_redundancy(50) + .with_distributor(Distributor::named("enthusiasts")) + .with_exec(be_interested_in_the_conference) + }) + }) + }) + .map_err(|_| anyhow!("couldn't setup the bastion"))?; + + Bastion::start(); + + // Wait a bit until everyone is ready + sleep(std::time::Duration::from_secs(5)); + + let staff = Distributor::named("staff"); + let enthusiasts = Distributor::named("enthusiasts"); + let attendees = Distributor::named("attendees"); + + // Enthusiast -> Ask one of the staff members "when is the conference going to happen ?" + let reply: Result<String, SendError> = run!(async { + staff + .request("when is the next conference going to happen?") + .await + .expect("couldn't receive reply") + }); + + tracing::error!("{:?}", reply); // Ok("Next month!") + + // "hey conference <awesomeconference> is going to happen. will you be there?" + // Broadcast / Question -> if people reply with YES => fill the 3rd group + let answers = enthusiasts + .ask_everyone("hey, the conference is going to happen, will you be there?") + .expect("couldn't ask everyone"); + + for answer in answers.into_iter() { + run!(async move { + MessageHandler::new(answer.await.expect("couldn't receive reply")) + .on_tell(|rsvp: RSVP, _| { + if rsvp.attends { + tracing::info!("{:?} will be there! :)", rsvp.child_ref.id()); + attendees + .subscribe(rsvp.child_ref) + .expect("couldn't subscribe attendee"); + } else { + tracing::error!("{:?} won't make it :(", rsvp.child_ref.id()); + } + }) + .on_fallback(|unknown, _sender_addr| { + tracing::error!( + "distributor_test: uh oh, I received a message I didn't understand\n {:?}", + unknown + ); + }); + }); + } + + // Ok now that attendees have subscribed, let's send information around! + tracing::info!("Let's send invitations!"); + // Staff -> send the actual schedule and misc infos to Attendees + let total_sent = attendees + .tell_everyone(ConferenceSchedule { + start: std::time::Duration::from_secs(60), + end: std::time::Duration::from_secs(3600), + misc: "it's going to be amazing!".to_string(), + }) + .context("couldn't let everyone know the conference is available!")?; + + tracing::error!("total number of attendees: {}", total_sent.len()); + + tracing::info!("the conference is running!"); + + // Let's wait until the conference is over 8D + sleep(std::time::Duration::from_secs(5)); + + // An attendee sends a thank you note to one staff member (and not bother everyone) + staff + .tell_one("the conference was amazing thank you so much!") + .context("couldn't thank the staff members :(")?; + + // And we're done! + Bastion::stop(); + + // BEWARE, this example doesn't return + Bastion::block_until_stopped(); + + Ok(()) +} + +async fn organize_the_event(ctx: BastionContext) -> Result<(), ()> { + loop { + MessageHandler::new(ctx.recv().await?) + .on_question(|message: &str, sender| { + tracing::info!("received a question: \n{}", message); + sender.reply("Next month!".to_string()).unwrap(); + }) + .on_tell(|message: &str, _| { + tracing::info!("received a message: \n{}", message); + }) + .on_fallback(|unknown, _sender_addr| { + tracing::error!( + "staff: uh oh, I received a message I didn't understand\n {:?}", + unknown + ); + }); + } +} + +async fn be_interested_in_the_conference(ctx: BastionContext) -> Result<(), ()> { + loop { + MessageHandler::new(ctx.recv().await?) + .on_tell(|message: std::sync::Arc<&str>, _| { + tracing::info!( + "child {}, received a broadcast message:\n{}", + ctx.current().id(), + message + ); + }) + .on_tell(|schedule: ConferenceSchedule, _| { + tracing::info!( + "child {}, received broadcast conference schedule!:\n{:?}", + ctx.current().id(), + schedule + ); + }) + .on_question(|message: &str, sender| { + tracing::info!("received a question: \n{}", message); + // ILL BE THERE! + sender + .reply(RSVP { + attends: rand::random(), + child_ref: ctx.current().clone(), + }) + .unwrap(); + }); + } +} + +#[cfg(feature = "tokio-runtime")] +fn sleep(duration: std::time::Duration) { + run!(async { + tokio::time::sleep(duration).await; + }); +} + +#[cfg(not(feature = "tokio-runtime"))] +fn sleep(duration: std::time::Duration) { + std::thread::sleep(duration); +} diff --git a/src/bastion/examples/fibonacci_message_handler.rs b/src/bastion/examples/fibonacci_message_handler.rs new file mode 100644 index 00000000..e6ca7220 --- /dev/null +++ b/src/bastion/examples/fibonacci_message_handler.rs @@ -0,0 +1,147 @@ +use bastion::prelude::*; + +use tracing::{error, info}; + +// This terribly slow implementation +// will allow us to be rough on the cpu +fn fib(n: usize) -> usize { + if n == 0 || n == 1 { + n + } else { + fib(n - 1) + fib(n - 2) + } +} + +// This terrible helper is converting `fib 50` into a tuple ("fib", 50) +// we might want to use actual serializable / deserializable structures +// in the real world +fn deserialize_into_fib_command(message: String) -> (String, usize) { + let arguments: Vec<&str> = message.split(' ').collect(); + let command = arguments.first().map(|s| s.to_string()).unwrap_or_default(); + let number = usize::from_str_radix(arguments.get(1).unwrap_or(&"0"), 10).unwrap_or(0); + (command, number) +} + +// This is the heavylifting. +// A child will wait for a message, and try to process it. +async fn fib_child_task(ctx: BastionContext) -> Result<(), ()> { + loop { + MessageHandler::new(ctx.recv().await?) + .on_question(|request: String, sender| { + let (command, number) = deserialize_into_fib_command(request); + if command == "fib" { + sender + .reply(format!("{}", fib(number))) + .expect("couldn't reply :("); + } else { + sender + .reply(format!( + "I'm sorry I didn't understand the task I was supposed to do" + )) + .expect("couldn't reply :("); + } + }) + .on_broadcast(|broadcast: &String, _sender_addr| { + info!("received broadcast: {:?}", *broadcast); + }) + .on_tell(|message: String, _sender_addr| { + info!("someone told me something: {}", message); + }) + .on_fallback(|unknown, _sender_addr| { + error!( + "uh oh, I received a message I didn't understand\n {:?}", + unknown + ); + }); + } +} + +// This little helper allows me to send a request, and get a reply. +// The types are `String` for this quick example, but there's a way for us to do better. +// We will see this in other examples. +async fn request(child: &ChildRef, body: String) -> std::io::Result<String> { + let answer = child + .ask_anonymously(body) + .expect("couldn't perform request"); + + Ok( + MessageHandler::new(answer.await.expect("couldn't receive answer")) + .on_tell(|reply, _sender_addr| reply) + .on_fallback(|unknown, _sender_addr| { + error!( + "uh oh, I received a message I didn't understand: {:?}", + unknown + ); + "".to_string() + }), + ) +} + +// RUST_LOG=info cargo run --example fibonacci_message_handler +fn main() { + // This will allow us to have nice colored logs when we run the program + env_logger::init(); + + // We need a bastion in order to run everything + Bastion::init(); + Bastion::start(); + + // Spawn 4 children that will execute our fibonacci task + let children = + Bastion::children(|children| children.with_redundancy(4).with_exec(fib_child_task)) + .expect("couldn't create children"); + + // Broadcasting 1 message to the children + // Have a look at the console output + // to see 1 log entry for each child! + children + .broadcast("Hello there :)".to_string()) + .expect("Couldn't broadcast to the children."); + + let mut fib_to_compute = 35; + for child in children.elems() { + child + .tell_anonymously("shhh here's a message, don't tell anyone.".to_string()) + .expect("Couldn't whisper to child."); + + let now = std::time::Instant::now(); + // by using run!, we are blocking. + // we could have used spawn! instead, + // to run everything in parallel. + let fib_reply = run!(request(child, format!("fib {}", fib_to_compute))) + .expect("send_command_to_child failed"); + + println!( + "fib({}) = {} - Computed in {}ms", + fib_to_compute, + fib_reply, + now.elapsed().as_millis() + ); + // Let's not go too far with the fib sequence + // Otherwise the computer may take a while! + fib_to_compute += 2; + } + Bastion::stop(); + Bastion::block_until_stopped(); +} + +// Compiling bastion v0.3.5-alpha (/home/ignition/Projects/oss/bastion/src/bastion) +// Finished dev [unoptimized + debuginfo] target(s) in 1.07s +// Running `target/debug/examples/fibonacci` +// [2020-05-08T14:00:53Z INFO bastion::system] System: Initializing. +// [2020-05-08T14:00:53Z INFO bastion::system] System: Launched. +// [2020-05-08T14:00:53Z INFO bastion::system] System: Starting. +// [2020-05-08T14:00:53Z INFO bastion::system] System: Launching Supervisor(00000000-0000-0000-0000-000000000000). +// [2020-05-08T14:00:53Z INFO fibonacci] someone told me something: shhh here's a message, don't tell anyone. +// [2020-05-08T14:00:53Z INFO fibonacci] received broadcast: "Hello there :)" +// [2020-05-08T14:00:53Z INFO fibonacci] received broadcast: "Hello there :)" +// [2020-05-08T14:00:53Z INFO fibonacci] received broadcast: "Hello there :)" +// fib(35) = 9227465 - Computed in 78ms +// [2020-05-08T14:00:53Z INFO fibonacci] received broadcast: "Hello there :)" +// [2020-05-08T14:00:53Z INFO fibonacci] someone told me something: shhh here's a message, don't tell anyone. +// fib(37) = 24157817 - Computed in 196ms +// [2020-05-08T14:00:53Z INFO fibonacci] someone told me something: shhh here's a message, don't tell anyone. +// fib(39) = 63245986 - Computed in 512ms +// [2020-05-08T14:00:54Z INFO fibonacci] someone told me something: shhh here's a message, don't tell anyone. +// fib(41) = 165580141 - Computed in 1327ms +// [2020-05-08T14:00:55Z INFO bastion::system] System: Stopping. diff --git a/src/bastion/examples/hello_tokio.rs b/src/bastion/examples/hello_tokio.rs new file mode 100644 index 00000000..57d88ab6 --- /dev/null +++ b/src/bastion/examples/hello_tokio.rs @@ -0,0 +1,104 @@ +#[cfg(feature = "tokio-runtime")] +use anyhow::Result as AnyResult; +#[cfg(feature = "tokio-runtime")] +use bastion::prelude::*; +#[cfg(feature = "tokio-runtime")] +use tokio; +#[cfg(feature = "tokio-runtime")] +use tracing::{error, warn, Level}; + +/// `cargo run --features=tokio-runtime --example hello_tokio` +/// +/// We are focusing on the contents of the msg! macro here. +/// If you would like to understand how the rest works, +/// Have a look at the `hello_world.rs` example instead :) +/// +/// Log output: +/// +/// Jan 31 14:55:55.677 WARN hello_tokio: just spawned! +/// Jan 31 14:55:56.678 WARN hello_tokio: Ok let's handle a message now. +/// Jan 31 14:55:56.678 ERROR hello_tokio: just received hello, world! +/// Jan 31 14:55:56.678 WARN hello_tokio: sleeping for 2 seconds without using the bastion executor +/// Jan 31 14:55:58.680 WARN hello_tokio: and done! +/// Jan 31 14:55:58.681 WARN hello_tokio: let's sleep for 5 seconds within a blocking block +/// Jan 31 14:56:03.682 WARN hello_tokio: awaited 5 seconds +/// Jan 31 14:56:03.682 ERROR hello_tokio: waited for the blocking! to be complete! +/// Jan 31 14:56:03.682 ERROR hello_tokio: not waiting for spawn! to be complete, moving on! +/// Jan 31 14:56:03.683 WARN hello_tokio: let's sleep for 10 seconds within a spawn block +/// Jan 31 14:56:13.683 WARN hello_tokio: the spawn! is complete +/// Jan 31 14:56:15.679 WARN hello_tokio: we're done, stopping the bastion! +#[cfg(feature = "tokio-runtime")] +#[tokio::main] +async fn main() -> AnyResult<()> { + // Initialize tracing logger + // so we get nice output on the console. + let subscriber = tracing_subscriber::fmt() + .with_max_level(Level::WARN) + .finish(); + tracing::subscriber::set_global_default(subscriber).unwrap(); + + Bastion::init(); + Bastion::start(); + let workers = Bastion::children(|children| { + children.with_exec(|ctx: BastionContext| { + async move { + warn!("just spawned!"); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + warn!("Ok let's handle a message now."); + msg! { + ctx.recv().await?, + msg: &'static str => { + // Printing the incoming msg + error!("just received {}", msg); + + warn!("sleeping for 2 seconds without using the bastion executor"); + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + warn!("and done!"); + + // let's wait until a tokio powered future is complete + run!(blocking! { + warn!("let's sleep for 5 seconds within a blocking block"); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + warn!("awaited 5 seconds"); + }); + error!("waited for the blocking! to be complete!"); + + // let's spawn a tokio powered future and move on + spawn! { + warn!("let's sleep for 10 seconds within a spawn block"); + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + warn!("the spawn! is complete"); + }; + error!("not waiting for spawn! to be complete, moving on!"); + }; + _: _ => (); + } + Ok(()) + } + }) + }) + .expect("Couldn't create the children group."); + + let asker = async { + workers.elems()[0] + .tell_anonymously("hello, world!") + .expect("Couldn't send the message."); + }; + run!(asker); + + // Let's wait until the blocking! and the spawn! are complete on the child side. + run!(blocking!({ + std::thread::sleep(std::time::Duration::from_secs(20)) + })); + + warn!("we're done, asking bastion to stop!"); + // We are done, stopping the bastion! + Bastion::stop(); + warn!("bastion stopped!"); + Ok(()) +} + +#[cfg(not(feature = "tokio-runtime"))] +fn main() { + panic!("this example requires the tokio-runtime feature: `cargo run --features=tokio-runtime --example hello_tokio`") +} diff --git a/src/bastion/examples/message_handler_multiple_types.rs b/src/bastion/examples/message_handler_multiple_types.rs new file mode 100644 index 00000000..44a94136 --- /dev/null +++ b/src/bastion/examples/message_handler_multiple_types.rs @@ -0,0 +1,62 @@ +use bastion::prelude::*; +use std::fmt::Debug; +use tracing::error; + +// This example shows that it is possible to use the MessageHandler to match +// over different types of message. + +async fn child_task(ctx: BastionContext) -> Result<(), ()> { + loop { + MessageHandler::new(ctx.recv().await?) + .on_question(|n: i32, sender| { + if n == 42 { + sender.reply(101).expect("Failed to reply to sender"); + } else { + error!("Expected number `42`, found `{}`", n); + } + }) + .on_question(|s: &str, sender| { + if s == "marco" { + sender.reply("polo").expect("Failed to reply to sender"); + } else { + panic!("Expected string `marco`, found `{}`", s); + } + }) + .on_fallback(|v, addr| panic!("Wrong message from {:?}: got {:?}", addr, v)) + } +} + +async fn request<T: 'static + Debug + Send + Sync>( + child: &ChildRef, + body: T, +) -> std::io::Result<()> { + let answer = child + .ask_anonymously(body) + .expect("Couldn't perform request") + .await + .expect("Couldn't receive answer"); + + MessageHandler::new(answer) + .on_tell(|n: i32, _| assert_eq!(n, 101)) + .on_tell(|s: &str, _| assert_eq!(s, "polo")) + .on_fallback(|_, _| panic!("Unknown message")); + + Ok(()) +} + +fn main() { + env_logger::init(); + + Bastion::init(); + Bastion::start(); + + let children = + Bastion::children(|c| c.with_exec(child_task)).expect("Failed to spawn children"); + + let child = &children.elems()[0]; + + run!(request(child, 42)).unwrap(); + run!(request(child, "marco")).unwrap(); + + // run!(request(child, "foo")).unwrap(); +} diff --git a/src/bastion/examples/prime_numbers.rs b/src/bastion/examples/prime_numbers.rs index 52ef2b5f..649529cd 100644 --- a/src/bastion/examples/prime_numbers.rs +++ b/src/bastion/examples/prime_numbers.rs @@ -52,7 +52,7 @@ mod prime_number { // the closing parenthesiss means it won't reach the number. // the maximum allowed value for maybe_prime is 9999. use rand::Rng; - let mut maybe_prime = rand::thread_rng().gen_range(min_bound, max_bound); + let mut maybe_prime = rand::thread_rng().gen_range(min_bound..max_bound); loop { if is_prime(maybe_prime) { return number_or_panic(maybe_prime); @@ -72,10 +72,10 @@ mod prime_number { fn number_or_panic(number_to_return: u128) -> u128 { // Let's roll a dice if rand::random::<u8>() % 6 == 0 { - panic!(format!( + panic!( "I was about to return {} but I chose to panic instead!", number_to_return - )) + ) } number_to_return } diff --git a/src/bastion/examples/scaling_groups.rs b/src/bastion/examples/scaling_groups.rs index d320f12f..4fc1745b 100644 --- a/src/bastion/examples/scaling_groups.rs +++ b/src/bastion/examples/scaling_groups.rs @@ -26,16 +26,18 @@ fn main() { // Supervisor that tracks only the single actor with input data fn input_supervisor(supervisor: Supervisor) -> Supervisor { - supervisor.children(|children| input_group(children)) + supervisor.children(input_group) } // Supervisor that tracks the actor group with rescaling in runtime. fn auto_resize_group_supervisor(supervisor: Supervisor) -> Supervisor { - supervisor.children(|children| auto_resize_group(children)) + supervisor.children(auto_resize_group) } +#[allow(clippy::unnecessary_mut_passed)] fn input_group(children: Children) -> Children { // we would have fully chained the children builder if it wasn't for the feature flag + #[allow(unused_mut)] let mut children = children.with_redundancy(1); #[cfg(feature = "scaling")] { @@ -68,6 +70,7 @@ fn input_group(children: Children) -> Children { fn auto_resize_group(children: Children) -> Children { // we would have fully chained the children builder if it wasn't for the feature flag + #[allow(unused_mut)] let mut children = children .with_redundancy(3) // Start with 3 actors .with_heartbeat_tick(Duration::from_secs(5)); // Do heartbeat each 5 seconds diff --git a/src/bastion/examples/tcp-servers.rs b/src/bastion/examples/tcp-servers.rs index 60bc3a3b..a40d6734 100644 --- a/src/bastion/examples/tcp-servers.rs +++ b/src/bastion/examples/tcp-servers.rs @@ -3,7 +3,9 @@ use bastion::prelude::*; use futures::io; #[cfg(target_os = "windows")] use std::io::{self, Read, Write}; -use std::net::{TcpListener, TcpStream, ToSocketAddrs}; +#[cfg(not(target_os = "windows"))] +use std::net::TcpListener; +use std::net::{TcpStream, ToSocketAddrs}; use std::sync::atomic::{AtomicUsize, Ordering}; #[cfg(not(target_os = "windows"))] @@ -19,7 +21,6 @@ async fn run(addr: impl ToSocketAddrs) -> io::Result<()> { // Spawn a task that echoes messages from the client back to it. spawn(echo(stream)); } - Ok(()) } #[cfg(target_os = "windows")] @@ -81,7 +82,7 @@ fn main() { let port = TCP_SERVERS.fetch_sub(1, Ordering::SeqCst) + 2000; let addr = format!("127.0.0.1:{}", port); - run(addr); + run(addr).await.unwrap(); Ok(()) }) diff --git a/src/bastion/src/README.md b/src/bastion/src/README.md new file mode 100644 index 00000000..e1f412f3 --- /dev/null +++ b/src/bastion/src/README.md @@ -0,0 +1,72 @@ +Create a conference. + +1st Group +Staff (5) - Going to organize the event // OK + +2nd Group +Enthusiasts (50) - interested in participating to the conference (haven't registered yet) // OK + +3rd Group +Attendees (empty for now) - Participate + +Enthusiast -> Ask one of the staff members "when is the conference going to happen ?" // OK +Broadcast / Question => Answer 0 or 1 Staff members are going to reply eventually? // OK + +Staff -> Send a Leaflet to all of the enthusiasts, letting them know that they can register. // OK + +"hey conference <awesomeconference> is going to happen. will you be there?" +Broadcast / Question -> if people reply with YES => fill the 3rd group +some enthusiasts are now attendees + +Staff -> send the actual schedule and misc infos to Attendees +Broadcast / Statement (Attendees) + +An attendee sends a thank you note to one staff member (and not bother everyone) +One message / Statement (Staff) // OK + +```rust + let staff = Distributor::named("staff"); + + let enthusiasts = Distributor::named("enthusiasts"); + + let attendees = Disitributor::named("attendees"); + + // Enthusiast -> Ask the whole staff "when is the conference going to happen ?" + ask_one(Message + Clone) -> Result<impl Future<Output = Reply>, CouldNotSendError> + // await_one // await_all + // first ? means "have we been able to send the question?" + // it s in a month + let replies = staff.ask_one("when is the conference going to happen ?")?.await?; + + ask_everyone(Message + Clone) -> Result<impl Stream<Item = Reply>, CouldNotSendError> + let participants = enthusiasts.ask_everyone("here's our super nice conference, it s happening people!").await?; + + for participant in participants { + // grab the sender and add it to the attendee recipient group + } + + // send the schedule + tell_everyone(Message + Clone) -> Result<(), CouldNotSendError> + attendees.tell_everyone("hey there, conf is in a week, here s where and how it s going to happen")?; + + // send a thank you note + tell(Message) -> Result<(), CouldNotSendError> + staff.tell_one("thank's it was amazing")?; + + children + .with_redundancy(10) + .with_distributor(Distributor::named("staff")) + // We create the function to exec when each children is called + .with_exec(move |ctx: BastionContext| async move { /* ... */ }) + children + .with_redundancy(100) + .with_distributor(Distributor::named("enthusiasts")) + // We create the function to exec when each children is called + .with_exec(move |ctx: BastionContext| async move { /* ... */ }) + + children + .with_redundancy(0) + .with_distributor(Distributor::named("attendees")) + // We create the function to exec when each children is called + .with_exec(move |ctx: BastionContext| async move { /* ... */ }) +``` diff --git a/src/bastion/src/actor/actor_ref.rs b/src/bastion/src/actor/actor_ref.rs new file mode 100644 index 00000000..8b7a4de5 --- /dev/null +++ b/src/bastion/src/actor/actor_ref.rs @@ -0,0 +1,2 @@ +#[derive(Debug, Clone)] +pub struct ActorRef; diff --git a/src/bastion/src/actor/context.rs b/src/bastion/src/actor/context.rs new file mode 100644 index 00000000..baef2ed8 --- /dev/null +++ b/src/bastion/src/actor/context.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use async_channel::unbounded; + +use crate::actor::local_state::LocalState; +use crate::actor::state::ActorState; +use crate::mailbox::traits::TypedMessage; +use crate::mailbox::Mailbox; +use crate::routing::path::ActorPath; + +/// A structure that defines actor's state, mailbox with +/// messages and a local storage for user's data. +/// +/// Each actor in Bastion has an attached context which +/// helps to understand what is the type of actor has been +/// launched in the system, its path, current execution state +/// and various data that can be attached to it. +pub struct Context { + /// Path to the actor in the system + path: Arc<ActorPath>, + /// Mailbox of the actor + //mailbox: Mailbox<TypedMessage>, + /// Local storage for actor's data + local_state: LocalState, + /// Current execution state of the actor + internal_state: ActorState, +} + +impl Context { + // FIXME: Pass the correct system_rx instead of the fake one + pub(crate) fn new(path: ActorPath) -> Self { + //let (_system_tx, system_rx) = unbounded(); + // let mailbox = Mailbox::new(system_rx); + + let path = Arc::new(path); + let local_state = LocalState::new(); + let internal_state = ActorState::new(); + + Context { + path, + //mailbox, + local_state, + internal_state, + } + } +} diff --git a/src/bastion/src/actor/definition.rs b/src/bastion/src/actor/definition.rs new file mode 100644 index 00000000..455c5d83 --- /dev/null +++ b/src/bastion/src/actor/definition.rs @@ -0,0 +1,160 @@ +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use crate::actor::traits::Actor; +use crate::routing::path::{ActorPath, Scope}; + +type CustomActorNameFn = dyn Fn() -> String + Send + 'static; + +/// A structure that holds configuration of the Bastion actor. +pub struct Definition { + /// A struct that implements actor's behaviour + actor: Option<Arc<dyn Actor>>, + /// Defines a used scope for instantiating actors. + scope: Scope, + /// Defines a function used for generating unique actor names. + actor_name_fn: Option<Arc<CustomActorNameFn>>, + /// Defines how much actors must be instantiated in the beginning. + redundancy: usize, +} + +impl Definition { + /// Returns a new instance of the actor's definition. + pub fn new() -> Self { + let scope = Scope::User; + let actor_name_fn = None; + let redundancy = 1; + let actor = None; + + Definition { + actor, + scope, + actor_name_fn, + redundancy, + } + } + + /// Sets the actor to schedule. + pub fn actor<T: 'static>(mut self, actor: T) -> Self + where + T: Actor, + { + self.actor = Some(Arc::new(actor)); + self + } + + /// Overrides the default scope in which actors must be spawned + pub fn scope(mut self, scope: Scope) -> Self { + self.scope = scope; + self + } + + /// Overrides the default behaviour for generating actor names + pub fn custom_actor_name<F>(mut self, func: F) -> Self + where + F: Fn() -> String + Send + 'static, + { + self.actor_name_fn = Some(Arc::new(func)); + self + } + + /// Overrides the default values for redundancy + pub fn redundancy(mut self, redundancy: usize) -> Self { + self.redundancy = match redundancy == std::usize::MIN { + true => redundancy.saturating_add(1), + false => redundancy, + }; + + self + } + + pub(crate) fn generate_actor_path(&self) -> ActorPath { + match &self.actor_name_fn { + Some(func) => { + let custom_name = func(); + ActorPath::default() + .scope(self.scope.clone()) + .name(&custom_name) + } + None => ActorPath::default().scope(self.scope.clone()), + } + } +} + +impl Debug for Definition { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt.debug_struct("Definition") + .field("scope", &self.scope) + .finish() + } +} + +#[cfg(test)] +mod tests { + use crate::actor::definition::Definition; + use crate::routing::path::Scope; + + fn fake_actor_name() -> String { + let index = 1; + format!("Actor_{}", index) + } + + #[test] + fn test_default_definition() { + let instance = Definition::new(); + + assert_eq!(instance.scope, Scope::User); + assert_eq!(instance.actor_name_fn.is_none(), true); + assert_eq!(instance.redundancy, 1); + } + + #[test] + fn test_definition_with_custom_actor_name() { + let instance = Definition::new().custom_actor_name(fake_actor_name); + + assert_eq!(instance.scope, Scope::User); + assert_eq!(instance.actor_name_fn.is_some(), true); + assert_eq!(instance.redundancy, 1); + + let actor_path = instance.generate_actor_path(); + assert_eq!(actor_path.to_string(), "bastion://node/user/Actor_1"); + assert_eq!(actor_path.is_local(), true); + assert_eq!(actor_path.is_user_scope(), true); + } + + #[test] + fn test_definition_with_custom_actor_name_closure() { + let instance = Definition::new().custom_actor_name(move || -> String { + let index = 1; + format!("Actor_{}", index) + }); + + assert_eq!(instance.scope, Scope::User); + assert_eq!(instance.actor_name_fn.is_some(), true); + assert_eq!(instance.redundancy, 1); + + let actor_path = instance.generate_actor_path(); + assert_eq!(actor_path.to_string(), "bastion://node/user/Actor_1"); + assert_eq!(actor_path.is_local(), true); + assert_eq!(actor_path.is_user_scope(), true); + } + + #[test] + fn test_definition_with_custom_scope_and_actor_name_closure() { + let instance = + Definition::new() + .scope(Scope::Temporary) + .custom_actor_name(move || -> String { + let index = 1; + format!("Actor_{}", index) + }); + + assert_eq!(instance.scope, Scope::Temporary); + assert_eq!(instance.actor_name_fn.is_some(), true); + assert_eq!(instance.redundancy, 1); + + let actor_path = instance.generate_actor_path(); + assert_eq!(actor_path.to_string(), "bastion://node/temporary/Actor_1"); + assert_eq!(actor_path.is_temporary_scope(), true); + } +} diff --git a/src/bastion/src/actor/local_state.rs b/src/bastion/src/actor/local_state.rs new file mode 100644 index 00000000..2311521c --- /dev/null +++ b/src/bastion/src/actor/local_state.rs @@ -0,0 +1,265 @@ +/// This module contains implementation of the local state for +/// Bastion actors. Each actor hold its own data and doesn't expose +/// it to others, so that it will be possible to do updates in runtime +/// without being affected by other actors or potential data races. +use std::any::{Any, TypeId}; +use std::collections::HashMap; + +#[derive(Debug)] +/// A unified storage for actor's data and intended to use +/// only in the context of the single actor. +pub(crate) struct LocalState { + table: HashMap<TypeId, LocalDataContainer>, +} + +#[derive(Debug)] +#[repr(transparent)] +/// Transparent type for the `Box<dyn Any + Send + Sync>` type that provides +/// simpler and easier to use API to developers. +pub struct LocalDataContainer(Box<dyn Any + Send + Sync>); + +impl LocalState { + /// Returns a new instance of local state for actor. + pub(crate) fn new() -> Self { + LocalState { + table: HashMap::with_capacity(1 << 10), + } + } + + /// Inserts the given value in the table. If the value + /// exists, it will be overridden. + pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) { + let container = LocalDataContainer::new(value); + self.table.insert(TypeId::of::<T>(), container); + } + + /// Checks the given values is storing in the table. + pub fn contains<T: Send + Sync + 'static>(&self) -> bool { + self.table.contains_key(&TypeId::of::<T>()) + } + + /// Runs given closure on the immutable state + pub fn with_state<T, F, R>(&self, f: F) -> Option<R> + where + T: Send + Sync + 'static, + F: FnOnce(Option<&T>) -> Option<R>, + { + self.get_container::<T>().and_then(|e| f(e.get())) + } + + /// Runs given closure on the mutable state + pub fn with_state_mut<T, F, R>(&mut self, mut f: F) -> Option<R> + where + T: Send + Sync + 'static, + F: FnMut(Option<&mut T>) -> Option<R>, + { + self.get_container_mut::<T>().and_then(|e| f(e.get_mut())) + } + + /// Deletes the entry from the table. + pub fn remove<T: Send + Sync + 'static>(&mut self) -> bool { + self.table.remove(&TypeId::of::<T>()).is_some() + } + + /// Returns immutable data to the caller. + pub fn get<T>(&self) -> Option<&T> + where + T: Send + Sync + 'static, + { + self.get_container::<T>().and_then(|e| e.0.downcast_ref()) + } + + /// Returns mutable data to the caller. + pub fn get_mut<T>(&mut self) -> Option<&mut T> + where + T: Send + Sync + 'static, + { + self.get_container_mut::<T>() + .and_then(|e| e.0.downcast_mut()) + } + + /// Returns immutable local data container to the caller if it exists. + #[inline] + fn get_container<T: Send + Sync + 'static>(&self) -> Option<&LocalDataContainer> { + self.table.get(&TypeId::of::<T>()) + } + + /// Returns mutable local data container to the caller if it exists. + #[inline] + fn get_container_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut LocalDataContainer> { + self.table.get_mut(&TypeId::of::<T>()) + } +} + +impl LocalDataContainer { + pub(crate) fn new<T: Send + Sync + 'static>(value: T) -> Self { + LocalDataContainer(Box::new(value)) + } + + /// Returns immutable data to the caller. + fn get<T: Send + Sync + 'static>(&self) -> Option<&T> { + self.0.downcast_ref() + } + + /// Returns mutable data to the caller. + fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> { + self.0.downcast_mut() + } +} + +#[cfg(test)] +mod tests { + use crate::actor::local_state::LocalState; + + #[derive(Clone, Debug, Eq, PartialEq)] + struct Data { + counter: u64, + } + + #[test] + fn test_insert() { + let mut instance = LocalState::new(); + + instance.insert(Data { counter: 0 }); + assert_eq!(instance.contains::<Data>(), true); + } + + #[test] + fn test_insert_with_duplicated_data() { + let mut instance = LocalState::new(); + + instance.insert(Data { counter: 0 }); + assert_eq!(instance.contains::<Data>(), true); + + instance.insert(Data { counter: 1 }); + assert_eq!(instance.contains::<Data>(), true); + } + + #[test] + fn test_contains_returns_false() { + let instance = LocalState::new(); + + assert_eq!(instance.contains::<usize>(), false); + } + + #[test] + fn test_get_container() { + let mut instance = LocalState::new(); + + let expected = Data { counter: 1 }; + + instance.insert(expected.clone()); + assert_eq!(instance.contains::<Data>(), true); + + let result_get = instance.get_container::<Data>(); + assert_eq!(result_get.is_some(), true); + + let container = result_get.unwrap(); + let data = container.get::<Data>(); + assert_eq!(data.is_some(), true); + assert_eq!(data.unwrap(), &expected); + } + + #[test] + fn test_get_container_with_mutable_data() { + let mut instance = LocalState::new(); + + let mut expected = Data { counter: 1 }; + + instance.insert(expected.clone()); + assert_eq!(instance.contains::<Data>(), true); + + // Get the current snapshot of data + let mut data = instance.get_mut::<Data>(); + assert_eq!(data.is_some(), true); + assert_eq!(data, Some(&mut expected)); + + data.map(|d| { + d.counter += 1; + d + }); + + // Replace the data onto new one + let expected_update = Data { counter: 2 }; + + // Check the data was updated + let result_updated_data = instance.get::<Data>(); + assert_eq!(result_updated_data.is_some(), true); + assert_eq!(result_updated_data.unwrap(), &expected_update); + } + + #[test] + fn test_immutable_run_on_state() { + let mut instance = LocalState::new(); + + let mut expected = Data { counter: 1 }; + + instance.insert(expected.clone()); + assert_eq!(instance.contains::<Data>(), true); + + // Get the current snapshot of data + let mut data: Option<Data> = instance.with_state::<Data, _, _>(|e| { + let mut k = e.cloned(); + k.map(|mut e| { + e.counter += 1; + e + }) + }); + assert_eq!(data.is_some(), true); + + // Expected data update + let expected_update = Data { counter: 2 }; + assert_eq!(data, Some(expected_update)); + } + + #[test] + fn test_mutable_run_on_state() { + let mut instance = LocalState::new(); + + let mut expected = Data { counter: 1 }; + + instance.insert(expected.clone()); + assert_eq!(instance.contains::<Data>(), true); + + // Get the current snapshot of data + let mut data: Option<Data> = instance.with_state_mut::<Data, _, _>(|mut e| { + e.map(|mut d| { + d.counter += 1; + d + }) + .cloned() + }); + assert_eq!(data.is_some(), true); + + // Expected data update + let expected_update = Data { counter: 2 }; + assert_eq!(data, Some(expected_update)); + } + + #[test] + fn test_get_container_returns_none() { + let mut instance = LocalState::new(); + + let container = instance.get_container::<usize>(); + assert_eq!(container.is_none(), true); + } + + #[test] + fn test_remove_returns_true() { + let mut instance = LocalState::new(); + + instance.insert(Data { counter: 0 }); + assert_eq!(instance.contains::<Data>(), true); + + let is_removed = instance.remove::<Data>(); + assert_eq!(is_removed, true); + } + + #[test] + fn test_remove_returns_false() { + let mut instance = LocalState::new(); + + let is_removed = instance.remove::<usize>(); + assert_eq!(is_removed, false); + } +} diff --git a/src/bastion/src/actor/mailbox.rs b/src/bastion/src/actor/mailbox.rs new file mode 100644 index 00000000..b615a83a --- /dev/null +++ b/src/bastion/src/actor/mailbox.rs @@ -0,0 +1,266 @@ +use crate::actor::actor_ref::ActorRef; +use crate::actor::state_codes::*; +use crate::errors::*; +use crate::message::TypedMessage; +use async_channel::{unbounded, Receiver, Sender}; +use lever::sync::atomics::AtomicBox; +use std::fmt::{self, Debug, Formatter}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +/// Struct that represents a message sender. +#[derive(Clone)] +pub struct MailboxTx<T> +where + T: TypedMessage, +{ + /// Indicated the transmitter part of the actor's channel + /// which is using for passing messages. + tx: Sender<Envelope<T>>, + /// A field for checks that the message has been delivered to + /// the specific actor. + scheduled: Arc<AtomicBool>, +} + +impl<T> MailboxTx<T> +where + T: TypedMessage, +{ + /// Return a new instance of MailboxTx that indicates sender. + pub(crate) fn new(tx: Sender<Envelope<T>>) -> Self { + let scheduled = Arc::new(AtomicBool::new(false)); + MailboxTx { tx, scheduled } + } + + /// Send the message to the actor by the channel. + pub fn try_send(&self, msg: Envelope<T>) -> BastionResult<()> { + self.tx + .try_send(msg) + .map_err(|e| BError::ChanSend(e.to_string())) + } +} + +/// A struct that holds everything related to messages that can be +/// retrieved from other actors. Each actor holds two queues: one for +/// messages that come from user-defined actors, and another for +/// internal messaging that must be handled separately. +/// +/// For each used queue, mailbox always holds the latest requested message +/// by a user, to guarantee that the message won't be lost if something +/// happens wrong. +#[derive(Clone)] +pub struct Mailbox<T> +where + T: TypedMessage, +{ + /// User guardian sender + user_tx: MailboxTx<T>, + /// User guardian receiver + user_rx: Receiver<Envelope<T>>, + /// System guardian receiver + system_rx: Receiver<Envelope<T>>, + /// The current processing message, received from the + /// latest call to the user's queue + last_user_message: Option<Envelope<T>>, + /// The current processing message, received from the + /// latest call to the system's queue + last_system_message: Option<Envelope<T>>, + /// Mailbox state machine + state: Arc<AtomicBox<MailboxState>>, +} + +// TODO: Add calls with recv with timeout +impl<T> Mailbox<T> +where + T: TypedMessage, +{ + /// Creates a new mailbox for the actor. + pub(crate) fn new(system_rx: Receiver<Envelope<T>>) -> Self { + let (tx, user_rx) = unbounded(); + let user_tx = MailboxTx::new(tx); + let state = Arc::new(AtomicBox::new(MailboxState::Scheduled)); + let last_user_message = None; + let last_system_message = None; + + Mailbox { + user_tx, + user_rx, + system_rx, + last_user_message, + last_system_message, + state, + } + } + + /// Forced receive message from user queue + pub async fn recv(&mut self) -> Envelope<T> { + let message = self + .user_rx + .recv() + .await + .map_err(|e| BError::ChanRecv(e.to_string())) + .unwrap(); + + self.last_user_message = Some(message); + self.last_user_message.clone().unwrap() + } + + /// Try receiving message from user queue + pub async fn try_recv(&mut self) -> BastionResult<Envelope<T>> { + if self.last_user_message.is_some() { + return Err(BError::UnackedMessage); + } + + match self.user_rx.try_recv() { + Ok(message) => { + self.last_user_message = Some(message); + Ok(self.last_user_message.clone().unwrap()) + } + Err(e) => Err(BError::ChanRecv(e.to_string())), + } + } + + /// Forced receive message from system queue + pub async fn sys_recv(&mut self) -> Envelope<T> { + let message = self + .system_rx + .recv() + .await + .map_err(|e| BError::ChanRecv(e.to_string())) + .unwrap(); + + self.last_system_message = Some(message); + self.last_system_message.clone().unwrap() + } + + /// Try receiving message from system queue + pub async fn try_sys_recv(&mut self) -> BastionResult<Envelope<T>> { + if self.last_system_message.is_some() { + return Err(BError::UnackedMessage); + } + + match self.system_rx.try_recv() { + Ok(message) => { + self.last_system_message = Some(message); + Ok(self.last_system_message.clone().unwrap()) + } + Err(e) => Err(BError::ChanRecv(e.to_string())), + } + } + + /// Returns the last retrieved message from the user channel + pub async fn get_last_user_message(&self) -> Option<Envelope<T>> { + self.last_user_message.clone() + } + + /// Returns the last retrieved message from the system channel + pub async fn get_last_system_message(&self) -> Option<Envelope<T>> { + self.last_system_message.clone() + } + + // + // Mailbox state machine + // + // For more information about the actor's state machine + // see the actor/state_codes.rs module. + // + + pub(crate) fn set_scheduled(&self) { + self.state.replace_with(|_| MailboxState::Scheduled); + } + + pub(crate) fn is_scheduled(&self) -> bool { + *self.state.get() == MailboxState::Scheduled + } + + pub(crate) fn set_sent(&self) { + self.state.replace_with(|_| MailboxState::Sent); + } + + pub(crate) fn is_sent(&self) -> bool { + *self.state.get() == MailboxState::Sent + } + + pub(crate) fn set_awaiting(&self) { + self.state.replace_with(|_| MailboxState::Awaiting); + } + + pub(crate) fn is_awaiting(&self) -> bool { + *self.state.get() == MailboxState::Awaiting + } +} + +/// Struct that represents an incoming message in the actor's mailbox. +#[derive(Clone)] +pub struct Envelope<T> +where + T: TypedMessage, +{ + /// The sending side of a channel. In actor's world + /// represented is a message sender. Can be used + /// for acking message when it possible. + sender: Option<ActorRef>, + /// An actual data sent by the channel + message: T, + /// Message type that helps to figure out how to deliver message + /// and how to ack it after the processing. + message_type: MessageType, +} + +/// Enum that provides information what type of the message +/// being sent through the channel. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum MessageType { + /// A message type that requires sending a confirmation to the + /// sender after begin the processing stage. + Ack, + /// A message that were broadcasted (e.g. via system dispatchers). This + /// message type doesn't require to be acked from the receiver's side. + Broadcast, + /// A message was sent directly and doesn't require confirmation for the + /// delivery and being processed. + Tell, +} + +impl<T> Envelope<T> +where + T: TypedMessage, +{ + /// Create a message with the given sender and inner data. + pub fn new(sender: Option<ActorRef>, message: T, message_type: MessageType) -> Self { + Envelope { + sender, + message, + message_type, + } + } + + /// Returns a message type. Can be use for pattern matching and filtering + /// incoming message from other actors. + pub fn message_type(&self) -> MessageType { + self.message_type.clone() + } + + /// Sends a confirmation to the message sender. + pub(crate) async fn ack(&self) { + match self.message_type { + MessageType::Ack => unimplemented!(), + MessageType::Broadcast => unimplemented!(), + MessageType::Tell => unimplemented!(), + } + } +} + +impl<T> Debug for Envelope<T> +where + T: TypedMessage, +{ + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt.debug_struct("Message") + .field("message", &self.message) + .field("message_type", &self.message_type) + .finish() + } +} + +// TODO: Add tests diff --git a/src/bastion/src/actor/mod.rs b/src/bastion/src/actor/mod.rs new file mode 100644 index 00000000..8410474b --- /dev/null +++ b/src/bastion/src/actor/mod.rs @@ -0,0 +1,6 @@ +pub mod actor_ref; +pub mod context; +pub mod definition; +pub mod local_state; +pub mod state; +pub mod traits; diff --git a/src/bastion/src/actor/state.rs b/src/bastion/src/actor/state.rs new file mode 100644 index 00000000..0b6f6e61 --- /dev/null +++ b/src/bastion/src/actor/state.rs @@ -0,0 +1,274 @@ +/// Module that holds state machine implementation for the Bastion actor. +/// Not available for changes for crate users, but used internally for clean +/// actor's state transitions in the readable and the debuggable manner. +/// +/// The whole state machine of the actor can be represented by the +/// following schema: +/// ```ignore +/// +---> Stopped ----+ +/// | | +/// | | +/// Init -> Sync -> Scheduled -+---> Terminated -+---> Deinit -> Removed +/// ↑ | | | +/// | ↓ | | +/// Awaiting +---> Failed -----+ +/// | | +/// | | +/// +---> Finished ---+ +/// +/// ``` +/// The transitions between the states is called by the actor's context +/// internally and aren't available to use and override by crate users. +/// +use crossbeam::atomic::AtomicCell; + +// Actor state holder +#[derive(Debug)] +pub(crate) struct ActorState { + inner: AtomicCell<InnerState>, +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +enum InnerState { + /// The first state for actors. This state is the initial point after + /// being created or added to the Bastion node in runtime. At this stage, + /// actor isn't doing any useful job and retrieving any messages from other + /// parts of the cluster yet. + /// However, it can do some initialization steps (e.g. register itself + /// in dispatchers or adding the initial data to the local state), + /// before being available to the rest of the cluster. + Init, + /// An intermediate state that used for sychronization purposes. Useful + /// for cases when necessary to have a consensus between multiple actors. + Sync, + /// The main state in which actor can stay for indefinite amount of time. + /// During this state, actor does useful work (e.g. processing the incoming + /// messages from other actors) that doesn't require any asynchronous calls. + Scheduled, + /// Special kind of the actor's state that helps to understand that actor + /// is awaiting for other futures (e.g. I/O, network) or awaiting a response + /// from other actor in the Bastion cluster. + Awaiting, + /// Actor has been stopped by the system or a user's call. + Stopped, + /// Actor has been terminated by the system or a user's call. + Terminated, + /// Actor has been stopped because of a raised panic during the execution + /// or returned an error. + Failed, + /// Actor has completed code execution with the success. + Finished, + /// The deinitialization state for the actor. During this stage the actor + /// must unregister itself from the node, used dispatchers and any other + /// parts where was initialized in the beginning. Can contain an additional + /// user logic before being removed from the cluster. + Deinit, + /// The final state of the actor. The actor can be removed gracefully from + /// the Bastion node, without any negative impact to the cluster. + Removed, +} + +impl ActorState { + pub(crate) fn new() -> Self { + ActorState { + inner: AtomicCell::new(InnerState::Init), + } + } + + pub(crate) fn set_sync(&self) { + self.inner.store(InnerState::Sync); + } + + pub(crate) fn set_scheduled(&self) { + self.inner.store(InnerState::Scheduled); + } + + pub(crate) fn set_awaiting(&self) { + self.inner.store(InnerState::Awaiting); + } + + pub(crate) fn set_stopped(&self) { + self.inner.store(InnerState::Stopped); + } + + pub(crate) fn set_terminated(&self) { + self.inner.store(InnerState::Terminated); + } + + pub(crate) fn set_failed(&self) { + self.inner.store(InnerState::Failed); + } + + pub(crate) fn set_finished(&self) { + self.inner.store(InnerState::Finished); + } + + pub(crate) fn set_deinit(&self) { + self.inner.store(InnerState::Deinit); + } + + pub(crate) fn set_removed(&self) { + self.inner.store(InnerState::Removed); + } + + pub(crate) fn is_init(&self) -> bool { + self.inner.load() == InnerState::Init + } + + pub(crate) fn is_sync(&self) -> bool { + self.inner.load() == InnerState::Sync + } + + pub(crate) fn is_scheduled(&self) -> bool { + self.inner.load() == InnerState::Scheduled + } + + pub(crate) fn is_awaiting(&self) -> bool { + self.inner.load() == InnerState::Awaiting + } + + pub(crate) fn is_stopped(&self) -> bool { + self.inner.load() == InnerState::Stopped + } + + pub(crate) fn is_terminated(&self) -> bool { + self.inner.load() == InnerState::Terminated + } + + pub(crate) fn is_failed(&self) -> bool { + self.inner.load() == InnerState::Failed + } + + pub(crate) fn is_finished(&self) -> bool { + self.inner.load() == InnerState::Finished + } + + pub(crate) fn is_deinit(&self) -> bool { + self.inner.load() == InnerState::Deinit + } + + pub(crate) fn is_removed(&self) -> bool { + self.inner.load() == InnerState::Removed + } +} + +#[cfg(test)] +mod tests { + use crate::actor::state::ActorState; + + #[test] + fn test_happy_path() { + let state = ActorState::new(); + + assert_eq!(state.is_init(), true); + + state.set_sync(); + assert_eq!(state.is_sync(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + + state.set_finished(); + assert_eq!(state.is_finished(), true); + + state.set_deinit(); + assert_eq!(state.is_deinit(), true); + + state.set_removed(); + assert_eq!(state.is_removed(), true); + } + + #[test] + fn test_happy_path_with_awaiting_state() { + let state = ActorState::new(); + + assert_eq!(state.is_init(), true); + + state.set_sync(); + assert_eq!(state.is_sync(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + + state.set_awaiting(); + assert_eq!(state.is_awaiting(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + + state.set_finished(); + assert_eq!(state.is_finished(), true); + + state.set_deinit(); + assert_eq!(state.is_deinit(), true); + + state.set_removed(); + assert_eq!(state.is_removed(), true); + } + + #[test] + fn test_path_with_stopped_state() { + let state = ActorState::new(); + + assert_eq!(state.is_init(), true); + + state.set_sync(); + assert_eq!(state.is_sync(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + + state.set_stopped(); + assert_eq!(state.is_stopped(), true); + + state.set_deinit(); + assert_eq!(state.is_deinit(), true); + + state.set_removed(); + assert_eq!(state.is_removed(), true); + } + + #[test] + fn test_path_with_terminated_state() { + let state = ActorState::new(); + + assert_eq!(state.is_init(), true); + + state.set_sync(); + assert_eq!(state.is_sync(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + + state.set_terminated(); + assert_eq!(state.is_terminated(), true); + + state.set_deinit(); + assert_eq!(state.is_deinit(), true); + + state.set_removed(); + assert_eq!(state.is_removed(), true); + } + + #[test] + fn test_path_with_failed_state() { + let state = ActorState::new(); + + assert_eq!(state.is_init(), true); + + state.set_sync(); + assert_eq!(state.is_sync(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + + state.set_failed(); + assert_eq!(state.is_failed(), true); + + state.set_deinit(); + assert_eq!(state.is_deinit(), true); + + state.set_removed(); + assert_eq!(state.is_removed(), true); + } +} diff --git a/src/bastion/src/actor/state_codes.rs b/src/bastion/src/actor/state_codes.rs new file mode 100644 index 00000000..32bc1221 --- /dev/null +++ b/src/bastion/src/actor/state_codes.rs @@ -0,0 +1,78 @@ +#[derive(PartialEq, PartialOrd)] +/// An enum that specifies a lifecycle of the message that has +/// been sent by the actor in the system. +/// +/// The whole lifecycle of the message can be described by the +/// next schema: +/// +/// +------ Message processed ----+ +/// ↓ | +/// Scheduled -> Sent -> Awaiting ---+ +/// ↑ | +/// +-- Retry --+ +/// +pub(crate) enum MailboxState { + /// Mailbox has been scheduled + Scheduled, + /// Message has been sent to destination + Sent, + /// Ack has currently been awaited + Awaiting, +} + +#[derive(PartialEq, PartialOrd)] +/// Special kind of enum that describes possible states of +/// the actor in the system. +/// +/// The whole state machine of the actor can be represented by +/// the following schema: +///````ignore +/// +---> Stopped ----+ +/// | | +/// | | +/// Init -> Sync -> Scheduled -+---> Terminated -+---> Deinit -> Removed +/// ↑ | | | +/// | ↓ | | +/// Awaiting +---> Failed -----+ +/// | | +/// | | +/// +---> Finished ---+ +///``` +pub(crate) enum ActorState { + /// The first state for actors. This state is the initial point + /// after being created or added to the Bastion node in runtime. + /// At this stage, actor isn't doing any useful job and retrieving + /// any messages from other parts of the cluster yet. + /// However, it can do some initialization steps (e.g. register itself + /// in dispatchers or adding the initial data to the local state), + /// before being available to the rest of the cluster. + Init, + /// Remote or local state synchronization. this behaves like a half state + /// to converging consensus between multiple actor states. + Sync, + /// The main state in which actor can stay for indefinite amount of time. + /// During this state, actor doing useful work (e.g. processing the incoming + /// message from other actors) that doesn't require any asynchronous calls. + Scheduled, + /// Special kind of the scheduled state which help to understand that + /// actor is awaiting for other futures or response messages from other + /// actors in the Bastion cluster. + Awaiting, + /// Actor has been stopped by the system or a user's call. + Stopped, + /// Actor has been terminated by the system or a user's call. + Terminated, + /// Actor has stopped doing any useful work because of a raised panic + /// or user's error during the execution. + Failed, + /// Actor has completed an execution with the success. + Finished, + /// The deinitialization state for the actor. During this stage the actor + /// must unregister itself from the node, used dispatchers and any other + /// parts where was initialized in the beginning. Can contain an additional + /// user logic before being removed from the cluster. + Deinit, + /// The final state of the actor. The actor can be removed + /// gracefully from the node, because is not available anymore. + Removed, +} diff --git a/src/bastion/src/actor/traits.rs b/src/bastion/src/actor/traits.rs new file mode 100644 index 00000000..2ac440d9 --- /dev/null +++ b/src/bastion/src/actor/traits.rs @@ -0,0 +1,16 @@ +use async_trait::async_trait; + +use crate::actor::context::Context; +use crate::errors::BastionResult; + +#[async_trait] +pub trait Actor: Sync { + async fn on_init(&self, _ctx: &mut Context) {} + async fn on_sync(&self, _ctx: &mut Context) {} + async fn on_stopped(&self, _ctx: &mut Context) {} + async fn on_terminated(&self, _ctx: &mut Context) {} + async fn on_failed(&self, _ctx: &mut Context) {} + async fn on_finished(&self, _ctx: &mut Context) {} + async fn on_deinit(&self, _ctx: &mut Context) {} + async fn handler(&self, ctx: &mut Context) -> BastionResult<()>; +} diff --git a/src/bastion/src/bastion.rs b/src/bastion/src/bastion.rs index af6f231b..b32122f3 100644 --- a/src/bastion/src/bastion.rs +++ b/src/bastion/src/bastion.rs @@ -4,10 +4,10 @@ use crate::children_ref::ChildrenRef; use crate::config::Config; use crate::context::{BastionContext, BastionId}; use crate::envelope::Envelope; +use crate::global_system::SYSTEM; use crate::message::{BastionMessage, Message}; use crate::path::BastionPathElement; use crate::supervisor::{Supervisor, SupervisorRef}; -use crate::system::SYSTEM; use core::future::Future; use tracing::{debug, trace}; @@ -29,7 +29,18 @@ distributed_api! { /// ```rust /// use bastion::prelude::*; /// -/// fn main() { +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// fn run() { /// /// Creating the system's configuration... /// let config = Config::new().hide_backtraces(); /// // ...and initializing the system with it (this is required)... @@ -172,6 +183,18 @@ impl Bastion { /// # Example /// /// ```rust + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// use bastion::prelude::*; /// /// Bastion::init(); @@ -181,10 +204,8 @@ impl Bastion { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Config`]: struct.Config.html - /// [`Bastion::init_with`]: #method.init_with pub fn init() { let config = Config::default(); Bastion::init_with(config) @@ -206,6 +227,18 @@ impl Bastion { /// ```rust /// use bastion::prelude::*; /// + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// let config = Config::new() /// .show_backtraces(); /// @@ -216,10 +249,8 @@ impl Bastion { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Config`]: struct.Config.html - /// [`Bastion::init`]: #method.init pub fn init_with(config: Config) { debug!("Bastion: Initializing with config: {:?}", config); if config.backtraces().is_hide() { @@ -227,7 +258,7 @@ impl Bastion { std::panic::set_hook(Box::new(|_| ())); } - lazy_static::initialize(&SYSTEM); + let _ = &SYSTEM; } /// Creates a new [`Supervisor`], passes it through the specified @@ -248,6 +279,18 @@ impl Bastion { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// let sp_ref: SupervisorRef = Bastion::supervisor(|sp| { @@ -259,10 +302,8 @@ impl Bastion { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Supervisor`]: supervisor/struct.Supervisor.html - /// [`SupervisorRef`]: supervisor/struct.SupervisorRef.html pub fn supervisor<S>(init: S) -> Result<SupervisorRef, ()> where S: FnOnce(Supervisor) -> Supervisor, @@ -307,6 +348,18 @@ impl Bastion { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// let children_ref: ChildrenRef = Bastion::children(|children| { @@ -328,10 +381,8 @@ impl Bastion { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Children`]: children/struct.Children.html - /// [`ChildrenRef`]: children/struct.ChildrenRef.html pub fn children<C>(init: C) -> Result<ChildrenRef, ()> where C: FnOnce(Children) -> Children, @@ -357,6 +408,18 @@ impl Bastion { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// let children_ref: ChildrenRef = Bastion::spawn(|ctx: BastionContext| { @@ -369,12 +432,8 @@ impl Bastion { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Children::with_exec`]: children/struct.Children.html#method.with_exec - /// [`Bastion::children`]: #method.children - /// [`Children`]: children/struct.Children.html - /// [`ChildrenRef`]: children/struct.ChildrenRef.html pub fn spawn<I, F>(action: I) -> Result<ChildrenRef, ()> where I: Fn(BastionContext) -> F + Send + 'static, @@ -410,7 +469,18 @@ impl Bastion { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// let msg = "A message containing data."; @@ -437,7 +507,7 @@ impl Bastion { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); - /// # } + /// # } /// ``` pub fn broadcast<M: Message>(msg: M) -> Result<(), M> { debug!("Bastion: Broadcasting message: {:?}", msg); @@ -459,6 +529,18 @@ impl Bastion { /// ```rust /// use bastion::prelude::*; /// + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// Bastion::init(); /// /// // Use bastion, spawn children and supervisors... @@ -470,6 +552,7 @@ impl Bastion { /// # /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn start() { debug!("Bastion: Starting."); @@ -488,6 +571,19 @@ impl Bastion { /// ```rust /// use bastion::prelude::*; /// + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// /// Bastion::init(); /// /// // Use bastion, spawn children and supervisors... @@ -499,6 +595,7 @@ impl Bastion { /// /// Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn stop() { debug!("Bastion: Stopping."); @@ -517,6 +614,18 @@ impl Bastion { /// ```rust /// use bastion::prelude::*; /// + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// Bastion::init(); /// /// // Use bastion, spawn children and supervisors... @@ -527,6 +636,7 @@ impl Bastion { /// /// Bastion::kill(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn kill() { debug!("Bastion: Killing."); @@ -547,12 +657,24 @@ impl Bastion { } /// Blocks the current thread until the system is stopped - /// (either by calling [`Bastion::stop()`] or + /// (either by calling [`Bastion::stop`] or /// [`Bastion::kill`]). /// /// # Example /// /// ```rust + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// use bastion::prelude::*; /// /// Bastion::init(); @@ -567,10 +689,8 @@ impl Bastion { /// Bastion::block_until_stopped(); /// // The system is now stopped. A child might have /// // stopped or killed it... + /// # } /// ``` - /// - /// [`Bastion::stop()`]: #method.stop - /// [`Bastion::kill()`]: #method.kill pub fn block_until_stopped() { debug!("Bastion: Blocking until system is stopped."); SYSTEM.wait_until_stopped(); diff --git a/src/bastion/src/broadcast.rs b/src/bastion/src/broadcast.rs index 893eccb4..fc73cba2 100644 --- a/src/bastion/src/broadcast.rs +++ b/src/bastion/src/broadcast.rs @@ -1,10 +1,10 @@ use crate::children_ref::ChildrenRef; use crate::context::BastionId; use crate::envelope::Envelope; +use crate::global_system::SYSTEM; use crate::message::BastionMessage; use crate::path::{BastionPath, BastionPathElement}; use crate::supervisor::SupervisorRef; -use crate::system::SYSTEM; use futures::channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; use futures::prelude::*; use fxhash::FxHashMap; diff --git a/src/bastion/src/callbacks.rs b/src/bastion/src/callbacks.rs index 0c271b96..293c7c54 100644 --- a/src/bastion/src/callbacks.rs +++ b/src/bastion/src/callbacks.rs @@ -8,6 +8,7 @@ pub(crate) enum CallbackType { AfterStop, BeforeRestart, BeforeStart, + AfterStart, } #[derive(Default, Clone)] @@ -19,6 +20,18 @@ pub(crate) enum CallbackType { /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -41,12 +54,14 @@ pub(crate) enum CallbackType { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` /// -/// [`Supervisor`]: supervisor/struct.Supervisor.html -/// [`Children`]: children/struct.Children.html +/// [`Supervisor`]: crate::supervisor::Supervisor +/// [`Children`]: crate::children::Children pub struct Callbacks { before_start: Option<Arc<dyn Fn() + Send + Sync>>, + after_start: Option<Arc<dyn Fn() + Send + Sync>>, before_restart: Option<Arc<dyn Fn() + Send + Sync>>, after_restart: Option<Arc<dyn Fn() + Send + Sync>>, after_stop: Option<Arc<dyn Fn() + Send + Sync>>, @@ -61,6 +76,18 @@ impl Callbacks { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -83,10 +110,11 @@ impl Callbacks { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`Supervisor::with_callbacks`]: supervisor/struct.Supervisor.html#method.with_callbacks - /// [`Children::with_callbacks`]: children/struct.Children.html#method.with_callbacks + /// [`Supervisor::with_callbacks`]: crate::supervisor::Supervisor::with_callbacks + /// [`Children::with_callbacks`]: crate::children::Children::with_callbacks pub fn new() -> Self { Callbacks::default() } @@ -107,6 +135,18 @@ impl Callbacks { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|supervisor| { @@ -138,11 +178,12 @@ impl Callbacks { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`Supervisor`]: supervisor/struct.Supervisor.html - /// [`Children`]: children/struct.Children.html - /// [`with_after_restart`]: #method.with_after_start + /// [`Supervisor`]: crate::supervisor::Supervisor + /// [`Children`]: crate::children::Children + /// [`with_after_restart`]: Self::with_after_restart pub fn with_before_start<C>(mut self, before_start: C) -> Self where C: Fn() + Send + Sync + 'static, @@ -152,6 +193,74 @@ impl Callbacks { self } + /// Sets the method that will get called right after the [`Supervisor`] + /// or [`Children`] is launched. + /// This method will be called after the child has subscribed to its distributors and dispatchers. + /// + /// Once the callback has run, the child has caught up it's message backlog, + /// and is waiting for new messages to process. + /// + /// # Example + /// + /// ```rust + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # + /// # Bastion::supervisor(|supervisor| { + /// supervisor.children(|children| { + /// let callbacks = Callbacks::new() + /// .with_after_start(|| println!("Children group ready to process messages.")); + /// + /// children + /// .with_exec(|ctx| { + /// // -- Children group started. + /// // with_after_start called + /// async move { + /// // ... + /// + /// // This will stop the children group... + /// Ok(()) + /// // Note that because the children group stopped by itself, + /// // if its supervisor restarts it, its `before_start` callback + /// // will get called and not `after_restart`. + /// } + /// // -- Children group stopped. + /// }) + /// .with_callbacks(callbacks) + /// }) + /// # }).unwrap(); + /// # + /// # Bastion::start(); + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + /// + /// [`Supervisor`]: crate::supervisor::Supervisor + /// [`Children`]: crate::children::Children + /// [`with_after_restart`]: Self::with_after_restart + pub fn with_after_start<C>(mut self, after_start: C) -> Self + where + C: Fn() + Send + Sync + 'static, + { + let after_start = Arc::new(after_start); + self.after_start = Some(after_start); + self + } + /// Sets the method that will get called before the [`Supervisor`] /// or [`Children`] is reset if: /// - the supervisor of the supervised element using this callback @@ -166,6 +275,18 @@ impl Callbacks { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|supervisor| { @@ -199,11 +320,12 @@ impl Callbacks { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`Supervisor`]: supervisor/struct.Supervisor.html - /// [`Children`]: children/struct.Children.html - /// [`with_after_stop`]: #method.with_after_stop + /// [`Supervisor`]: crate::supervisor::Supervisor + /// [`Children`]: crate::children::Children + /// [`with_after_stop`]: Self::with_after_stop pub fn with_before_restart<C>(mut self, before_restart: C) -> Self where C: Fn() + Send + Sync + 'static, @@ -227,6 +349,18 @@ impl Callbacks { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|supervisor| { @@ -260,11 +394,12 @@ impl Callbacks { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`Supervisor`]: supervisor/struct.Supervisor.html - /// [`Children`]: children/struct.Children.html - /// [`with_before_start`]: #method.with_before_start + /// [`Supervisor`]: crate::supervisor::Supervisor + /// [`Children`]: crate::children::Children + /// [`with_before_start`]: Self::method.with_before_start pub fn with_after_restart<C>(mut self, after_restart: C) -> Self where C: Fn() + Send + Sync + 'static, @@ -292,6 +427,18 @@ impl Callbacks { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|supervisor| { @@ -323,11 +470,12 @@ impl Callbacks { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`Supervisor`]: supervisor/struct.Supervisor.html - /// [`Children`]: children/struct.Children.html - /// [`with_before_restart`]: #method.with_before_restart + /// [`Supervisor`]: crate::supervisor::Supervisor + /// [`Children`]: crate::children::Children + /// [`with_before_restart`]: Self::with_before_restart pub fn with_after_stop<C>(mut self, after_stop: C) -> Self where C: Fn() + Send + Sync + 'static, @@ -350,7 +498,7 @@ impl Callbacks { /// assert!(callbacks.has_before_start()); /// ``` /// - /// [`with_before_start`]: #method.with_before_start + /// [`with_before_start`]: Self::with_before_start pub fn has_before_start(&self) -> bool { self.before_start.is_some() } @@ -368,7 +516,7 @@ impl Callbacks { /// assert!(callbacks.has_before_restart()); /// ``` /// - /// [`with_before_restart`]: #method.with_before_restart + /// [`with_before_restart`]: Self::with_before_restart pub fn has_before_restart(&self) -> bool { self.before_restart.is_some() } @@ -386,7 +534,7 @@ impl Callbacks { /// assert!(callbacks.has_after_restart()); /// ``` /// - /// [`with_after_restart`]: #method.with_after_restart + /// [`with_after_restart`]: Self::with_after_restart pub fn has_after_restart(&self) -> bool { self.after_restart.is_some() } @@ -404,7 +552,7 @@ impl Callbacks { /// assert!(callbacks.has_after_stop()); /// ``` /// - /// [`with_after_stop`]: #method.with_after_stop + /// [`with_after_stop`]: Self::with_after_stop pub fn has_after_stop(&self) -> bool { self.after_stop.is_some() } @@ -415,6 +563,12 @@ impl Callbacks { } } + pub(crate) fn after_start(&self) { + if let Some(after_start) = &self.after_start { + after_start() + } + } + pub(crate) fn before_restart(&self) { if let Some(before_restart) = &self.before_restart { before_restart() diff --git a/src/bastion/src/child.rs b/src/bastion/src/child.rs index b4eca127..ac380ca7 100644 --- a/src/bastion/src/child.rs +++ b/src/bastion/src/child.rs @@ -5,12 +5,12 @@ use crate::callbacks::{CallbackType, Callbacks}; use crate::child_ref::ChildRef; use crate::context::{BastionContext, BastionId, ContextState}; use crate::envelope::Envelope; +use crate::global_system::SYSTEM; use crate::message::BastionMessage; #[cfg(feature = "scaling")] use crate::resizer::ActorGroupStats; -use crate::system::SYSTEM; use anyhow::Result as AnyResult; -use async_mutex::Mutex; + use bastion_executor::pool; use futures::pending; use futures::poll; @@ -35,11 +35,11 @@ pub(crate) struct Child { callbacks: Callbacks, // The future that this child is executing. exec: Exec, - // A lock behind which is the child's context state. + // The child's context state. // This is used to store the messages that were received // for the child's associated future to be able to // retrieve them. - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, // Messages that were received before the child was // started. Those will be "replayed" once a start message // is received. @@ -71,7 +71,7 @@ impl Child { exec: Exec, callbacks: Callbacks, bcast: Broadcast, - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, child_ref: ChildRef, ) -> Self { debug!("Child({}): Initializing.", bcast.id()); @@ -125,12 +125,14 @@ impl Child { fn stopped(&mut self) { debug!("Child({}): Stopped.", self.id()); self.remove_from_dispatchers(); + let _ = self.remove_from_distributors(); self.bcast.stopped(); } fn faulted(&mut self) { debug!("Child({}): Faulted.", self.id()); self.remove_from_dispatchers(); + let _ = self.remove_from_distributors(); let parent = self.bcast.parent().clone().into_children().unwrap(); let path = self.bcast.path().clone(); @@ -200,9 +202,7 @@ impl Child { sign, } => { debug!("Child({}): Received a message: {:?}", self.id(), msg); - let state = self.state.clone(); - let mut guard = state.lock().await; - guard.push_message(msg, sign); + self.state.push_message(msg, sign); } Envelope { msg: BastionMessage::RestartRequired { .. }, @@ -283,22 +283,22 @@ impl Child { CallbackType::BeforeRestart => self.callbacks.before_restart(), CallbackType::AfterRestart => self.callbacks.after_restart(), CallbackType::AfterStop => self.callbacks.after_stop(), + CallbackType::AfterStart => self.callbacks.after_start(), } } #[cfg(feature = "scaling")] async fn update_stats(&mut self) { - let guard = self.state.lock().await; - let context_state = guard.as_ref(); - let storage = guard.stats(); + let mailbox_size = self.state.mailbox_size(); + let storage = self.state.stats(); let mut stats = ActorGroupStats::load(storage.clone()); - stats.update_average_mailbox_size(context_state.mailbox_size()); + stats.update_average_mailbox_size(mailbox_size); stats.store(storage); - let actor_stats_table = guard.actor_stats(); + let actor_stats_table = self.state.actor_stats(); actor_stats_table - .insert(self.bcast.id().clone(), context_state.mailbox_size()) + .insert(self.bcast.id().clone(), mailbox_size) .ok(); } @@ -308,6 +308,12 @@ impl Child { error!("couldn't add actor to the registry: {}", e); return; }; + if let Err(e) = self.register_to_distributors() { + error!("couldn't add actor to the distributors: {}", e); + return; + }; + + self.callbacks.after_start(); loop { #[cfg(feature = "scaling")] @@ -403,6 +409,35 @@ impl Child { Ok(()) } + /// Adds the actor into each distributor declared in the parent node. + fn register_to_distributors(&self) -> AnyResult<()> { + if let Some(parent) = self.bcast.parent().clone().into_children() { + let child_ref = self.child_ref.clone(); + let distributors = parent.distributors(); + + let global_dispatcher = SYSTEM.dispatcher(); + distributors + .iter() + .map(|&distributor| { + global_dispatcher.register_recipient(&distributor, child_ref.clone()) + }) + .collect::<AnyResult<Vec<_>>>()?; + } + Ok(()) + } + + /// Cleanup the actor's record from each declared distributor. + fn remove_from_distributors(&self) -> AnyResult<()> { + if let Some(parent) = self.bcast.parent().clone().into_children() { + let child_ref = self.child_ref.clone(); + let distributors = parent.distributors(); + + let global_dispatcher = SYSTEM.dispatcher(); + global_dispatcher.remove_recipient(distributors, child_ref)?; + } + Ok(()) + } + /// Cleanup the actor's record from each declared dispatcher. fn remove_from_dispatchers(&self) { if let Some(parent) = self.bcast.parent().clone().into_children() { @@ -416,9 +451,10 @@ impl Child { #[cfg(feature = "scaling")] async fn cleanup_actors_stats(&mut self) { - let guard = self.state.lock().await; - let actor_stats_table = guard.actor_stats(); - actor_stats_table.remove(&self.bcast.id().clone()).ok(); + self.state + .actor_stats() + .remove(&self.bcast.id().clone()) + .ok(); } } diff --git a/src/bastion/src/child_ref.rs b/src/bastion/src/child_ref.rs index 139c830c..7cd90e7e 100644 --- a/src/bastion/src/child_ref.rs +++ b/src/bastion/src/child_ref.rs @@ -1,10 +1,10 @@ //! //! Allows users to communicate with Child through the mailboxes. -use crate::broadcast::Sender; use crate::context::BastionId; use crate::envelope::{Envelope, RefAddr}; use crate::message::{Answer, BastionMessage, Message}; use crate::path::BastionPath; +use crate::{broadcast::Sender, prelude::SendError}; use std::cmp::{Eq, PartialEq}; use std::fmt::Debug; use std::hash::{Hash, Hasher}; @@ -67,6 +67,18 @@ impl ChildRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -82,6 +94,7 @@ impl ChildRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn id(&self) -> &BastionId { &self.id @@ -98,6 +111,18 @@ impl ChildRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -114,6 +139,7 @@ impl ChildRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn is_public(&self) -> bool { self.is_public @@ -135,7 +161,18 @@ impl ChildRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// // The message that will be "told"... /// const TELL_MSG: &'static str = "A message containing data (tell)."; @@ -178,6 +215,75 @@ impl ChildRef { self.send(env).map_err(|env| env.into_msg().unwrap()) } + /// Try to send a message to the child this `ChildRef` is referencing. + /// This message is intended to be used outside of Bastion context when + /// there is no way for receiver to identify message sender + /// + /// This method returns `()` if it succeeded, or a `SendError`(../child_ref/enum.SendError.html) + /// otherwise. + /// + /// # Argument + /// + /// * `msg` - The message to send. + /// + /// # Example + /// + /// ```rust + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// // The message that will be "told"... + /// const TELL_MSG: &'static str = "A message containing data (tell)."; + /// + /// # let children_ref = + /// // Create a new child... + /// Bastion::children(|children| { + /// children.with_exec(|ctx: BastionContext| { + /// async move { + /// // ...which will receive the message "told"... + /// msg! { ctx.recv().await?, + /// msg: &'static str => { + /// assert_eq!(msg, TELL_MSG); + /// // Handle the message... + /// }; + /// // This won't happen because this example + /// // only "tells" a `&'static str`... + /// _: _ => (); + /// } + /// + /// Ok(()) + /// } + /// }) + /// }).expect("Couldn't create the children group."); + /// + /// # let child_ref = &children_ref.elems()[0]; + /// // Later, the message is "told" to the child... + /// child_ref.try_tell_anonymously(TELL_MSG).expect("Couldn't send the message."); + /// # + /// # Bastion::start(); + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn try_tell_anonymously<M: Message>(&self, msg: M) -> Result<(), SendError> { + debug!("ChildRef({}): Try Telling message: {:?}", self.id(), msg); + let msg = BastionMessage::tell(msg); + let env = Envelope::from_dead_letters(msg); + self.try_send(env).map_err(Into::into) + } + /// Sends a message to the child this `ChildRef` is referencing, /// allowing it to answer. /// This message is intended to be used outside of Bastion context when @@ -195,7 +301,18 @@ impl ChildRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// // The message that will be "asked"... /// const ASK_MSG: &'static str = "A message containing data (ask)."; @@ -254,11 +371,9 @@ impl ChildRef { /// # Bastion::block_until_stopped(); /// # } /// ``` - /// - /// [`Answer`]: message/struct.Answer.html pub fn ask_anonymously<M: Message>(&self, msg: M) -> Result<Answer, M> { debug!("ChildRef({}): Asking message: {:?}", self.id(), msg); - let (msg, answer) = BastionMessage::ask(msg); + let (msg, answer) = BastionMessage::ask(msg, self.addr()); let env = Envelope::from_dead_letters(msg); // FIXME: panics? self.send(env).map_err(|env| env.into_msg().unwrap())?; @@ -266,6 +381,100 @@ impl ChildRef { Ok(answer) } + /// Try to send a message to the child this `ChildRef` is referencing, + /// allowing it to answer. + /// This message is intended to be used outside of Bastion context when + /// there is no way for receiver to identify message sender + /// + /// This method returns [`Answer`](../message/struct.Answer.html) if it succeeded, or a `SendError`(../child_ref/enum.SendError.html) + /// otherwise. + /// + /// # Argument + /// + /// * `msg` - The message to send. + /// + /// # Example + /// + /// ``` + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// // The message that will be "asked"... + /// const ASK_MSG: &'static str = "A message containing data (ask)."; + /// // The message the will be "answered"... + /// const ANSWER_MSG: &'static str = "A message containing data (answer)."; + /// + /// # let children_ref = + /// // Create a new child... + /// Bastion::children(|children| { + /// children.with_exec(|ctx: BastionContext| { + /// async move { + /// // ...which will receive the message asked... + /// msg! { ctx.recv().await?, + /// msg: &'static str =!> { + /// assert_eq!(msg, ASK_MSG); + /// // Handle the message... + /// + /// // ...and eventually answer to it... + /// answer!(ctx, ANSWER_MSG); + /// }; + /// // This won't happen because this example + /// // only "asks" a `&'static str`... + /// _: _ => (); + /// } + /// + /// Ok(()) + /// } + /// }) + /// }).expect("Couldn't create the children group."); + /// + /// # Bastion::children(|children| { + /// # children.with_exec(move |ctx: BastionContext| { + /// # let child_ref = children_ref.elems()[0].clone(); + /// # async move { + /// // Later, the message is "asked" to the child... + /// let answer: Answer = child_ref.try_ask_anonymously(ASK_MSG).expect("Couldn't send the message."); + /// + /// // ...and the child's answer is received... + /// msg! { answer.await.expect("Couldn't receive the answer."), + /// msg: &'static str => { + /// assert_eq!(msg, ANSWER_MSG); + /// // Handle the answer... + /// }; + /// // This won't happen because this example + /// // only answers a `&'static str`... + /// _: _ => (); + /// } + /// # + /// # Ok(()) + /// # } + /// # }) + /// # }).unwrap(); + /// # + /// # Bastion::start(); + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn try_ask_anonymously<M: Message>(&self, msg: M) -> Result<Answer, SendError> { + debug!("ChildRef({}): Try Asking message: {:?}", self.id(), msg); + let (msg, answer) = BastionMessage::ask(msg, self.addr()); + let env = Envelope::from_dead_letters(msg); + self.try_send(env).map(|_| answer) + } + /// Sends a message to the child this `ChildRef` is referencing /// to tell it to stop its execution. /// @@ -277,6 +486,18 @@ impl ChildRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # let children_ref = /// # Bastion::children(|children| { @@ -304,6 +525,7 @@ impl ChildRef { /// # /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn stop(&self) -> Result<(), ()> { debug!("ChildRef({}): Stopping.", self.id); @@ -323,6 +545,18 @@ impl ChildRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -332,6 +566,7 @@ impl ChildRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn kill(&self) -> Result<(), ()> { debug!("ChildRef({}): Killing.", self.id()); @@ -352,6 +587,11 @@ impl ChildRef { .map_err(|err| err.into_inner()) } + pub(crate) fn try_send(&self, env: Envelope) -> Result<(), SendError> { + trace!("ChildRef({}): Sending message: {:?}", self.id(), env); + self.sender.unbounded_send(env).map_err(Into::into) + } + pub(crate) fn sender(&self) -> &Sender { &self.sender } @@ -361,7 +601,7 @@ impl ChildRef { &self.path } - /// Return the [`name`] of the child + /// Return the `name` of the child pub fn name(&self) -> &str { &self.name } diff --git a/src/bastion/src/children.rs b/src/bastion/src/children.rs index 65d62372..84c8d412 100644 --- a/src/bastion/src/children.rs +++ b/src/bastion/src/children.rs @@ -1,6 +1,5 @@ //! //! Children are a group of child supervised under a supervisor -use crate::broadcast::{Broadcast, Parent, Sender}; use crate::callbacks::{CallbackType, Callbacks}; use crate::child::{Child, Init}; use crate::child_ref::ChildRef; @@ -8,13 +7,17 @@ use crate::children_ref::ChildrenRef; use crate::context::{BastionContext, BastionId, ContextState}; use crate::dispatcher::Dispatcher; use crate::envelope::Envelope; +use crate::global_system::SYSTEM; use crate::message::BastionMessage; use crate::path::BastionPathElement; #[cfg(feature = "scaling")] use crate::resizer::{ActorGroupStats, OptimalSizeExploringResizer, ScalingRule}; -use crate::system::SYSTEM; +use crate::{ + broadcast::{Broadcast, Parent, Sender}, + distributor::Distributor, +}; use anyhow::Result as AnyResult; -use async_mutex::Mutex; + use bastion_executor::pool; use futures::pending; use futures::poll; @@ -49,6 +52,18 @@ use tracing::{debug, trace, warn}; /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// let children_ref: ChildrenRef = Bastion::children(|children| { @@ -70,11 +85,12 @@ use tracing::{debug, trace, warn}; /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` /// -/// [`with_redundancy`]: #method.with_redundancy -/// [`with_exec`]: #method.with_exec -/// [`SupervisionStrategy`]: supervisor/enum.SupervisionStrategy.html +/// [`with_redundancy`]: Self::with_redundancy +/// [`with_exec`]: Self::with_exec +/// [`SupervisionStrategy`]: crate::supervisor::SupervisionStrategy pub struct Children { bcast: Broadcast, // The currently launched elements of the group. @@ -93,6 +109,7 @@ pub struct Children { started: bool, // List of dispatchers attached to each actor in the group. dispatchers: Vec<Arc<Box<Dispatcher>>>, + distributors: Vec<Distributor>, // The name of children name: Option<String>, #[cfg(feature = "scaling")] @@ -118,6 +135,7 @@ impl Children { let pre_start_msgs = Vec::new(); let started = false; let dispatchers = Vec::new(); + let distributors = Vec::new(); let name = None; #[cfg(feature = "scaling")] let resizer = Box::new(OptimalSizeExploringResizer::default()); @@ -133,6 +151,7 @@ impl Children { pre_start_msgs, started, dispatchers, + distributors, name, #[cfg(feature = "scaling")] resizer, @@ -157,6 +176,18 @@ impl Children { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -168,6 +199,7 @@ impl Children { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn id(&self) -> &BastionId { self.bcast.id() @@ -214,7 +246,9 @@ impl Children { .map(|dispatcher| dispatcher.dispatcher_type()) .collect(); - ChildrenRef::new(id, sender, path, children, dispatchers) + let distributors = self.distributors.clone(); + + ChildrenRef::new(id, sender, path, children, dispatchers, distributors) } /// Sets the name of this children group. @@ -244,6 +278,18 @@ impl Children { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -263,6 +309,7 @@ impl Children { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn with_exec<I, F>(mut self, init: I) -> Self where @@ -290,6 +337,18 @@ impl Children { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -300,9 +359,10 @@ impl Children { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`with_exec`]: #method.with_exec + /// [`with_exec`]: Self::with_exec pub fn with_redundancy(mut self, redundancy: usize) -> Self { trace!( "Children({}): Setting redundancy: {}", @@ -314,6 +374,10 @@ impl Children { } else { self.redundancy = redundancy; } + #[cfg(feature = "scaling")] + { + self.resizer.set_lower_bound(self.redundancy as u64); + } self } @@ -324,7 +388,7 @@ impl Children { /// /// # Arguments /// - /// * `redundancy` - An instance of struct that implements the + /// * `dispatcher` - An instance of struct that implements the /// [`DispatcherHandler`] trait. /// /// # Example @@ -332,6 +396,18 @@ impl Children { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -344,13 +420,60 @@ impl Children { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// [`DispatcherHandler`]: ../dispatcher/trait.DispatcherHandler.html + /// [`DispatcherHandler`]: crate::dispatcher::DispatcherHandler pub fn with_dispatcher(mut self, dispatcher: Dispatcher) -> Self { self.dispatchers.push(Arc::new(Box::new(dispatcher))); self } + /// Appends a distributor to the children. + /// + /// By default supervised elements aren't added to any distributor. + /// + /// # Arguments + /// + /// * `distributor` - An instance of struct that implements the + /// [`RecipientHandler`] trait. + /// + /// # Example + /// + /// ```rust + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # + /// Bastion::children(|children| { + /// children + /// .with_distributor(Distributor::named("my distributor")) + /// }).expect("Couldn't create the children group."); + /// # + /// # Bastion::start(); + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + /// [`RecipientHandler`]: crate::dispatcher::RecipientHandler + pub fn with_distributor(mut self, distributor: Distributor) -> Self { + // Try to register the distributor as soon as we're aware of it + let _ = SYSTEM.dispatcher().register_distributor(&distributor); + self.distributors.push(distributor); + self + } + #[cfg(feature = "scaling")] /// Sets a custom resizer for the Children. /// @@ -365,11 +488,23 @@ impl Children { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { /// children + /// .with_redundancy(1) /// .with_resizer( /// OptimalSizeExploringResizer::default() /// .with_lower_bound(10) @@ -382,8 +517,8 @@ impl Children { /// # Bastion::block_until_stopped(); /// # } /// ``` - /// [`Resizer`]: ../resizer/struct.Resizer.html pub fn with_resizer(mut self, resizer: OptimalSizeExploringResizer) -> Self { + self.redundancy = resizer.lower_bound() as usize; self.resizer = Box::new(resizer); self } @@ -404,6 +539,18 @@ impl Children { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -426,9 +573,8 @@ impl Children { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Callbacks`]: struct.Callbacks.html pub fn with_callbacks(mut self, callbacks: Callbacks) -> Self { trace!( "Children({}): Setting callbacks: {:?}", @@ -453,6 +599,18 @@ impl Children { /// # use bastion::prelude::*; /// # use std::time::Duration; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -471,8 +629,8 @@ impl Children { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// [`std::time::Duration`]: https://doc.rust-lang.org/nightly/core/time/struct.Duration.html pub fn with_heartbeat_tick(mut self, interval: Duration) -> Self { trace!( "Children({}): Set heartbeat tick to {:?}", @@ -543,6 +701,9 @@ impl Children { if let Err(e) = self.remove_dispatchers() { warn!("couldn't remove all dispatchers from the registry: {}", e); }; + if let Err(e) = self.remove_distributors() { + warn!("couldn't remove all distributors from the registry: {}", e); + }; self.bcast.stopped(); } @@ -551,6 +712,9 @@ impl Children { if let Err(e) = self.remove_dispatchers() { warn!("couldn't remove all dispatchers from the registry: {}", e); }; + if let Err(e) = self.remove_distributors() { + warn!("couldn't remove all distributors from the registry: {}", e); + }; self.bcast.faulted(); } @@ -604,7 +768,7 @@ impl Children { } } - fn restart_child(&mut self, old_id: &BastionId, old_state: Arc<Mutex<Pin<Box<ContextState>>>>) { + fn restart_child(&mut self, old_id: &BastionId, old_state: Arc<Pin<Box<ContextState>>>) { let parent = Parent::children(self.as_ref()); let bcast = Broadcast::new(parent, BastionPathElement::Child(old_id.clone())); @@ -616,14 +780,12 @@ impl Children { let children = self.as_ref(); let supervisor = self.bcast.parent().clone().into_supervisor(); - let state = Arc::new(Mutex::new(Box::pin(ContextState::new()))); - let ctx = BastionContext::new( id.clone(), child_ref.clone(), children, supervisor, - state.clone(), + old_state.clone(), ); let exec = (self.init.0)(ctx); @@ -643,6 +805,7 @@ impl Children { debug!("Children({}): Restarting Child({}).", self.id(), bcast.id()); let callbacks = self.callbacks.clone(); + let state = Arc::new(Box::pin(ContextState::new())); let child = Child::new(exec, callbacks, bcast, state, child_ref); debug!( "Children({}): Launching faulted Child({}).", @@ -890,7 +1053,7 @@ impl Children { #[cfg(feature = "scaling")] self.init_data_for_scaling(&mut state); - let state = Arc::new(Mutex::new(Box::pin(state))); + let state = Arc::new(Box::pin(state)); let ctx = BastionContext::new( id.clone(), @@ -939,7 +1102,7 @@ impl Children { let children = self.as_ref(); let supervisor = self.bcast.parent().clone().into_supervisor(); - let state = Arc::new(Mutex::new(Box::pin(ContextState::new()))); + let state = Arc::new(Box::pin(ContextState::new())); let ctx = BastionContext::new(id, child_ref.clone(), children, supervisor, state.clone()); let init = self.get_heartbeat_fut(); @@ -997,4 +1160,24 @@ impl Children { } Ok(()) } + + /// Registers all declared local distributors in the global dispatcher. + pub(crate) fn register_distributors(&self) -> AnyResult<()> { + let global_dispatcher = SYSTEM.dispatcher(); + + for distributor in self.distributors.iter() { + global_dispatcher.register_distributor(distributor)?; + } + Ok(()) + } + + /// Removes all declared local distributors from the global dispatcher. + pub(crate) fn remove_distributors(&self) -> AnyResult<()> { + let global_dispatcher = SYSTEM.dispatcher(); + + for distributor in self.distributors.iter() { + global_dispatcher.remove_distributor(distributor)?; + } + Ok(()) + } } diff --git a/src/bastion/src/children_ref.rs b/src/bastion/src/children_ref.rs index 44313d61..2cd2063b 100644 --- a/src/bastion/src/children_ref.rs +++ b/src/bastion/src/children_ref.rs @@ -1,13 +1,13 @@ //! //! Allows users to communicate with children through the mailboxes. use crate::broadcast::Sender; -use crate::child_ref::ChildRef; use crate::context::BastionId; use crate::dispatcher::DispatcherType; use crate::envelope::Envelope; +use crate::global_system::SYSTEM; use crate::message::{BastionMessage, Message}; use crate::path::BastionPath; -use crate::system::SYSTEM; +use crate::{child_ref::ChildRef, distributor::Distributor}; use std::cmp::{Eq, PartialEq}; use std::fmt::Debug; use std::sync::Arc; @@ -22,6 +22,7 @@ pub struct ChildrenRef { path: Arc<BastionPath>, children: Vec<ChildRef>, dispatchers: Vec<DispatcherType>, + distributors: Vec<Distributor>, } impl ChildrenRef { @@ -31,6 +32,7 @@ impl ChildrenRef { path: Arc<BastionPath>, children: Vec<ChildRef>, dispatchers: Vec<DispatcherType>, + distributors: Vec<Distributor>, ) -> Self { ChildrenRef { id, @@ -38,6 +40,7 @@ impl ChildrenRef { path, children, dispatchers, + distributors, } } @@ -52,6 +55,18 @@ impl ChildrenRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// let children_ref = Bastion::children(|children| { @@ -64,6 +79,7 @@ impl ChildrenRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn id(&self) -> &BastionId { &self.id @@ -77,6 +93,18 @@ impl ChildrenRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -85,13 +113,46 @@ impl ChildrenRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`ChildRef`]: children/struct.ChildRef.html pub fn dispatchers(&self) -> &Vec<DispatcherType> { &self.dispatchers } + /// Returns a list of distributors that can be used for + /// communication with other actors in the same group(s). + /// + /// # Example + /// + /// ```rust + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # + /// # let children_ref = Bastion::children(|children| children).unwrap(); + /// let distributors = children_ref.distributors(); + /// # + /// # Bastion::start(); + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn distributors(&self) -> &Vec<Distributor> { + &self.distributors + } + /// Returns a list of [`ChildRef`] referencing the elements /// of the children group this `ChildrenRef` is referencing. /// @@ -100,6 +161,18 @@ impl ChildrenRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -108,9 +181,8 @@ impl ChildrenRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`ChildRef`]: children/struct.ChildRef.html pub fn elems(&self) -> &[ChildRef] { &self.children } @@ -135,7 +207,18 @@ impl ChildrenRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -166,7 +249,7 @@ impl ChildrenRef { /// # } /// ``` /// - /// [`elems`]: #method.elems + /// [`elems`]: Self::elems pub fn broadcast<M: Message>(&self, msg: M) -> Result<(), M> { debug!( "ChildrenRef({}): Broadcasting message: {:?}", @@ -191,6 +274,18 @@ impl ChildrenRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -199,6 +294,7 @@ impl ChildrenRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn stop(&self) -> Result<(), ()> { debug!("ChildrenRef({}): Stopping.", self.id()); @@ -219,6 +315,18 @@ impl ChildrenRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -227,6 +335,7 @@ impl ChildrenRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn kill(&self) -> Result<(), ()> { debug!("ChildrenRef({}): Killing.", self.id()); diff --git a/src/bastion/src/config.rs b/src/bastion/src/config.rs index ceecf258..118e121e 100644 --- a/src/bastion/src/config.rs +++ b/src/bastion/src/config.rs @@ -10,6 +10,18 @@ /// ```rust /// use bastion::prelude::*; /// +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// let config = Config::new().show_backtraces(); /// /// Bastion::init_with(config); @@ -19,9 +31,10 @@ /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` /// -/// [`Bastion::init_with`]: struct.Bastion.html#method.init_with +/// [`Bastion::init_with`]: crate::Bastion::init_with pub struct Config { backtraces: Backtraces, } @@ -40,8 +53,6 @@ impl Config { /// Creates a new configuration with the following default /// behaviors: /// - All backtraces are shown (see [`Config::show_backtraces`]). - /// - /// [`Config::show_backtraces`]: #method.show_backtraces pub fn new() -> Self { Config::default() } @@ -57,6 +68,18 @@ impl Config { /// ```rust /// use bastion::prelude::*; /// + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// let config = Config::new().show_backtraces(); /// /// Bastion::init_with(config); @@ -67,6 +90,7 @@ impl Config { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn show_backtraces(mut self) -> Self { self.backtraces = Backtraces::show(); @@ -83,6 +107,18 @@ impl Config { /// ```rust /// use bastion::prelude::*; /// + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// let config = Config::new().hide_backtraces(); /// /// Bastion::init_with(config); @@ -93,9 +129,8 @@ impl Config { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Config::show_backtraces`]: #method.show_backtraces pub fn hide_backtraces(mut self) -> Self { self.backtraces = Backtraces::hide(); self diff --git a/src/bastion/src/context.rs b/src/bastion/src/context.rs index de805830..93967da4 100644 --- a/src/bastion/src/context.rs +++ b/src/bastion/src/context.rs @@ -8,14 +8,14 @@ use crate::dispatcher::{BroadcastTarget, DispatcherType, NotificationType}; use crate::envelope::{Envelope, RefAddr, SignedMessage}; use crate::message::{Answer, BastionMessage, Message, Msg}; use crate::supervisor::SupervisorRef; -use crate::{prelude::ReceiveError, system::SYSTEM}; -use async_mutex::Mutex; +use crate::{global_system::SYSTEM, prelude::ReceiveError}; + +use crossbeam_queue::SegQueue; use futures::pending; use futures::FutureExt; use futures_timer::Delay; #[cfg(feature = "scaling")] use lever::table::lotable::LOTable; -use std::collections::VecDeque; use std::fmt::{self, Display, Formatter}; use std::pin::Pin; #[cfg(feature = "scaling")] @@ -42,6 +42,18 @@ pub const NIL_ID: BastionId = BastionId(Uuid::nil()); /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -57,11 +69,12 @@ pub const NIL_ID: BastionId = BastionId(Uuid::nil()); /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` pub struct BastionId(pub(crate) Uuid); #[derive(Debug)] -/// A child's execution context, allowing its [`exec`] future +/// A child's execution context, allowing its [`with_exec`] future /// to receive messages and access a [`ChildRef`] referencing /// it, a [`ChildrenRef`] referencing its children group and /// a [`SupervisorRef`] referencing its supervisor. @@ -71,6 +84,18 @@ pub struct BastionId(pub(crate) Uuid); /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -103,18 +128,21 @@ pub struct BastionId(pub(crate) Uuid); /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` +/// +/// [`with_exec`]: crate::children::Children::with_exec pub struct BastionContext { id: BastionId, child: ChildRef, children: ChildrenRef, supervisor: Option<SupervisorRef>, - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, } #[derive(Debug)] pub(crate) struct ContextState { - messages: VecDeque<SignedMessage>, + messages: SegQueue<SignedMessage>, #[cfg(feature = "scaling")] stats: Arc<AtomicU64>, #[cfg(feature = "scaling")] @@ -135,7 +163,7 @@ impl BastionContext { child: ChildRef, children: ChildrenRef, supervisor: Option<SupervisorRef>, - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, ) -> Self { debug!("BastionContext({}): Creating.", id); BastionContext { @@ -155,6 +183,18 @@ impl BastionContext { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -172,9 +212,8 @@ impl BastionContext { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`ChildRef`]: children/struct.ChildRef.html pub fn current(&self) -> &ChildRef { &self.child } @@ -187,6 +226,18 @@ impl BastionContext { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -204,9 +255,8 @@ impl BastionContext { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`ChildrenRef`]: children/struct.ChildrenRef.html pub fn parent(&self) -> &ChildrenRef { &self.children } @@ -222,6 +272,18 @@ impl BastionContext { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// // When calling the method from a children group supervised @@ -258,9 +320,9 @@ impl BastionContext { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`SupervisorRef`]: supervisor/struct.SupervisorRef.html /// [`Bastion::children`]: struct.Bastion.html#method.children pub fn supervisor(&self) -> Option<&SupervisorRef> { self.supervisor.as_ref() @@ -283,6 +345,18 @@ impl BastionContext { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -300,23 +374,25 @@ impl BastionContext { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`recv`]: #method.recv - /// [`try_recv_timeout`]: #method.try_recv_timeout - /// [`SignedMessage`]: ../prelude/struct.SignedMessage.html + /// [`recv`]: Self::method.recv + /// [`try_recv_timeout`]: Self::method.try_recv_timeout pub async fn try_recv(&self) -> Option<SignedMessage> { - self.try_recv_timeout(std::time::Duration::from_nanos(0)) - .await - .map(|msg| { - trace!("BastionContext({}): Received message: {:?}", self.id, msg); - msg - }) - .map_err(|e| { - trace!("BastionContext({}): Received no message.", self.id); - e - }) - .ok() + // We want to let a tick pass + // otherwise guard will never contain anything. + Delay::new(Duration::from_millis(0)).await; + + trace!("BastionContext({}): Trying to receive message.", self.id); + + if let Some(msg) = self.state.pop_message() { + trace!("BastionContext({}): Received message: {:?}", self.id, msg); + Some(msg) + } else { + trace!("BastionContext({}): Received no message.", self.id); + None + } } /// Retrieves asynchronously a message received by the element @@ -337,6 +413,18 @@ impl BastionContext { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -353,23 +441,18 @@ impl BastionContext { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`try_recv`]: #method.try_recv - /// [`try_recv_timeout`]: #method.try_recv_timeout - /// [`SignedMessage`]: ../prelude/struct.SignedMessage.html + /// [`try_recv`]: Self::try_recv + /// [`try_recv_timeout`]: Self::try_recv_timeout pub async fn recv(&self) -> Result<SignedMessage, ()> { debug!("BastionContext({}): Waiting to receive message.", self.id); loop { - let state = self.state.clone(); - let mut guard = state.lock().await; - - if let Some(msg) = guard.pop_message() { + if let Some(msg) = self.state.pop_message() { trace!("BastionContext({}): Received message: {:?}", self.id, msg); return Ok(msg); } - - drop(guard); pending!(); } } @@ -393,6 +476,18 @@ impl BastionContext { /// # use bastion::prelude::*; /// # use std::time::Duration; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -414,26 +509,23 @@ impl BastionContext { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`recv`]: #method.recv - /// [`try_recv`]: #method.try_recv - /// [`SignedMessage`]: ../prelude/struct.SignedMessage.html + /// [`recv`]: Self::recv + /// [`try_recv`]: Self::try_recv + /// [`SignedMessage`]: .crate::enveloppe::SignedMessage pub async fn try_recv_timeout(&self, timeout: Duration) -> Result<SignedMessage, ReceiveError> { - if timeout == std::time::Duration::from_nanos(0) { - debug!("BastionContext({}): Trying to receive message.", self.id); - } else { - debug!( - "BastionContext({}): Waiting to receive message within {} milliseconds.", - self.id, - timeout.as_millis() - ); - } + debug!( + "BastionContext({}): Waiting to receive message within {} milliseconds.", + self.id, + timeout.as_millis() + ); futures::select! { message = self.recv().fuse() => { message.map_err(|_| ReceiveError::Other) }, - duration = Delay::new(timeout).fuse() => { + _duration = Delay::new(timeout).fuse() => { Err(ReceiveError::Timeout(timeout)) } } @@ -446,6 +538,18 @@ impl BastionContext { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// @@ -462,9 +566,11 @@ impl BastionContext { /// # /// # Bastion::start(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`RefAddr`]: /prelude/struct.Answer.html + // TODO(scrabsha): should we link to Answer or to RefAddr? + // [`RefAddr`]: /prelude/struct.Answer.html pub fn signature(&self) -> RefAddr { RefAddr::new( self.current().path().clone(), @@ -484,6 +590,18 @@ impl BastionContext { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -503,9 +621,8 @@ impl BastionContext { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`RefAddr`]: ../prelude/struct.RefAddr.html pub fn tell<M: Message>(&self, to: &RefAddr, msg: M) -> Result<(), M> { debug!( "{:?}: Telling message: {:?} to: {:?}", @@ -536,7 +653,18 @@ impl BastionContext { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// // The message that will be "asked"... /// const ASK_MSG: &'static str = "A message containing data (ask)."; @@ -595,8 +723,6 @@ impl BastionContext { /// # Bastion::block_until_stopped(); /// # } /// ``` - /// - /// [`Answer`]: /message/struct.Answer.html pub fn ask<M: Message>(&self, to: &RefAddr, msg: M) -> Result<Answer, M> { debug!( "{:?}: Asking message: {:?} to: {:?}", @@ -604,7 +730,7 @@ impl BastionContext { msg, to ); - let (msg, answer) = BastionMessage::ask(msg); + let (msg, answer) = BastionMessage::ask(msg, self.signature()); let env = Envelope::new_with_sign(msg, self.signature()); // FIXME: panics? to.sender() @@ -636,7 +762,6 @@ impl BastionContext { /// the [`BroadcastTarget`] value. /// * `message` - The broadcasted message. /// - /// [`BroadcastTarget`]: ../dispatcher/enum.DispatcherType.html pub fn broadcast_message<M: Message>(&self, target: BroadcastTarget, message: M) { let msg = Arc::new(SignedMessage { msg: Msg::broadcast(message), @@ -651,7 +776,7 @@ impl BastionContext { impl ContextState { pub(crate) fn new() -> Self { ContextState { - messages: VecDeque::new(), + messages: SegQueue::new(), #[cfg(feature = "scaling")] stats: Arc::new(AtomicU64::new(0)), #[cfg(feature = "scaling")] @@ -679,12 +804,12 @@ impl ContextState { self.actor_stats.clone() } - pub(crate) fn push_message(&mut self, msg: Msg, sign: RefAddr) { - self.messages.push_back(SignedMessage::new(msg, sign)) + pub(crate) fn push_message(&self, msg: Msg, sign: RefAddr) { + self.messages.push(SignedMessage::new(msg, sign)) } - pub(crate) fn pop_message(&mut self) -> Option<SignedMessage> { - self.messages.pop_front() + pub(crate) fn pop_message(&self) -> Option<SignedMessage> { + self.messages.pop() } #[cfg(feature = "scaling")] @@ -706,19 +831,31 @@ mod context_tests { use crate::Bastion; use std::panic; - #[test] + #[cfg(feature = "tokio-runtime")] + mod tokio_tests { + #[tokio::test] + async fn test_context() { + super::test_context() + } + } + + #[cfg(not(feature = "tokio-runtime"))] + mod no_tokio_tests { + #[test] + fn test_context() { + super::test_context() + } + } + fn test_context() { Bastion::init(); Bastion::start(); - run_test(test_recv); - run_test(test_try_recv); - run_test(test_try_recv_fail); - run_test(test_try_recv_timeout); - run_test(test_try_recv_timeout_fail); - - Bastion::stop(); - Bastion::block_until_stopped(); + test_recv(); + test_try_recv(); + test_try_recv_fail(); + test_try_recv_timeout(); + test_try_recv_timeout_fail(); } fn test_recv() { @@ -762,7 +899,7 @@ mod context_tests { } fn test_try_recv_fail() { - let children = Bastion::children(|children| { + Bastion::children(|children| { children.with_exec(|ctx: BastionContext| async move { assert!(ctx.try_recv().await.is_none()); Ok(()) @@ -777,7 +914,7 @@ mod context_tests { let children = Bastion::children(|children| { children.with_exec(|ctx: BastionContext| async move { - msg! { ctx.try_recv_timeout(std::time::Duration::from_millis(1)).await.expect("recv_timeout failed"), + msg! { ctx.try_recv_timeout(std::time::Duration::from_millis(5)).await.expect("recv_timeout failed"), ref msg: &'static str => { assert_eq!(msg, &"test recv timeout"); }; @@ -809,15 +946,6 @@ mod context_tests { run!(async { Delay::new(std::time::Duration::from_millis(2)).await }); // The child panicked, but we should still be able to send things to it - assert!(children.broadcast("test recv timeout").is_ok()); - } - - fn run_test<T>(test: T) -> () - where - T: FnOnce() -> () + panic::UnwindSafe, - { - let result = panic::catch_unwind(|| test()); - - assert!(result.is_ok()) + children.broadcast("test recv timeout").unwrap(); } } diff --git a/src/bastion/src/dispatcher.rs b/src/bastion/src/dispatcher.rs index 7306e9d8..4596659c 100644 --- a/src/bastion/src/dispatcher.rs +++ b/src/bastion/src/dispatcher.rs @@ -2,22 +2,34 @@ //! Special module that allows users to interact and communicate with a //! group of actors through the dispatchers that holds information about //! actors grouped together. -use crate::child_ref::ChildRef; -use crate::envelope::SignedMessage; +use crate::{ + child_ref::ChildRef, + message::{Answer, Message}, + prelude::SendError, +}; +use crate::{distributor::Distributor, envelope::SignedMessage}; use anyhow::Result as AnyResult; use lever::prelude::*; -use std::fmt::{self, Debug}; use std::hash::{Hash, Hasher}; +use std::sync::RwLock; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, }; -use tracing::{debug, trace, warn}; +use std::{ + collections::HashMap, + fmt::{self, Debug}, +}; +use tracing::{debug, trace}; /// Type alias for the concurrency hashmap. Each key-value pair stores /// the Bastion identifier as the key and the module name as the value. pub type DispatcherMap = LOTable<ChildRef, String>; +/// Type alias for the recipients hashset. +/// Each key-value pair stores the Bastion identifier as the key. +pub type RecipientMap = LOTable<ChildRef, ()>; + #[derive(Debug, Clone)] /// Defines types of the notifications handled by the dispatcher /// when the group of actors is changing. @@ -43,6 +55,27 @@ pub enum BroadcastTarget { Group(String), } +/// A `Recipient` is responsible for maintaining it's list +/// of recipients, and deciding which child gets to receive which message. +pub trait Recipient { + /// Provide this function to declare which recipient will receive the next message + fn next(&self) -> Option<ChildRef>; + /// Return all recipients that will receive a broadcast message + fn all(&self) -> Vec<ChildRef>; + /// Add this actor to your list of recipients + fn register(&self, actor: ChildRef); + /// Remove this actor from your list of recipients + fn remove(&self, actor: &ChildRef); +} + +/// A `RecipientHandler` is a `Recipient` implementor, that can be stored in the dispatcher +pub trait RecipientHandler: Recipient + Send + Sync + Debug {} + +impl RecipientHandler for RoundRobinHandler {} + +/// The default handler, which does round-robin. +pub type DefaultRecipientHandler = RoundRobinHandler; + #[derive(Debug, Clone, Eq, PartialEq)] /// Defines the type of the dispatcher. /// @@ -67,6 +100,48 @@ pub type DefaultDispatcherHandler = RoundRobinHandler; #[derive(Default, Debug)] pub struct RoundRobinHandler { index: AtomicUsize, + recipients: RecipientMap, +} + +impl RoundRobinHandler { + fn public_recipients(&self) -> Vec<ChildRef> { + self.recipients + .iter() + .filter_map(|entry| { + if entry.0.is_public() { + Some(entry.0) + } else { + None + } + }) + .collect() + } +} + +impl Recipient for RoundRobinHandler { + fn next(&self) -> Option<ChildRef> { + let entries = self.public_recipients(); + + if entries.is_empty() { + return None; + } + + let current_index = self.index.load(Ordering::SeqCst) % entries.len(); + self.index.store(current_index + 1, Ordering::SeqCst); + entries.get(current_index).map(std::clone::Clone::clone) + } + + fn all(&self) -> Vec<ChildRef> { + self.public_recipients() + } + + fn register(&self, actor: ChildRef) { + let _ = self.recipients.insert(actor, ()); + } + + fn remove(&self, actor: &ChildRef) { + let _ = self.recipients.remove(&actor); + } } impl DispatcherHandler for RoundRobinHandler { @@ -80,25 +155,31 @@ impl DispatcherHandler for RoundRobinHandler { } // Each child in turn will receive a message. fn broadcast_message(&self, entries: &DispatcherMap, message: &Arc<SignedMessage>) { - let entries = entries + let public_childrefs = entries .iter() - .filter(|entry| entry.0.is_public()) + .filter_map(|entry| { + if entry.0.is_public() { + Some(entry.0) + } else { + None + } + }) .collect::<Vec<_>>(); - if entries.is_empty() { + if public_childrefs.is_empty() { debug!("no public children to broadcast message to"); return; } - let current_index = self.index.load(Ordering::SeqCst) % entries.len(); + let current_index = self.index.load(Ordering::SeqCst) % public_childrefs.len(); - if let Some(entry) = entries.get(current_index) { - warn!( + if let Some(entry) = public_childrefs.get(current_index) { + debug!( "sending message to child {}/{} - {}", current_index + 1, entries.len(), - entry.0.path() + entry.path() ); - entry.0.tell_anonymously(message.clone()).unwrap(); + entry.tell_anonymously(message.clone()).unwrap(); self.index.store(current_index + 1, Ordering::SeqCst); }; } @@ -269,6 +350,8 @@ impl Into<DispatcherType> for String { pub(crate) struct GlobalDispatcher { /// Storage for all registered group of actors. pub dispatchers: LOTable<DispatcherType, Arc<Box<Dispatcher>>>, + // TODO: switch to LOTable once lever implements write optimized granularity + pub distributors: Arc<RwLock<HashMap<Distributor, Box<(dyn RecipientHandler)>>>>, } impl GlobalDispatcher { @@ -276,6 +359,12 @@ impl GlobalDispatcher { pub(crate) fn new() -> Self { GlobalDispatcher { dispatchers: LOTable::new(), + distributors: Arc::new(RwLock::new(HashMap::new())) + // TODO: switch to LOTable once lever implements write optimized granularity + // distributors: LOTableBuilder::new() + //.with_concurrency(TransactionConcurrency::Optimistic) + //.with_isolation(TransactionIsolation::Serializable) + //.build(), } } @@ -332,19 +421,17 @@ impl GlobalDispatcher { /// Broadcasts the given message in according with the specified target. pub(crate) fn broadcast_message(&self, target: BroadcastTarget, message: &Arc<SignedMessage>) { - let mut acked_dispatchers: Vec<DispatcherType> = Vec::new(); - - match target { + let acked_dispatchers = match target { BroadcastTarget::All => self .dispatchers .iter() .map(|pair| pair.0.name().into()) - .for_each(|group_name| acked_dispatchers.push(group_name)), + .collect(), BroadcastTarget::Group(name) => { let target_dispatcher = name.into(); - acked_dispatchers.push(target_dispatcher); + vec![target_dispatcher] } - } + }; for dispatcher_type in acked_dispatchers { match self.dispatchers.get(&dispatcher_type) { @@ -354,7 +441,7 @@ impl GlobalDispatcher { // TODO: Put the message into the dead queue None => { let name = dispatcher_type.name(); - warn!( + debug!( "The message can't be delivered to the group with the '{}' name.", name ); @@ -363,13 +450,95 @@ impl GlobalDispatcher { } } + pub(crate) fn tell<M>(&self, distributor: Distributor, message: M) -> Result<(), SendError> + where + M: Message, + { + let child = self.next(distributor)?.ok_or(SendError::EmptyRecipient)?; + child.try_tell_anonymously(message).map(Into::into) + } + + pub(crate) fn ask<M>(&self, distributor: Distributor, message: M) -> Result<Answer, SendError> + where + M: Message, + { + let child = self.next(distributor)?.ok_or(SendError::EmptyRecipient)?; + child.try_ask_anonymously(message).map(Into::into) + } + + pub(crate) fn ask_everyone<M>( + &self, + distributor: Distributor, + message: M, + ) -> Result<Vec<Answer>, SendError> + where + M: Message + Clone, + { + let all_children = self.all(distributor)?; + if all_children.is_empty() { + Err(SendError::EmptyRecipient) + } else { + all_children + .iter() + .map(|child| child.try_ask_anonymously(message.clone())) + .collect::<Result<Vec<_>, _>>() + } + } + + pub(crate) fn tell_everyone<M>( + &self, + distributor: Distributor, + message: M, + ) -> Result<Vec<()>, SendError> + where + M: Message + Clone, + { + let all_children = self.all(distributor)?; + if all_children.is_empty() { + Err(SendError::EmptyRecipient) + } else { + all_children + .iter() + .map(|child| child.try_tell_anonymously(message.clone())) + .collect() + } + } + + fn next(&self, distributor: Distributor) -> Result<Option<ChildRef>, SendError> { + self.distributors + .read() + .map_err(|error| { + SendError::Other(anyhow::anyhow!( + "couldn't get read lock on distributors {:?}", + error + )) + })? + .get(&distributor) + .map(|recipient| recipient.next()) + .ok_or_else(|| SendError::from(distributor)) + } + + fn all(&self, distributor: Distributor) -> Result<Vec<ChildRef>, SendError> { + self.distributors + .read() + .map_err(|error| { + SendError::Other(anyhow::anyhow!( + "couldn't get read lock on distributors {:?}", + error + )) + })? + .get(&distributor) + .map(|recipient| recipient.all()) + .ok_or_else(|| SendError::from(distributor)) + } + /// Adds dispatcher to the global registry. pub(crate) fn register_dispatcher(&self, dispatcher: &Arc<Box<Dispatcher>>) -> AnyResult<()> { let dispatcher_type = dispatcher.dispatcher_type(); let is_registered = self.dispatchers.contains_key(&dispatcher_type); if is_registered && dispatcher_type != DispatcherType::Anonymous { - warn!( + debug!( "The dispatcher with the '{:?}' name already registered in the cluster.", dispatcher_type ); @@ -386,6 +555,72 @@ impl GlobalDispatcher { self.dispatchers.remove(&dispatcher.dispatcher_type())?; Ok(()) } + + /// Appends the information about actor to the recipients. + pub(crate) fn register_recipient( + &self, + distributor: &Distributor, + child_ref: ChildRef, + ) -> AnyResult<()> { + let mut distributors = self.distributors.write().map_err(|error| { + anyhow::anyhow!("couldn't get read lock on distributors {:?}", error) + })?; + if let Some(recipients) = distributors.get(&distributor) { + recipients.register(child_ref); + } else { + let recipients = DefaultRecipientHandler::default(); + recipients.register(child_ref); + distributors.insert( + distributor.clone(), + Box::new(recipients) as Box<(dyn RecipientHandler)>, + ); + }; + Ok(()) + } + + pub(crate) fn remove_recipient( + &self, + distributor_list: &[Distributor], + child_ref: ChildRef, + ) -> AnyResult<()> { + let distributors = self.distributors.write().map_err(|error| { + anyhow::anyhow!("couldn't get read lock on distributors {:?}", error) + })?; + distributor_list.iter().for_each(|distributor| { + distributors + .get(&distributor) + .map(|recipients| recipients.remove(&child_ref)); + }); + Ok(()) + } + + /// Adds distributor to the global registry. + pub(crate) fn register_distributor(&self, distributor: &Distributor) -> AnyResult<()> { + let mut distributors = self.distributors.write().map_err(|error| { + anyhow::anyhow!("couldn't get read lock on distributors {:?}", error) + })?; + if distributors.contains_key(&distributor) { + debug!( + "The distributor with the '{:?}' name already registered in the cluster.", + distributor + ); + } else { + distributors.insert( + distributor.clone(), + Box::new(DefaultRecipientHandler::default()), + ); + } + Ok(()) + } + + /// Removes distributor from the global registry. + pub(crate) fn remove_distributor(&self, distributor: &Distributor) -> AnyResult<()> { + let mut distributors = self.distributors.write().map_err(|error| { + anyhow::anyhow!("couldn't get read lock on distributors {:?}", error) + })?; + distributors.remove(distributor); + Ok(()) + } } #[cfg(test)] diff --git a/src/bastion/src/distributor.rs b/src/bastion/src/distributor.rs new file mode 100644 index 00000000..646b42aa --- /dev/null +++ b/src/bastion/src/distributor.rs @@ -0,0 +1,750 @@ +//! `Distributor` is a mechanism that allows you to send messages to children. + +use crate::{ + global_system::{STRING_INTERNER, SYSTEM}, + message::{Answer, Message, MessageHandler}, + prelude::{ChildRef, SendError}, +}; +use anyhow::Result as AnyResult; +use futures::channel::oneshot; +use lasso::Spur; +use std::{ + fmt::Debug, + sync::mpsc::{channel, Receiver}, +}; + +// Copy is fine here because we're working +// with interned strings here +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// The `Distributor` is the main message passing mechanism we will use. +/// it provides methods that will allow us to send messages +/// and add/remove actors to the Distribution list +pub struct Distributor(Spur); + +impl Distributor { + /// Create a new distributor to send messages to + /// # Example + /// + /// ```rust + /// # use bastion::prelude::*; + /// # + /// # + /// # fn run() { + /// # + /// let distributor = Distributor::named("my target group"); + /// // distributor is now ready to use + /// # } + /// ``` + pub fn named(name: impl AsRef<str>) -> Self { + Self(STRING_INTERNER.get_or_intern(name.as_ref())) + } + + /// Ask a question to a recipient attached to the `Distributor` + /// and wait for a reply. + /// + /// This can be achieved manually using a `MessageHandler` and `ask_one`. + /// Ask a question to a recipient attached to the `Distributor` + /// + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # async fn run() { + /// # Bastion::init(); + /// # Bastion::start(); + /// + /// # Bastion::supervisor(|supervisor| { + /// # supervisor.children(|children| { + /// // attach a named distributor to the children + /// children + /// # .with_redundancy(1) + /// .with_distributor(Distributor::named("my distributor")) + /// .with_exec(|ctx: BastionContext| { + /// async move { + /// loop { + /// // The message handler needs an `on_question` section + /// // that matches the `question` you're going to send, + /// // and that will reply with the Type the request expects. + /// // In our example, we ask a `&str` question, and expect a `bool` reply. + /// MessageHandler::new(ctx.recv().await?) + /// .on_question(|message: &str, sender| { + /// if message == "is it raining today?" { + /// sender.reply(true).unwrap(); + /// } + /// }); + /// } + /// Ok(()) + /// } + /// }) + /// # }) + /// # }); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// let reply: Result<String, SendError> = distributor + /// .request("is it raining today?") + /// .await + /// .expect("couldn't receive reply"); + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn request<R: Message>( + &self, + question: impl Message, + ) -> oneshot::Receiver<Result<R, SendError>> { + let (sender, receiver) = oneshot::channel(); + let s = *self; + spawn!(async move { + match SYSTEM.dispatcher().ask(s, question) { + Ok(response) => match response.await { + Ok(message) => { + let message_to_send = MessageHandler::new(message) + .on_tell(|reply: R, _| Ok(reply)) + .on_fallback(|_, _| { + Err(SendError::Other(anyhow::anyhow!( + "received a message with the wrong type" + ))) + }); + let _ = sender.send(message_to_send); + } + Err(e) => { + let _ = sender.send(Err(SendError::Other(anyhow::anyhow!( + "couldn't receive reply: {:?}", + e + )))); + } + }, + Err(error) => { + let _ = sender.send(Err(error)); + } + }; + }); + + receiver + } + + /// Ask a question to a recipient attached to the `Distributor` + /// and wait for a reply. + /// + /// this is the sync variant of the `request` function, backed by a futures::channel::oneshot + /// # Example + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # Bastion::start(); + /// # Bastion::supervisor(|supervisor| { + /// # supervisor.children(|children| { + /// // attach a named distributor to the children + /// children + /// # .with_redundancy(1) + /// .with_distributor(Distributor::named("my distributor")) + /// .with_exec(|ctx: BastionContext| { + /// async move { + /// loop { + /// // The message handler needs an `on_question` section + /// // that matches the `question` you're going to send, + /// // and that will reply with the Type the request expects. + /// // In our example, we ask a `&str` question, and expect a `bool` reply. + /// MessageHandler::new(ctx.recv().await?) + /// .on_question(|message: &str, sender| { + /// if message == "is it raining today?" { + /// sender.reply(true).unwrap(); + /// } + /// }); + /// } + /// Ok(()) + /// } + /// }) + /// # }) + /// # }); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// let reply: Result<bool, SendError> = distributor + /// .request_sync("is it raining today?") + /// .recv() + /// .expect("couldn't receive reply"); // Ok(true) + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn request_sync<R: Message>( + &self, + question: impl Message, + ) -> Receiver<Result<R, SendError>> { + let (sender, receiver) = channel(); + let s = *self; + spawn!(async move { + match SYSTEM.dispatcher().ask(s, question) { + Ok(response) => { + if let Ok(message) = response.await { + let message_to_send = MessageHandler::new(message) + .on_tell(|reply: R, _| Ok(reply)) + .on_fallback(|_, _| { + Err(SendError::Other(anyhow::anyhow!( + "received a message with the wrong type" + ))) + }); + let _ = sender.send(message_to_send); + } else { + let _ = sender.send(Err(SendError::Other(anyhow::anyhow!( + "couldn't receive reply" + )))); + } + } + Err(error) => { + let _ = sender.send(Err(error)); + } + }; + }); + + receiver + } + + /// Ask a question to a recipient attached to the `Distributor` + /// + /// # Example + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # Bastion::supervisor(|supervisor| { + /// # supervisor.children(|children| { + /// children + /// .with_redundancy(1) + /// .with_distributor(Distributor::named("my distributor")) + /// .with_exec(|ctx: BastionContext| { // ... + /// # async move { + /// # loop { + /// # let _: Option<SignedMessage> = ctx.try_recv().await; + /// # } + /// # Ok(()) + /// # } + /// }) + /// # }) + /// # }); + /// # + /// # Bastion::start(); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// let answer: Answer = distributor.ask_one("hello?").expect("couldn't send question"); + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn ask_one(&self, question: impl Message) -> Result<Answer, SendError> { + SYSTEM.dispatcher().ask(*self, question) + } + + /// Ask a question to all recipients attached to the `Distributor` + /// + /// Requires a `Message` that implements `Clone`. (it will be cloned and passed to each recipient) + /// # Example + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # Bastion::supervisor(|supervisor| { + /// # supervisor.children(|children| { + /// # children + /// # .with_redundancy(1) + /// # .with_distributor(Distributor::named("my distributor")) + /// # .with_exec(|ctx: BastionContext| { + /// # async move { + /// # loop { + /// # let _: Option<SignedMessage> = ctx.try_recv().await; + /// # } + /// # Ok(()) + /// # } + /// # }) + /// # }) + /// # }); + /// # + /// # Bastion::start(); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// let answer: Vec<Answer> = distributor.ask_everyone("hello?".to_string()).expect("couldn't send question"); + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn ask_everyone(&self, question: impl Message + Clone) -> Result<Vec<Answer>, SendError> { + SYSTEM.dispatcher().ask_everyone(*self, question) + } + + /// Send a Message to a recipient attached to the `Distributor` + /// + /// # Example + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # Bastion::supervisor(|supervisor| { + /// # supervisor.children(|children| { + /// # children + /// # .with_redundancy(1) + /// # .with_distributor(Distributor::named("my distributor")) + /// # .with_exec(|ctx: BastionContext| { + /// # async move { + /// # loop { + /// # let _: Option<SignedMessage> = ctx.try_recv().await; + /// # } + /// # Ok(()) + /// # } + /// # }) + /// # }) + /// # }); + /// # + /// # Bastion::start(); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// let answer: () = distributor.tell_one("hello?").expect("couldn't send question"); + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn tell_one(&self, message: impl Message) -> Result<(), SendError> { + SYSTEM.dispatcher().tell(*self, message) + } + + /// Send a Message to each recipient attached to the `Distributor` + /// + /// Requires a `Message` that implements `Clone`. (it will be cloned and passed to each recipient) + /// # Example + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # Bastion::supervisor(|supervisor| { + /// # supervisor.children(|children| { + /// # children + /// # .with_redundancy(1) + /// # .with_distributor(Distributor::named("my distributor")) + /// # .with_exec(|ctx: BastionContext| { + /// # async move { + /// # loop { + /// # let _: Option<SignedMessage> = ctx.try_recv().await; + /// # } + /// # Ok(()) + /// # } + /// # }) + /// # }) + /// # }); + /// # + /// # Bastion::start(); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// let answer: () = distributor.tell_one("hello?").expect("couldn't send question"); + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn tell_everyone(&self, message: impl Message + Clone) -> Result<Vec<()>, SendError> { + SYSTEM.dispatcher().tell_everyone(*self, message) + } + + /// subscribe a `ChildRef` to the named `Distributor` + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # let children = + /// # Bastion::children(|children| { + /// # children + /// # .with_redundancy(1) + /// # .with_distributor(Distributor::named("my distributor")) + /// # .with_exec(|ctx: BastionContext| { + /// # async move { + /// # loop { + /// # let _: Option<SignedMessage> = ctx.try_recv().await; + /// # } + /// # Ok(()) + /// # } + /// # }) + /// # }).unwrap(); + /// # + /// # Bastion::start(); + /// # + /// let child_ref = children.elems()[0].clone(); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// // child_ref will now be elligible to receive messages dispatched through distributor + /// distributor.subscribe(child_ref).expect("couldn't subscribe child to distributor"); + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn subscribe(&self, child_ref: ChildRef) -> AnyResult<()> { + SYSTEM.dispatcher().register_recipient(self, child_ref) + } + + /// unsubscribe a `ChildRef` to the named `Distributor` + /// + /// ```no_run + /// # use bastion::prelude::*; + /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { + /// # Bastion::init(); + /// # let children = + /// # Bastion::children(|children| { + /// # children + /// # .with_redundancy(1) + /// # .with_distributor(Distributor::named("my distributor")) + /// # .with_exec(|ctx: BastionContext| { + /// # async move { + /// # loop { + /// # let _: Option<SignedMessage> = ctx.try_recv().await; + /// # } + /// # Ok(()) + /// # } + /// # }) + /// # }).unwrap(); + /// # + /// # Bastion::start(); + /// # + /// let child_ref = children.elems()[0].clone(); + /// + /// let distributor = Distributor::named("my distributor"); + /// + /// // child_ref will not receive messages dispatched through the distributor anymore + /// distributor.unsubscribe(child_ref).expect("couldn't unsubscribe child to distributor"); + /// + /// # Bastion::stop(); + /// # Bastion::block_until_stopped(); + /// # } + /// ``` + pub fn unsubscribe(&self, child_ref: ChildRef) -> AnyResult<()> { + let global_dispatcher = SYSTEM.dispatcher(); + global_dispatcher.remove_recipient(&vec![*self], child_ref) + } + + pub(crate) fn interned(&self) -> &Spur { + &self.0 + } +} + +#[cfg(test)] +mod distributor_tests { + use crate::prelude::*; + use futures::channel::mpsc::channel; + use futures::{SinkExt, StreamExt}; + + const TEST_DISTRIBUTOR: &str = "test distributor"; + const SUBSCRIBE_TEST_DISTRIBUTOR: &str = "subscribe test"; + + #[cfg(feature = "tokio-runtime")] + #[tokio::test] + async fn test_tokio_distributor() { + blocking!({ + run_tests(); + }); + } + + #[cfg(not(feature = "tokio-runtime"))] + #[test] + fn distributor_tests() { + run_tests(); + } + + fn run_tests() { + setup(); + + test_tell(); + test_ask(); + test_request(); + test_subscribe(); + } + + fn test_subscribe() { + let temp_distributor = Distributor::named("temp distributor"); + + assert!( + temp_distributor.tell_one("hello!").is_err(), + "should not be able to send message to an empty distributor" + ); + + let one_child: ChildRef = run!(async { + Distributor::named(SUBSCRIBE_TEST_DISTRIBUTOR) + .request(()) + .await + .unwrap() + .unwrap() + }); + temp_distributor.subscribe(one_child.clone()).unwrap(); + + temp_distributor + .tell_one("hello!") + .expect("should be able to send message a distributor that has a subscriber"); + + temp_distributor.unsubscribe(one_child).unwrap(); + + assert!( + temp_distributor.tell_one("hello!").is_err(), + "should not be able to send message to a distributor who's sole subscriber unsubscribed" + ); + } + + fn test_tell() { + let test_distributor = Distributor::named(TEST_DISTRIBUTOR); + + test_distributor + .tell_one("don't panic and carry a towel") + .unwrap(); + + let sent = test_distributor + .tell_everyone("so long, and thanks for all the fish") + .unwrap(); + + assert_eq!( + 5, + sent.len(), + "test distributor is supposed to have 5 children" + ); + } + + fn test_ask() { + let test_distributor = Distributor::named(TEST_DISTRIBUTOR); + + let question: String = + "What is the answer to life, the universe and everything?".to_string(); + + run!(async { + let answer = test_distributor.ask_one(question.clone()).unwrap(); + MessageHandler::new(answer.await.unwrap()) + .on_tell(|answer: u8, _| { + assert_eq!(42, answer); + }) + .on_fallback(|unknown, _sender_addr| { + panic!("unknown message\n {:?}", unknown); + }); + }); + + run!(async { + let answers = test_distributor.ask_everyone(question.clone()).unwrap(); + assert_eq!( + 5, + answers.len(), + "test distributor is supposed to have 5 children" + ); + let meanings = futures::future::join_all(answers.into_iter().map(|answer| async { + MessageHandler::new(answer.await.unwrap()) + .on_tell(|answer: u8, _| { + assert_eq!(42, answer); + answer + }) + .on_fallback(|unknown, _sender_addr| { + panic!("unknown message\n {:?}", unknown); + }) + })) + .await; + + assert_eq!( + 42 * 5, + meanings.iter().sum::<u8>(), + "5 children returning 42 should sum to 42 * 5" + ); + }); + } + + fn test_request() { + let test_distributor = Distributor::named(TEST_DISTRIBUTOR); + + let question: String = + "What is the answer to life, the universe and everything?".to_string(); + + run!(async { + let answer: u8 = test_distributor + .request(question.clone()) + .await + .unwrap() + .unwrap(); + assert_eq!(42, answer); + }); + + let answer_sync: u8 = test_distributor + .request_sync(question) + .recv() + .unwrap() + .unwrap(); + + assert_eq!(42, answer_sync); + } + + fn setup() { + Bastion::init(); + Bastion::start(); + + const NUM_CHILDREN: usize = 5; + + // This channel and the use of callbacks will allow us to know when all of the children are spawned. + let (sender, receiver) = channel(NUM_CHILDREN); + + Bastion::supervisor(|supervisor| { + let test_ready = sender.clone(); + let subscribe_test_ready = sender.clone(); + supervisor + .children(|children| { + children + .with_redundancy(NUM_CHILDREN) + .with_distributor(Distributor::named(TEST_DISTRIBUTOR)) + .with_callbacks(Callbacks::new().with_after_start(move || { + let mut test_ready = test_ready.clone(); + spawn!(async move { test_ready.send(()).await }); + })) + .with_exec(|ctx| async move { + loop { + let child_ref = ctx.current().clone(); + MessageHandler::new(ctx.recv().await?) + .on_question(|_: String, sender| { + let _ = sender.reply(42_u8); + }) + // send your child ref + .on_question(|_: (), sender| { + let _ = sender.reply(child_ref); + }); + } + }) + // Subscribe / unsubscribe tests + }) + .children(|children| { + children + .with_distributor(Distributor::named(SUBSCRIBE_TEST_DISTRIBUTOR)) + .with_callbacks(Callbacks::new().with_after_start(move || { + let mut subscribe_test_ready = subscribe_test_ready.clone(); + spawn!(async move { subscribe_test_ready.send(()).await }); + })) + .with_exec(|ctx| async move { + loop { + let child_ref = ctx.current().clone(); + MessageHandler::new(ctx.recv().await?).on_question( + |_: (), sender| { + let _ = sender.reply(child_ref); + }, + ); + } + }) + }) + }) + .unwrap(); + + // Wait until the children have spawned + run!(async { + // NUM_CHILDREN for the test distributor group, + // 1 for the subscribe test group + receiver.take(NUM_CHILDREN + 1).collect::<Vec<_>>().await; + }); + } +} diff --git a/src/bastion/src/envelope.rs b/src/bastion/src/envelope.rs index a476ed69..4b7033e1 100644 --- a/src/bastion/src/envelope.rs +++ b/src/bastion/src/envelope.rs @@ -3,9 +3,9 @@ //! and instruct Bastion how to send messages back to them use crate::broadcast::Sender; +use crate::global_system::SYSTEM; use crate::message::{BastionMessage, Message, Msg}; use crate::path::BastionPath; -use crate::system::SYSTEM; use std::sync::Arc; #[derive(Debug)] @@ -22,6 +22,18 @@ pub(crate) struct Envelope { /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -37,6 +49,7 @@ pub(crate) struct Envelope { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` pub struct SignedMessage { pub(crate) msg: Msg, @@ -60,6 +73,18 @@ impl SignedMessage { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -75,6 +100,7 @@ impl SignedMessage { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn signature(&self) -> &RefAddr { &self.sign @@ -89,6 +115,18 @@ impl SignedMessage { /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// Bastion::children(|children| { @@ -106,6 +144,7 @@ impl SignedMessage { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` pub struct RefAddr { path: Arc<BastionPath>, @@ -134,7 +173,18 @@ impl RefAddr { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -174,7 +224,18 @@ impl RefAddr { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); diff --git a/src/bastion/src/error.rs b/src/bastion/src/error.rs new file mode 100644 index 00000000..fcee1404 --- /dev/null +++ b/src/bastion/src/error.rs @@ -0,0 +1,94 @@ +//! +//! Describes the error types that may happen within bastion. +//! Given Bastion has a let it crash strategy, most error aren't noticeable. +//! A ReceiveError may however be raised when calling try_recv() or try_recv_timeout() +//! More errors may happen in the future. + +use crate::envelope::Envelope; +use crate::message::Msg; +use crate::system::STRING_INTERNER; +use crate::{distributor::Distributor, message::BastionMessage}; +use futures::channel::mpsc::TrySendError; +use std::fmt::Debug; +use std::result; +use std::result; +use std::time::Duration; +use thiserror::Error; + +pub type BastionResult<T> = result::Result<T, BastionError>; + +// TODO [igni]: make it one bastionerror enum and merge everything + +#[derive(Debug)] +pub enum BastionError { + Receive(ReceiveError), + ChanSend(String), + ChanRecv(String), + UnackedMessage, +} + +#[derive(Debug)] +/// These errors happen +/// when [`try_recv`] or [`try_recv_timeout`] are invoked +/// +/// [`try_recv`]: crate::context::BastionContext::try_recv +/// [`try_recv_timeout`]: crate::context::BastionContext::try_recv_timeout +pub enum ReceiveError { + /// We didn't receive a message on time + Timeout(Duration), + /// Generic error. Not used yet + Other, +} + +#[derive(Error, Debug)] +/// `SendError`s occur when a message couldn't be dispatched through a distributor +pub enum SendError { + #[error("couldn't send message. Channel Disconnected.")] + /// Channel has been closed before we could send a message + Disconnected(Msg), + #[error("couldn't send message. Channel is Full.")] + /// Channel is full, can't send a message + Full(Msg), + #[error("couldn't send a message I should have not sent. {0}")] + /// This error is returned when we try to send a message + /// that is not a BastionMessage::Message variant + Other(anyhow::Error), + #[error("No available Distributor matching {0}")] + /// The distributor we're trying to dispatch messages to is not registered in the system + NoDistributor(String), + #[error("Distributor has 0 Recipients")] + /// The distributor we're trying to dispatch messages to has no recipients + EmptyRecipient, +} + +impl From<TrySendError<Envelope>> for SendError { + fn from(tse: TrySendError<Envelope>) -> Self { + let is_disconnected = tse.is_disconnected(); + match tse.into_inner().msg { + BastionMessage::Message(msg) => { + if is_disconnected { + Self::Disconnected(msg) + } else { + Self::Full(msg) + } + } + other => Self::Other(anyhow::anyhow!("{:?}", other)), + } + } +} + +impl From<Distributor> for SendError { + fn from(distributor: Distributor) -> Self { + Self::NoDistributor(STRING_INTERNER.resolve(distributor.interned()).to_string()) + } +} + +#[derive(Error, Debug)] +pub enum BastionError { + #[error("The message cannot be sent via the channel. Reason: {0}")] + ChanSend(String), + #[error("The message cannot be received from the channel. Reason: {0}")] + ChanRecv(String), + #[error("Before requesting a next message the previous message must be acked.")] + UnackedMessage, +} diff --git a/src/bastion/src/errors.rs b/src/bastion/src/errors.rs index e2c9144e..b1415da0 100644 --- a/src/bastion/src/errors.rs +++ b/src/bastion/src/errors.rs @@ -1,7 +1,83 @@ +//! +//! Describes the error types that may happen within bastion. +//! Given Bastion has a let it crash strategy, most error aren't noticeable. +//! A ReceiveError may however be raised when calling try_recv() or try_recv_timeout() +//! More errors may happen in the future. + +use crate::envelope::Envelope; +use crate::global_system::STRING_INTERNER; +use crate::message::Msg; +use crate::{distributor::Distributor, message::BastionMessage}; +use futures::channel::mpsc::TrySendError; +use std::fmt::Debug; +use std::result; use std::time::Duration; +use thiserror::Error; + +pub type BastionResult<T> = result::Result<T, BastionError>; + +// TODO [igni]: make it one bastionerror enum and merge everything + +#[derive(Debug)] +pub enum BastionError { + Receive(ReceiveError), + ChanSend(String), + ChanRecv(String), + UnackedMessage, +} #[derive(Debug)] +/// These errors happen +/// when [`try_recv`] or [`try_recv_timeout`] are invoked +/// +/// [`try_recv`]: crate::context::BastionContext::try_recv +/// [`try_recv_timeout`]: crate::context::BastionContext::try_recv_timeout pub enum ReceiveError { + /// We didn't receive a message on time Timeout(Duration), + /// Generic error. Not used yet Other, } + +#[derive(Error, Debug)] +/// `SendError`s occur when a message couldn't be dispatched through a distributor +pub enum SendError { + #[error("couldn't send message. Channel Disconnected.")] + /// Channel has been closed before we could send a message + Disconnected(Msg), + #[error("couldn't send message. Channel is Full.")] + /// Channel is full, can't send a message + Full(Msg), + #[error("couldn't send a message I should have not sent. {0}")] + /// This error is returned when we try to send a message + /// that is not a BastionMessage::Message variant + Other(anyhow::Error), + #[error("No available Distributor matching {0}")] + /// The distributor we're trying to dispatch messages to is not registered in the system + NoDistributor(String), + #[error("Distributor has 0 Recipients")] + /// The distributor we're trying to dispatch messages to has no recipients + EmptyRecipient, +} + +impl From<TrySendError<Envelope>> for SendError { + fn from(tse: TrySendError<Envelope>) -> Self { + let is_disconnected = tse.is_disconnected(); + match tse.into_inner().msg { + BastionMessage::Message(msg) => { + if is_disconnected { + Self::Disconnected(msg) + } else { + Self::Full(msg) + } + } + other => Self::Other(anyhow::anyhow!("{:?}", other)), + } + } +} + +impl From<Distributor> for SendError { + fn from(distributor: Distributor) -> Self { + Self::NoDistributor(STRING_INTERNER.resolve(distributor.interned()).to_string()) + } +} diff --git a/src/bastion/src/executor.rs b/src/bastion/src/executor.rs index 7e6d0c1e..cd8ef5cd 100644 --- a/src/bastion/src/executor.rs +++ b/src/bastion/src/executor.rs @@ -10,10 +10,23 @@ use std::future::Future; /// # Example /// ``` /// # use std::{thread, time}; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// use bastion::executor::blocking; /// let task = blocking(async move { /// thread::sleep(time::Duration::from_millis(3000)); /// }); +/// # } /// ``` pub fn blocking<F, R>(future: F) -> RecoverableHandle<R> where @@ -29,6 +42,18 @@ where /// # Example /// ``` /// # use bastion::prelude::*; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// use bastion::executor::run; /// let future1 = async move { /// 123 @@ -45,6 +70,7 @@ where /// /// let result = run(future2); /// assert_eq!(result, 5); +/// # } /// ``` pub fn run<F, T>(future: F) -> T where @@ -58,11 +84,24 @@ where /// # Example /// ``` /// # use bastion::prelude::*; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// use bastion::executor::{spawn, run}; /// let handle = spawn(async { /// panic!("test"); /// }); /// run(handle); +/// # } /// ``` pub fn spawn<F, T>(future: F) -> RecoverableHandle<T> where diff --git a/src/bastion/src/system.rs b/src/bastion/src/global_system.rs similarity index 98% rename from src/bastion/src/system.rs rename to src/bastion/src/global_system.rs index 69f229b2..9ff74f15 100644 --- a/src/bastion/src/system.rs +++ b/src/bastion/src/global_system.rs @@ -12,15 +12,17 @@ use futures::prelude::*; use futures::stream::FuturesUnordered; use futures::{pending, poll}; use fxhash::{FxHashMap, FxHashSet}; -use lazy_static::lazy_static; +use lasso::ThreadedRodeo; use lightproc::prelude::*; +use once_cell::sync::Lazy; use std::sync::{Arc, Condvar, Mutex}; use std::task::Poll; use tracing::{debug, error, info, trace, warn}; -lazy_static! { - pub(crate) static ref SYSTEM: GlobalSystem = System::init(); -} +pub(crate) static STRING_INTERNER: Lazy<Arc<ThreadedRodeo>> = + Lazy::new(|| Arc::new(Default::default())); + +pub(crate) static SYSTEM: Lazy<GlobalSystem> = Lazy::new(System::init); pub(crate) struct GlobalSystem { sender: Sender, diff --git a/src/bastion/src/lib.rs b/src/bastion/src/lib.rs index a41955a0..c79eae09 100644 --- a/src/bastion/src/lib.rs +++ b/src/bastion/src/lib.rs @@ -65,11 +65,13 @@ pub use self::config::Config; #[macro_use] mod macros; +mod actor; mod bastion; mod broadcast; mod callbacks; mod child; mod config; +mod global_system; mod system; pub mod child_ref; @@ -81,14 +83,18 @@ pub mod envelope; pub mod executor; #[cfg(not(target_os = "windows"))] pub mod io; +mod mailbox; pub mod message; pub mod path; #[cfg(feature = "scaling")] pub mod resizer; +mod routing; pub mod supervisor; pub mod errors; +pub mod distributor; + distributed_api! { // pub mod dist_messages; pub mod distributed; @@ -108,11 +114,12 @@ pub mod prelude { BroadcastTarget, DefaultDispatcherHandler, Dispatcher, DispatcherHandler, DispatcherMap, DispatcherType, NotificationType, }; + pub use crate::distributor::Distributor; pub use crate::envelope::{RefAddr, SignedMessage}; pub use crate::errors::*; #[cfg(not(target_os = "windows"))] pub use crate::io::*; - pub use crate::message::{Answer, AnswerSender, Message, Msg}; + pub use crate::message::{Answer, AnswerSender, Message, MessageHandler, Msg}; pub use crate::msg; pub use crate::path::{BastionPath, BastionPathElement}; #[cfg(feature = "scaling")] diff --git a/src/bastion/src/macros.rs b/src/bastion/src/macros.rs index 68d068e9..d577075e 100644 --- a/src/bastion/src/macros.rs +++ b/src/bastion/src/macros.rs @@ -8,7 +8,18 @@ /// /// ``` /// # use bastion::prelude::*; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// let children = children! { /// // the default redundancy is 1 /// redundancy: 100, @@ -133,7 +144,18 @@ macro_rules! children { /// # Example /// ``` /// # use bastion::prelude::*; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// let sp = supervisor! { /// callbacks: Callbacks::default(), /// strategy: SupervisionStrategy::OneForAll, @@ -189,7 +211,18 @@ macro_rules! supervisor { /// # use std::{thread, time}; /// # use lightproc::proc_stack::ProcStack; /// # use bastion::prelude::*; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// let task = blocking! { /// thread::sleep(time::Duration::from_millis(3000)); /// }; @@ -211,7 +244,18 @@ macro_rules! blocking { /// # Example /// ``` /// # use bastion::prelude::*; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// let future1 = async move { /// 123 /// }; @@ -245,7 +289,18 @@ macro_rules! run { /// # Example /// ``` /// # use bastion::prelude::*; +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// let handle = spawn! { /// panic!("test"); /// }; diff --git a/src/bastion/src/mailbox/envelope.rs b/src/bastion/src/mailbox/envelope.rs new file mode 100644 index 00000000..726011ec --- /dev/null +++ b/src/bastion/src/mailbox/envelope.rs @@ -0,0 +1,63 @@ +use std::fmt::{self, Debug, Formatter}; + +use crate::actor::actor_ref::ActorRef; +use crate::mailbox::message::MessageType; +use crate::mailbox::traits::TypedMessage; + +/// Struct that represents an incoming message in the actor's mailbox. +#[derive(Clone)] +pub struct Envelope<T> +where + T: TypedMessage, +{ + /// The sending side of a channel. In actor's world + /// represented is a message sender. Can be used + /// for acking message when it possible. + sender: Option<ActorRef>, + /// An actual data sent by the channel + message: T, + /// Message type that helps to figure out how to deliver message + /// and how to ack it after the processing. + message_type: MessageType, +} + +impl<T> Envelope<T> +where + T: TypedMessage, +{ + /// Create a message with the given sender and inner data. + pub fn new(sender: Option<ActorRef>, message: T, message_type: MessageType) -> Self { + Envelope { + sender, + message, + message_type, + } + } + + /// Returns a message type. Can be use for pattern matching and filtering + /// incoming message from other actors. + pub fn message_type(&self) -> MessageType { + self.message_type.clone() + } + + /// Sends a confirmation to the message sender. + pub(crate) async fn ack(&self) { + match self.message_type { + MessageType::Ack => unimplemented!(), + MessageType::Broadcast => unimplemented!(), + MessageType::Tell => unimplemented!(), + } + } +} + +impl<T> Debug for Envelope<T> +where + T: TypedMessage, +{ + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + fmt.debug_struct("Message") + .field("message", &self.message) + .field("message_type", &self.message_type) + .finish() + } +} diff --git a/src/bastion/src/mailbox/message.rs b/src/bastion/src/mailbox/message.rs new file mode 100644 index 00000000..9007dbf0 --- /dev/null +++ b/src/bastion/src/mailbox/message.rs @@ -0,0 +1,14 @@ +/// Enum that provides information what type of the message +/// being sent through the channel. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum MessageType { + /// A message type that requires sending a confirmation to the + /// sender after begin the processing stage. + Ack, + /// A message that can be broadcasted (e.g. via system dispatchers). This + /// message type doesn't require to be acked from the receiver's side. + Broadcast, + /// A message was sent directly and doesn't require confirmation for the + /// delivery and being processed. + Tell, +} diff --git a/src/bastion/src/mailbox/mod.rs b/src/bastion/src/mailbox/mod.rs new file mode 100644 index 00000000..7afeabb6 --- /dev/null +++ b/src/bastion/src/mailbox/mod.rs @@ -0,0 +1,166 @@ +mod envelope; +mod state; + +pub mod message; +pub mod traits; + +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +use async_channel::{unbounded, Receiver, Sender}; + +use crate::errors::{BastionError, BastionResult}; +use crate::mailbox::envelope::Envelope; +use crate::mailbox::state::MailboxState; +use crate::mailbox::traits::TypedMessage; + +/// Struct that represents a message sender. +#[derive(Clone)] +pub struct MailboxTx<T> +where + T: TypedMessage, +{ + /// Indicated the transmitter part of the actor's channel + /// which is using for passing messages. + tx: Sender<Envelope<T>>, + /// A field for checks that the message has been delivered to + /// the specific actor. + scheduled: Arc<AtomicBool>, +} + +impl<T> MailboxTx<T> +where + T: TypedMessage, +{ + /// Return a new instance of MailboxTx that indicates sender. + pub(crate) fn new(tx: Sender<Envelope<T>>) -> Self { + let scheduled = Arc::new(AtomicBool::new(false)); + MailboxTx { tx, scheduled } + } + + /// Send the message to the actor by the channel. + pub fn try_send(&self, msg: Envelope<T>) -> BastionResult<()> { + self.tx + .try_send(msg) + .map_err(|e| BastionError::ChanSend(e.to_string())) + } +} + +/// A struct that holds everything related to messages that can be +/// retrieved from other actors. Each actor holds two queues: one for +/// messages that come from user-defined actors, and another for +/// internal messaging that must be handled separately. +/// +/// For each used queue, mailbox always holds the latest requested message +/// by a user, to guarantee that the message won't be lost if something +/// happens wrong. +#[derive(Clone)] +pub struct Mailbox<T> +where + T: TypedMessage, +{ + /// User guardian sender + user_tx: MailboxTx<T>, + /// User guardian receiver + user_rx: Receiver<Envelope<T>>, + /// System guardian receiver + system_rx: Receiver<Envelope<T>>, + /// The current processing message, received from the + /// latest call to the user's queue + last_user_message: Option<Envelope<T>>, + /// The current processing message, received from the + /// latest call to the system's queue + last_system_message: Option<Envelope<T>>, + /// Mailbox state machine + state: Arc<MailboxState>, +} + +// TODO: Add calls with recv with timeout +impl<T> Mailbox<T> +where + T: TypedMessage, +{ + /// Creates a new mailbox for the actor. + pub(crate) fn new(system_rx: Receiver<Envelope<T>>) -> Self { + let (tx, user_rx) = unbounded(); + let user_tx = MailboxTx::new(tx); + let last_user_message = None; + let last_system_message = None; + let state = Arc::new(MailboxState::new()); + + Mailbox { + user_tx, + user_rx, + system_rx, + last_user_message, + last_system_message, + state, + } + } + + /// Forced receive message from user queue + pub async fn recv(&mut self) -> Envelope<T> { + let message = self + .user_rx + .recv() + .await + .map_err(|e| BastionError::ChanRecv(e.to_string())) + .unwrap(); + + self.last_user_message = Some(message); + self.last_user_message.clone().unwrap() + } + + /// Try receiving message from user queue + pub async fn try_recv(&mut self) -> BastionResult<Envelope<T>> { + if self.last_user_message.is_some() { + return Err(BastionError::UnackedMessage); + } + + match self.user_rx.try_recv() { + Ok(message) => { + self.last_user_message = Some(message); + Ok(self.last_user_message.clone().unwrap()) + } + Err(e) => Err(BastionError::ChanRecv(e.to_string())), + } + } + + /// Forced receive message from system queue + pub async fn sys_recv(&mut self) -> Envelope<T> { + let message = self + .system_rx + .recv() + .await + .map_err(|e| BastionError::ChanRecv(e.to_string())) + .unwrap(); + + self.last_system_message = Some(message); + self.last_system_message.clone().unwrap() + } + + /// Try receiving message from system queue + pub async fn try_sys_recv(&mut self) -> BastionResult<Envelope<T>> { + if self.last_system_message.is_some() { + return Err(BastionError::UnackedMessage); + } + + match self.system_rx.try_recv() { + Ok(message) => { + self.last_system_message = Some(message); + Ok(self.last_system_message.clone().unwrap()) + } + Err(e) => Err(BastionError::ChanRecv(e.to_string())), + } + } + + /// Returns the last retrieved message from the user channel + pub async fn get_last_user_message(&self) -> Option<Envelope<T>> { + self.last_user_message.clone() + } + + /// Returns the last retrieved message from the system channel + pub async fn get_last_system_message(&self) -> Option<Envelope<T>> { + self.last_system_message.clone() + } +} diff --git a/src/bastion/src/mailbox/state.rs b/src/bastion/src/mailbox/state.rs new file mode 100644 index 00000000..c22c5bd2 --- /dev/null +++ b/src/bastion/src/mailbox/state.rs @@ -0,0 +1,106 @@ +use crossbeam::atomic::AtomicCell; + +// Mailbox state holder +#[derive(Debug)] +pub(crate) struct MailboxState { + inner: AtomicCell<InnerState>, +} + +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +/// An enum that specifies a lifecycle of the message that has +/// been sent by the actor in the system. +/// +/// The whole lifecycle of the message can be described by the +/// next schema: +/// ```ignore +/// +/// +---- Message processed -----+ +/// ↓ | +/// Scheduled -> Sent -> Awaiting --+ +/// ↑ | +/// +-- Retry --+ +/// +/// ``` +enum InnerState { + /// Message has been scheduled to delivery + Scheduled, + /// Message has been sent to destination + Sent, + /// Ack has currently been awaited + Awaiting, +} + +impl MailboxState { + pub(crate) fn new() -> Self { + MailboxState { + inner: AtomicCell::new(InnerState::Scheduled), + } + } + + pub(crate) fn set_scheduled(&self) { + self.inner.store(InnerState::Scheduled) + } + + pub(crate) fn set_sent(&self) { + self.inner.store(InnerState::Sent) + } + + pub(crate) fn set_awaiting(&self) { + self.inner.store(InnerState::Awaiting) + } + + pub(crate) fn is_scheduled(&self) -> bool { + self.inner.load() == InnerState::Scheduled + } + + pub(crate) fn is_sent(&self) -> bool { + self.inner.load() == InnerState::Sent + } + + pub(crate) fn is_awaiting(&self) -> bool { + self.inner.load() == InnerState::Awaiting + } +} + +#[cfg(test)] +mod tests { + use crate::mailbox::state::MailboxState; + + #[test] + fn test_normal_path() { + let state = MailboxState::new(); + + assert_eq!(state.is_scheduled(), true); + + state.set_sent(); + assert_eq!(state.is_sent(), true); + + state.set_awaiting(); + assert_eq!(state.is_awaiting(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + } + + #[test] + fn test_path_with_retry() { + let state = MailboxState::new(); + + assert_eq!(state.is_scheduled(), true); + + state.set_sent(); + assert_eq!(state.is_sent(), true); + + state.set_awaiting(); + assert_eq!(state.is_awaiting(), true); + + state.set_sent(); + assert_eq!(state.is_sent(), true); + + state.set_awaiting(); + assert_eq!(state.is_awaiting(), true); + + state.set_scheduled(); + assert_eq!(state.is_scheduled(), true); + } +} diff --git a/src/bastion/src/mailbox/traits.rs b/src/bastion/src/mailbox/traits.rs new file mode 100644 index 00000000..c068800b --- /dev/null +++ b/src/bastion/src/mailbox/traits.rs @@ -0,0 +1,13 @@ +use std::fmt::Debug; + +/// A trait that message needs to implement for typed actors (it is +/// already automatically implemented but forces message to +/// implement the following traits: [`Any`], [`Send`], +/// [`Sync`] and [`Debug`]). +/// +/// [`Any`]: https://doc.rust-lang.org/std/any/trait.Any.html +/// [`Send`]: https://doc.rust-lang.org/std/marker/trait.Send.html +/// [`Sync`]: https://doc.rust-lang.org/std/marker/trait.Sync.html +/// [`Debug`]: https://doc.rust-lang.org/std/fmt/trait.Debug.html +pub trait TypedMessage: Clone + Send + Debug + 'static {} +impl<T> TypedMessage for T where T: Clone + Send + Debug + 'static {} diff --git a/src/bastion/src/message.rs b/src/bastion/src/message.rs index c1ccb914..688fab28 100644 --- a/src/bastion/src/message.rs +++ b/src/bastion/src/message.rs @@ -11,7 +11,7 @@ use crate::children::Children; use crate::context::{BastionId, ContextState}; use crate::envelope::{RefAddr, SignedMessage}; use crate::supervisor::{SupervisionStrategy, Supervisor}; -use async_mutex::Mutex; + use futures::channel::oneshot::{self, Receiver}; use std::any::{type_name, Any}; use std::fmt::Debug; @@ -26,20 +26,37 @@ use tracing::{debug, trace}; /// implement the following traits: [`Any`], [`Send`], /// [`Sync`] and [`Debug`]). /// +/// [`Any`]: std::any::Any +/// [`Send`]: std::marker::Send +/// [`Sync`]: std::marker::Sync +/// [`Debug`]: std::fmt::Debug +pub trait Message: Any + Send + Sync + Debug {} +impl<T> Message for T where T: Any + Send + Sync + Debug {} + +/// A trait that message needs to implement for typed actors (it is +/// already automatically implemented but forces message to +/// implement the following traits: [`Any`], [`Send`], +/// [`Sync`] and [`Debug`]). +/// /// [`Any`]: https://doc.rust-lang.org/std/any/trait.Any.html /// [`Send`]: https://doc.rust-lang.org/std/marker/trait.Send.html /// [`Sync`]: https://doc.rust-lang.org/std/marker/trait.Sync.html /// [`Debug`]: https://doc.rust-lang.org/std/fmt/trait.Debug.html -pub trait Message: Any + Send + Sync + Debug {} -impl<T> Message for T where T: Any + Send + Sync + Debug {} +pub trait TypedMessage: Clone + Send + Debug + 'static {} +impl<T> TypedMessage for T where T: Clone + Send + Debug + 'static {} +/// Allows to respond to questions. +/// +/// This type features the [`respond`] method, that allows to respond to a +/// question. +/// +/// [`respond`]: #method.respond #[derive(Debug)] -#[doc(hidden)] -pub struct AnswerSender(oneshot::Sender<SignedMessage>); +pub struct AnswerSender(oneshot::Sender<SignedMessage>, RefAddr); #[derive(Debug)] /// A [`Future`] returned when successfully "asking" a -/// message using [`ChildRef::ask`] and which resolves to +/// message using [`ChildRef::ask_anonymously`] and which resolves to /// a `Result<Msg, ()>` where the [`Msg`] is the message /// answered by the child (see the [`msg!`] macro for more /// information). @@ -49,7 +66,18 @@ pub struct AnswerSender(oneshot::Sender<SignedMessage>); /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// // The message that will be "asked"... /// const ASK_MSG: &'static str = "A message containing data (ask)."; @@ -109,10 +137,8 @@ pub struct AnswerSender(oneshot::Sender<SignedMessage>); /// # } /// ``` /// -/// [`Future`]: https://doc.rust-lang.org/std/future/trait.Future.html -/// [`ChildRef::ask`]: ../children/struct.ChildRef.html#method.ask -/// [`Msg`]: message/struct.Msg.html -/// [`msg!`]: macro.msg.html +/// [`Future`]: std::future::Future +/// [`ChildRef::ask_anonymously`]: crate::child_ref::ChildRef::ask_anonymously pub struct Answer(Receiver<SignedMessage>); #[derive(Debug)] @@ -125,7 +151,18 @@ pub struct Answer(Receiver<SignedMessage>); /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// Bastion::children(|children| { /// children.with_exec(|ctx: BastionContext| { @@ -171,9 +208,8 @@ pub struct Answer(Receiver<SignedMessage>); /// # } /// ``` /// -/// [`BastionContext::recv`]: context/struct.BastionContext.html#method.recv -/// [`BastionContext::try_recv`]: context/struct.BastionContext.html#method.try_recv -/// [`msg!`]: macro.msg.html +/// [`BastionContext::recv`]: crate::context::BastionContext::recv +/// [`BastionContext::try_recv`]: crate::context::BastionContext::try_recv pub struct Msg(MsgInner); #[derive(Debug)] @@ -200,7 +236,7 @@ pub(crate) enum BastionMessage { InstantiatedChild { parent_id: BastionId, child_id: BastionId, - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, }, Message(Msg), RestartRequired { @@ -214,13 +250,13 @@ pub(crate) enum BastionMessage { RestartSubtree, RestoreChild { id: BastionId, - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, }, DropChild { id: BastionId, }, SetState { - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, }, Stopped { id: BastionId, @@ -238,14 +274,17 @@ pub(crate) enum Deployment { } impl AnswerSender { - // FIXME: we can't let manipulating Signature in a public API - // but now it's being called only by a macro so we are trusting it - #[doc(hidden)] - pub fn send<M: Message>(self, msg: M, sign: RefAddr) -> Result<(), M> { + /// Sends data back to the original sender. + /// + /// Returns `Ok` if the data was sent successfully, otherwise returns the + /// original data. + pub fn reply<M: Message>(self, msg: M) -> Result<(), M> { debug!("{:?}: Sending answer: {:?}", self, msg); let msg = Msg::tell(msg); trace!("{:?}: Sending message: {:?}", self, msg); - self.0 + + let AnswerSender(sender, sign) = self; + sender .send(SignedMessage::new(msg, sign)) .map_err(|smsg| smsg.msg.try_unwrap().unwrap()) } @@ -262,10 +301,10 @@ impl Msg { Msg(inner) } - pub(crate) fn ask<M: Message>(msg: M) -> (Self, Answer) { + pub(crate) fn ask<M: Message>(msg: M, sign: RefAddr) -> (Self, Answer) { let msg = Box::new(msg); let (sender, recver) = oneshot::channel(); - let sender = AnswerSender(sender); + let sender = AnswerSender(sender, sign); let answer = Answer(recver); let sender = Some(sender); @@ -378,6 +417,16 @@ impl Msg { } } +impl AsRef<dyn Any> for Msg { + fn as_ref(&self) -> &dyn Any { + match &self.0 { + MsgInner::Broadcast(msg) => msg.as_ref(), + MsgInner::Tell(msg) => msg.as_ref(), + MsgInner::Ask { msg, .. } => msg.as_ref(), + } + } +} + impl BastionMessage { pub(crate) fn start() -> Self { BastionMessage::Start @@ -418,7 +467,7 @@ impl BastionMessage { pub(crate) fn instantiated_child( parent_id: BastionId, child_id: BastionId, - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, ) -> Self { BastionMessage::InstantiatedChild { parent_id, @@ -437,8 +486,8 @@ impl BastionMessage { BastionMessage::Message(msg) } - pub(crate) fn ask<M: Message>(msg: M) -> (Self, Answer) { - let (msg, answer) = Msg::ask(msg); + pub(crate) fn ask<M: Message>(msg: M, sign: RefAddr) -> (Self, Answer) { + let (msg, answer) = Msg::ask(msg, sign); (BastionMessage::Message(msg), answer) } @@ -454,7 +503,7 @@ impl BastionMessage { BastionMessage::RestartSubtree } - pub(crate) fn restore_child(id: BastionId, state: Arc<Mutex<Pin<Box<ContextState>>>>) -> Self { + pub(crate) fn restore_child(id: BastionId, state: Arc<Pin<Box<ContextState>>>) -> Self { BastionMessage::RestoreChild { id, state } } @@ -462,7 +511,7 @@ impl BastionMessage { BastionMessage::DropChild { id } } - pub(crate) fn set_state(state: Arc<Mutex<Pin<Box<ContextState>>>>) -> Self { + pub(crate) fn set_state(state: Arc<Pin<Box<ContextState>>>) -> Self { BastionMessage::SetState { state } } @@ -572,7 +621,18 @@ impl Future for Answer { /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// // The message that will be broadcasted... /// const BCAST_MSG: &'static str = "A message containing data (broadcast)."; @@ -625,9 +685,8 @@ impl Future for Answer { /// # } /// ``` /// -/// [`Msg`]: children/struct.Msg.html -/// [`BastionContext::recv`]: context/struct.BastionContext.html#method.recv -/// [`BastionContext::try_recv`]: context/struct.BastionContext.html#method.try_recv +/// [`BastionContext::recv`]: crate::context::BastionContext::recv +/// [`BastionContext::try_recv`]: crate::context::BastionContext::try_recv macro_rules! msg { ($msg:expr, $($tokens:tt)+) => { msg!(@internal $msg, (), (), (), $($tokens)+) @@ -734,7 +793,7 @@ macro_rules! msg { ($ctx:expr, $answer:expr) => { { let sign = $ctx.signature(); - sender.send($answer, sign) + sender.reply($answer) } }; } @@ -776,7 +835,18 @@ macro_rules! msg { /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # let children_ref = /// // Create a new child... @@ -818,6 +888,263 @@ macro_rules! answer { ($msg:expr, $answer:expr) => {{ let (mut msg, sign) = $msg.extract(); let sender = msg.take_sender().expect("failed to take render"); - sender.send($answer, sign) + sender.reply($answer) }}; } + +#[derive(Debug)] +enum MessageHandlerState<O> { + Matched(O), + Unmatched(SignedMessage), +} + +impl<O> MessageHandlerState<O> { + fn take_message(self) -> Result<SignedMessage, O> { + match self { + MessageHandlerState::Unmatched(msg) => Ok(msg), + MessageHandlerState::Matched(output) => Err(output), + } + } + + fn output_or_else(self, f: impl FnOnce(SignedMessage) -> O) -> O { + match self { + MessageHandlerState::Matched(output) => output, + MessageHandlerState::Unmatched(msg) => f(msg), + } + } +} + +/// Matches a [`Msg`] (as returned by [`BastionContext::recv`] +/// or [`BastionContext::try_recv`]) with different types. +/// +/// This type may replace the [`msg!`] macro in the future. +/// +/// The [`new`] function creates a new [`MessageHandler`], which is then +/// matched on with the `on_*` functions. +/// +/// There are different kind of messages: +/// - messages that are broadcasted, which can be matched with the +/// [`on_broadcast`] method, +/// - messages that can be responded to, which are matched with the +/// [`on_question`] method, +/// - messages that can not be responded to, which are matched with +/// [`on_tell`], +/// - fallback case, which matches everything, entitled [`on_fallback`]. +/// +/// The closure passed to the functions described previously must return the +/// same type. This value is retrieved when [`on_fallback`] is invoked. +/// +/// Questions can be responded to by calling [`reply`] on the provided +/// sender. +/// +/// # Example +/// +/// ```rust +/// # use bastion::prelude::*; +/// # use bastion::message::MessageHandler; +/// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { +/// # Bastion::init(); +/// // The message that will be broadcasted... +/// const BCAST_MSG: &'static str = "A message containing data (broadcast)."; +/// // The message that will be "told" to the child... +/// const TELL_MSG: &'static str = "A message containing data (tell)."; +/// // The message that will be "asked" to the child... +/// const ASK_MSG: &'static str = "A message containing data (ask)."; +/// +/// Bastion::children(|children| { +/// children.with_exec(|ctx: BastionContext| { +/// async move { +/// # ctx.tell(&ctx.current().addr(), TELL_MSG).unwrap(); +/// # ctx.ask(&ctx.current().addr(), ASK_MSG).unwrap(); +/// # +/// loop { +/// MessageHandler::new(ctx.recv().await?) +/// // We match on broadcasts of &str +/// .on_broadcast(|msg: &&str, _sender_addr| { +/// assert_eq!(*msg, BCAST_MSG); +/// // Handle the message... +/// }) +/// // We match on messages of &str +/// .on_tell(|msg: &str, _sender_addr| { +/// assert_eq!(msg, TELL_MSG); +/// // Handle the message... +/// }) +/// // We match on questions of &str +/// .on_question(|msg: &str, sender| { +/// assert_eq!(msg, ASK_MSG); +/// // Handle the message... +/// +/// // ...and eventually answer to it... +/// sender.reply("An answer to the message."); +/// }) +/// // We are only broadcasting, "telling" and "asking" a +/// // `&str` in this example, so we know that this won't +/// // happen... +/// .on_fallback(|msg, _sender_addr| ()); +/// } +/// } +/// }) +/// }).expect("Couldn't start the children group."); +/// # +/// # Bastion::start(); +/// # Bastion::broadcast(BCAST_MSG).unwrap(); +/// # Bastion::stop(); +/// # Bastion::block_until_stopped(); +/// # } +/// ``` +/// +/// [`BastionContext::recv`]: crate::context::BastionContext::recv +/// [`BastionContext::try_recv`]: crate::context::BastionContext::try_recv +/// [`new`]: Self::new +/// [`on_broadcast`]: Self::on_broadcast +/// [`on_question`]: Self::on_question +/// [`on_tell`]: Self::on_tell +/// [`on_fallback`]: Self::on_fallback +/// [`reply`]: AnswerSender::reply +#[derive(Debug)] +pub struct MessageHandler<O> { + state: MessageHandlerState<O>, +} + +impl<O> MessageHandler<O> { + /// Creates a new [`MessageHandler`] with an incoming message. + pub fn new(msg: SignedMessage) -> MessageHandler<O> { + let state = MessageHandlerState::Unmatched(msg); + MessageHandler { state } + } + + /// Matches on a question of a specific type. + /// + /// This will consume the inner data and call `f` if the contained message + /// can be replied to. + pub fn on_question<T, F>(self, f: F) -> MessageHandler<O> + where + T: 'static, + F: FnOnce(T, AnswerSender) -> O, + { + match self.try_into_question::<T>() { + Ok((arg, sender)) => { + let val = f(arg, sender); + MessageHandler::matched(val) + } + Err(this) => this, + } + } + + /// Calls a fallback function if the message has still not matched yet. + /// + /// This consumes the [`MessageHandler`], so that no matching can be + /// performed anymore. + pub fn on_fallback<F>(self, f: F) -> O + where + F: FnOnce(&dyn Any, RefAddr) -> O, + { + self.state + .output_or_else(|SignedMessage { msg, sign }| f(msg.as_ref(), sign)) + } + + /// Calls a function if the incoming message is a broadcast and has a + /// specific type. + pub fn on_broadcast<T, F>(self, f: F) -> MessageHandler<O> + where + T: 'static + Send + Sync, + F: FnOnce(&T, RefAddr) -> O, + { + match self.try_into_broadcast::<T>() { + Ok((arg, addr)) => { + let val = f(arg.as_ref(), addr); + MessageHandler::matched(val) + } + Err(this) => this, + } + } + + /// Calls a function if the incoming message can't be replied to and has a + /// specific type. + pub fn on_tell<T, F>(self, f: F) -> MessageHandler<O> + where + T: Debug + 'static, + F: FnOnce(T, RefAddr) -> O, + { + match self.try_into_tell::<T>() { + Ok((msg, addr)) => { + let val = f(msg, addr); + MessageHandler::matched(val) + } + Err(this) => this, + } + } + + fn matched(output: O) -> MessageHandler<O> { + let state = MessageHandlerState::Matched(output); + MessageHandler { state } + } + + fn try_into_question<T: 'static>(self) -> Result<(T, AnswerSender), MessageHandler<O>> { + debug!("try_into_question with type {}", std::any::type_name::<T>()); + match self.state.take_message() { + Ok(SignedMessage { + msg: + Msg(MsgInner::Ask { + msg, + sender: Some(sender), + }), + .. + }) if msg.is::<T>() => { + let msg: Box<dyn Any> = msg; + Ok((*msg.downcast::<T>().unwrap(), sender)) + } + + Ok(anything) => Err(MessageHandler::new(anything)), + Err(output) => Err(MessageHandler::matched(output)), + } + } + + fn try_into_broadcast<T: Send + Sync + 'static>( + self, + ) -> Result<(Arc<T>, RefAddr), MessageHandler<O>> { + debug!( + "try_into_broadcast with type {}", + std::any::type_name::<T>() + ); + match self.state.take_message() { + Ok(SignedMessage { + msg: Msg(MsgInner::Broadcast(msg)), + sign, + }) if msg.is::<T>() => { + let msg: Arc<dyn Any + Send + Sync + 'static> = msg; + Ok((msg.downcast::<T>().unwrap(), sign)) + } + + Ok(anything) => Err(MessageHandler::new(anything)), + Err(output) => Err(MessageHandler::matched(output)), + } + } + + fn try_into_tell<T: Debug + 'static>(self) -> Result<(T, RefAddr), MessageHandler<O>> { + debug!("try_into_tell with type {}", std::any::type_name::<T>()); + match self.state.take_message() { + Ok(SignedMessage { + msg: Msg(MsgInner::Tell(msg)), + sign, + }) if msg.is::<T>() => { + let msg: Box<dyn Any> = msg; + Ok((*msg.downcast::<T>().unwrap(), sign)) + } + Ok(anything) => Err(MessageHandler::new(anything)), + Err(output) => Err(MessageHandler::matched(output)), + } + } +} diff --git a/src/bastion/src/path.rs b/src/bastion/src/path.rs index ab4fec16..ee856ae0 100644 --- a/src/bastion/src/path.rs +++ b/src/bastion/src/path.rs @@ -17,7 +17,18 @@ use std::result::Result; /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// /// # Bastion::children(|children| { @@ -72,7 +83,18 @@ impl BastionPath { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -113,7 +135,18 @@ impl BastionPath { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -154,7 +187,18 @@ impl BastionPath { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let children_ref = Bastion::children(|children| children).unwrap(); @@ -205,19 +249,8 @@ impl fmt::Display for BastionPath { impl fmt::Debug for BastionPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.this { - Some(this @ BastionPathElement::Supervisor(_)) => write!( - f, - "/{}", - self.parent_chain - .iter() - .map(|id| BastionPathElement::Supervisor(id.clone())) - .chain(vec![this.clone()]) - .map(|el| format!("{:?}", el)) - .collect::<Vec<String>>() - .join("/") - ), - // TODO: combine with the pattern above when or-patterns become stable - Some(this @ BastionPathElement::Children(_)) => write!( + Some(this @ BastionPathElement::Supervisor(_)) + | Some(this @ BastionPathElement::Children(_)) => write!( f, "/{}", self.parent_chain @@ -263,6 +296,18 @@ impl fmt::Debug for BastionPath { /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// @@ -284,6 +329,7 @@ impl fmt::Debug for BastionPath { /// # /// # Bastion::start(); /// # Bastion::block_until_stopped(); +/// # } /// ``` pub enum BastionPathElement { #[doc(hidden)] @@ -333,6 +379,18 @@ impl BastionPathElement { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// /// Bastion::children(|children| { @@ -353,6 +411,7 @@ impl BastionPathElement { /// # /// # Bastion::start(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn is_child(&self) -> bool { matches!(self, BastionPathElement::Child(_)) diff --git a/src/bastion/src/resizer.rs b/src/bastion/src/resizer.rs index a6a20266..e46c5744 100644 --- a/src/bastion/src/resizer.rs +++ b/src/bastion/src/resizer.rs @@ -112,9 +112,23 @@ impl OptimalSizeExploringResizer { self.actor_stats.clone() } + /// Returns lower bound of the number of actors in the scaling group. + pub(crate) fn lower_bound(&self) -> u64 { + self.lower_bound + } + + /// Set lower bound of the autoscaling group. + pub(crate) fn set_lower_bound(&mut self, lower_bound: u64) { + self.lower_bound = lower_bound; + } + /// Overrides the minimal amount of actors available to use. pub fn with_lower_bound(mut self, lower_bound: u64) -> Self { - self.lower_bound = lower_bound; + if lower_bound == u64::MIN { + self.lower_bound = lower_bound.saturating_add(1); + } else { + self.lower_bound = lower_bound; + } self } diff --git a/src/bastion/src/routing/mod.rs b/src/bastion/src/routing/mod.rs new file mode 100644 index 00000000..f8a360ae --- /dev/null +++ b/src/bastion/src/routing/mod.rs @@ -0,0 +1,5 @@ +pub mod path; +pub mod target; + +pub use crate::routing::path::{ActorPath, NodeType, Scope}; +pub use crate::routing::target::Target; diff --git a/src/bastion/src/routing/path.rs b/src/bastion/src/routing/path.rs new file mode 100644 index 00000000..460857d5 --- /dev/null +++ b/src/bastion/src/routing/path.rs @@ -0,0 +1,399 @@ +//! +//! Module with structs for handling paths on the cluster, a system +//! or a local group level. +//! +use std::net::SocketAddr; +use std::string::ToString; +use uuid::Uuid; + +/// Special wrapper for handling actor's path and +/// message distribution. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ActorPath { + /// Node name in the cluster. + node_name: String, + /// Defines actors in the local or the remote node. + node_type: NodeType, + /// Defines actors in the top-level namespace. + scope: Scope, + /// A unique name of the actor or namespace + name: String, +} + +/// A part of path that defines remote or local machine +/// with running supervisors and actors. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum NodeType { + /// The message must be delivered in terms of + /// the local node. + Local, + /// The message must be delivered to the remote + /// node in the cluster by the certain host and port. + Remote(SocketAddr), +} + +/// A part of path that defines to what part of the node +/// the message must be delivered. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Scope { + /// Broadcast the message to user-defined actors, defined + /// before starting an application. + User, + /// Broadcast the message to top-level built-in actors. For + /// example it can be logging, configuration, heartbeat actors. + System, + /// The message wasn't delivered because the node was + /// stopped or not available. + DeadLetter, + /// The message must be delivered to short-living actors or subtrees of + /// actors spawned in runtime. + Temporary, +} + +impl ActorPath { + /// Returns a ActorPath instance, constructed from parts. + pub(crate) fn new(node_name: &str, node_type: NodeType, scope: Scope, name: &str) -> Self { + ActorPath { + node_name: node_name.to_string(), + node_type, + scope, + name: name.to_string(), + } + } + + /// Replaces the existing node name onto the new one. + pub fn node_name(mut self, node_name: &str) -> Self { + self.node_name = node_name.to_string(); + self + } + + /// Replaces the existing node type onto the new one. + pub fn node_type(mut self, node_type: NodeType) -> Self { + self.node_type = node_type; + self + } + + /// Replaces the existing scope onto the new one. + pub fn scope(mut self, scope: Scope) -> Self { + self.scope = scope; + self + } + + /// Replaces the existing actor name onto the new one. + pub fn name(mut self, name: &str) -> Self { + self.name = name.trim_start_matches("/").to_string(); + self + } + + /// Method for checking that the path is related to the local node + pub fn is_local(&self) -> bool { + self.node_type == NodeType::Local + } + + /// Method for checking that the path is related to the remote node + pub fn is_remote(&self) -> bool { + match self.node_type { + NodeType::Remote(_) => true, + _ => false, + } + } + + /// Method for checking that path is addressing to user-defined actors + pub fn is_user_scope(&self) -> bool { + self.scope == Scope::User + } + + /// Method for checking that path is addressing to system actors + pub fn is_system_scope(&self) -> bool { + self.scope == Scope::System + } + + /// Method for checking that path is addressing to dead letter scope + pub fn is_dead_letter_scope(&self) -> bool { + self.scope == Scope::DeadLetter + } + + /// Method for checking that path is addressing to temporary actors + pub fn is_temporary_scope(&self) -> bool { + self.scope == Scope::Temporary + } +} + +impl Default for ActorPath { + fn default() -> Self { + let unique_id = Uuid::new_v4().to_string(); + ActorPath::new("node", NodeType::Local, Scope::User, &unique_id) + } +} + +impl ToString for ActorPath { + fn to_string(&self) -> String { + let node_type = self.node_type.to_string(); + let scope = self.scope.as_str(); + format!( + "bastion://{}{}/{}/{}", + self.node_name, node_type, scope, self.name + ) + } +} + +impl ToString for NodeType { + fn to_string(&self) -> String { + match self { + NodeType::Local => String::new(), + NodeType::Remote(address) => format!("@{}", address.to_string()), + } + } +} + +impl Scope { + fn as_str(&self) -> &str { + match self { + Scope::User => "user", + Scope::System => "system", + Scope::DeadLetter => "dead_letter", + Scope::Temporary => "temporary", + } + } +} + +#[cfg(test)] +mod tests { + use crate::routing::path::{ActorPath, NodeType, Scope}; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + + #[test] + fn construct_local_user_path_group() { + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Local) + .scope(Scope::User) + .name("processing/1"); + + assert_eq!(instance.to_string(), "bastion://test/user/processing/1"); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_user_scope(), true); + } + + #[test] + fn construct_local_system_path_group() { + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Local) + .scope(Scope::System) + .name("processing/1"); + + assert_eq!(instance.to_string(), "bastion://test/system/processing/1"); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_system_scope(), true); + } + + #[test] + fn construct_local_dead_letter_path_group() { + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Local) + .scope(Scope::DeadLetter) + .name("processing/1"); + + assert_eq!( + instance.to_string(), + "bastion://test/dead_letter/processing/1" + ); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_dead_letter_scope(), true); + } + + #[test] + fn construct_local_temporary_path_group() { + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Local) + .scope(Scope::Temporary) + .name("processing/1"); + + assert_eq!( + instance.to_string(), + "bastion://test/temporary/processing/1" + ); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_temporary_scope(), true); + } + + #[test] + fn construct_remote_user_path_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Remote(address)) + .scope(Scope::User) + .name("processing/1"); + + assert_eq!( + instance.to_string(), + "bastion://test@127.0.0.1:8080/user/processing/1" + ); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_user_scope(), true); + } + + #[test] + fn construct_remote_system_path_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Remote(address)) + .scope(Scope::System) + .name("processing/1"); + + assert_eq!( + instance.to_string(), + "bastion://test@127.0.0.1:8080/system/processing/1" + ); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_system_scope(), true); + } + + #[test] + fn construct_remote_dead_letter_path_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Remote(address)) + .scope(Scope::DeadLetter) + .name("processing/1"); + + assert_eq!( + instance.to_string(), + "bastion://test@127.0.0.1:8080/dead_letter/processing/1" + ); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_dead_letter_scope(), true); + } + + #[test] + fn construct_remote_temporary_path_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_name("test") + .node_type(NodeType::Remote(address)) + .scope(Scope::Temporary) + .name("processing/1"); + + assert_eq!( + instance.to_string(), + "bastion://test@127.0.0.1:8080/temporary/processing/1" + ); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_temporary_scope(), true); + } + + #[test] + fn construct_local_user_path_without_group() { + let instance = ActorPath::default() + .node_type(NodeType::Local) + .scope(Scope::User) + .name("1"); + + assert_eq!(instance.to_string(), "bastion://node/user/1"); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_user_scope(), true); + } + + #[test] + fn construct_local_system_path_without_group() { + let instance = ActorPath::default() + .node_type(NodeType::Local) + .scope(Scope::System) + .name("1"); + + assert_eq!(instance.to_string(), "bastion://node/system/1"); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_system_scope(), true); + } + + #[test] + fn construct_local_dead_letter_path_without_group() { + let instance = ActorPath::default() + .node_type(NodeType::Local) + .scope(Scope::DeadLetter) + .name("1"); + + assert_eq!(instance.to_string(), "bastion://node/dead_letter/1"); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_dead_letter_scope(), true); + } + + #[test] + fn construct_local_temporary_path_without_group() { + let instance = ActorPath::default() + .node_type(NodeType::Local) + .scope(Scope::Temporary) + .name("1"); + + assert_eq!(instance.to_string(), "bastion://node/temporary/1"); + assert_eq!(instance.is_local(), true); + assert_eq!(instance.is_temporary_scope(), true); + } + + #[test] + fn construct_remote_user_path_without_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_type(NodeType::Remote(address)) + .scope(Scope::User) + .name("1"); + + assert_eq!(instance.to_string(), "bastion://node@127.0.0.1:8080/user/1"); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_user_scope(), true); + } + + #[test] + fn construct_remote_system_path_without_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_type(NodeType::Remote(address)) + .scope(Scope::System) + .name("1"); + + assert_eq!( + instance.to_string(), + "bastion://node@127.0.0.1:8080/system/1" + ); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_system_scope(), true); + } + + #[test] + fn construct_remote_dead_letter_path_without_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_type(NodeType::Remote(address)) + .scope(Scope::DeadLetter) + .name("1"); + + assert_eq!( + instance.to_string(), + "bastion://node@127.0.0.1:8080/dead_letter/1" + ); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_dead_letter_scope(), true); + } + + #[test] + fn construct_remote_temporary_path_without_group() { + let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let instance = ActorPath::default() + .node_type(NodeType::Remote(address)) + .scope(Scope::Temporary) + .name("1"); + + assert_eq!( + instance.to_string(), + "bastion://node@127.0.0.1:8080/temporary/1" + ); + assert_eq!(instance.is_remote(), true); + assert_eq!(instance.is_temporary_scope(), true); + } +} diff --git a/src/bastion/src/routing/target.rs b/src/bastion/src/routing/target.rs new file mode 100644 index 00000000..b7b082b1 --- /dev/null +++ b/src/bastion/src/routing/target.rs @@ -0,0 +1,261 @@ +use crate::routing::ActorPath; +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + static ref WILDCARD_REGEX: Regex = Regex::new(r"(\*/)").unwrap(); +} + +/// An enum for handling targeting messages to the certain +/// actor, a group of actors, a namespace or a scope. +#[derive(Debug, Clone)] +pub enum Target<'a> { + /// The message must be delivered to the actor(s) by + /// the matched path. + Path(ActorPath), + /// The message must be delivered to actor(s), organized + /// under the named group. + Group(&'a str), +} + +/// A wrapper for the Target enum type that provides an additional +/// functionality in matching actor paths with the desired pattern. +pub(crate) struct EnvelopeTarget<'a> { + target: Target<'a>, + regex: Option<Regex>, +} + +impl<'a> EnvelopeTarget<'a> { + /// Compares the given path with the declared path + pub(crate) fn is_match(&self, actor_path: &ActorPath) -> bool { + match &self.target { + // Just do a regular matching by path, or via the regular + // expression if the path contains wildcard symbols. + Target::Path(path) => match &self.regex { + Some(regex) => { + let stringified_path = actor_path.to_string(); + regex.is_match(&stringified_path) + } + None => actor_path == path, + }, + // False, because the group name is not a part of the actor's path. + Target::Group(_) => false, + } + } + + /// Returns a group name, extracted from the target field. + pub(crate) fn get_group_name(&self) -> Option<&'a str> { + match self.target { + Target::Group(group_name) => Some(group_name), + _ => None, + } + } +} + +impl<'a> From<Target<'a>> for EnvelopeTarget<'a> { + fn from(target: Target<'a>) -> Self { + let regex = match &target { + // For a path regex is optional. But it needs to be generated, if + // the user specified wildcards, such as asterisks symbols. + Target::Path(path) => { + let stringified_path = path.to_string(); + + match stringified_path.contains("/*") { + // Exists at least one wildcard that needs to be replaced + // onto the regular expression. + true => { + let raw_regex = match stringified_path.ends_with("/*") { + // Necessary to replace the ending, so that any path with + // any nested level can be considered as a correct one. + true => { + let mut fixed_path = stringified_path; + let index = fixed_path.rfind("/*").unwrap(); + fixed_path.replace_range(index.., r"/[[\w\d\-_]/?]+"); + fixed_path + } + // Common case: the wildcard closed by the "/" character + false => stringified_path, + }; + + let raw_regex = format!( + "{}$", + WILDCARD_REGEX.replace_all(&raw_regex, r"[\w\d\-_.]+/") + ); + let compiled_regex = Regex::new(&raw_regex).unwrap(); + Some(compiled_regex) + } + // Wildcard wasn't specified in the path: no needed to + // generate a regular expression here. + false => None, + } + } + // For a group regex doesn't required. Always do + // a direct string comparison in dispatchers. + Target::Group(_) => None, + }; + + EnvelopeTarget { target, regex } + } +} + +#[cfg(test)] +mod message_target_tests { + use crate::routing::path::ActorPath; + use crate::routing::target::{EnvelopeTarget, Target}; + + #[test] + fn test_get_group_name_returns_str_reference() { + let target = Target::Group("test"); + let envelope_target = EnvelopeTarget::from(target); + + let result = envelope_target.get_group_name(); + assert_eq!(result.is_some(), true); + assert_eq!(result.unwrap(), "test"); + } + + #[test] + fn test_get_group_name_returns_none_for_target_path_type() { + let path = ActorPath::default().name("test"); + let target = Target::Path(path.clone()); + let envelope_target = EnvelopeTarget::from(target); + + let result = envelope_target.get_group_name(); + assert_eq!(result.is_none(), true); + } + + #[test] + fn test_match_by_path_with_without_a_wildcard_returns_true() { + let path = ActorPath::default().name("test"); + let target = Target::Path(path.clone()); + let envelope_target = EnvelopeTarget::from(target); + + assert_eq!(envelope_target.is_match(&path), true); + } + + #[test] + fn test_match_by_path_with_without_a_wildcard_returns_false() { + let path = ActorPath::default().name("test"); + let target = Target::Path(path); + let envelope_target = EnvelopeTarget::from(target); + + let validated_path = ActorPath::default().name("not_matched"); + assert_eq!(envelope_target.is_match(&validated_path), false); + } + + #[test] + fn test_match_by_path_with_a_single_wildcard_in_the_beginning() { + let path = ActorPath::default().name("*/processing/a"); + let target = Target::Path(path); + let envelope_target = EnvelopeTarget::from(target); + + let valid_path_1 = ActorPath::default().name("first/processing/a"); + let valid_path_2 = ActorPath::default().name("second/processing/a"); + let invalid_path = ActorPath::default().name("third/handling/a"); + assert_eq!(envelope_target.is_match(&valid_path_1), true); + assert_eq!(envelope_target.is_match(&valid_path_2), true); + assert_eq!(envelope_target.is_match(&invalid_path), false); + } + + #[test] + fn test_match_by_path_with_a_single_wildcard_in_the_middle() { + let path = ActorPath::default().name("first/*/a"); + let target = Target::Path(path); + let envelope_target = EnvelopeTarget::from(target); + + let valid_path = ActorPath::default().name("first/processing/a"); + let invalid_path_1 = ActorPath::default().name("first/processing/b"); + let invalid_path_2 = ActorPath::default().name("first/processing/nested/a"); + let invalid_path_3 = ActorPath::default().name("second/handling/nested/a"); + assert_eq!(envelope_target.is_match(&valid_path), true); + assert_eq!(envelope_target.is_match(&invalid_path_1), false); + assert_eq!(envelope_target.is_match(&invalid_path_2), false); + assert_eq!(envelope_target.is_match(&invalid_path_3), false); + } + + #[test] + fn test_match_by_path_with_a_single_wildcard_in_the_end() { + let path = ActorPath::default().name("first/*"); + let target = Target::Path(path); + let envelope_target = EnvelopeTarget::from(target); + + let valid_path_1 = ActorPath::default().name("first/a"); + let valid_path_2 = ActorPath::default().name("first/processing/a"); + let valid_path_3 = ActorPath::default().name("first/processing/b"); + let valid_path_4 = ActorPath::default().name("first/processing/nested/a"); + let invalid_path_1 = ActorPath::default().name("second/a"); + let invalid_path_2 = ActorPath::default().name("second/nested/a"); + assert_eq!(envelope_target.is_match(&valid_path_1), true); + assert_eq!(envelope_target.is_match(&valid_path_2), true); + assert_eq!(envelope_target.is_match(&valid_path_3), true); + assert_eq!(envelope_target.is_match(&valid_path_4), true); + assert_eq!(envelope_target.is_match(&invalid_path_1), false); + assert_eq!(envelope_target.is_match(&invalid_path_2), false); + } + + #[test] + fn test_match_by_path_with_a_single_and_limited_nesting_wildcard() { + let path = ActorPath::default().name("first/*/"); + let target = Target::Path(path); + let envelope_target = EnvelopeTarget::from(target); + + let valid_path_1 = ActorPath::default().name("first/a/"); + let valid_path_2 = ActorPath::default().name("first/b/"); + let invalid_path_1 = ActorPath::default().name("first/handling/a"); + let invalid_path_2 = ActorPath::default().name("first/processing/b"); + let invalid_path_3 = ActorPath::default().name("first/processing/nested/a"); + let invalid_path_4 = ActorPath::default().name("second/a"); + let invalid_path_5 = ActorPath::default().name("second/nested/b"); + assert_eq!(envelope_target.is_match(&valid_path_1), true); + assert_eq!(envelope_target.is_match(&valid_path_2), true); + assert_eq!(envelope_target.is_match(&invalid_path_1), false); + assert_eq!(envelope_target.is_match(&invalid_path_2), false); + assert_eq!(envelope_target.is_match(&invalid_path_3), false); + assert_eq!(envelope_target.is_match(&invalid_path_4), false); + assert_eq!(envelope_target.is_match(&invalid_path_5), false); + } + + #[test] + fn test_match_by_path_with_multiple_wildcards() { + let path = ActorPath::default().name("*/*/a"); + let target = Target::Path(path); + let envelope_target = EnvelopeTarget::from(target); + + let valid_path_1 = ActorPath::default().name("first/handling/a"); + let valid_path_2 = ActorPath::default().name("first/processing/a"); + let invalid_path_1 = ActorPath::default().name("first/processing/nested/a"); + let invalid_path_2 = ActorPath::default().name("second/a"); + let invalid_path_3 = ActorPath::default().name("second/nested/b"); + assert_eq!(envelope_target.is_match(&valid_path_1), true); + assert_eq!(envelope_target.is_match(&valid_path_2), true); + assert_eq!(envelope_target.is_match(&invalid_path_1), false); + assert_eq!(envelope_target.is_match(&invalid_path_2), false); + assert_eq!(envelope_target.is_match(&invalid_path_3), false); + } + + #[test] + fn test_match_by_path_with_a_single_wildcard_against_a_path_with_special_symbols() { + let path = ActorPath::default().name("first/*/a"); + let target = Target::Path(path); + let envelope_target = EnvelopeTarget::from(target); + + let valid_path_1 = ActorPath::default().name("first/hand.ling/a"); + let valid_path_2 = ActorPath::default().name("first/proce-ssing/a"); + let valid_path_3 = ActorPath::default().name("first/some_thing/a"); + let invalid_path_2 = ActorPath::default().name("second/a"); + let invalid_path_3 = ActorPath::default().name("second/nested/b"); + assert_eq!(envelope_target.is_match(&valid_path_1), true); + assert_eq!(envelope_target.is_match(&valid_path_2), true); + assert_eq!(envelope_target.is_match(&valid_path_3), true); + assert_eq!(envelope_target.is_match(&invalid_path_2), false); + assert_eq!(envelope_target.is_match(&invalid_path_3), false); + } + + #[test] + fn test_match_by_group_returns_false() { + let target = Target::Group("test"); + let envelope_target = EnvelopeTarget::from(target); + + let actor_path = ActorPath::default(); + assert_eq!(envelope_target.is_match(&actor_path), false); + } +} diff --git a/src/bastion/src/supervisor.rs b/src/bastion/src/supervisor.rs index d12a086d..273ea9e2 100644 --- a/src/bastion/src/supervisor.rs +++ b/src/bastion/src/supervisor.rs @@ -9,7 +9,7 @@ use crate::context::{BastionId, ContextState}; use crate::envelope::Envelope; use crate::message::{BastionMessage, Deployment, Message}; use crate::path::{BastionPath, BastionPathElement}; -use async_mutex::Mutex; + use bastion_executor::pool; use futures::prelude::*; use futures::stream::FuturesOrdered; @@ -44,6 +44,18 @@ use tracing::{debug, trace, warn}; /// ```rust /// # use bastion::prelude::*; /// # +/// # #[cfg(feature = "tokio-runtime")] +/// # #[tokio::main] +/// # async fn main() { +/// # run(); +/// # } +/// # +/// # #[cfg(not(feature = "tokio-runtime"))] +/// # fn main() { +/// # run(); +/// # } +/// # +/// # fn run() { /// # Bastion::init(); /// # /// let sp_ref: SupervisorRef = Bastion::supervisor(|sp| { @@ -55,12 +67,11 @@ use tracing::{debug, trace, warn}; /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); +/// # } /// ``` /// -/// [`Children`]: children/struct.Children.html -/// [`SupervisionStrategy`]: supervisor/enum.SupervisionStrategy.html -/// [`with_strategy`]: #method.with_strategy -/// [`Bastion::children`]: struct.Bastion.html#method.children +/// [`Bastion::children`]: crate::Bastion::children +/// [`with_strategy`]: Self::with_strategy pub struct Supervisor { bcast: Broadcast, // The order in which children and supervisors were added. @@ -106,7 +117,7 @@ pub struct Supervisor { #[derive(Debug, Clone)] struct TrackedChildState { id: BastionId, - state: Arc<Mutex<Pin<Box<ContextState>>>>, + state: Arc<Pin<Box<ContextState>>>, restarts_counts: usize, } @@ -127,7 +138,7 @@ enum ActorSearchMethod { /// A "reference" to a [`Supervisor`], allowing to /// communicate with it. /// -/// [`Supervisor`]: supervisor/struct.Supervisor.html +// [`Supervisor`]: supervisor/struct.Supervisor.html pub struct SupervisorRef { id: BastionId, sender: Sender, @@ -187,8 +198,8 @@ pub enum RestartPolicy { /// restoring failed actors. It it fails after N attempts, /// the supervisor will remove an actor. /// -/// The default strategy used is `ActorRestartStrategy::Immediate` -/// with the `RestartPolicy::Always` restart policy. +/// The default strategy used is [`ActorRestartStrategy::Immediate`] +/// with the [`RestartPolicy::Always`] restart policy. #[derive(Debug, Clone, PartialEq)] pub struct RestartStrategy { restart_policy: RestartPolicy, @@ -199,7 +210,9 @@ pub struct RestartStrategy { /// The strategy for restating an actor as far as it /// returned an failure. /// -/// The default strategy is `Immediate`. +/// The default strategy is [`Immediate`]. +/// +/// [`Immediate`]: ActorRestartStrategy::Immediate pub enum ActorRestartStrategy { /// Restart an actor as soon as possible, since the moment /// the actor finished with a failure. @@ -358,6 +371,18 @@ impl Supervisor { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::supervisor(|sp| { @@ -370,6 +395,7 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn id(&self) -> &BastionId { &self.bcast.id() @@ -414,6 +440,18 @@ impl Supervisor { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|parent| { @@ -427,10 +465,10 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`SupervisorRef`]: ../struct.SupervisorRef.html - /// [`supervisor_ref`]: #method.supervisor_ref + /// [`supervisor_ref`]: Self::supervisor_ref pub fn supervisor<S>(self, init: S) -> Self where S: FnOnce(Supervisor) -> Supervisor, @@ -477,6 +515,18 @@ impl Supervisor { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|mut parent| { @@ -491,10 +541,10 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`SupervisorRef`]: ../struct.SupervisorRef.html - /// [`supervisor`]: #method.supervisor + /// [`supervisor`]: Self::supervisor pub fn supervisor_ref<S>(&mut self, init: S) -> SupervisorRef where S: FnOnce(Supervisor) -> Supervisor, @@ -542,6 +592,18 @@ impl Supervisor { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|sp| { @@ -563,11 +625,12 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`Children`]: children/struct.Children.html - /// [`ChildrenRef`]: children/struct.ChildrenRef.html - /// [`children_ref`]: #method.children_ref + // [`Children`]: children/struct.Children.html + // [`ChildrenRef`]: children/struct.ChildrenRef.html + /// [`children_ref`]: Self::children_ref pub fn children<C>(self, init: C) -> Self where C: FnOnce(Children) -> Children, @@ -584,10 +647,18 @@ impl Supervisor { let children = Children::new(bcast); let mut children = init(children); debug!("Children({}): Initialized.", children.id()); + // FIXME: children group elems launched without the group itself being launched if let Err(e) = children.register_dispatchers() { warn!("couldn't register all dispatchers into the registry: {}", e); }; + if let Err(e) = children.register_distributors() { + warn!( + "couldn't register all distributors into the registry: {}", + e + ); + }; + children.launch_elems(); debug!( @@ -619,6 +690,18 @@ impl Supervisor { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # Bastion::supervisor(|mut sp| { @@ -641,11 +724,10 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` /// - /// [`Children`]: children/struct.Children.html - /// [`ChildrenRef`]: children/struct.ChildrenRef.html - /// [`children`]: #method.children + /// [`children`]: Self::children pub fn children_ref<C>(&self, init: C) -> ChildrenRef where C: FnOnce(Children) -> Children, @@ -708,6 +790,18 @@ impl Supervisor { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::supervisor(|sp| { @@ -718,11 +812,8 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`SupervisionStrategy::OneForOne`]: supervisor/enum.SupervisionStrategy.html#variant.OneForOne - /// [`SupervisionStrategy::OneForAll`]: supervisor/enum.SupervisionStrategy.html#variant.OneForAll - /// [`SupervisionStrategy::RestForOne`]: supervisor/enum.SupervisionStrategy.html#variant.RestForOne pub fn with_strategy(mut self, strategy: SupervisionStrategy) -> Self { trace!( "Supervisor({}): Setting strategy: {:?}", @@ -746,6 +837,18 @@ impl Supervisor { /// # use bastion::prelude::*; /// # use std::time::Duration; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # Bastion::supervisor(|sp| { /// sp.with_restart_strategy( @@ -763,6 +866,7 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn with_restart_strategy(mut self, restart_strategy: RestartStrategy) -> Self { trace!( @@ -790,6 +894,18 @@ impl Supervisor { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// Bastion::supervisor(|sp| { @@ -803,9 +919,8 @@ impl Supervisor { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Callbacks`]: struct.Callbacks.html pub fn with_callbacks(mut self, callbacks: Callbacks) -> Self { trace!( "Supervisor({}): Setting callbacks: {:?}", @@ -1400,6 +1515,18 @@ impl SupervisorRef { /// ```rust /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// let supervisor_ref = Bastion::supervisor(|sp| { @@ -1412,6 +1539,7 @@ impl SupervisorRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn id(&self) -> &BastionId { &self.id @@ -1435,6 +1563,18 @@ impl SupervisorRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let mut parent_ref = Bastion::supervisor(|sp| sp).unwrap(); @@ -1447,9 +1587,8 @@ impl SupervisorRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Supervisor`]: supervisor/struct.Supervisor.html pub fn supervisor<S>(&self, init: S) -> Result<Self, ()> where S: FnOnce(Supervisor) -> Supervisor, @@ -1485,7 +1624,7 @@ impl SupervisorRef { /// `SupervisorRef` is referencing to supervise it. /// /// This methods returns a [`ChildrenRef`] referencing the newly - /// created children group it it succeeded, or `Err(())` + /// created children group if it succeeded, or `Err(())` /// otherwise. /// /// # Arguments @@ -1498,6 +1637,18 @@ impl SupervisorRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let sp_ref = Bastion::supervisor(|sp| sp).unwrap(); @@ -1518,10 +1669,8 @@ impl SupervisorRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`Children`]: children/struct.Children.html - /// [`ChildrenRef`]: children/struct.ChildrenRef.html pub fn children<C>(&self, init: C) -> Result<ChildrenRef, ()> where C: FnOnce(Children) -> Children, @@ -1596,6 +1745,18 @@ impl SupervisorRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let sp_ref = Bastion::supervisor(|sp| sp).unwrap(); @@ -1605,11 +1766,8 @@ impl SupervisorRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` - /// - /// [`SupervisionStrategy::OneForOne`]: supervisor/enum.SupervisionStrategy.html#variant.OneForOne - /// [`SupervisionStrategy::OneForAll`]: supervisor/enum.SupervisionStrategy.html#variant.OneForAll - /// [`SupervisionStrategy::RestForOne`]: supervisor/enum.SupervisionStrategy.html#variant.RestForOne pub fn strategy(&self, strategy: SupervisionStrategy) -> Result<(), ()> { debug!( "SupervisorRef({}): Setting strategy: {:?}", @@ -1637,7 +1795,18 @@ impl SupervisorRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let sp_ref = Bastion::supervisor(|sp| sp).unwrap(); @@ -1693,6 +1862,18 @@ impl SupervisorRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let sp_ref = Bastion::supervisor(|sp| sp).unwrap(); @@ -1701,6 +1882,7 @@ impl SupervisorRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn stop(&self) -> Result<(), ()> { debug!("SupervisorRef({}): Stopping.", self.id()); @@ -1721,6 +1903,18 @@ impl SupervisorRef { /// ``` /// # use bastion::prelude::*; /// # + /// # #[cfg(feature = "tokio-runtime")] + /// # #[tokio::main] + /// # async fn main() { + /// # run(); + /// # } + /// # + /// # #[cfg(not(feature = "tokio-runtime"))] + /// # fn main() { + /// # run(); + /// # } + /// # + /// # fn run() { /// # Bastion::init(); /// # /// # let sp_ref = Bastion::supervisor(|sp| sp).unwrap(); @@ -1729,6 +1923,7 @@ impl SupervisorRef { /// # Bastion::start(); /// # Bastion::stop(); /// # Bastion::block_until_stopped(); + /// # } /// ``` pub fn kill(&self) -> Result<(), ()> { debug!("SupervisorRef({}): Killing.", self.id()); @@ -1750,7 +1945,7 @@ impl SupervisorRef { } impl TrackedChildState { - fn new(id: BastionId, state: Arc<Mutex<Pin<Box<ContextState>>>>) -> Self { + fn new(id: BastionId, state: Arc<Pin<Box<ContextState>>>) -> Self { TrackedChildState { id, state, @@ -1762,7 +1957,7 @@ impl TrackedChildState { self.id.clone() } - fn state(&self) -> Arc<Mutex<Pin<Box<ContextState>>>> { + fn state(&self) -> Arc<Pin<Box<ContextState>>> { self.state.clone() } @@ -1845,11 +2040,11 @@ impl RestartStrategy { /// # Arguments /// /// * `restart_policy` - Defines a restart policy to use for failed actor: - /// - [`RestartStrategy::Always`] would restart the + /// - [`RestartPolicy::Always`] would restart the /// failed actor each time as it fails. - /// - [`RestartStrategy::Never`] would not restart the + /// - [`RestartPolicy::Never`] would not restart the /// failed actor and remove it from tracking. - /// - [`RestartStrategy::Tries`] would restart the + /// - [`RestartPolicy::Tries`] would restart the /// failed actor a limited amount of times. If can't be started, /// then will remove it from tracking. /// @@ -1873,13 +2068,6 @@ impl RestartStrategy { /// let restart_strategy = RestartStrategy::default() /// .with_actor_restart_strategy(actor_restart_strategy); /// ``` - /// - /// [`RestartStrategy::Always`]: enum.RestartPolicy.html#variant.Always - /// [`RestartStrategy::Never`]: enum.RestartPolicy.html#variant.Never - /// [`RestartStrategy::Tries`]: enum.RestartPolicy.html#variant.Tries - /// [`ActorRestartStrategy::Immediate`]: enum.ActorRestartStrategy.html#variant.Immediate - /// [`ActorRestartStrategy::LinearBackOff`]: enum.ActorRestartStrategy.html#variant.LinearBackOff - /// [`ActorRestartStrategy::ExponentialBackOff`]: enum.ActorRestartStrategy.html#variant.ExponentialBackOff pub fn new(restart_policy: RestartPolicy, strategy: ActorRestartStrategy) -> Self { RestartStrategy { restart_policy, diff --git a/src/bastion/src/system/global_state.rs b/src/bastion/src/system/global_state.rs new file mode 100644 index 00000000..0758e152 --- /dev/null +++ b/src/bastion/src/system/global_state.rs @@ -0,0 +1,196 @@ +use std::sync::Arc; +/// This module contains implementation of the global state that +/// available to all actors in runtime. To provide safety and avoid +/// data races, the implementation is heavily relies on software +/// transaction memory (or shortly STM) mechanisms to eliminate any +/// potential data races and provide consistency across actors. +use std::{ + any::{Any, TypeId}, + sync::RwLock, +}; +use std::{collections::hash_map::Entry, ops::Deref}; + +use lever::sync::atomics::AtomicBox; +use lever::table::lotable::LOTable; +use lightproc::proc_state::AsAny; +use std::collections::HashMap; + +use crate::errors::{BastionError, BastionResult}; + +#[derive(Debug)] +pub struct GlobalState { + table: Arc<RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>, // todo: remove the arc<rwlock< once we figure it out +} + +impl GlobalState { + /// Returns a new instance of global state. + pub(crate) fn new() -> Self { + GlobalState { + table: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Inserts the given value in the global state. If the value + /// exists, it will be overridden. + pub fn insert<T: Send + Sync + 'static>(&mut self, value: T) -> bool { + self.table + .write() + .unwrap() + .insert( + TypeId::of::<T>(), + Arc::new(value) as Arc<dyn Any + Send + Sync>, + ) + .is_some() + } + + /// Invokes a function with the requested data type. + pub fn read<T: Send + Sync + 'static>(&mut self, f: impl FnOnce(Option<&T>)) { + self.table + .read() + .unwrap() + .get(&TypeId::of::<T>()) + .map(|value| f(value.downcast_ref())); + } + + /// Invokes a function with the requested data type. + pub fn write<T: std::fmt::Debug + Send + Sync + 'static, F>(&mut self, f: F) + where + F: Fn(Option<&T>) -> Option<T>, + { + let mut hm = self.table.write().unwrap(); + let stuff_to_insert = match hm.entry(TypeId::of::<T>()) { + Entry::Occupied(data) => f(data.get().downcast_ref()), + Entry::Vacant(_) => f(None), + }; + + if let Some(stuff) = stuff_to_insert { + hm.insert( + TypeId::of::<T>(), + Arc::new(stuff) as Arc<dyn Any + Send + Sync>, + ); + } else { + hm.remove(&TypeId::of::<T>()); + }; + } + + /// Checks the given values is storing in the global state. + pub fn contains<T: Send + Sync + 'static>(&self) -> bool { + self.table.read().unwrap().contains_key(&TypeId::of::<T>()) + } + + /// Deletes the entry from the global state. + pub fn remove<T: Send + Sync + 'static>(&mut self) -> bool { + self.table + .write() + .unwrap() + .remove(&TypeId::of::<T>()) + .is_some() + } +} + +#[cfg(test)] +mod tests { + use crate::system::global_state::GlobalState; + + #[derive(Clone, Debug, Eq, PartialEq)] + struct TestData { + counter: u64, + } + + #[test] + fn test_insert() { + let mut instance = GlobalState::new(); + let test_data = TestData { counter: 0 }; + + instance.insert(test_data.clone()); + assert_eq!(instance.contains::<TestData>(), true); + } + + #[test] + fn test_insert_with_overriding_data() { + let mut instance = GlobalState::new(); + + let first_insert_data = TestData { counter: 0 }; + instance.insert(first_insert_data.clone()); + assert_eq!(instance.contains::<TestData>(), true); + + let second_insert_data = TestData { counter: 1 }; + instance.insert(second_insert_data.clone()); + assert_eq!(instance.contains::<TestData>(), true); + } + + #[test] + fn test_contains_returns_true() { + let mut instance = GlobalState::new(); + assert_eq!(instance.contains::<TestData>(), false); + + instance.insert(TestData { counter: 0 }); + assert_eq!(instance.contains::<TestData>(), true); + } + + #[test] + fn test_contains_returns_false() { + let instance = GlobalState::new(); + + assert_eq!(instance.contains::<usize>(), false); + } + + #[test] + fn test_remove_returns_true() { + let mut instance = GlobalState::new(); + + instance.insert(TestData { counter: 0 }); + assert_eq!(instance.contains::<TestData>(), true); + + let is_removed = instance.remove::<TestData>(); + assert_eq!(is_removed, true); + } + + #[test] + fn test_remove_returns_false() { + let mut instance = GlobalState::new(); + + let is_removed = instance.remove::<usize>(); + assert_eq!(is_removed, false); + } + + #[test] + fn test_write_read() { + let mut instance = GlobalState::new(); + + #[derive(Debug, PartialEq, Clone)] + struct Hello { + foo: bool, + bar: usize, + } + + let expected = Hello { foo: true, bar: 42 }; + + instance.insert(expected.clone()); + + instance.read(|actual: Option<&Hello>| { + assert_eq!(&expected, actual.unwrap()); + }); + + let expected_updated = Hello { + foo: false, + bar: 43, + }; + + instance.write::<Hello, _>(|maybe_to_update| { + let to_update = maybe_to_update.unwrap(); + + let updated = Hello { + foo: !to_update.foo, + bar: to_update.bar + 1, + }; + + Some(updated) + }); + + instance.read(|updated: Option<&Hello>| { + let updated = updated.unwrap(); + assert_eq!(updated, &expected_updated); + }); + } +} diff --git a/src/bastion/src/system/mod.rs b/src/bastion/src/system/mod.rs new file mode 100644 index 00000000..373dd204 --- /dev/null +++ b/src/bastion/src/system/mod.rs @@ -0,0 +1,7 @@ +mod global_state; +mod node; + +use crate::system::node::Node; +use once_cell::sync::Lazy; + +pub static SYSTEM: Lazy<Node> = Lazy::new(Node::new); diff --git a/src/bastion/src/system/node.rs b/src/bastion/src/system/node.rs new file mode 100644 index 00000000..5c77e368 --- /dev/null +++ b/src/bastion/src/system/node.rs @@ -0,0 +1,29 @@ +use crate::system::global_state::GlobalState; + +#[derive(Debug)] +/// An implementation of the Bastion's node. By default +/// it's available as a singleton object. The node stores all +/// required information for running various actor implementations, +/// +/// Out-of-the-box it also provides: +/// - Adding actor definitions in runtime +/// - API for starting, stopping or terminating actors +/// - Global state available to all actors +/// - Message dispatching +/// +pub struct Node { + global_state: GlobalState, +} + +impl Node { + /// Returns a new instance of the Bastion Node. + pub(crate) fn new() -> Self { + let global_state = GlobalState::new(); + + Node { global_state } + } + + // TODO: Add errors handling? + /// Initializes the Bastion node if it hasn't already been done. + pub async fn init(&self) {} +} diff --git a/src/bastion/tests/message_signatures.rs b/src/bastion/tests/message_signatures.rs index d4131fe7..6c107725 100644 --- a/src/bastion/tests/message_signatures.rs +++ b/src/bastion/tests/message_signatures.rs @@ -19,7 +19,7 @@ fn spawn_responders() -> ChildrenRef { msg! { ctx.recv().await?, msg: &'static str =!> { if msg == "Hello" { - assert!(signature!().is_sender_identified(), false); + assert!(signature!().is_sender_identified(), "sender is not identified"); answer!(ctx, "Goodbye").unwrap(); } }; @@ -42,11 +42,28 @@ fn spawn_responders() -> ChildrenRef { .expect("Couldn't create the children group.") } -#[test] -fn answer_and_tell_signatures() { - setup(); - Bastion::spawn(run).unwrap(); - teardown(); +#[cfg(feature = "tokio-runtime")] +mod tokio_tests { + use super::*; + + #[tokio::test] + async fn answer_and_tell_signatures() { + setup(); + Bastion::spawn(run).unwrap(); + teardown(); + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod no_tokio_tests { + use super::*; + + #[test] + fn answer_and_tell_signatures() { + setup(); + Bastion::spawn(run).unwrap(); + teardown(); + } } async fn run(ctx: BastionContext) -> Result<(), ()> { diff --git a/src/bastion/tests/prop_children_broadcast.proptest-regressions b/src/bastion/tests/prop_children_broadcast.proptest-regressions new file mode 100644 index 00000000..0581f2f2 --- /dev/null +++ b/src/bastion/tests/prop_children_broadcast.proptest-regressions @@ -0,0 +1,8 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 70c25a250dab8458e4c6eced0bd8d0b0925a85cab0e33fe71a3c527b8b3780c2 # shrinks to message = "" +cc 0ce425dcb58b6d604391a2b073f13559ce8e882abe9aeaa760fe7c97b2f5d610 # shrinks to message = "\u{10a39}🢰ûѨ*� .﹪/3t\\ஞ0\"Ⱥ\'ቒ1" diff --git a/src/bastion/tests/prop_children_broadcast.rs b/src/bastion/tests/prop_children_broadcast.rs index 12216e3b..df072d81 100644 --- a/src/bastion/tests/prop_children_broadcast.rs +++ b/src/bastion/tests/prop_children_broadcast.rs @@ -4,32 +4,53 @@ use std::sync::Once; static START: Once = Once::new(); -proptest! { - #![proptest_config(ProptestConfig::with_cases(1_000))] - #[test] - fn proptest_bcast_message(message in "\\PC*") { - START.call_once(|| { - Bastion::init(); - }); - Bastion::start(); - - if let Ok(_chrn) = Bastion::children(|children: Children| { - children - .with_exec(move |ctx: BastionContext| { - async move { - msg! { ctx.recv().await?, - ref _msg: &'static str => {}; - // This won't happen because this example - // only "asks" a `&'static str`... - _: _ => {}; - } +#[cfg(feature = "tokio-runtime")] +mod tokio_proptests { + use super::*; + proptest! { + #![proptest_config(ProptestConfig::with_cases(1_000))] + #[test] + fn proptest_bcast_message(message in "\\PC*") { + tokio_test::block_on(async { + super::test_with_message(message); + }); + } + } +} +#[cfg(not(feature = "tokio-runtime"))] +mod not_tokio_proptests { + use super::*; - Ok(()) - } - }) - }){ - let message: &'static str = Box::leak(message.into_boxed_str()); - Bastion::broadcast(message).expect("broadcast failed"); + proptest! { + #![proptest_config(ProptestConfig::with_cases(1_000))] + #[test] + fn proptest_bcast_message(message in "\\PC*") { + super::test_with_message(message); } } } + +fn test_with_message(message: String) { + START.call_once(|| { + Bastion::init(); + }); + Bastion::start(); + + if let Ok(_chrn) = Bastion::children(|children: Children| { + children.with_exec(move |ctx: BastionContext| { + async move { + msg! { ctx.recv().await?, + ref _msg: &'static str => {}; + // This won't happen because this example + // only "asks" a `&'static str`... + _: _ => {}; + } + + Ok(()) + } + }) + }) { + let message: &'static str = Box::leak(message.into_boxed_str()); + Bastion::broadcast(message).expect("broadcast failed"); + } +} diff --git a/src/bastion/tests/prop_children_message.proptest-regressions b/src/bastion/tests/prop_children_message.proptest-regressions new file mode 100644 index 00000000..0b089acd --- /dev/null +++ b/src/bastion/tests/prop_children_message.proptest-regressions @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 8428d777018d9727329e319192aecdc1187db6917e2a518fd7fd2e285cae0d8d # shrinks to message = "" diff --git a/src/bastion/tests/prop_children_message.rs b/src/bastion/tests/prop_children_message.rs index ba2ea5f2..d6278fb1 100644 --- a/src/bastion/tests/prop_children_message.rs +++ b/src/bastion/tests/prop_children_message.rs @@ -5,42 +5,65 @@ use std::sync::Once; static START: Once = Once::new(); -proptest! { - #![proptest_config(ProptestConfig::with_cases(1_000))] - #[test] - fn proptest_intra_message(message in "\\PC*") { - START.call_once(|| { - Bastion::init(); - }); - Bastion::start(); - - let message = Arc::new(message); - - let _ = Bastion::children(|children| { - children - .with_exec(move |ctx: BastionContext| { - let message = (*message).clone(); - async move { - let message: &'static str = Box::leak(message.into_boxed_str()); - let answer = ctx - .ask(&ctx.current().addr(), message) - .expect("Couldn't send the message."); - - msg! { ctx.recv().await?, - msg: &'static str =!> { - let _ = answer!(ctx, msg); - }; - _: _ => (); - } - - msg! { answer.await?, - _msg: &'static str => {}; - _: _ => {}; - } - - Ok(()) - } - }) - }); +#[cfg(feature = "tokio-runtime")] +mod tokio_proptests { + use super::*; + proptest! { + #![proptest_config(ProptestConfig::with_cases(1_000))] + #[test] + fn proptest_intra_message(message in "\\PC*") { + tokio::runtime::Runtime::new().unwrap().block_on(async { + super::test_with_message(message); + + }); + } + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod not_tokio_proptests { + use super::*; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(1_000))] + #[test] + fn proptest_intra_message(message in "\\PC*") { + super::test_with_message(message); + } } } + +fn test_with_message(message: String) { + START.call_once(|| { + Bastion::init(); + }); + Bastion::start(); + + let message = Arc::new(message); + + let _ = Bastion::children(|children| { + children.with_exec(move |ctx: BastionContext| { + let message = (*message).clone(); + async move { + let message: &'static str = Box::leak(message.into_boxed_str()); + let answer = ctx + .ask(&ctx.current().addr(), message) + .expect("Couldn't send the message."); + + msg! { ctx.recv().await?, + msg: &'static str =!> { + let _ = answer!(ctx, msg); + }; + _: _ => (); + } + + msg! { answer.await?, + _msg: &'static str => {}; + _: _ => {}; + } + + Ok(()) + } + }) + }); +} diff --git a/src/bastion/tests/prop_children_redundancy.proptest-regressions b/src/bastion/tests/prop_children_redundancy.proptest-regressions new file mode 100644 index 00000000..3ed3661e --- /dev/null +++ b/src/bastion/tests/prop_children_redundancy.proptest-regressions @@ -0,0 +1,8 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 3133d59d522ffc52f22b079bc8288d5886ff9879d77ab2963132823345052892 # shrinks to r = 0 +cc 886d80a24e6a7e9241ad63841ded2f66f85d477041f292c5029542de1b38e40e # shrinks to r = 0 diff --git a/src/bastion/tests/prop_children_redundancy.rs b/src/bastion/tests/prop_children_redundancy.rs index a5a03eb4..3d5ee508 100644 --- a/src/bastion/tests/prop_children_redundancy.rs +++ b/src/bastion/tests/prop_children_redundancy.rs @@ -4,30 +4,55 @@ use std::sync::Once; static START: Once = Once::new(); -proptest! { - #![proptest_config(ProptestConfig::with_cases(1_000))] - #[test] - fn proptest_redundancy(r in std::usize::MIN..32) { - START.call_once(|| { - Bastion::init(); - }); - Bastion::start(); +#[cfg(feature = "tokio-runtime")] +mod tokio_proptests { + use super::*; - Bastion::children(|children| { - children - // shrink over the redundancy - .with_redundancy(r) - .with_exec(|_ctx: BastionContext| { - async move { - // It's a proptest, - // we just don't want the loop - // to be optimized away - #[allow(clippy::drop_copy)] - loop { - std::mem::drop(()); - } - } - }) - }).expect("Coudn't spawn children."); + proptest! { + #![proptest_config(ProptestConfig::with_cases(1_000))] + #[test] + fn proptest_redundancy(r in std::usize::MIN..32) { + let _ = tokio_test::task::spawn(async { + super::test_with_usize(r); + }); + } + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod not_tokio_proptests { + use super::*; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(1_000))] + #[test] + fn proptest_redundancy(r in std::usize::MIN..32) { + super::test_with_usize(r); + } } } + +fn test_with_usize(r: usize) { + START.call_once(|| { + Bastion::init(); + }); + Bastion::start(); + + Bastion::children(|children| { + children + // shrink over the redundancy + .with_redundancy(r) + .with_exec(|_ctx: BastionContext| { + async move { + // It's a proptest, + // we just don't want the loop + // to be optimized away + #[allow(clippy::drop_copy)] + loop { + std::mem::drop(()); + } + } + }) + }) + .expect("Coudn't spawn children."); +} diff --git a/src/bastion/tests/run_blocking.rs b/src/bastion/tests/run_blocking.rs new file mode 100644 index 00000000..00ff3908 --- /dev/null +++ b/src/bastion/tests/run_blocking.rs @@ -0,0 +1,78 @@ +use bastion::prelude::*; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; +use std::thread; +use std::time::Duration; + +#[cfg(feature = "tokio-runtime")] +mod tokio_tests { + #[tokio::test] + async fn test_run_blocking() { + super::run() + } +} + +#[cfg(not(feature = "tokio-runtime"))] +mod not_tokio_tests { + #[test] + fn test_run_blocking() { + super::run() + } +} + +fn run() { + Bastion::init(); + Bastion::start(); + + let c = Bastion::children(|children| { + // We are creating the function to exec + children.with_exec(|ctx: BastionContext| { + let received_messages = Arc::new(AtomicUsize::new(0)); + async move { + let received_messages = Arc::clone(&received_messages); + loop { + msg! { + ctx.recv().await?, + msg: &'static str =!> + { + assert_eq!(msg, "hello"); + let messages = received_messages.fetch_add(1, Ordering::SeqCst); + answer!(ctx, messages + 1).expect("couldn't reply :("); + }; + _: _ => panic!(); + } + } + } + }) + }) + .unwrap(); + + let child = c.elems()[0].clone(); + + let output = (0..100) + .map(|_| { + let child = child.clone(); + run!(blocking!( + async move { + let duration = Duration::from_millis(1); + thread::sleep(duration); + msg! { + child.clone() + .ask_anonymously("hello").unwrap().await.unwrap(), + output: usize => output; + _: _ => panic!(); + } + } + .await + )) + .unwrap() + }) + .collect::<Vec<_>>(); + + Bastion::stop(); + Bastion::block_until_stopped(); + + assert_eq!((1..=100).map(|i| i).collect::<Vec<_>>(), output); +} diff --git a/src/bastion/tests/tokio_runtime.rs b/src/bastion/tests/tokio_runtime.rs new file mode 100644 index 00000000..4e7408c4 --- /dev/null +++ b/src/bastion/tests/tokio_runtime.rs @@ -0,0 +1,144 @@ +#[cfg(feature = "tokio-runtime")] +mod tokio_tests { + + use bastion::prelude::*; + + #[tokio::test] + async fn test_simple_await() { + tokio::time::sleep(std::time::Duration::from_nanos(1)).await; + } + + #[tokio::test] + async fn test_within_bastion() { + Bastion::init(); + Bastion::start(); + + test_within_children().await; + test_within_message_receive().await; + test_within_message_receive_blocking().await; + test_within_message_receive_spawn().await; + + Bastion::stop(); + } + + async fn test_within_children() { + Bastion::children(|children| { + children.with_exec(|_| async move { + tokio::time::sleep(std::time::Duration::from_nanos(1)).await; + Ok(()) + }) + }) + .expect("Couldn't create the children group."); + } + + async fn test_within_message_receive() { + let workers = Bastion::children(|children| { + children.with_exec(|ctx| async move { + msg! { + ctx.recv().await?, + question: &'static str =!> { + if question != "marco" { + panic!("didn't receive expected message"); + } + tokio::time::sleep(std::time::Duration::from_nanos(1)).await; + answer!(ctx, "polo").expect("couldn't send answer"); + }; + _: _ => { + panic!("didn't receive &str"); + }; + } + Ok(()) + }) + }) + .expect("Couldn't create the children group."); + + let answer = workers.elems()[0] + .ask_anonymously("marco") + .expect("Couldn't send the message."); + + msg! { answer.await.expect("couldn't receive answer"), + reply: &'static str => { + if reply != "polo" { + panic!("didn't receive expected message"); + } + }; + _: _ => { panic!("didn't receive &str"); }; + } + } + + async fn test_within_message_receive_blocking() { + let workers = Bastion::children(|children| { + children.with_exec(|ctx| async move { + msg! { + ctx.recv().await?, + question: &'static str =!> { + if question != "marco" { + panic!("didn't receive expected message"); + } + run!(blocking! { + let _ = tokio::time::sleep(std::time::Duration::from_nanos(1)).await; + println!("done"); + }); + answer!(ctx, "polo").expect("couldn't send answer"); + }; + _: _ => { + panic!("didn't receive &str"); + }; + } + Ok(()) + }) + }) + .expect("Couldn't create the children group."); + + let answer = workers.elems()[0] + .ask_anonymously("marco") + .expect("Couldn't send the message."); + + msg! { answer.await.expect("couldn't receive answer"), + reply: &'static str => { + if reply != "polo" { + panic!("didn't receive expected message"); + } + }; + _: _ => { panic!("didn't receive &str"); }; + } + } + + async fn test_within_message_receive_spawn() { + let workers = Bastion::children(|children| { + children.with_exec(|ctx| async move { + msg! { + ctx.recv().await?, + question: &'static str =!> { + if question != "marco" { + panic!("didn't receive expected message"); + } + run!(blocking! { + let _ = tokio::time::sleep(std::time::Duration::from_nanos(1)).await; + println!("done"); + }); + answer!(ctx, "polo").expect("couldn't send answer"); + }; + _: _ => { + panic!("didn't receive &str"); + }; + } + Ok(()) + }) + }) + .expect("Couldn't create the children group."); + + let answer = workers.elems()[0] + .ask_anonymously("marco") + .expect("Couldn't send the message."); + + msg! { answer.await.expect("couldn't receive answer"), + reply: &'static str => { + if reply != "polo" { + panic!("didn't receive expected message"); + } + }; + _: _ => { panic!("didn't receive &str"); }; + } + } +} diff --git a/src/lightproc/Cargo.toml b/src/lightproc/Cargo.toml index 50254302..282efd7c 100644 --- a/src/lightproc/Cargo.toml +++ b/src/lightproc/Cargo.toml @@ -16,10 +16,10 @@ edition = "2018" maintenance = { status = "actively-developed" } [dependencies] -crossbeam-utils = "0.7" +crossbeam-utils = "0.8" pin-utils = "0.1.0" [dev-dependencies] -crossbeam = "0.7" +crossbeam = "0.8" futures-executor = "0.3" lazy_static = "1.4.0" diff --git a/tools/miri.sh b/tools/miri.sh new file mode 100755 index 00000000..e0775f44 --- /dev/null +++ b/tools/miri.sh @@ -0,0 +1,10 @@ +rustup component add miri +cargo miri setup +cargo clean +# Do some Bastion shake +pushd src/bastion && \ + MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo miri test --features lever/nightly dispatcher && \ + MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo miri test --features lever/nightly path && \ + MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo miri test --features lever/nightly broadcast && \ + MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-ignore-leaks" cargo miri test --features lever/nightly children_ref && \ +popd