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 @@
------------------
-
-
Highly-available Distributed Fault-tolerant Runtime
+---
-
-
- Latest Release
-
-
-
-
-
-
-
-
-
-
- License
-
-
-
-
-
-
-
- Doc [Bastion]
-
-
-
-
-
-
-
+
+
+ Latest Release
+
+
+
+
+
+ License
+
+
+
+
+
+
+
+ Doc [Bastion]
+
+
+
+
+
+ Downloads
+
+
+
+
+
+
+
Doc [Bastion Executor]
-
-
-
-
-
-
-
- Doc [LightProc]
-
-
-
-
-
-
-
- Build Status
-
-
-
-
-
-
-
- Downloads
-
-
-
-
-
-
-
- Discord
-
-
-
-
-
-
+
+
+
+
+
+ Discord
+
+
+
+
+
+
+
+
+
+ Doc [LightProc]
+
+
+
+
+
+ Build Status
+
+
+
+
+
+
---
+Highly-available Distributed Fault-tolerant Runtime
+
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 bastion/examples
+
+[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 bastion/examples
-
-[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 "]
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::>>();
-
- run(join_all(handles), ProcStack::default());
+ .collect::>()
});
}
// 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::>()
+ });
+}
+
+// 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::>>();
-
- run(join_all(handles), proc_stack);
+ .collect::>();
});
}
-// 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(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(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,
- receiver: Receiver,
-}
-
-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 = Mutex::new(CoreId { id: 0 });
-
- /// Sliding window for pool task frequency calculation
- static ref FREQ_QUEUE: Mutex> = {
- Mutex::new(VecDeque::with_capacity(FREQUENCY_QUEUE_SIZE.saturating_add(1)))
- };
-
- /// Dynamic pool thread count variable
- static ref POOL_SIZE: Mutex = 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(future: F, stack: ProcStack) -> RecoverableHandle
+where
+ F: Future + 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) -> 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,
+ receiver: Receiver,
+}
- // 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 = 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 = 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(future: F, stack: ProcStack) -> RecoverableHandle
-where
- F: Future + 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,
-}
-
-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> {
- let mut stealers = Vec::>::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 = 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,
+ mean_last_updated_at: RwLock,
+}
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) -> 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; 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> {
get_core_ids_helper()
}
+/// This function tries to retrieve
+/// the number of active "cores" on the system.
+pub fn get_num_cores() -> Option {
+ 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(future: F, stack: ProcStack) -> RecoverableHandle
where
F: Future + 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,
- ///
- /// Stealers of the workers
- pub(crate) stealers: Vec>,
- ///
- /// 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(future: F, stack: ProcStack) -> RecoverableHandle
+where
+ F: Future + 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::().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,
+ receiver: Receiver,
+}
+
+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 = OnceCell::new();
+
+static POOL: Lazy = 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 {
impl Steal {
/// 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 fmt::Debug for Steal {
}
impl FromIterator> for Steal {
- /// 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(iter: I) -> Steal
where
I: IntoIterator- >,
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
= 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,
+ runner: Arc,
+ last_frequency: AtomicU64,
+ frequencies: TTas>,
+}
+
+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) -> 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) -> 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>> = 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 {
- 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, affinity: usize) -> Option {
- 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) {
- load_balancer::stats().store_load(affinity, local.worker_run_queue_size());
-}
-
-pub(crate) fn main_loop(affinity: usize, local: Worker) {
- 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 "]
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
@@ -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 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, 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, 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 = 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 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 {
+ 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(
+ 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::() % 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 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, 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, 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,
+ /// Mailbox of the actor
+ //mailbox: Mailbox,
+ /// 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>,
+ /// Defines a used scope for instantiating actors.
+ scope: Scope,
+ /// Defines a function used for generating unique actor names.
+ actor_name_fn: Option>,
+ /// 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(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(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,
+}
+
+#[derive(Debug)]
+#[repr(transparent)]
+/// Transparent type for the `Box` type that provides
+/// simpler and easier to use API to developers.
+pub struct LocalDataContainer(Box);
+
+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(&mut self, value: T) {
+ let container = LocalDataContainer::new(value);
+ self.table.insert(TypeId::of::(), container);
+ }
+
+ /// Checks the given values is storing in the table.
+ pub fn contains(&self) -> bool {
+ self.table.contains_key(&TypeId::of::())
+ }
+
+ /// Runs given closure on the immutable state
+ pub fn with_state(&self, f: F) -> Option
+ where
+ T: Send + Sync + 'static,
+ F: FnOnce(Option<&T>) -> Option,
+ {
+ self.get_container::().and_then(|e| f(e.get()))
+ }
+
+ /// Runs given closure on the mutable state
+ pub fn with_state_mut(&mut self, mut f: F) -> Option
+ where
+ T: Send + Sync + 'static,
+ F: FnMut(Option<&mut T>) -> Option,
+ {
+ self.get_container_mut::().and_then(|e| f(e.get_mut()))
+ }
+
+ /// Deletes the entry from the table.
+ pub fn remove(&mut self) -> bool {
+ self.table.remove(&TypeId::of::()).is_some()
+ }
+
+ /// Returns immutable data to the caller.
+ pub fn get(&self) -> Option<&T>
+ where
+ T: Send + Sync + 'static,
+ {
+ self.get_container::().and_then(|e| e.0.downcast_ref())
+ }
+
+ /// Returns mutable data to the caller.
+ pub fn get_mut(&mut self) -> Option<&mut T>
+ where
+ T: Send + Sync + 'static,
+ {
+ self.get_container_mut::()
+ .and_then(|e| e.0.downcast_mut())
+ }
+
+ /// Returns immutable local data container to the caller if it exists.
+ #[inline]
+ fn get_container(&self) -> Option<&LocalDataContainer> {
+ self.table.get(&TypeId::of::())
+ }
+
+ /// Returns mutable local data container to the caller if it exists.
+ #[inline]
+ fn get_container_mut(&mut self) -> Option<&mut LocalDataContainer> {
+ self.table.get_mut(&TypeId::of::())
+ }
+}
+
+impl LocalDataContainer {
+ pub(crate) fn new(value: T) -> Self {
+ LocalDataContainer(Box::new(value))
+ }
+
+ /// Returns immutable data to the caller.
+ fn get(&self) -> Option<&T> {
+ self.0.downcast_ref()
+ }
+
+ /// Returns mutable data to the caller.
+ fn get_mut(&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::(), true);
+ }
+
+ #[test]
+ fn test_insert_with_duplicated_data() {
+ let mut instance = LocalState::new();
+
+ instance.insert(Data { counter: 0 });
+ assert_eq!(instance.contains::(), true);
+
+ instance.insert(Data { counter: 1 });
+ assert_eq!(instance.contains::(), true);
+ }
+
+ #[test]
+ fn test_contains_returns_false() {
+ let instance = LocalState::new();
+
+ assert_eq!(instance.contains::(), false);
+ }
+
+ #[test]
+ fn test_get_container() {
+ let mut instance = LocalState::new();
+
+ let expected = Data { counter: 1 };
+
+ instance.insert(expected.clone());
+ assert_eq!(instance.contains::(), true);
+
+ let result_get = instance.get_container::();
+ assert_eq!(result_get.is_some(), true);
+
+ let container = result_get.unwrap();
+ let data = container.get::();
+ 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::(), true);
+
+ // Get the current snapshot of data
+ let mut data = instance.get_mut::();
+ 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::();
+ 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::(), true);
+
+ // Get the current snapshot of data
+ let mut data: Option = instance.with_state::(|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::(), true);
+
+ // Get the current snapshot of data
+ let mut data: Option = instance.with_state_mut::(|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::();
+ 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::(), true);
+
+ let is_removed = instance.remove::();
+ assert_eq!(is_removed, true);
+ }
+
+ #[test]
+ fn test_remove_returns_false() {
+ let mut instance = LocalState::new();
+
+ let is_removed = instance.remove::();
+ 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
+where
+ T: TypedMessage,
+{
+ /// Indicated the transmitter part of the actor's channel
+ /// which is using for passing messages.
+ tx: Sender>,
+ /// A field for checks that the message has been delivered to
+ /// the specific actor.
+ scheduled: Arc,
+}
+
+impl MailboxTx
+where
+ T: TypedMessage,
+{
+ /// Return a new instance of MailboxTx that indicates sender.
+ pub(crate) fn new(tx: Sender>) -> 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) -> 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
+where
+ T: TypedMessage,
+{
+ /// User guardian sender
+ user_tx: MailboxTx,
+ /// User guardian receiver
+ user_rx: Receiver>,
+ /// System guardian receiver
+ system_rx: Receiver>,
+ /// The current processing message, received from the
+ /// latest call to the user's queue
+ last_user_message: Option>,
+ /// The current processing message, received from the
+ /// latest call to the system's queue
+ last_system_message: Option>,
+ /// Mailbox state machine
+ state: Arc>,
+}
+
+// TODO: Add calls with recv with timeout
+impl Mailbox
+where
+ T: TypedMessage,
+{
+ /// Creates a new mailbox for the actor.
+ pub(crate) fn new(system_rx: Receiver>) -> 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 {
+ 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> {
+ 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 {
+ 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> {
+ 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> {
+ self.last_user_message.clone()
+ }
+
+ /// Returns the last retrieved message from the system channel
+ pub async fn get_last_system_message(&self) -> Option> {
+ 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
+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,
+ /// 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 Envelope
+where
+ T: TypedMessage,
+{
+ /// Create a message with the given sender and inner data.
+ pub fn new(sender: Option, 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 Debug for Envelope
+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,
+}
+
+#[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(init: S) -> Result
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(init: C) -> Result
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(action: I) -> Result
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(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>,
+ after_start: Option>,
before_restart: Option>,
after_restart: Option>,
after_stop: Option>,
@@ -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(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(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(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(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(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>>>,
+ state: Arc>>,
// 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>>>,
+ state: Arc>>,
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::>>()?;
+ }
+ 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(&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(&self, msg: M) -> Result {
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(&self, msg: M) -> Result {
+ 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>>,
+ distributors: Vec,
// The name of children
name: Option,
#[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(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>>>) {
+ fn restart_child(&mut self, old_id: &BastionId, old_state: Arc>>) {
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,
children: Vec,
dispatchers: Vec,
+ distributors: Vec,
}
impl ChildrenRef {
@@ -31,6 +32,7 @@ impl ChildrenRef {
path: Arc,
children: Vec,
dispatchers: Vec,
+ distributors: Vec,
) -> 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 {
&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 {
+ &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(&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,
- state: Arc>>>,
+ state: Arc>>,
}
#[derive(Debug)]
pub(crate) struct ContextState {
- messages: VecDeque,
+ messages: SegQueue,
#[cfg(feature = "scaling")]
stats: Arc,
#[cfg(feature = "scaling")]
@@ -135,7 +163,7 @@ impl BastionContext {
child: ChildRef,
children: ChildrenRef,
supervisor: Option,
- state: Arc>>>,
+ state: Arc>>,
) -> 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 {
- 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 {
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 {
- 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(&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(&self, to: &RefAddr, msg: M) -> Result {
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(&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 {
- self.messages.pop_front()
+ pub(crate) fn pop_message(&self) -> Option {
+ 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(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;
+/// Type alias for the recipients hashset.
+/// Each key-value pair stores the Bastion identifier as the key.
+pub type RecipientMap = LOTable;
+
#[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;
+ /// Return all recipients that will receive a broadcast message
+ fn all(&self) -> Vec;
+ /// 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 {
+ 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 {
+ 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 {
+ 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) {
- 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::>();
- 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 for String {
pub(crate) struct GlobalDispatcher {
/// Storage for all registered group of actors.
pub dispatchers: LOTable>>,
+ // TODO: switch to LOTable once lever implements write optimized granularity
+ pub distributors: Arc>>>,
}
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) {
- let mut acked_dispatchers: Vec = 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(&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(&self, distributor: Distributor, message: M) -> Result
+ 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(
+ &self,
+ distributor: Distributor,
+ message: M,
+ ) -> Result, 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::, _>>()
+ }
+ }
+
+ pub(crate) fn tell_everyone