Skip to main content

brk/
run.rs

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    /// Optional per-connection ceiling; it does not assert a server breakpoint.
21    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
52/// Synchronous owner of CPU workers. Phase callbacks run on the caller, with
53/// workers gated before start and fully drained before finish. No process-global
54/// signal handler is installed. Caller cancellation never detaches owned work.
55pub 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}