1use std::sync::mpsc;
2use std::time::{Duration, Instant};
3
4use tokio::sync::watch;
5
6use crate::worker::{Command, Workers, receive};
7use crate::{
8 Acceptance, BreakReason, Broken, CancellationToken, Counts, Done, Observer,
9 Phase, PhaseStats, Stats, Target,
10};
11
12#[derive(Clone, Copy, Debug)]
13pub struct Plan {
14 pub conn: usize,
15 pub start: usize,
16 pub pace: usize,
17 pub phase_duration: Duration,
18 pub timeout: Duration,
19 pub accept: f64,
20 pub max_concurrency: Option<usize>,
22}
23
24impl Plan {
25 pub fn validate(&self) -> Result<Acceptance, BreakReason> {
26 if self.conn == 0 || self.start == 0 || self.pace == 0 {
27 return Err(BreakReason::Invalid(
28 "conn, start and pace must be greater than zero",
29 ));
30 }
31 if self.phase_duration.is_zero() || self.timeout.is_zero() {
32 return Err(BreakReason::Invalid(
33 "phase and timeout must be greater than zero",
34 ));
35 }
36 if self.max_concurrency.is_some_and(|max| max < self.start) {
37 return Err(BreakReason::Invalid(
38 "max must admit the initial start level",
39 ));
40 }
41 self.conn.checked_mul(self.start).ok_or(BreakReason::Overflow)?;
42 Instant::now()
43 .checked_add(self.phase_duration)
44 .and_then(|end| end.checked_add(self.timeout))
45 .ok_or(BreakReason::Invalid(
46 "time budgets exceed the clock range",
47 ))?;
48 Acceptance::try_from(self.accept).map_err(BreakReason::Invalid)
49 }
50}
51
52pub fn run(
56 target: &Target,
57 plan: Plan,
58 cancellation: &CancellationToken,
59 observer: &mut (impl Observer + ?Sized),
60) -> Result<Done, Broken> {
61 let mut stats = Stats::default();
62 let result = execute(target, plan, cancellation, observer, &mut stats);
63 match result {
64 Ok(()) => Ok(Done { stats }),
65 Err(reason) => Err(Broken { reason, stats: Box::new(stats) }),
66 }
67}
68
69fn execute(
70 target: &Target,
71 plan: Plan,
72 cancellation: &CancellationToken,
73 observer: &mut (impl Observer + ?Sized),
74 stats: &mut Stats,
75) -> Result<(), BreakReason> {
76 let acceptance = plan.validate()?;
77 if cancellation.is_cancelled() {
78 return Err(BreakReason::Cancelled);
79 }
80 let prepared = target.resolve()?;
81 let stop = cancellation.child_token();
82 let workers = Workers::start(&prepared, plan.conn, stop.clone())?;
83 stats.cpu_workers = workers.len();
84 let result = phases(&workers, plan, acceptance, &stop, observer, stats);
85 let finished = workers.finish();
86 result.and(finished)
87}
88
89fn phases(
90 workers: &Workers,
91 plan: Plan,
92 acceptance: Acceptance,
93 stop: &CancellationToken,
94 observer: &mut (impl Observer + ?Sized),
95 stats: &mut Stats,
96) -> Result<(), BreakReason> {
97 let mut index = 0_usize;
98 let mut level = plan.start;
99 loop {
100 if stop.is_cancelled() {
101 return Err(BreakReason::Cancelled);
102 }
103 let phase = Phase {
104 index,
105 connections: plan.conn,
106 requests_per_connection: level,
107 total_concurrency: plan
108 .conn
109 .checked_mul(level)
110 .ok_or(BreakReason::Overflow)?,
111 duration: plan.phase_duration,
112 };
113 let (start, waiting) = watch::channel(None);
114 let phase_stop = CancellationToken::new();
115 let mut readiness = Vec::new();
116 let mut replies = Vec::new();
117 readiness
118 .try_reserve(workers.len())
119 .map_err(|_| BreakReason::Resources("phase readiness handles"))?;
120 replies
121 .try_reserve(workers.len())
122 .map_err(|_| BreakReason::Resources("phase result handles"))?;
123 for worker in 0..workers.len() {
124 let (ready, ready_wait) = mpsc::channel();
125 let (reply, reply_wait) = mpsc::channel();
126 workers.send(
127 worker,
128 Command {
129 phase,
130 timeout: plan.timeout,
131 start: waiting.clone(),
132 phase_stop: phase_stop.clone(),
133 ready,
134 reply,
135 },
136 )?;
137 readiness.push(ready_wait);
138 replies.push(reply_wait);
139 }
140 for ready in readiness {
141 receive(ready)?;
142 }
143 observer.phase_started(&phase);
144 let began = Instant::now();
145 start.send_replace(Some(began));
146 let mut counts = Counts::default();
147 let mut at_target = 0;
148 let mut failure = None;
149 for reply in replies {
150 match receive(reply) {
151 Ok(summary) => {
152 counts.add(summary.counts);
153 at_target += summary.at_target;
154 if let Some(reason) = summary.failure {
155 failure.get_or_insert(reason);
156 }
157 }
158 Err(reason) => {
159 phase_stop.cancel();
160 failure.get_or_insert(reason);
161 }
162 }
163 }
164 let measured = PhaseStats {
165 phase,
166 counts,
167 elapsed: began.elapsed(),
168 connections_at_target: at_target,
169 completed: failure.is_none() && !stop.is_cancelled(),
170 cancelled: stop.is_cancelled(),
171 };
172 stats.phases += 1;
173 stats.total.add(counts);
174 stats.last_phase = Some(measured);
175 observer.phase_finished(&measured);
176 if let Some(reason) = failure {
177 return Err(reason);
178 }
179 if measured.cancelled {
180 return Err(BreakReason::Cancelled);
181 }
182 if at_target != plan.conn {
183 return Err(BreakReason::Unfulfilled);
184 }
185 if !measured.accepted(acceptance) {
186 return Err(BreakReason::Acceptance);
187 }
188 stats.last_accepted = Some(measured);
189 if stop.is_cancelled() {
190 return Err(BreakReason::Cancelled);
191 }
192 if plan.max_concurrency == Some(level) {
193 return Ok(());
194 }
195 let next = level.checked_add(plan.pace).ok_or(BreakReason::Overflow)?;
196 if plan.max_concurrency.is_some_and(|max| next > max) {
197 return Ok(());
198 }
199 level = next;
200 index = index.checked_add(1).ok_or(BreakReason::Overflow)?;
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 fn plan() -> Plan {
209 Plan {
210 conn: 4,
211 start: 10,
212 pace: 5,
213 phase_duration: Duration::from_secs(10),
214 timeout: Duration::from_secs(2),
215 accept: 99.99,
216 max_concurrency: None,
217 }
218 }
219
220 #[test]
221 fn rejects_invalid_inputs_before_workers_or_traffic() {
222 let plan = plan();
223 assert!(plan.validate().is_ok());
224 for invalid in [
225 Plan { conn: 0, ..plan },
226 Plan { start: 0, ..plan },
227 Plan { pace: 0, ..plan },
228 Plan { phase_duration: Duration::ZERO, ..plan },
229 Plan { timeout: Duration::ZERO, ..plan },
230 Plan { max_concurrency: Some(9), ..plan },
231 Plan { accept: f64::NAN, ..plan },
232 Plan { conn: usize::MAX, ..plan },
233 ] {
234 assert!(invalid.validate().is_err());
235 }
236 }
237}