mirror of
https://gitlab.com/fabinfra/fabaccess/bffh.git
synced 2024-11-22 06:47:56 +01:00
Executor compiles
This commit is contained in:
parent
24be65b3d9
commit
55d6609e33
34
runtime/executor/Cargo.toml
Normal file
34
runtime/executor/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "executor"
|
||||||
|
version = "0.3.0"
|
||||||
|
publish = false
|
||||||
|
description = "Executor"
|
||||||
|
authors = []
|
||||||
|
keywords = []
|
||||||
|
categories = []
|
||||||
|
readme = "README.md"
|
||||||
|
license = "Apache-2.0/MIT"
|
||||||
|
edition = "2021"
|
||||||
|
exclude = [
|
||||||
|
"scripts/*",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lightproc = { path = "../lightproc" }
|
||||||
|
|
||||||
|
crossbeam-utils = "0.8"
|
||||||
|
crossbeam-channel = "0.5"
|
||||||
|
crossbeam-epoch = "0.9"
|
||||||
|
crossbeam-deque = "0.8.1"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
libc = "0.2"
|
||||||
|
num_cpus = "1.13"
|
||||||
|
pin-utils = "0.1.0"
|
||||||
|
|
||||||
|
# Allocator
|
||||||
|
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"
|
94
runtime/executor/README.md
Normal file
94
runtime/executor/README.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Bastion Executor
|
||||||
|
|
||||||
|
<table align=left style='float: left; margin: 4px 10px 0px 0px; border: 1px solid #000000;'>
|
||||||
|
<tr>
|
||||||
|
<td>Latest Release</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://crates.io/crates/bastion">
|
||||||
|
<img alt="Crates.io" src="https://img.shields.io/crates/v/bastion-executor.svg?style=popout-square">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>License</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://github.com/bastion-rs/bastion/blob/master/LICENSE">
|
||||||
|
<img alt="Crates.io" src="https://img.shields.io/crates/l/bastion.svg?style=popout-square">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Build Status</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://actions-badge.atrox.dev/bastion-rs/bastion/goto">
|
||||||
|
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fbastion-rs%2Fbastion%2Fbadge&style=flat" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Downloads</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://crates.io/crates/bastion-executor">
|
||||||
|
<img alt="Crates.io" src="https://img.shields.io/crates/d/bastion-executor.svg?style=popout-square">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Discord</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://discord.gg/DqRqtRT">
|
||||||
|
<img src="https://img.shields.io/discord/628383521450360842.svg?logo=discord" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
Bastion Executor is NUMA-aware SMP based Fault-tolerant Executor
|
||||||
|
|
||||||
|
Bastion Executor is a highly-available, fault-tolerant, async communication
|
||||||
|
oriented executor. Bastion's main idea is supplying a fully async runtime
|
||||||
|
with fault-tolerance to work on heavy loads.
|
||||||
|
|
||||||
|
Main differences between other executors are:
|
||||||
|
* Uses SMP based execution scheme to exploit cache affinity on multiple cores and execution is
|
||||||
|
equally distributed over the system resources, which means utilizing the all system.
|
||||||
|
* Uses NUMA-aware allocation for scheduler's queues and exploit locality on server workloads.
|
||||||
|
* Tailored for creating middleware and working with actor model like concurrency and distributed communication.
|
||||||
|
|
||||||
|
**NOTE:** Bastion Executor is independent of it's framework implementation.
|
||||||
|
It uses [lightproc](https://docs.rs/lightproc) to encapsulate and provide fault-tolerance to your future based workloads.
|
||||||
|
You can use your futures with [lightproc](https://docs.rs/lightproc) to run your workloads on Bastion Executor without the need to have framework.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use bastion_executor::prelude::*;
|
||||||
|
use lightproc::proc_stack::ProcStack;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let pid = 1;
|
||||||
|
let stack = ProcStack::default()
|
||||||
|
.with_pid(pid)
|
||||||
|
.with_after_panic(move || println!("after panic {}", pid.clone()));
|
||||||
|
|
||||||
|
let handle = spawn(
|
||||||
|
async {
|
||||||
|
panic!("test");
|
||||||
|
},
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pid = 2;
|
||||||
|
let stack = ProcStack::default().with_pid(pid);
|
||||||
|
|
||||||
|
run(
|
||||||
|
async {
|
||||||
|
handle.await;
|
||||||
|
},
|
||||||
|
stack.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
67
runtime/executor/benches/blocking.rs
Normal file
67
runtime/executor/benches/blocking.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use bastion_executor::blocking;
|
||||||
|
use lightproc::proc_stack::ProcStack;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio-runtime")]
|
||||||
|
mod tokio_benchs {
|
||||||
|
use super::*;
|
||||||
|
#[bench]
|
||||||
|
fn blocking(b: &mut Bencher) {
|
||||||
|
tokio_test::block_on(async { _blocking(b) });
|
||||||
|
}
|
||||||
|
#[bench]
|
||||||
|
fn blocking_single(b: &mut Bencher) {
|
||||||
|
tokio_test::block_on(async {
|
||||||
|
_blocking_single(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "tokio-runtime"))]
|
||||||
|
mod no_tokio_benchs {
|
||||||
|
use super::*;
|
||||||
|
#[bench]
|
||||||
|
fn blocking(b: &mut Bencher) {
|
||||||
|
_blocking(b);
|
||||||
|
}
|
||||||
|
#[bench]
|
||||||
|
fn blocking_single(b: &mut Bencher) {
|
||||||
|
_blocking_single(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark for a 10K burst task spawn
|
||||||
|
fn _blocking(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
(0..10_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark for a single blocking task spawn
|
||||||
|
fn _blocking_single(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
25
runtime/executor/benches/perf.rs
Normal file
25
runtime/executor/benches/perf.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use bastion_executor::prelude::*;
|
||||||
|
use lightproc::proc_stack::ProcStack;
|
||||||
|
use test::{black_box, Bencher};
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn increment(b: &mut Bencher) {
|
||||||
|
let mut sum = 0;
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
run(
|
||||||
|
async {
|
||||||
|
(0..10_000_000).for_each(|_| {
|
||||||
|
sum += 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
black_box(sum);
|
||||||
|
}
|
69
runtime/executor/benches/run_blocking.rs
Normal file
69
runtime/executor/benches/run_blocking.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use bastion_executor::blocking;
|
||||||
|
use bastion_executor::run::run;
|
||||||
|
use futures::future::join_all;
|
||||||
|
use lightproc::proc_stack::ProcStack;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio-runtime")]
|
||||||
|
mod tokio_benchs {
|
||||||
|
use super::*;
|
||||||
|
#[bench]
|
||||||
|
fn blocking(b: &mut Bencher) {
|
||||||
|
tokio_test::block_on(async { _blocking(b) });
|
||||||
|
}
|
||||||
|
#[bench]
|
||||||
|
fn blocking_single(b: &mut Bencher) {
|
||||||
|
tokio_test::block_on(async {
|
||||||
|
_blocking_single(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "tokio-runtime"))]
|
||||||
|
mod no_tokio_benchs {
|
||||||
|
use super::*;
|
||||||
|
#[bench]
|
||||||
|
fn blocking(b: &mut Bencher) {
|
||||||
|
_blocking(b);
|
||||||
|
}
|
||||||
|
#[bench]
|
||||||
|
fn blocking_single(b: &mut Bencher) {
|
||||||
|
_blocking_single(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark for a 10K burst task spawn
|
||||||
|
fn _blocking(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
(0..10_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark for a single blocking task spawn
|
||||||
|
fn _blocking_single(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
70
runtime/executor/benches/spawn.rs
Normal file
70
runtime/executor/benches/spawn.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
|
||||||
|
use bastion_executor::load_balancer;
|
||||||
|
use bastion_executor::prelude::spawn;
|
||||||
|
use futures_timer::Delay;
|
||||||
|
use lightproc::proc_stack::ProcStack;
|
||||||
|
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
|
||||||
|
fn _spawn_lot(b: &mut Bencher) {
|
||||||
|
let proc_stack = ProcStack::default();
|
||||||
|
b.iter(|| {
|
||||||
|
let _ = (0..10_000)
|
||||||
|
.map(|_| {
|
||||||
|
spawn(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
Delay::new(duration).await;
|
||||||
|
},
|
||||||
|
proc_stack.clone(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark for a single task spawn
|
||||||
|
fn _spawn_single(b: &mut Bencher) {
|
||||||
|
let proc_stack = ProcStack::default();
|
||||||
|
b.iter(|| {
|
||||||
|
spawn(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
Delay::new(duration).await;
|
||||||
|
},
|
||||||
|
proc_stack.clone(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
71
runtime/executor/benches/stats.rs
Normal file
71
runtime/executor/benches/stats.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
|
||||||
|
extern crate test;
|
||||||
|
use bastion_executor::load_balancer::{core_count, get_cores, stats, SmpStats};
|
||||||
|
use bastion_executor::placement;
|
||||||
|
use std::thread;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
fn stress_stats<S: SmpStats + Sync + Send>(stats: &'static S) {
|
||||||
|
let mut handles = Vec::with_capacity(*core_count());
|
||||||
|
for core in get_cores() {
|
||||||
|
let handle = thread::spawn(move || {
|
||||||
|
placement::set_for_current(*core);
|
||||||
|
for i in 0..100 {
|
||||||
|
stats.store_load(core.id, 10);
|
||||||
|
if i % 3 == 0 {
|
||||||
|
let _sorted_load = stats.get_sorted_load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handles.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
handle.join().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// previous lock based stats benchmark 1,352,791 ns/iter (+/- 2,682,013)
|
||||||
|
|
||||||
|
// 158,278 ns/iter (+/- 117,103)
|
||||||
|
#[bench]
|
||||||
|
fn lockless_stats_bench(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
42
runtime/executor/examples/spawn_async.rs
Normal file
42
runtime/executor/examples/spawn_async.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
use std::panic::resume_unwind;
|
||||||
|
use std::time::Duration;
|
||||||
|
use executor::pool;
|
||||||
|
use executor::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
std::panic::set_hook(Box::new(|info| {
|
||||||
|
let tid = std::thread::current().id();
|
||||||
|
println!("Panicking ThreadId: {:?}", tid);
|
||||||
|
std::io::stdout().flush();
|
||||||
|
println!("panic hook: {:?}", info);
|
||||||
|
}));
|
||||||
|
let tid = std::thread::current().id();
|
||||||
|
println!("Main ThreadId: {:?}", tid);
|
||||||
|
|
||||||
|
let handle = spawn(
|
||||||
|
async {
|
||||||
|
panic!("test");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
run(
|
||||||
|
async {
|
||||||
|
handle.await;
|
||||||
|
},
|
||||||
|
ProcStack {},
|
||||||
|
);
|
||||||
|
|
||||||
|
let pool = pool::get();
|
||||||
|
let manager = pool::get_manager().unwrap();
|
||||||
|
println!("After panic: {:?}", pool);
|
||||||
|
println!("{:#?}", manager);
|
||||||
|
|
||||||
|
let h = std::thread::spawn(|| {
|
||||||
|
panic!("This is a test");
|
||||||
|
});
|
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_secs(30));
|
||||||
|
|
||||||
|
println!("After panic");
|
||||||
|
}
|
5
runtime/executor/scripts/test_blocking_thread_pool.sh
Normal file
5
runtime/executor/scripts/test_blocking_thread_pool.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
|
||||||
|
cargo test longhauling_task_join -- --ignored --exact --nocapture
|
||||||
|
cargo test slow_join_interrupted -- --ignored --exact --nocapture
|
||||||
|
cargo test slow_join -- --ignored --exact --nocapture
|
165
runtime/executor/src/blocking.rs
Normal file
165
runtime/executor/src/blocking.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
//!
|
||||||
|
//! Pool of threads to run heavy processes
|
||||||
|
//!
|
||||||
|
//! We spawn futures onto the pool with [`spawn_blocking`] method of global run queue or
|
||||||
|
//! with corresponding [`Worker`]'s spawn method.
|
||||||
|
//!
|
||||||
|
//! [`Worker`]: crate::run_queue::Worker
|
||||||
|
|
||||||
|
use crate::thread_manager::{DynamicPoolManager, DynamicRunner};
|
||||||
|
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use lightproc::lightproc::LightProc;
|
||||||
|
use lightproc::recoverable_handle::RecoverableHandle;
|
||||||
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::iter::Iterator;
|
||||||
|
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;
|
||||||
|
|
||||||
|
const THREAD_RECV_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
/// Spawns a blocking task.
|
||||||
|
///
|
||||||
|
/// The task will be spawned onto a thread pool specifically dedicated to blocking tasks.
|
||||||
|
pub fn spawn_blocking<F, R>(future: F) -> RecoverableHandle<R>
|
||||||
|
where
|
||||||
|
F: Future<Output = R> + Send + 'static,
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
let (task, handle) = LightProc::recoverable(future, schedule);
|
||||||
|
task.schedule();
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("static: empty queue, parking with timeout");
|
||||||
|
thread::park_timeout(park_timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn run_dynamic(&self, parker: impl 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pool interface between the scheduler and thread pool
|
||||||
|
struct Pool {
|
||||||
|
sender: Sender<LightProc>,
|
||||||
|
receiver: Receiver<LightProc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static DYNAMIC_POOL_MANAGER: OnceCell<DynamicPoolManager<BlockingRunner>> = OnceCell::new();
|
||||||
|
|
||||||
|
static POOL: Lazy<Pool> = Lazy::new(|| {
|
||||||
|
#[cfg(feature = "tokio-runtime")]
|
||||||
|
{
|
||||||
|
let runner = 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(),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = BlockingRunner {};
|
||||||
|
|
||||||
|
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 }
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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]
|
||||||
|
fn low_watermark() -> &'static u64 {
|
||||||
|
lazy_static! {
|
||||||
|
static ref LOW_WATERMARK: u64 = {
|
||||||
|
env::var_os("BASTION_BLOCKING_THREADS")
|
||||||
|
.map(|x| x.to_str().unwrap().parse::<u64>().unwrap())
|
||||||
|
.unwrap_or(DEFAULT_LOW_WATERMARK)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
&*LOW_WATERMARK
|
||||||
|
}
|
50
runtime/executor/src/lib.rs
Normal file
50
runtime/executor/src/lib.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//!
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! Bastion Executor is NUMA-aware SMP based Fault-tolerant Executor
|
||||||
|
//!
|
||||||
|
//! Bastion Executor is a highly-available, fault-tolerant, async communication
|
||||||
|
//! oriented executor. Bastion's main idea is supplying a fully async runtime
|
||||||
|
//! with fault-tolerance to work on heavy loads.
|
||||||
|
//!
|
||||||
|
//! Main differences between other executors are:
|
||||||
|
//! * Uses SMP based execution scheme to exploit cache affinity on multiple cores and execution is
|
||||||
|
//! equally distributed over the system resources, which means utilizing the all system.
|
||||||
|
//! * Uses NUMA-aware allocation for scheduler's queues and exploit locality on server workloads.
|
||||||
|
//! * Tailored for creating middleware and working with actor model like concurrency and distributed communication.
|
||||||
|
//!
|
||||||
|
//! **NOTE:** Bastion Executor is independent of it's framework implementation.
|
||||||
|
//! It uses [lightproc] to encapsulate and provide fault-tolerance to your future based workloads.
|
||||||
|
//! You can use your futures with [lightproc] to run your workloads on Bastion Executor without the need to have framework.
|
||||||
|
//!
|
||||||
|
//! [lightproc]: https://docs.rs/lightproc
|
||||||
|
//!
|
||||||
|
|
||||||
|
#![doc(
|
||||||
|
html_logo_url = "https://raw.githubusercontent.com/bastion-rs/bastion/master/img/bastion-logo.png"
|
||||||
|
)]
|
||||||
|
// Force missing implementations
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![warn(missing_debug_implementations)]
|
||||||
|
#![warn(unused_imports)]
|
||||||
|
#![forbid(unused_must_use)]
|
||||||
|
#![forbid(unused_import_braces)]
|
||||||
|
|
||||||
|
pub mod blocking;
|
||||||
|
pub mod load_balancer;
|
||||||
|
pub mod placement;
|
||||||
|
pub mod pool;
|
||||||
|
pub mod run;
|
||||||
|
pub mod sleepers;
|
||||||
|
mod thread_manager;
|
||||||
|
pub mod worker;
|
||||||
|
mod proc_stack;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Prelude of Bastion Executor
|
||||||
|
pub mod prelude {
|
||||||
|
pub use crate::blocking::*;
|
||||||
|
pub use crate::pool::*;
|
||||||
|
pub use crate::run::*;
|
||||||
|
pub use crate::proc_stack::*;
|
||||||
|
}
|
234
runtime/executor/src/load_balancer.rs
Normal file
234
runtime/executor/src/load_balancer.rs
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
//!
|
||||||
|
//! Module for gathering statistics about the run queues of the runtime
|
||||||
|
//!
|
||||||
|
//! Load balancer calculates sampled mean to provide average process execution amount
|
||||||
|
//! to all runtime.
|
||||||
|
//!
|
||||||
|
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::{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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LOAD_BALANCER: Lazy<LoadBalancer> = Lazy::new(|| {
|
||||||
|
let lb = LoadBalancer::new(placement::get_core_ids().unwrap());
|
||||||
|
debug!("Instantiated load_balancer: {:?}", lb);
|
||||||
|
lb
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Load-balancer struct which allows us to update the mean load
|
||||||
|
pub struct LoadBalancer {
|
||||||
|
/// The number of cores
|
||||||
|
/// available for this program
|
||||||
|
pub num_cores: usize,
|
||||||
|
/// The core Ids available for this program
|
||||||
|
/// This doesn't take affinity into account
|
||||||
|
pub cores: Vec<CoreId>,
|
||||||
|
mean_last_updated_at: RwLock<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadBalancer {
|
||||||
|
/// Creates a new LoadBalancer.
|
||||||
|
/// if you're looking for `num_cores` and `cores`
|
||||||
|
/// Have a look at `load_balancer::core_count()`
|
||||||
|
/// and `load_balancer::get_cores()` respectively.
|
||||||
|
pub fn new(cores: Vec<CoreId>) -> Self {
|
||||||
|
Self {
|
||||||
|
num_cores: cores.len(),
|
||||||
|
cores,
|
||||||
|
mean_last_updated_at: RwLock::new(Instant::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for LoadBalancer {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
|
||||||
|
fmt.debug_struct("LoadBalancer")
|
||||||
|
.field("num_cores", &self.num_cores)
|
||||||
|
.field("cores", &self.cores)
|
||||||
|
.field("mean_last_updated_at", &self.mean_last_updated_at)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadBalancer {
|
||||||
|
/// Iterates the statistics to get the mean load across the cores
|
||||||
|
pub fn update_load_mean(&self) {
|
||||||
|
// Check if update should occur
|
||||||
|
if !self.should_update() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.mean_last_updated_at
|
||||||
|
.write()
|
||||||
|
.map(|mut last_updated_at| {
|
||||||
|
*last_updated_at = Instant::now();
|
||||||
|
})
|
||||||
|
.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.
|
||||||
|
const MAX_CORE: usize = 256;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Holding all statistics related to the run queue
|
||||||
|
///
|
||||||
|
/// Contains:
|
||||||
|
/// * Mean level of processes in the run queues
|
||||||
|
/// * SMP queue distributions
|
||||||
|
pub struct Stats {
|
||||||
|
smp_load: [AtomicUsize; MAX_CORE],
|
||||||
|
mean_level: AtomicUsize,
|
||||||
|
updating_mean: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Stats {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
fmt.debug_struct("Stats")
|
||||||
|
.field("smp_load", &&self.smp_load[..])
|
||||||
|
.field("mean_level", &self.mean_level)
|
||||||
|
.field("updating_mean", &self.updating_mean)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stats {
|
||||||
|
/// new returns LockLessStats
|
||||||
|
pub fn new(num_cores: usize) -> Stats {
|
||||||
|
let smp_load: [AtomicUsize; MAX_CORE] = {
|
||||||
|
let mut data: [MaybeUninit<AtomicUsize>; MAX_CORE] =
|
||||||
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
for core_data in data.iter_mut().take(num_cores) {
|
||||||
|
unsafe {
|
||||||
|
std::ptr::write(core_data.as_mut_ptr(), AtomicUsize::new(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for core_data in data.iter_mut().take(MAX_CORE).skip(num_cores) {
|
||||||
|
unsafe {
|
||||||
|
std::ptr::write(core_data.as_mut_ptr(), AtomicUsize::new(usize::MAX));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe { std::mem::transmute::<_, [AtomicUsize; MAX_CORE]>(data) }
|
||||||
|
};
|
||||||
|
Stats {
|
||||||
|
smp_load,
|
||||||
|
mean_level: AtomicUsize::new(0),
|
||||||
|
updating_mean: AtomicBool::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Sync for Stats {}
|
||||||
|
unsafe impl Send for Stats {}
|
||||||
|
|
||||||
|
impl SmpStats for Stats {
|
||||||
|
fn store_load(&self, affinity: usize, load: usize) {
|
||||||
|
self.smp_load[affinity].store(load, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sorted_load(&self) -> ArrayVec<(usize, usize), MAX_CORE> {
|
||||||
|
let mut sorted_load = ArrayVec::new();
|
||||||
|
|
||||||
|
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((core, load)) };
|
||||||
|
}
|
||||||
|
sorted_load.sort_by(|x, y| y.1.cmp(&x.1));
|
||||||
|
sorted_load
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mean(&self) -> usize {
|
||||||
|
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().take(num_cores) {
|
||||||
|
if let Some(tmp) = sum.checked_add(item.load(Ordering::Acquire)) {
|
||||||
|
sum = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mean_level
|
||||||
|
.store(sum.wrapping_div(num_cores), Ordering::Release);
|
||||||
|
|
||||||
|
self.updating_mean.store(false, Ordering::Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Static access to runtime statistics
|
||||||
|
#[inline]
|
||||||
|
pub fn stats() -> &'static Stats {
|
||||||
|
lazy_static! {
|
||||||
|
static ref LOCKLESS_STATS: Stats = Stats::new(*core_count());
|
||||||
|
}
|
||||||
|
&*LOCKLESS_STATS
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Retrieve core count for the runtime scheduling purposes
|
||||||
|
#[inline]
|
||||||
|
pub fn core_count() -> &'static usize {
|
||||||
|
&LOAD_BALANCER.num_cores
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Retrieve cores for the runtime scheduling purposes
|
||||||
|
#[inline]
|
||||||
|
pub fn get_cores() -> &'static [CoreId] {
|
||||||
|
&*LOAD_BALANCER.cores
|
||||||
|
}
|
414
runtime/executor/src/placement.rs
Normal file
414
runtime/executor/src/placement.rs
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
//! Core placement configuration and management
|
||||||
|
//!
|
||||||
|
//! Placement module enables thread placement onto the cores.
|
||||||
|
//! CPU level affinity assignment is done here.
|
||||||
|
|
||||||
|
/// This function tries to retrieve information
|
||||||
|
/// on all the "cores" active on this system.
|
||||||
|
pub fn get_core_ids() -> Option<Vec<CoreId>> {
|
||||||
|
get_core_ids_helper()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function tries to retrieve
|
||||||
|
/// the number of active "cores" on the system.
|
||||||
|
pub fn get_num_cores() -> Option<usize> {
|
||||||
|
get_core_ids().map(|ids| ids.len())
|
||||||
|
}
|
||||||
|
///
|
||||||
|
/// Sets the current threads affinity
|
||||||
|
pub fn set_for_current(core_id: CoreId) {
|
||||||
|
tracing::trace!("Executor: placement: set affinity on core {}", core_id.id);
|
||||||
|
set_for_current_helper(core_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// CoreID implementation to identify system cores.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct CoreId {
|
||||||
|
/// Used core ID
|
||||||
|
pub id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux Section
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[inline]
|
||||||
|
fn get_core_ids_helper() -> Option<Vec<CoreId>> {
|
||||||
|
linux::get_core_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[inline]
|
||||||
|
fn set_for_current_helper(core_id: CoreId) {
|
||||||
|
linux::set_for_current(core_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux {
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use libc::{cpu_set_t, sched_getaffinity, sched_setaffinity, CPU_ISSET, CPU_SET, CPU_SETSIZE};
|
||||||
|
|
||||||
|
use super::CoreId;
|
||||||
|
|
||||||
|
pub fn get_core_ids() -> Option<Vec<CoreId>> {
|
||||||
|
if let Some(full_set) = get_affinity_mask() {
|
||||||
|
let mut core_ids: Vec<CoreId> = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..CPU_SETSIZE as usize {
|
||||||
|
if unsafe { CPU_ISSET(i, &full_set) } {
|
||||||
|
core_ids.push(CoreId { id: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(core_ids)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_for_current(core_id: CoreId) {
|
||||||
|
// Turn `core_id` into a `libc::cpu_set_t` with only
|
||||||
|
// one core active.
|
||||||
|
let mut set = new_cpu_set();
|
||||||
|
|
||||||
|
unsafe { CPU_SET(core_id.id, &mut set) };
|
||||||
|
|
||||||
|
// Set the current thread's core affinity.
|
||||||
|
unsafe {
|
||||||
|
sched_setaffinity(
|
||||||
|
0, // Defaults to current thread
|
||||||
|
mem::size_of::<cpu_set_t>(),
|
||||||
|
&set,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_affinity_mask() -> Option<cpu_set_t> {
|
||||||
|
let mut set = new_cpu_set();
|
||||||
|
|
||||||
|
// Try to get current core affinity mask.
|
||||||
|
let result = unsafe {
|
||||||
|
sched_getaffinity(
|
||||||
|
0, // Defaults to current thread
|
||||||
|
mem::size_of::<cpu_set_t>(),
|
||||||
|
&mut set,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
Some(set)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_cpu_set() -> cpu_set_t {
|
||||||
|
unsafe { mem::zeroed::<cpu_set_t>() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linux_get_affinity_mask() {
|
||||||
|
match get_affinity_mask() {
|
||||||
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linux_get_core_ids() {
|
||||||
|
match get_core_ids() {
|
||||||
|
Some(set) => {
|
||||||
|
assert_eq!(set.len(), num_cpus::get());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linux_set_for_current() {
|
||||||
|
let ids = get_core_ids().unwrap();
|
||||||
|
|
||||||
|
assert!(!ids.is_empty());
|
||||||
|
|
||||||
|
set_for_current(ids[0]);
|
||||||
|
|
||||||
|
// Ensure that the system pinned the current thread
|
||||||
|
// to the specified core.
|
||||||
|
let mut core_mask = new_cpu_set();
|
||||||
|
unsafe { CPU_SET(ids[0].id, &mut core_mask) };
|
||||||
|
|
||||||
|
let new_mask = get_affinity_mask().unwrap();
|
||||||
|
|
||||||
|
let mut is_equal = true;
|
||||||
|
|
||||||
|
for i in 0..CPU_SETSIZE as usize {
|
||||||
|
let is_set1 = unsafe { CPU_ISSET(i, &core_mask) };
|
||||||
|
let is_set2 = unsafe { CPU_ISSET(i, &new_mask) };
|
||||||
|
|
||||||
|
if is_set1 != is_set2 {
|
||||||
|
is_equal = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(is_equal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows Section
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[inline]
|
||||||
|
fn get_core_ids_helper() -> Option<Vec<CoreId>> {
|
||||||
|
windows::get_core_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[inline]
|
||||||
|
fn set_for_current_helper(core_id: CoreId) {
|
||||||
|
windows::set_for_current(core_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
extern crate winapi;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod windows {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use winapi::shared::basetsd::{DWORD_PTR, PDWORD_PTR};
|
||||||
|
use winapi::um::processthreadsapi::{GetCurrentProcess, GetCurrentThread};
|
||||||
|
use winapi::um::winbase::{GetProcessAffinityMask, SetThreadAffinityMask};
|
||||||
|
|
||||||
|
use super::CoreId;
|
||||||
|
|
||||||
|
pub fn get_core_ids() -> Option<Vec<CoreId>> {
|
||||||
|
if let Some(mask) = get_affinity_mask() {
|
||||||
|
// Find all active cores in the bitmask.
|
||||||
|
let mut core_ids: Vec<CoreId> = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..usize::MIN.count_zeros() as usize {
|
||||||
|
let test_mask = 1 << i;
|
||||||
|
|
||||||
|
if (mask & test_mask) == test_mask {
|
||||||
|
core_ids.push(CoreId { id: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(core_ids)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_for_current(core_id: CoreId) {
|
||||||
|
// Convert `CoreId` back into mask.
|
||||||
|
let mask: DWORD_PTR = 1 << core_id.id;
|
||||||
|
|
||||||
|
// Set core affinity for current thread.
|
||||||
|
unsafe {
|
||||||
|
SetThreadAffinityMask(GetCurrentThread(), mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_affinity_mask() -> Option<usize> {
|
||||||
|
let mut process_mask: usize = 0;
|
||||||
|
let mut system_mask: usize = 0;
|
||||||
|
|
||||||
|
let res = unsafe {
|
||||||
|
GetProcessAffinityMask(
|
||||||
|
GetCurrentProcess(),
|
||||||
|
&mut process_mask as PDWORD_PTR,
|
||||||
|
&mut system_mask as PDWORD_PTR,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Successfully retrieved affinity mask
|
||||||
|
if res != 0 {
|
||||||
|
Some(process_mask)
|
||||||
|
}
|
||||||
|
// Failed to retrieve affinity mask
|
||||||
|
else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use num_cpus;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_macos_get_core_ids() {
|
||||||
|
match get_core_ids() {
|
||||||
|
Some(set) => {
|
||||||
|
assert_eq!(set.len(), num_cpus::get());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_macos_set_for_current() {
|
||||||
|
let ids = get_core_ids().unwrap();
|
||||||
|
|
||||||
|
assert!(ids.len() > 0);
|
||||||
|
|
||||||
|
set_for_current(ids[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MacOS Section
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[inline]
|
||||||
|
fn get_core_ids_helper() -> Option<Vec<CoreId>> {
|
||||||
|
macos::get_core_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[inline]
|
||||||
|
fn set_for_current_helper(core_id: CoreId) {
|
||||||
|
macos::set_for_current(core_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos {
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use libc::{c_int, c_uint, pthread_self};
|
||||||
|
|
||||||
|
use super::CoreId;
|
||||||
|
|
||||||
|
type KernReturnT = c_int;
|
||||||
|
type IntegerT = c_int;
|
||||||
|
type NaturalT = c_uint;
|
||||||
|
type ThreadT = c_uint;
|
||||||
|
type ThreadPolicyFlavorT = NaturalT;
|
||||||
|
type MachMsgTypeNumberT = NaturalT;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct ThreadAffinityPolicyDataT {
|
||||||
|
affinity_tag: IntegerT,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThreadPolicyT = *mut ThreadAffinityPolicyDataT;
|
||||||
|
|
||||||
|
const THREAD_AFFINITY_POLICY: ThreadPolicyFlavorT = 4;
|
||||||
|
|
||||||
|
#[link(name = "System", kind = "framework")]
|
||||||
|
extern "C" {
|
||||||
|
fn thread_policy_set(
|
||||||
|
thread: ThreadT,
|
||||||
|
flavor: ThreadPolicyFlavorT,
|
||||||
|
policy_info: ThreadPolicyT,
|
||||||
|
count: MachMsgTypeNumberT,
|
||||||
|
) -> KernReturnT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_core_ids() -> Option<Vec<CoreId>> {
|
||||||
|
Some(
|
||||||
|
(0..(num_cpus::get()))
|
||||||
|
.map(|n| CoreId { id: n as usize })
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_for_current(core_id: CoreId) {
|
||||||
|
let thread_affinity_policy_count: MachMsgTypeNumberT =
|
||||||
|
mem::size_of::<ThreadAffinityPolicyDataT>() as MachMsgTypeNumberT
|
||||||
|
/ mem::size_of::<IntegerT>() as MachMsgTypeNumberT;
|
||||||
|
|
||||||
|
let mut info = ThreadAffinityPolicyDataT {
|
||||||
|
affinity_tag: core_id.id as IntegerT,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
thread_policy_set(
|
||||||
|
pthread_self() as ThreadT,
|
||||||
|
THREAD_AFFINITY_POLICY,
|
||||||
|
&mut info as ThreadPolicyT,
|
||||||
|
thread_affinity_policy_count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_windows_get_core_ids() {
|
||||||
|
match get_core_ids() {
|
||||||
|
Some(set) => {
|
||||||
|
assert_eq!(set.len(), num_cpus::get());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_windows_set_for_current() {
|
||||||
|
let ids = get_core_ids().unwrap();
|
||||||
|
|
||||||
|
assert!(ids.len() > 0);
|
||||||
|
|
||||||
|
set_for_current(ids[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub Section
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||||
|
#[inline]
|
||||||
|
fn get_core_ids_helper() -> Option<Vec<CoreId>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||||
|
#[inline]
|
||||||
|
fn set_for_current_helper(core_id: CoreId) {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_core_ids() {
|
||||||
|
match get_core_ids() {
|
||||||
|
Some(set) => {
|
||||||
|
assert_eq!(set.len(), num_cpus::get());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_for_current() {
|
||||||
|
let ids = get_core_ids().unwrap();
|
||||||
|
|
||||||
|
assert!(!ids.is_empty());
|
||||||
|
|
||||||
|
set_for_current(ids[0]);
|
||||||
|
}
|
||||||
|
}
|
223
runtime/executor/src/pool.rs
Normal file
223
runtime/executor/src/pool.rs
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
//!
|
||||||
|
//! Pool of threads to run lightweight processes
|
||||||
|
//!
|
||||||
|
//! 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::lightproc::LightProc;
|
||||||
|
use lightproc::recoverable_handle::RecoverableHandle;
|
||||||
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::iter::Iterator;
|
||||||
|
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.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use executor::prelude::*;
|
||||||
|
///
|
||||||
|
/// # #[cfg(feature = "tokio-runtime")]
|
||||||
|
/// # #[tokio::main]
|
||||||
|
/// # async fn main() {
|
||||||
|
/// # start();
|
||||||
|
/// # }
|
||||||
|
/// #
|
||||||
|
/// # #[cfg(not(feature = "tokio-runtime"))]
|
||||||
|
/// # fn main() {
|
||||||
|
/// # start();
|
||||||
|
/// # }
|
||||||
|
/// #
|
||||||
|
/// # fn start() {
|
||||||
|
///
|
||||||
|
/// let handle = spawn(
|
||||||
|
/// async {
|
||||||
|
/// panic!("test");
|
||||||
|
/// },
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// run(
|
||||||
|
/// async {
|
||||||
|
/// handle.await;
|
||||||
|
/// },
|
||||||
|
/// ProcStack { },
|
||||||
|
/// );
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn spawn<F, R>(future: F) -> RecoverableHandle<R>
|
||||||
|
where
|
||||||
|
F: Future<Output = R> + Send + 'static,
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
let (task, handle) = LightProc::recoverable(future, worker::schedule);
|
||||||
|
task.schedule();
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawns a blocking task.
|
||||||
|
///
|
||||||
|
/// The task will be spawned onto a thread pool specifically dedicated to blocking tasks.
|
||||||
|
pub fn spawn_blocking<F, R>(future: F) -> RecoverableHandle<R>
|
||||||
|
where
|
||||||
|
F: Future<Output = R> + Send + 'static,
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
let (task, handle) = LightProc::recoverable(future, schedule);
|
||||||
|
task.schedule();
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Acquire the static Pool reference
|
||||||
|
#[inline]
|
||||||
|
pub fn get() -> &'static Pool {
|
||||||
|
&*POOL
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_manager() -> Option<&'static DynamicPoolManager<AsyncRunner>> {
|
||||||
|
DYNAMIC_POOL_MANAGER.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pool {
|
||||||
|
///
|
||||||
|
/// Spawn a process (which contains future + process stack) onto the executor via [Pool] interface.
|
||||||
|
pub fn spawn<F, R>(&self, future: F) -> RecoverableHandle<R>
|
||||||
|
where
|
||||||
|
F: Future<Output = R> + Send + 'static,
|
||||||
|
R: Send + 'static,
|
||||||
|
{
|
||||||
|
let (task, handle) = LightProc::recoverable(future, worker::schedule);
|
||||||
|
task.schedule();
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// 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]
|
||||||
|
fn low_watermark() -> &'static u64 {
|
||||||
|
lazy_static! {
|
||||||
|
static ref LOW_WATERMARK: u64 = {
|
||||||
|
env::var_os("BASTION_BLOCKING_THREADS")
|
||||||
|
.map(|x| x.to_str().unwrap().parse::<u64>().unwrap())
|
||||||
|
.unwrap_or(DEFAULT_LOW_WATERMARK)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
&*LOW_WATERMARK
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If low watermark isn't configured this is the default scaler value.
|
||||||
|
/// This value is used for the heuristics of the scaler
|
||||||
|
const DEFAULT_LOW_WATERMARK: u64 = 2;
|
||||||
|
|
||||||
|
/// Pool interface between the scheduler and thread pool
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Pool {
|
||||||
|
sender: Sender<LightProc>,
|
||||||
|
receiver: Receiver<LightProc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AsyncRunner {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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: impl 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) {
|
||||||
|
task.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static DYNAMIC_POOL_MANAGER: OnceCell<DynamicPoolManager<AsyncRunner>> = OnceCell::new();
|
||||||
|
|
||||||
|
static POOL: Lazy<Pool> = Lazy::new(|| {
|
||||||
|
#[cfg(feature = "tokio-runtime")]
|
||||||
|
{
|
||||||
|
let runner = 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 = 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 }
|
||||||
|
});
|
5
runtime/executor/src/proc_stack.rs
Normal file
5
runtime/executor/src/proc_stack.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ProcStack {
|
||||||
|
|
||||||
|
}
|
154
runtime/executor/src/run.rs
Normal file
154
runtime/executor/src/run.rs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
//!
|
||||||
|
//! Blocking run of the async processes
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
use crate::worker;
|
||||||
|
use crossbeam_utils::sync::{Parker, Unparker};
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::mem;
|
||||||
|
use std::mem::{ManuallyDrop, MaybeUninit};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||||
|
use crate::proc_stack::ProcStack;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// This method blocks the current thread until passed future is resolved with an output.
|
||||||
|
///
|
||||||
|
/// It is called `block_on` or `blocking` in some executors.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use executor::prelude::*;
|
||||||
|
/// use lightproc::prelude::*;
|
||||||
|
/// let mut sum = 0;
|
||||||
|
///
|
||||||
|
/// run(
|
||||||
|
/// async {
|
||||||
|
/// (0..10_000_000).for_each(|_| {
|
||||||
|
/// sum += 1;
|
||||||
|
/// });
|
||||||
|
/// },
|
||||||
|
/// ProcStack::default(),
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn run<F, T>(future: F, stack: ProcStack) -> T
|
||||||
|
where
|
||||||
|
F: Future<Output = T>,
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
// An explicitly uninitialized `T`. Until `assume_init` is called this will not call any
|
||||||
|
// drop code for T
|
||||||
|
let mut out = MaybeUninit::uninit();
|
||||||
|
|
||||||
|
// Wrap the future into one that stores the result into `out`.
|
||||||
|
let future = {
|
||||||
|
let out = out.as_mut_ptr();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
*out = future.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pin the future onto the stack.
|
||||||
|
pin_utils::pin_mut!(future);
|
||||||
|
|
||||||
|
// Extend the lifetime of the future to 'static.
|
||||||
|
let future = mem::transmute::<
|
||||||
|
Pin<&'_ mut dyn Future<Output = ()>>,
|
||||||
|
Pin<&'static mut dyn Future<Output = ()>>,
|
||||||
|
>(future);
|
||||||
|
|
||||||
|
// Block on the future and and wait for it to complete.
|
||||||
|
worker::set_stack(&stack, || block(future));
|
||||||
|
|
||||||
|
// Assume that if the future completed and didn't panic it fully initialized its output
|
||||||
|
out.assume_init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block<F, T>(f: F) -> T
|
||||||
|
where
|
||||||
|
F: Future<Output = T>,
|
||||||
|
{
|
||||||
|
thread_local! {
|
||||||
|
// May hold a pre-allocated parker that can be reused for efficiency.
|
||||||
|
//
|
||||||
|
// Note that each invocation of `block` needs its own parker. In particular, if `block`
|
||||||
|
// recursively calls itself, we must make sure that each recursive call uses a distinct
|
||||||
|
// parker instance.
|
||||||
|
static CACHE: Cell<Option<Parker>> = Cell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pin_utils::pin_mut!(f);
|
||||||
|
|
||||||
|
CACHE.with(|cache| {
|
||||||
|
// Reuse a cached parker or create a new one for this invocation of `block`.
|
||||||
|
let parker: Parker = cache.take().unwrap_or_else(|| Parker::new());
|
||||||
|
|
||||||
|
let ptr = Unparker::into_raw(parker.unparker().clone());
|
||||||
|
let vt = vtable();
|
||||||
|
|
||||||
|
// Waker must not be dropped until it's no longer required. We also happen to know that a
|
||||||
|
// Parker contains at least one reference to `Unparker` so the relevant `Unparker` will not
|
||||||
|
// be dropped at least until the `Parker` is.
|
||||||
|
let waker = unsafe { Waker::from_raw(RawWaker::new(ptr, vt)) };
|
||||||
|
let cx = &mut Context::from_waker(&waker);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Poll::Ready(t) = f.as_mut().poll(cx) {
|
||||||
|
// Save the parker for the next invocation of `block`.
|
||||||
|
cache.set(Some(parker));
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
parker.park();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vtable() -> &'static RawWakerVTable {
|
||||||
|
/// This function will be called when the RawWaker gets cloned, e.g. when the Waker in which
|
||||||
|
/// the RawWaker is stored gets cloned.
|
||||||
|
//
|
||||||
|
/// The implementation of this function must retain all resources that are required for this
|
||||||
|
/// additional instance of a RawWaker and associated task. Calling wake on the resulting
|
||||||
|
/// RawWaker should result in a wakeup of the same task that would have been awoken by the
|
||||||
|
/// original RawWaker.
|
||||||
|
unsafe fn clone_raw(ptr: *const ()) -> RawWaker {
|
||||||
|
// [`Unparker`] implements `Clone` and upholds the contract stated above. The current
|
||||||
|
// Implementation is simply an Arc over the actual inner values.
|
||||||
|
let unparker = Unparker::from_raw(ptr).clone();
|
||||||
|
RawWaker::new(Unparker::into_raw(unparker), vtable())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function will be called when wake is called on the Waker. It must wake up the task
|
||||||
|
/// associated with this RawWaker.
|
||||||
|
///
|
||||||
|
/// The implementation of this function must make sure to release any resources that are
|
||||||
|
/// associated with this instance of a RawWaker and associated task.
|
||||||
|
unsafe fn wake_raw(ptr: *const ()) {
|
||||||
|
// We reconstruct the Unparker from the pointer here thus ensuring it is dropped at the
|
||||||
|
// end of this function call.
|
||||||
|
Unparker::from_raw(ptr).unpark();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function will be called when wake_by_ref is called on the Waker. It must wake up the
|
||||||
|
/// task associated with this RawWaker.
|
||||||
|
///
|
||||||
|
/// This function is similar to wake, but must not consume the provided data pointer.
|
||||||
|
unsafe fn wake_by_ref_raw(ptr: *const ()) {
|
||||||
|
// We **must not** drop the resulting Unparker so we wrap it in `ManuallyDrop`.
|
||||||
|
let unparker = ManuallyDrop::new(Unparker::from_raw(ptr));
|
||||||
|
unparker.unpark();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function gets called when a RawWaker gets dropped.
|
||||||
|
///
|
||||||
|
/// The implementation of this function must make sure to release any resources that are
|
||||||
|
/// associated with this instance of a RawWaker and associated task.
|
||||||
|
unsafe fn drop_raw(ptr: *const ()) {
|
||||||
|
drop(Unparker::from_raw(ptr))
|
||||||
|
}
|
||||||
|
|
||||||
|
&RawWakerVTable::new(clone_raw, wake_raw, wake_by_ref_raw, drop_raw)
|
||||||
|
}
|
67
runtime/executor/src/sleepers.rs
Normal file
67
runtime/executor/src/sleepers.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//!
|
||||||
|
//! Where workers went to parking while no workload is in their worker queue.
|
||||||
|
//!
|
||||||
|
//! If a workload received pool will wake them up.
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Condvar, Mutex};
|
||||||
|
|
||||||
|
/// The place where worker threads go to sleep.
|
||||||
|
///
|
||||||
|
/// Similar to how thread parking works, if a notification comes up while no threads are sleeping,
|
||||||
|
/// the next thread that attempts to go to sleep will pick up the notification immediately.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(clippy::mutex_atomic)]
|
||||||
|
pub struct Sleepers {
|
||||||
|
/// How many threads are currently a sleep.
|
||||||
|
sleep: Mutex<usize>,
|
||||||
|
|
||||||
|
/// A condvar for notifying sleeping threads.
|
||||||
|
wake: Condvar,
|
||||||
|
|
||||||
|
/// Set to `true` if a notification came up while nobody was sleeping.
|
||||||
|
notified: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::mutex_atomic)]
|
||||||
|
impl Default for Sleepers {
|
||||||
|
/// Creates a new `Sleepers`.
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
sleep: Mutex::new(0),
|
||||||
|
wake: Condvar::new(),
|
||||||
|
notified: AtomicBool::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::mutex_atomic)]
|
||||||
|
impl Sleepers {
|
||||||
|
/// Creates a new `Sleepers`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts the current thread to sleep.
|
||||||
|
pub fn wait(&self) {
|
||||||
|
let mut sleep = self.sleep.lock().unwrap();
|
||||||
|
|
||||||
|
if !self.notified.swap(false, Ordering::SeqCst) {
|
||||||
|
*sleep += 1;
|
||||||
|
std::mem::drop(self.wake.wait(sleep).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Notifies one thread.
|
||||||
|
pub fn notify_one(&self) {
|
||||||
|
if !self.notified.load(Ordering::SeqCst) {
|
||||||
|
let mut sleep = self.sleep.lock().unwrap();
|
||||||
|
|
||||||
|
if *sleep > 0 {
|
||||||
|
*sleep -= 1;
|
||||||
|
self.wake.notify_one();
|
||||||
|
} else {
|
||||||
|
self.notified.store(true, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
404
runtime/executor/src/thread_manager.rs
Normal file
404
runtime/executor/src/thread_manager.rs
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
//! 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::{HashMap, VecDeque};
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
Mutex,
|
||||||
|
},
|
||||||
|
thread::{self, Thread},
|
||||||
|
};
|
||||||
|
use std::any::Any;
|
||||||
|
use std::panic::resume_unwind;
|
||||||
|
use std::thread::{JoinHandle, ThreadId};
|
||||||
|
use crossbeam_deque::Worker;
|
||||||
|
use crossbeam_utils::sync::{Parker, Unparker};
|
||||||
|
use tracing::{debug, trace};
|
||||||
|
use lightproc::lightproc::LightProc;
|
||||||
|
|
||||||
|
/// The default thread park timeout before checking for new tasks.
|
||||||
|
const THREAD_PARK_TIMEOUT: Duration = Duration::from_millis(1);
|
||||||
|
|
||||||
|
/// Frequency histogram's sliding window size.
|
||||||
|
/// Defines how many frequencies will be considered for adaptation.
|
||||||
|
const FREQUENCY_QUEUE_SIZE: usize = 10;
|
||||||
|
|
||||||
|
/// If low watermark isn't configured this is the default scaler value.
|
||||||
|
/// This value is used for the heuristics of the scaler
|
||||||
|
const DEFAULT_LOW_WATERMARK: u64 = 2;
|
||||||
|
|
||||||
|
/// Pool scaler interval time (milliseconds).
|
||||||
|
/// This is the actual interval which makes adaptation calculation.
|
||||||
|
const SCALER_POLL_INTERVAL: u64 = 90;
|
||||||
|
|
||||||
|
/// Exponential moving average smoothing coefficient for limited window.
|
||||||
|
/// Smoothing factor is estimated with: 2 / (N + 1) where N is sample size.
|
||||||
|
const EMA_COEFFICIENT: f64 = 2_f64 / (FREQUENCY_QUEUE_SIZE as f64 + 1_f64);
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref ROUND_ROBIN_PIN: Mutex<CoreId> = Mutex::new(CoreId { id: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `DynamicRunner` is piloted by `DynamicPoolManager`.
|
||||||
|
/// Upon request it needs to be able to provide runner routines for:
|
||||||
|
/// * Static threads.
|
||||||
|
/// * Dynamic threads.
|
||||||
|
/// * Standalone threads.
|
||||||
|
///
|
||||||
|
/// Your implementation of `DynamicRunner`
|
||||||
|
/// will allow you to define what tasks must be accomplished.
|
||||||
|
///
|
||||||
|
/// Run static threads:
|
||||||
|
///
|
||||||
|
/// run_static should never return, and park for park_timeout instead.
|
||||||
|
///
|
||||||
|
/// Run dynamic threads:
|
||||||
|
/// run_dynamic should never return, and call `parker()` when it has no more tasks to process.
|
||||||
|
/// It will be unparked automatically by the `DynamicPoolManager` if needs be.
|
||||||
|
///
|
||||||
|
/// Run standalone threads:
|
||||||
|
/// run_standalone should return once it has no more tasks to process.
|
||||||
|
/// The `DynamicPoolManager` will spawn other standalone threads if needs be.
|
||||||
|
pub trait DynamicRunner {
|
||||||
|
fn run_static(&self, park_timeout: Duration) -> ! {
|
||||||
|
let parker = Parker::new();
|
||||||
|
self.run_dynamic(|| parker.park_timeout(park_timeout));
|
||||||
|
}
|
||||||
|
fn run_dynamic(&self, parker: impl 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<Runner> {
|
||||||
|
static_threads: usize,
|
||||||
|
dynamic_threads: usize,
|
||||||
|
parked_threads: ArrayQueue<Unparker>,
|
||||||
|
runner: Runner,
|
||||||
|
last_frequency: AtomicU64,
|
||||||
|
frequencies: TTas<VecDeque<u64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Runner: Debug> Debug for DynamicPoolManager<Runner> {
|
||||||
|
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("runner", &self.runner)
|
||||||
|
.field("last_frequency", &self.last_frequency)
|
||||||
|
.field("frequencies", &self.frequencies.try_lock())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Runner: DynamicRunner + Sync + Send> DynamicPoolManager<Runner> {
|
||||||
|
pub fn new(static_threads: usize, runner: Runner) -> 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!("spooling up {} static worker threads", self.static_threads);
|
||||||
|
(0..self.static_threads).for_each(|n| {
|
||||||
|
let runner = &self.runner;
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(format!("static #{}", n))
|
||||||
|
.spawn(move || {
|
||||||
|
Self::affinity_pinner();
|
||||||
|
runner.run_static(THREAD_PARK_TIMEOUT);
|
||||||
|
})
|
||||||
|
.expect("failed to spawn static worker thread");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamic thread manager that will allow us to unpark threads when needed
|
||||||
|
trace!("spooling up {} dynamic worker threads", self.dynamic_threads);
|
||||||
|
(0..self.dynamic_threads).for_each(|n| {
|
||||||
|
let runner = &self.runner;
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(format!("dynamic #{}", n))
|
||||||
|
.spawn(move || {
|
||||||
|
Self::affinity_pinner();
|
||||||
|
let parker = Parker::new();
|
||||||
|
let unparker = parker.unparker();
|
||||||
|
runner.run_dynamic(|| self.park_thread(&parker, unparker));
|
||||||
|
})
|
||||||
|
.expect("failed to spawn dynamic worker thread");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pool manager to check frequency of task rates
|
||||||
|
// and take action by scaling the pool accordingly.
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("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("failed to spawn pool manager thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 runner = &self.runner;
|
||||||
|
thread::Builder::new()
|
||||||
|
.name("standalone worker".to_string())
|
||||||
|
.spawn(move || {
|
||||||
|
Self::affinity_pinner();
|
||||||
|
runner.run_standalone();
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parks a thread until [`unpark_thread`] unparks it
|
||||||
|
pub fn park_thread(&self, parker: &Parker, unparker: &Unparker) {
|
||||||
|
if let Err(unparker) = self.parked_threads
|
||||||
|
// Unparker is an Arc internally so this is (comparatively) cheap to do.
|
||||||
|
.push(unparker.clone()) {
|
||||||
|
panic!("Failed to park with {:?}", unparker);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("parking thread {:?}", std::thread::current().id());
|
||||||
|
parker.park();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pops a thread from the parked_threads queue and unparks it.
|
||||||
|
///
|
||||||
|
/// Returns true if there were threads to unpark
|
||||||
|
fn unpark_thread(&self) -> bool {
|
||||||
|
trace!("parked_threads: len is {}", self.parked_threads.len());
|
||||||
|
if let Some(unparker) = self.parked_threads.pop() {
|
||||||
|
debug!("Unparking thread with {:?}", &unparker);
|
||||||
|
unparker.unpark();
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Affinity pinner for blocking pool
|
||||||
|
///
|
||||||
|
/// Pinning isn't going to be enabled for single core systems.
|
||||||
|
#[inline]
|
||||||
|
fn affinity_pinner() {
|
||||||
|
if 1 != *load_balancer::core_count() {
|
||||||
|
let mut core = ROUND_ROBIN_PIN.lock().unwrap();
|
||||||
|
placement::set_for_current(*core);
|
||||||
|
core.id = (core.id + 1) % *load_balancer::core_count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exponentially Weighted Moving Average calculation
|
||||||
|
///
|
||||||
|
/// This allows us to find the EMA value.
|
||||||
|
/// This value represents the trend of tasks mapped onto the thread pool.
|
||||||
|
/// Calculation is following:
|
||||||
|
/// ```text
|
||||||
|
/// +--------+-----------------+----------------------------------+
|
||||||
|
/// | Symbol | Identifier | Explanation |
|
||||||
|
/// +--------+-----------------+----------------------------------+
|
||||||
|
/// | α | EMA_COEFFICIENT | smoothing factor between 0 and 1 |
|
||||||
|
/// | Yt | freq | frequency sample at time t |
|
||||||
|
/// | St | acc | EMA at time t |
|
||||||
|
/// +--------+-----------------+----------------------------------+
|
||||||
|
/// ```
|
||||||
|
/// Under these definitions formula is following:
|
||||||
|
/// ```text
|
||||||
|
/// EMA = α * [ Yt + (1 - α)*Yt-1 + ((1 - α)^2)*Yt-2 + ((1 - α)^3)*Yt-3 ... ] + St
|
||||||
|
/// ```
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `freq_queue` - Sliding window of frequency samples
|
||||||
|
#[inline]
|
||||||
|
fn calculate_ema(freq_queue: &VecDeque<u64>) -> f64 {
|
||||||
|
freq_queue.iter().enumerate().fold(0_f64, |acc, (i, freq)| {
|
||||||
|
acc + ((*freq as f64) * ((1_f64 - EMA_COEFFICIENT).powf(i as f64) as f64))
|
||||||
|
}) * EMA_COEFFICIENT as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adaptive pool scaling function
|
||||||
|
///
|
||||||
|
/// This allows to spawn new threads to make room for incoming task pressure.
|
||||||
|
/// Works in the background detached from the pool system and scales up the pool based
|
||||||
|
/// on the request rate.
|
||||||
|
///
|
||||||
|
/// It uses frequency based calculation to define work. Utilizing average processing rate.
|
||||||
|
fn scale_pool(&'static self) {
|
||||||
|
// Fetch current frequency, it does matter that operations are ordered in this approach.
|
||||||
|
let current_frequency = self.last_frequency.swap(0, Ordering::SeqCst);
|
||||||
|
let mut freq_queue = self.frequencies.lock();
|
||||||
|
|
||||||
|
// Make it safe to start for calculations by adding initial frequency scale
|
||||||
|
if freq_queue.len() == 0 {
|
||||||
|
freq_queue.push_back(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate message rate for the given time window
|
||||||
|
let frequency = (current_frequency as f64 / SCALER_POLL_INTERVAL as f64) as u64;
|
||||||
|
|
||||||
|
// Calculates current time window's EMA value (including last sample)
|
||||||
|
let prev_ema_frequency = Self::calculate_ema(&freq_queue);
|
||||||
|
|
||||||
|
// Add seen frequency data to the frequency histogram.
|
||||||
|
freq_queue.push_back(frequency);
|
||||||
|
if freq_queue.len() == FREQUENCY_QUEUE_SIZE.saturating_add(1) {
|
||||||
|
freq_queue.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates current time window's EMA value (including last sample)
|
||||||
|
let curr_ema_frequency = Self::calculate_ema(&freq_queue);
|
||||||
|
|
||||||
|
// Adapts the thread count of pool
|
||||||
|
//
|
||||||
|
// Sliding window of frequencies visited by the pool manager.
|
||||||
|
// Pool manager creates EMA value for previous window and current window.
|
||||||
|
// Compare them to determine scaling amount based on the trends.
|
||||||
|
// If current EMA value is bigger, we will scale up.
|
||||||
|
if curr_ema_frequency > prev_ema_frequency {
|
||||||
|
// "Scale by" amount can be seen as "how much load is coming".
|
||||||
|
// "Scale" amount is "how many threads we should spawn".
|
||||||
|
let scale_by: f64 = curr_ema_frequency - prev_ema_frequency;
|
||||||
|
let scale = num_cpus::get().min(
|
||||||
|
((DEFAULT_LOW_WATERMARK as f64 * scale_by) + DEFAULT_LOW_WATERMARK as f64) as usize,
|
||||||
|
);
|
||||||
|
trace!("unparking {} threads", scale);
|
||||||
|
|
||||||
|
// It is time to scale the pool!
|
||||||
|
self.provision_threads(scale);
|
||||||
|
} else if (curr_ema_frequency - prev_ema_frequency).abs() < 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
runtime/executor/src/worker.rs
Normal file
93
runtime/executor/src/worker.rs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
//!
|
||||||
|
//! SMP parallelism based cache affine worker implementation
|
||||||
|
//!
|
||||||
|
//! 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::pool;
|
||||||
|
|
||||||
|
use lightproc::prelude::*;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::ptr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use crossbeam_deque::{Stealer, Worker};
|
||||||
|
use crate::proc_stack::ProcStack;
|
||||||
|
|
||||||
|
/// The timeout we'll use when parking before an other Steal attempt
|
||||||
|
pub const THREAD_PARK_TIMEOUT: Duration = Duration::from_millis(1);
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static STACK: Cell<*const ProcStack> = Cell::new(ptr::null_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Set the current process's stack during the run of the future.
|
||||||
|
pub(crate) fn set_stack<F, R>(stack: *const ProcStack, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce() -> R,
|
||||||
|
{
|
||||||
|
struct ResetStack<'a>(&'a Cell<*const ProcStack>);
|
||||||
|
|
||||||
|
impl Drop for ResetStack<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.set(ptr::null());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STACK.with(|st| {
|
||||||
|
st.set(stack);
|
||||||
|
// create a guard to reset STACK even if the future panics. This is important since we
|
||||||
|
// must not drop the pointed-to ProcStack here in any case.
|
||||||
|
let _guard = ResetStack(st);
|
||||||
|
|
||||||
|
f()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub(crate) fn get_proc_stack<F, R>(f: F) -> Option<R>
|
||||||
|
where
|
||||||
|
F: FnOnce(&ProcStack) -> R,
|
||||||
|
{
|
||||||
|
let res = STACK.try_with(|st| unsafe { st.get().as_ref().map(f) });
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(Some(val)) => Some(val),
|
||||||
|
Ok(None) | Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Get the stack currently in use for this thread
|
||||||
|
pub fn current() -> ProcStack {
|
||||||
|
get_proc_stack(|proc| proc.clone())
|
||||||
|
.expect("`proc::current()` called outside the context of the proc")
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub(crate) fn schedule(proc: LightProc) {
|
||||||
|
pool::schedule(proc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A worker thread running futures locally and stealing work from other workers if it runs empty.
|
||||||
|
pub struct WorkerThread {
|
||||||
|
queue: Worker<LightProc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkerThread {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
queue: Worker::new_fifo(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stealer(&self) -> Stealer<LightProc> {
|
||||||
|
self.queue.stealer()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&self) {
|
||||||
|
if let Some(lightproc) = self.queue.pop() {
|
||||||
|
lightproc.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
runtime/executor/tests/lib.rs
Normal file
26
runtime/executor/tests/lib.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use bastion_executor::{placement, pool};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn affinity_replacement() {
|
||||||
|
let core_ids = placement::get_core_ids().unwrap();
|
||||||
|
dbg!(core_ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
runtime/executor/tests/run_blocking.rs
Normal file
38
runtime/executor/tests/run_blocking.rs
Normal file
@ -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);
|
||||||
|
}
|
155
runtime/executor/tests/thread_pool.rs
Normal file
155
runtime/executor/tests/thread_pool.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
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 std::time::Instant;
|
||||||
|
|
||||||
|
// Test for slow joins without task bursts during joins.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn slow_join() {
|
||||||
|
let thread_join_time_max = 11_000;
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
// Send an initial batch of million bursts.
|
||||||
|
let handles = (0..1_000_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<RecoverableHandle<()>>>();
|
||||||
|
|
||||||
|
run(join_all(handles), ProcStack::default());
|
||||||
|
|
||||||
|
// Let them join to see how it behaves under different workloads.
|
||||||
|
let duration = Duration::from_millis(thread_join_time_max);
|
||||||
|
thread::sleep(duration);
|
||||||
|
|
||||||
|
// Spawn yet another batch of work on top of it
|
||||||
|
let handles = (0..10_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(100);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<RecoverableHandle<()>>>();
|
||||||
|
|
||||||
|
run(join_all(handles), ProcStack::default());
|
||||||
|
|
||||||
|
// Slow joins shouldn't cause internal slow down
|
||||||
|
let elapsed = start.elapsed().as_millis() - thread_join_time_max as u128;
|
||||||
|
println!("Slow task join. Monotonic exec time: {:?} ns", elapsed);
|
||||||
|
|
||||||
|
// Previous implementation is around this threshold.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for slow joins with task burst.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn slow_join_interrupted() {
|
||||||
|
let thread_join_time_max = 2_000;
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
// Send an initial batch of million bursts.
|
||||||
|
let handles = (0..1_000_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<RecoverableHandle<()>>>();
|
||||||
|
|
||||||
|
run(join_all(handles), ProcStack::default());
|
||||||
|
|
||||||
|
// Let them join to see how it behaves under different workloads.
|
||||||
|
// This time join under the time window.
|
||||||
|
let duration = Duration::from_millis(thread_join_time_max);
|
||||||
|
thread::sleep(duration);
|
||||||
|
|
||||||
|
// Spawn yet another batch of work on top of it
|
||||||
|
let handles = (0..10_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(100);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<RecoverableHandle<()>>>();
|
||||||
|
|
||||||
|
run(join_all(handles), ProcStack::default());
|
||||||
|
|
||||||
|
// Slow joins shouldn't cause internal slow down
|
||||||
|
let elapsed = start.elapsed().as_millis() - thread_join_time_max as u128;
|
||||||
|
println!("Slow task join. Monotonic exec time: {:?} ns", elapsed);
|
||||||
|
|
||||||
|
// Previous implementation is around this threshold.
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test is expensive but it proves that longhauling tasks are working in adaptive thread pool.
|
||||||
|
// Thread pool which spawns on-demand will panic with this test.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn longhauling_task_join() {
|
||||||
|
let thread_join_time_max = 11_000;
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
// First batch of overhauling tasks
|
||||||
|
let _ = (0..100_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(1000);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<RecoverableHandle<()>>>();
|
||||||
|
|
||||||
|
// Let them join to see how it behaves under different workloads.
|
||||||
|
let duration = Duration::from_millis(thread_join_time_max);
|
||||||
|
thread::sleep(duration);
|
||||||
|
|
||||||
|
// Send yet another medium sized batch to see how it scales.
|
||||||
|
let handles = (0..10_000)
|
||||||
|
.map(|_| {
|
||||||
|
blocking::spawn_blocking(
|
||||||
|
async {
|
||||||
|
let duration = Duration::from_millis(100);
|
||||||
|
thread::sleep(duration);
|
||||||
|
},
|
||||||
|
ProcStack::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<RecoverableHandle<()>>>();
|
||||||
|
|
||||||
|
run(join_all(handles), ProcStack::default());
|
||||||
|
|
||||||
|
// Slow joins shouldn't cause internal slow down
|
||||||
|
let elapsed = start.elapsed().as_millis() - thread_join_time_max as u128;
|
||||||
|
println!(
|
||||||
|
"Long-hauling task join. Monotonic exec time: {:?} ns",
|
||||||
|
elapsed
|
||||||
|
);
|
||||||
|
|
||||||
|
// Previous implementation will panic when this test is running.
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user