loates/
logical.rs

1use std::{borrow::Cow, fmt::Write, time::Duration};
2
3use crate::{
4    data::DatastoreModifier, executor::DataExecutor, runner::ExecutionRuntimeCtx,
5    user::AsyncUserBuilder,
6};
7
8/// Rate of iteration.
9#[derive(Debug, Clone, Copy)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize))]
11pub struct Rate(
12    /// Number of iterations
13    pub usize,
14    /// Time interval in which to perform those iterations
15    pub Duration,
16);
17
18impl From<Rate> for (usize, Duration) {
19    fn from(value: Rate) -> Self {
20        (value.0, value.1)
21    }
22}
23
24impl std::fmt::Display for Rate {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.write_fmt(format_args!("{}", self.0))?;
27        f.write_char('/')?;
28        f.write_fmt(format_args!("{:?}", self.1))?;
29        Ok(())
30    }
31}
32
33/// Executor type that is to be used within an execution.
34#[derive(Debug, Clone)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize))]
36#[cfg_attr(feature = "serde", serde(rename_all_fields = "camelCase"))]
37#[cfg_attr(feature = "serde", serde(tag = "type"))]
38pub enum Executor {
39    /// Excecute the user call only once then exit.
40    Once,
41    /// Contantly drive all the the users for a certain duration doing as many iterations as possible.
42    Constant {
43        /// Number of users
44        users: usize,
45        /// Duration of execution
46        duration: Duration,
47    },
48    /// Share N iterations among K users.
49    /// This executor does not divide and allocate iterations to users beforehand,
50    /// so if a user is able to go through iterations faster during runtime, it will end up doing more iterations that others.
51    Shared {
52        /// Number of users
53        users: usize,
54        /// Number of iterations
55        iterations: usize,
56        /// Duration of execution
57        duration: Duration,
58    },
59    /// Have each user run certain number of iterations.
60    /// Test finishes when all users have finished their execution.
61    PerUser {
62        /// Number of users
63        users: usize,
64        /// Number of iterations each user will perform.
65        iterations: usize,
66    },
67    /// Executor for performing iterations at a given rate.
68    /// Time taken for completion of an iteration is variable, thus this
69    /// executor cannot guarantee perfect throughput. If executor does not
70    /// meet the rate deifined in its config then it will try to compensate
71    /// and match the given rate by allocating more users duing runtime.
72    ConstantArrivalRate {
73        /// Number of users to pre-allocate
74        pre_allocate_users: usize,
75        /// Rate of iteration
76        rate: Rate,
77        /// Maximum number of users that could be spawned by this executor
78        max_users: usize,
79        /// Duration of execution
80        duration: Duration,
81    },
82    /// Executor with stages, where in each stage executor allocates certain number of users for a specific duration and have them run as many iterations as possible.
83    /// Use this executor when you want to ramp the number of users up or down during specific periods of time.
84    RampingUser {
85        /// Number of users to pre-allocate
86        pre_allocate_users: usize,
87        /// stages of this execution. Sequence of number of user and duration
88        stages: Vec<(usize, Duration)>,
89    },
90    /// Executor with stages, where in each stage executor is given an arrival rate for a certain duration.
91    /// Similar to ConstantArrivalRate executor, it tries to match the given rate of iteration.
92    /// If iteration rate falls short, it compenstates for lack of iterations by spawning more users during runtime.
93    /// Use this executor when you want to change the iteration rates during specific periods of time.
94    RampingArrivalRate {
95        /// Number of users to pre-allocate
96        pre_allocate_users: usize,
97        /// Maximum number of users that could be spawned by this executor
98        max_users: usize,
99        /// stages of this execution. Sequence of Rate and duration
100        stages: Vec<(Rate, Duration)>,
101    },
102}
103
104impl std::fmt::Display for Executor {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        match self {
107            Executor::Once => f.write_str("Once"),
108            Executor::Constant { users, duration } => {
109                write!(f, "Constant ({} users) {:?}", users, duration)
110            }
111            Executor::Shared {
112                users, iterations, ..
113            } => write!(f, "Shared ({} users) {}", users, iterations),
114            Executor::PerUser { users, iterations } => {
115                write!(f, "PerUser ({} users) {}", users, iterations)
116            }
117            Executor::ConstantArrivalRate { rate, duration, .. } => {
118                write!(f, "ConstantArrivalRate {} for {:?}", rate, duration)
119            }
120            Executor::RampingUser { stages, .. } => {
121                write!(f, "RampingUser ({} stages)", stages.len())
122            }
123            Executor::RampingArrivalRate { stages, .. } => {
124                write!(f, "RampingArrivalRate ({}, stages)", stages.len())
125            }
126        }
127    }
128}
129
130#[async_trait::async_trait]
131pub(crate) trait ExecutionProvider {
132    fn config(&self) -> &Executor;
133    async fn execution<'a>(
134        &'a self,
135        ctx: &'a mut ExecutionRuntimeCtx,
136    ) -> Box<dyn crate::executor::Executor + 'a>;
137}
138
139/// Named collection of executions which should run in parallel to each other.    
140///
141/// A scenario is conceptually a test model which simulates a traffic pattern / load.
142/// For more detailed guide on how to organize a scenario and use multiple Execution in a test. Look at [examples](https://github.com/trueleo/loates/examples).  
143pub struct Scenario<'env> {
144    pub(crate) label: Cow<'static, str>,
145    pub(crate) execution_provider: Vec<Box<dyn ExecutionProvider + 'env>>,
146}
147
148impl<'env> Scenario<'env> {
149    /// Create a new scenario with a label and a single execution. More execution can be added using [with_executor](Self::with_executor) method
150    pub fn new<Ub>(label: impl Into<Cow<'static, str>>, execution: Execution<'env, Ub>) -> Self
151    where
152        Ub: for<'a> AsyncUserBuilder<'a> + 'env,
153    {
154        Self {
155            label: label.into(),
156            execution_provider: vec![Box::new(execution)],
157        }
158    }
159
160    /// Append a new executor to this scenario.
161    pub fn with_executor<Ub>(mut self, execution: Execution<'env, Ub>) -> Self
162    where
163        Ub: for<'a> AsyncUserBuilder<'a> + 'env,
164    {
165        self.execution_provider.push(Box::new(execution));
166        self
167    }
168}
169
170/// Logical execution plan that outlines which user type to spawn during runtime and under which [`Executor`].
171///
172/// A [`Scenario`] can contain one or more of these *execution plans*.
173pub struct Execution<'env, Ub> {
174    user_builder: Ub,
175    datastore_modifiers: Vec<Box<dyn DatastoreModifier + 'env>>,
176    executor: Executor,
177}
178
179impl<'env, Ub> Execution<'env, Ub> {
180    /// Create a new Execution with a [`user builder`](AsyncUserBuilder) and an [`Executor`]
181    pub fn new(user_builder: Ub, executor: Executor) -> Self {
182        Self {
183            user_builder,
184            datastore_modifiers: vec![],
185            executor,
186        }
187    }
188}
189
190impl Execution<'static, ()> {
191    /// Create a new Execution plan using builder pattern
192    pub fn builder() -> Execution<'static, ()> {
193        Self {
194            user_builder: (),
195            datastore_modifiers: Vec::new(),
196            executor: Executor::Once,
197        }
198    }
199
200    /// Register user builder that will be used in this execution.
201    pub fn with_user_builder<'env, F>(self, user_builder: F) -> Execution<'env, F>
202    where
203        F: for<'a> AsyncUserBuilder<'a> + 'env,
204    {
205        Execution::<'env, _> {
206            user_builder,
207            executor: self.executor,
208            datastore_modifiers: self.datastore_modifiers,
209        }
210    }
211}
212
213impl<'env, Ub> Execution<'env, Ub>
214where
215    Ub: for<'a> AsyncUserBuilder<'a> + 'env,
216{
217    /// Append a new datastore initializer to this execution. When perparing to run a scenario, this will be used to initialize [`RuntimeDataStore`](crate::data::RuntimeDataStore) created for this execution.
218    pub fn with_data<T: DatastoreModifier + 'env>(mut self, f: T) -> Self {
219        self.datastore_modifiers
220            .push(Box::new(f) as Box<dyn DatastoreModifier + 'env>);
221        self
222    }
223
224    /// [`Executor`] type which should be used for this execution.
225    pub fn with_executor(mut self, executor: Executor) -> Self {
226        self.executor = executor;
227        self
228    }
229
230    /// Convert this Execution to a Scenario with provided label.
231    pub fn to_scenario(self, label: impl Into<Cow<'static, str>>) -> Scenario<'env> {
232        Scenario::new(label, self)
233    }
234}
235
236#[async_trait::async_trait]
237impl<'env, Ub> ExecutionProvider for Execution<'env, Ub>
238where
239    Ub: for<'a> AsyncUserBuilder<'a>,
240{
241    fn config(&self) -> &Executor {
242        &self.executor
243    }
244
245    async fn execution<'a>(
246        &'a self,
247        ctx: &'a mut ExecutionRuntimeCtx,
248    ) -> Box<dyn crate::executor::Executor + 'a> {
249        for modifiers in self.datastore_modifiers.iter() {
250            ctx.modify(&**modifiers).await;
251        }
252        let user_builder = &self.user_builder;
253        let executor = self.executor.clone();
254        Box::new(
255            DataExecutor::<Ub>::new(ctx.datastore_mut(), user_builder, executor)
256                .await
257                .unwrap(),
258        ) as Box<dyn crate::executor::Executor + '_>
259    }
260}