use std::time::Duration;
use crate::Acceptance;
#[derive(Clone, Copy, Debug, Default)]
pub struct Counts {
pub sent: u64,
pub attempted: u64,
pub successful: u64,
pub timeout: u64,
pub transport: u64,
pub status: u64,
pub body: u64,
}
impl Counts {
pub fn failed(self) -> u64 {
self.attempted - self.successful
}
pub(crate) fn add(
&mut self,
other: Self,
) {
self.sent += other.sent;
self.attempted += other.attempted;
self.successful += other.successful;
self.timeout += other.timeout;
self.transport += other.transport;
self.status += other.status;
self.body += other.body;
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Phase {
pub index: usize,
pub connections: usize,
pub requests_per_connection: usize,
pub total_concurrency: usize,
pub duration: Duration,
}
#[derive(Clone, Copy, Debug)]
pub struct PhaseStats {
pub phase: Phase,
pub counts: Counts,
pub elapsed: Duration,
pub connections_at_target: usize,
pub completed: bool,
pub cancelled: bool,
}
impl PhaseStats {
pub fn accepted(
&self,
acceptance: Acceptance,
) -> bool {
self.completed
&& !self.cancelled
&& self.connections_at_target == self.phase.connections
&& acceptance.accepts(self.counts.successful, self.counts.attempted)
}
pub fn requests_per_sec(&self) -> f64 {
self.counts.successful as f64 / self.elapsed.as_secs_f64()
}
}
#[derive(Clone, Debug, Default)]
pub struct Stats {
pub cpu_workers: usize,
pub phases: usize,
pub total: Counts,
pub last_accepted: Option<PhaseStats>,
pub last_phase: Option<PhaseStats>,
}
#[derive(Debug)]
pub struct Done {
pub stats: Stats,
}
#[derive(Debug, thiserror::Error)]
pub enum BreakReason {
#[error("invalid plan: {0}")]
Invalid(&'static str),
#[error("client I/O: {0}")]
Io(#[from] std::io::Error),
#[error("client resources: {0}")]
Resources(&'static str),
#[error("HTTP protocol: {0}")]
Protocol(&'static str),
#[error("client worker: {0}")]
Worker(&'static str),
#[error("the client did not establish the declared concurrency")]
Unfulfilled,
#[error("the phase exceeded the HTTP acceptance budget")]
Acceptance,
#[error("concurrency representation exhausted")]
Overflow,
#[error("cancelled")]
Cancelled,
}
#[derive(Debug, thiserror::Error)]
#[error("{reason}")]
pub struct Broken {
pub reason: BreakReason,
pub stats: Box<Stats>,
}
pub trait Observer {
fn phase_started(
&mut self,
_phase: &Phase,
) {
}
fn phase_finished(
&mut self,
_stats: &PhaseStats,
) {
}
}
impl Observer for () {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perfect_status_counts_do_not_pass_when_concurrency_was_not_established()
{
let mut measured = PhaseStats {
phase: Phase {
index: 0,
connections: 4,
requests_per_connection: 100,
total_concurrency: 400,
duration: Duration::from_secs(1),
},
counts: Counts {
attempted: 10_000,
successful: 10_000,
..Counts::default()
},
elapsed: Duration::from_secs(1),
connections_at_target: 3,
completed: true,
cancelled: false,
};
let accept = "99.99".parse().unwrap();
assert!(!measured.accepted(accept));
measured.connections_at_target = 4;
assert!(measured.accepted(accept));
measured.counts.successful = 9_998;
assert!(!measured.accepted(accept));
}
}