use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DurationSeconds};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
mod goal_tps;
mod saturate;
use goal_tps::run_goal_tps;
use saturate::run_saturate;
const DEFAULT_SATURATE_ERROR_RATE: f64 = 0.03;
pub type BoxedFut = Pin<Box<dyn Future<Output = ()> + Send>>;
#[serde_as]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub(crate) struct ScenarioConfig {
pub name: String,
#[serde_as(as = "DurationSeconds")]
pub duration: Duration,
pub kind: ScenarioKind,
}
impl ScenarioConfig {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
duration: Default::default(),
kind: Default::default(),
}
}
}
#[derive(Default, Clone, Copy, Serialize, Deserialize, Debug)]
pub(crate) enum ScenarioKind {
#[default]
Once,
Tps {
goal_tps: u32,
},
Saturate {
error_rate: f64,
},
}
#[pin_project::pin_project]
pub struct Scenario {
fut: fn() -> BoxedFut,
runner_fut: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
config: ScenarioConfig,
}
impl Scenario {
pub fn new(name: &str, fut: fn() -> BoxedFut) -> Self {
Self {
fut,
runner_fut: None,
config: ScenarioConfig::new(name),
}
}
pub fn saturate(mut self) -> Self {
self.config.kind = ScenarioKind::Saturate {
error_rate: DEFAULT_SATURATE_ERROR_RATE,
};
self
}
pub fn saturate_error_rate(mut self, error_rate: f64) -> Self {
self.config.kind = ScenarioKind::Saturate { error_rate };
self
}
pub fn tps(mut self, tps: u32) -> Self {
self.config.kind = ScenarioKind::Tps { goal_tps: tps };
self
}
pub fn duration(mut self, duration: Duration) -> Self {
self.config.duration = duration;
self
}
pub(crate) fn set_config(mut self, config: ScenarioConfig) -> Self {
self.config = config;
self
}
}
impl Future for Scenario {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.runner_fut.is_none() {
let fut = self.fut;
let config = self.config.clone();
self.runner_fut = Some(Box::pin(async move { run_scenario(fut, config).await }));
}
if let Some(runner) = &mut self.runner_fut {
runner.as_mut().poll(cx)
} else {
unreachable!()
}
}
}
async fn run_scenario(scenario: fn() -> BoxedFut, config: ScenarioConfig) {
match config.kind {
ScenarioKind::Once => scenario().await,
ScenarioKind::Tps { goal_tps } => run_goal_tps(scenario, config, goal_tps).await,
ScenarioKind::Saturate { error_rate } => run_saturate(scenario, config, error_rate).await,
}
}