use tokio::time::Instant;
use tokio::{sync::Notify, task::JoinHandle};
use typed_builder::TypedBuilder;
use super::Executor;
use crate::{aggregate::Aggregate, scenario::Scenario};
use internals::*;
use futures::future::join_all;
use std::{future::Future, sync::Arc, time::Duration};
#[derive(Clone, Copy, Debug)]
pub struct Stage {
pub duration: Duration,
pub target: f64,
}
impl Stage {
pub fn new(duration: Duration, target: f64) -> Self {
Self { duration, target }
}
}
const MAX_TOKENS: usize = usize::MAX >> 3;
#[derive(TypedBuilder)]
pub struct StageExecutor {
pub stages: Vec<Stage>,
#[builder(default = Duration::from_millis(100))]
pub tick: Duration,
#[builder(default = MAX_TOKENS)]
pub bucket_capacity: usize,
#[builder(default = num_cpus::get() * 120)]
pub workers: usize,
}
impl<A, F, Fut> Executor<A, F, Fut> for StageExecutor
where
Self: Send + Sync + Sized,
A: Aggregate + 'static,
F: Fn() -> Fut + Send + Sync + Clone + 'static,
Fut: Future<Output = A::Metric> + Send,
{
async fn exec(&self, scenario: &Scenario<A, F, Fut>) -> Result<A, Box<dyn std::error::Error>> {
let (ctx, shutdown_tx) = ExecutionContext::new();
tracing::info!("Spawning token governor task...");
let governor = tokio::spawn(token_governor_task(
ctx.clone(),
self.stages.clone(),
self.tick,
self.bucket_capacity,
));
tracing::info!("Spawning {} workers...", self.workers);
let handles = spawn_workers(ctx.clone(), self.workers, scenario.action.clone()).await;
tracing::info!("Running scenario: {}!", scenario.name);
ctx.start.notify_waiters();
governor.await.expect("Error in token governor task");
tracing::info!("Governor finished, signaling shutdown...");
shutdown_tx.send(true)?;
tracing::info!("Retrieving data from workers...");
let aggs: Vec<A> = join_all(handles)
.await
.into_iter()
.map(|res| res.expect("Task panicked"))
.collect();
tracing::info!("Processing results...");
let mut final_agg = A::new();
for agg in aggs {
final_agg.merge(agg);
}
tracing::info!("Done running scenario: {}!", scenario.name);
Ok(final_agg)
}
}
#[cfg(feature = "internals")]
pub use internals::*;
mod internals {
use super::*;
use tokio::sync::{
watch::{channel, Receiver, Sender},
Semaphore,
};
#[derive(Clone)]
pub struct ExecutionContext {
pub start: Arc<Notify>,
pub shutdown: Receiver<bool>,
pub tokens: Arc<Semaphore>,
}
impl ExecutionContext {
pub fn new() -> (Self, Sender<bool>) {
let (tx, rx) = channel(false);
(
Self {
start: Arc::new(Notify::new()),
shutdown: rx,
tokens: Arc::new(Semaphore::new(0)),
},
tx,
)
}
}
pub async fn token_governor_task(
mut ctx: ExecutionContext,
stages: Vec<Stage>,
tick: Duration,
bucket_capacity: usize,
) {
let main_task = || async {
let mut rate = 0.0;
let mut fractional = 0.0;
ctx.start.notified().await;
tracing::debug!("Governor task started.");
for stage in stages.into_iter() {
if stage.duration.is_zero() {
rate = stage.target;
continue;
}
let stage_start = Instant::now();
let mut next_tick = Instant::now();
let start_rate = rate;
let end_rate = stage.target;
loop {
let elapsed = Instant::now().duration_since(stage_start);
if elapsed >= stage.duration {
break;
}
next_tick += tick;
let (add_total, f) = calc_token_limit(
elapsed,
stage.duration,
start_rate,
end_rate,
fractional,
tick,
);
fractional = f;
if add_total > 0 {
let avail = ctx.tokens.available_permits();
if avail < bucket_capacity {
let free_cap = bucket_capacity - avail;
let add = add_total.min(free_cap);
if add > 0 {
ctx.tokens.add_permits(add);
}
}
}
tokio::time::sleep_until(next_tick).await;
}
rate = end_rate;
}
};
tokio::select! {
_ = main_task() => {
tracing::debug!("Governor task finished all stages.");
}
_ = ctx.shutdown.wait_for(|b|*b) => {
tracing::debug!("Governor received shutdown signal.");
}
};
}
pub fn calc_token_limit(
elapsed: Duration,
stage_duration: Duration,
start_rate: f64,
end_rate: f64,
fractional: f64,
tick: Duration,
) -> (usize, f64) {
let t = (elapsed.as_secs_f64() / stage_duration.as_secs_f64()).min(1.0);
let tick_rate = start_rate + (end_rate - start_rate) * t;
let add_f = tick_rate * tick.as_secs_f64();
let add_total_f = (add_f + fractional).floor();
let fractional = (add_f + fractional) - (add_total_f);
let add_total = if add_total_f >= (MAX_TOKENS as f64) {
MAX_TOKENS
} else if add_total_f < 0.0 {
0
} else {
add_total_f as usize
};
(add_total, fractional)
}
pub async fn spawn_workers<A, F, Fut>(
ctx: ExecutionContext,
workers: usize,
action: F,
) -> Vec<JoinHandle<A>>
where
A: Aggregate + 'static,
F: Fn() -> Fut + Send + Sync + Clone + 'static,
Fut: Future<Output = A::Metric> + Send,
{
(0..workers)
.map(|i| {
let mut ctx = ctx.clone();
let action = action.clone();
tokio::spawn(async move {
let mut agg = A::new();
tracing::debug!("Worker {i} spawned.");
let main_task = async {
ctx.start.notified().await;
tracing::debug!("Worker {i} started.");
loop {
let permit = match ctx.tokens.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => {
tracing::debug!(
"Worker {i} failed to acquire token (semaphore closed).",
);
break;
}
};
permit.forget();
let metric = action().await;
agg.consume(&metric);
}
};
tokio::select! {
_ = main_task => {},
_ = ctx.shutdown.wait_for(|b| *b) => {
}
};
tracing::debug!("Worker {i} shutting down.",);
agg
})
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Metric;
#[derive(Clone, PartialEq, PartialOrd)]
struct EmptyMetric;
impl Metric for EmptyMetric {}
#[derive(Clone)]
struct EmptyAggregate;
impl Aggregate for EmptyAggregate {
type Metric = EmptyMetric;
fn new() -> Self {
Self {}
}
fn consume(&mut self, _: &Self::Metric) {}
fn merge(&mut self, _: Self) {}
}
#[tokio::test]
async fn spawn_expected_number_of_workers() {
let n = 10;
let (ctx, _) = ExecutionContext::new();
let action = || async { EmptyMetric {} };
let workers: Vec<JoinHandle<EmptyAggregate>> = spawn_workers(ctx, n, action).await;
assert_eq!(workers.len(), n);
}
mod calc_token_limit {
use super::*;
#[test]
fn linearity() {
let mut end_rate = 100.;
let mut expected_t = 1;
for _ in 0..10 {
let (t, f) = calc_token_limit(
Duration::from_secs(1),
Duration::from_secs(10),
0.,
end_rate,
0.,
Duration::from_millis(100),
);
assert_eq!(t, expected_t);
assert_eq!(f, 0.);
end_rate *= 10.;
expected_t *= 10;
}
}
#[test]
fn fractional_accumulation() {
let dur = 10;
let start_rate = 12.5;
let end_rate = 12.5;
let mut facc = 0.;
let expected_fs = [0.25, 0.5, 0.75, 0.];
for i in 0..10 {
let (t, f) = calc_token_limit(
Duration::from_secs(1),
Duration::from_secs(dur),
start_rate,
end_rate,
facc,
Duration::from_millis(100),
);
facc = f;
let expected_f = expected_fs[i % 4];
let expected_t = if expected_f == 0. { 2 } else { 1 };
assert_eq!(t, expected_t);
assert_eq!(f, expected_f)
}
}
#[test]
fn ramp_down() {
let stage_duration = Duration::from_secs(10);
let tick = Duration::from_millis(100);
let start_rate = 100.0;
let end_rate = 0.0;
for i in 0..10 {
let elapsed = Duration::from_secs(i);
let (t, f) =
calc_token_limit(elapsed, stage_duration, start_rate, end_rate, 0.0, tick);
let expected_t = (10 - i) as usize;
assert_eq!(t, expected_t);
assert_eq!(f, 0.0);
}
let (t_end, f_end) = calc_token_limit(
stage_duration,
stage_duration,
start_rate,
end_rate,
0.0,
tick,
);
assert_eq!(t_end, 0);
assert_eq!(f_end, 0.0);
}
#[test]
fn hold_steady() {
let stage_duration = Duration::from_secs(10);
let tick = Duration::from_millis(100);
let start_rate = 100.;
let end_rate = start_rate;
for i in 0..10 {
let elapsed = Duration::from_secs(i);
let (t, f) =
calc_token_limit(elapsed, stage_duration, start_rate, end_rate, 0.0, tick);
let expected_t = 10;
assert_eq!(t, expected_t);
assert_eq!(f, 0.0);
}
}
#[test]
fn ramp_up() {
let stage_duration = Duration::from_secs(10);
let tick = Duration::from_millis(100);
let start_rate = 0.;
let end_rate = 100.;
for i in 0..10 {
let elapsed = Duration::from_secs(i);
let (t, f) =
calc_token_limit(elapsed, stage_duration, start_rate, end_rate, 0., tick);
let expected_t = (i) as usize;
assert_eq!(t, expected_t);
assert_eq!(f, 0.);
}
}
#[test]
fn elapsed_over_duartion_cap_at_end_rate() {
for i in 0..10 {
let elapsed = 10 + i;
let (t, f) = calc_token_limit(
Duration::from_secs(elapsed),
Duration::from_secs(10),
0.,
100.,
0.,
Duration::from_millis(100),
);
assert_eq!(t, 10);
assert_eq!(f, 0.);
}
}
#[test]
fn negative_value_returns_0() {
let (t, f) = calc_token_limit(
Duration::from_secs(1),
Duration::from_secs(10),
-100.,
-100.,
0.,
Duration::from_millis(100),
);
assert_eq!(t, 0);
assert_eq!(f, 0.0);
}
#[test]
fn extreme_rate_cap_at_max_tokens() {
let (t, f) = calc_token_limit(
Duration::from_secs(1),
Duration::from_secs(1),
f64::MAX,
f64::MAX,
0.,
Duration::from_secs(1),
);
assert_eq!(t, MAX_TOKENS);
assert_eq!(f, 0.);
}
}
}