Skip to main content

httpress/
config.rs

1//! Configuration types and contexts for benchmarks.
2//!
3//! This module contains types used to configure benchmarks and pass context
4//! to hooks and generator functions:
5//!
6//! - **Configuration**: [`HttpMethod`], [`RequestConfig`], [`RequestSource`]
7//! - **Generator Contexts**: [`RequestContext`], [`RateContext`]
8//! - **Hook Contexts**: [`BeforeRequestContext`], [`AfterRequestContext`]
9//! - **Hook Control**: [`HookAction`]
10//! - **Function Types**: [`RequestGenerator`], [`RateFunction`], [`BeforeRequestHook`], [`AfterRequestHook`]
11//!
12//! # Examples
13//!
14//! Using request context for dynamic URLs:
15//! ```no_run
16//! use httpress::{Benchmark, RequestContext, RequestConfig, HttpMethod};
17//! use std::collections::HashMap;
18//!
19//! # #[tokio::main]
20//! # async fn main() -> httpress::Result<()> {
21//! Benchmark::builder()
22//!     .request_fn(|ctx: RequestContext| {
23//!         RequestConfig {
24//!             url: format!("http://localhost:3000/user/{}", ctx.request_number),
25//!             method: HttpMethod::Get,
26//!             headers: HashMap::new(),
27//!             body: None,
28//!         }
29//!     })
30//!     .requests(100)
31//!     .build()?;
32//! # Ok(())
33//! # }
34//! ```
35
36use std::collections::HashMap;
37use std::sync::Arc;
38use std::time::Duration;
39
40use bytes::Bytes;
41
42use indicatif::ProgressBar;
43
44use crate::cli::Args;
45use crate::error::Error;
46use crate::progress::{ProgressFn, create_progress_bar, update_progress_bar};
47
48/// Defines when the benchmark should stop
49#[derive(Debug, Clone)]
50pub enum StopCondition {
51    /// Stop after N requests
52    Requests(usize),
53    /// Stop after duration
54    Duration(Duration),
55    /// Run until interrupted (Ctrl+C)
56    Infinite,
57}
58
59/// HTTP method for requests.
60///
61/// Specifies the HTTP method to use when making requests to the target server.
62///
63/// # Examples
64///
65/// ```
66/// use httpress::HttpMethod;
67///
68/// let method = HttpMethod::Post;
69/// ```
70#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
71pub enum HttpMethod {
72    /// HTTP GET method.
73    Get,
74    /// HTTP POST method.
75    Post,
76    /// HTTP PUT method.
77    Put,
78    /// HTTP DELETE method.
79    Delete,
80    /// HTTP PATCH method.
81    Patch,
82    /// HTTP HEAD method.
83    Head,
84    /// HTTP OPTIONS method.
85    Options,
86}
87
88/// Configuration for a single HTTP request.
89///
90/// Used by custom request generator functions to specify the details of each request.
91/// When using [`BenchmarkBuilder::request_fn`](crate::BenchmarkBuilder::request_fn),
92/// your function returns this struct to configure each individual request.
93///
94/// # Examples
95///
96/// ```
97/// use httpress::{RequestConfig, HttpMethod};
98/// use std::collections::HashMap;
99/// use bytes::Bytes;
100///
101/// let config = RequestConfig {
102///     url: "http://localhost:3000/api/users".to_string(),
103///     method: HttpMethod::Post,
104///     headers: HashMap::from([
105///         ("Content-Type".to_string(), "application/json".to_string()),
106///         ("Authorization".to_string(), "Bearer token123".to_string()),
107///     ]),
108///     body: Some(Bytes::from(r#"{"name": "John"}"#)),
109/// };
110/// ```
111#[derive(Debug, Clone)]
112pub struct RequestConfig {
113    /// The target URL for this request.
114    pub url: String,
115
116    /// The HTTP method to use.
117    pub method: HttpMethod,
118
119    /// HTTP headers to include in the request.
120    pub headers: HashMap<String, String>,
121
122    /// Optional request body.
123    pub body: Option<Bytes>,
124}
125
126/// Context passed to request generator functions.
127///
128/// Provides information about the current request context, allowing you to generate
129/// different requests based on worker ID and request number.
130///
131/// # Examples
132///
133/// ```no_run
134/// # use httpress::{Benchmark, RequestContext, RequestConfig, HttpMethod};
135/// # use std::collections::HashMap;
136/// # #[tokio::main]
137/// # async fn main() -> httpress::Result<()> {
138/// Benchmark::builder()
139///     .request_fn(|ctx: RequestContext| {
140///         // Rotate through 100 different user IDs
141///         let user_id = ctx.request_number % 100;
142///
143///         // Add worker ID to headers for debugging
144///         let mut headers = HashMap::new();
145///         headers.insert("X-Worker-Id".to_string(), ctx.worker_id.to_string());
146///
147///         RequestConfig {
148///             url: format!("http://localhost:3000/user/{}", user_id),
149///             method: HttpMethod::Get,
150///             headers,
151///             body: None,
152///         }
153///     })
154///     .concurrency(10)
155///     .requests(1000)
156///     .build()?;
157/// # Ok(())
158/// # }
159/// ```
160#[derive(Debug, Clone, Copy)]
161pub struct RequestContext {
162    /// ID of the worker executing this request (0-based).
163    ///
164    /// Each concurrent worker has a unique ID from 0 to (concurrency - 1).
165    pub worker_id: usize,
166
167    /// Sequential request number for this worker (0-based).
168    ///
169    /// This increments for each request made by this specific worker.
170    pub request_number: usize,
171}
172
173/// Context passed to rate generator functions.
174///
175/// Provides runtime information about the benchmark state, allowing you to dynamically
176/// adjust the request rate based on elapsed time, request counts, or success rates.
177///
178/// # Examples
179///
180/// ```no_run
181/// # use httpress::{Benchmark, RateContext};
182/// # use std::time::Duration;
183/// # #[tokio::main]
184/// # async fn main() -> httpress::Result<()> {
185/// Benchmark::builder()
186///     .url("http://localhost:3000")
187///     .rate_fn(|ctx: RateContext| {
188///         let elapsed_secs = ctx.elapsed.as_secs_f64();
189///
190///         // Linear ramp from 100 to 1000 req/s over 10 seconds
191///         if elapsed_secs < 10.0 {
192///             let progress = elapsed_secs / 10.0;
193///             100.0 + (900.0 * progress)
194///         } else {
195///             1000.0
196///         }
197///     })
198///     .duration(Duration::from_secs(30))
199///     .build()?;
200/// # Ok(())
201/// # }
202/// ```
203#[derive(Debug, Clone, Copy)]
204pub struct RateContext {
205    /// Time elapsed since benchmark start.
206    pub elapsed: Duration,
207
208    /// Total requests completed so far (success + failure).
209    pub total_requests: usize,
210
211    /// Successful requests so far (HTTP status 2xx).
212    pub successful_requests: usize,
213
214    /// Failed requests so far (non-2xx status or connection errors).
215    pub failed_requests: usize,
216
217    /// Current configured rate in requests per second (for reference).
218    ///
219    /// This reflects the rate returned by the previous call to the rate function.
220    pub current_rate: f64,
221}
222
223/// Type alias for request generator functions.
224///
225/// A request generator is a function that creates a [`RequestConfig`] for each request
226/// based on the provided [`RequestContext`]. This allows you to dynamically generate
227/// requests with different URLs, methods, headers, or bodies.
228///
229/// # Type Signature
230///
231/// ```text
232/// Fn(RequestContext) -> RequestConfig + Send + Sync + 'static
233/// ```
234///
235/// # Examples
236///
237/// See [`BenchmarkBuilder::request_fn`](crate::BenchmarkBuilder::request_fn) for usage examples.
238pub type RequestGenerator = Arc<dyn Fn(RequestContext) -> RequestConfig + Send + Sync>;
239
240/// Type alias for rate generator functions.
241///
242/// A rate function dynamically determines the request rate (requests per second) based
243/// on the current benchmark state provided in [`RateContext`]. This enables advanced
244/// patterns like rate ramping, adaptive rate control, or scheduled rate changes.
245///
246/// # Type Signature
247///
248/// ```text
249/// Fn(RateContext) -> f64 + Send + Sync + 'static
250/// ```
251///
252/// The returned `f64` value represents the desired requests per second.
253///
254/// # Examples
255///
256/// See [`BenchmarkBuilder::rate_fn`](crate::BenchmarkBuilder::rate_fn) for usage examples.
257pub type RateFunction = Arc<dyn Fn(RateContext) -> f64 + Send + Sync>;
258
259/// Context passed to before-request hook functions.
260///
261/// Provides information about the benchmark state before a request is sent.
262/// Before-request hooks can use this to implement rate limiting, circuit breakers,
263/// or conditional request execution.
264///
265/// # Examples
266///
267/// ```no_run
268/// # use httpress::{Benchmark, BeforeRequestContext, HookAction};
269/// # #[tokio::main]
270/// # async fn main() -> httpress::Result<()> {
271/// Benchmark::builder()
272///     .url("http://localhost:3000")
273///     .before_request(|ctx: BeforeRequestContext| {
274///         // Circuit breaker: stop sending requests if too many failures
275///         let failure_rate = ctx.failed_requests as f64 / ctx.total_requests.max(1) as f64;
276///         if failure_rate > 0.5 && ctx.total_requests > 100 {
277///             println!("Circuit breaker triggered at {}% failures", failure_rate * 100.0);
278///             HookAction::Abort
279///         } else {
280///             HookAction::Continue
281///         }
282///     })
283///     .requests(1000)
284///     .build()?;
285/// # Ok(())
286/// # }
287/// ```
288#[derive(Debug, Clone, Copy)]
289pub struct BeforeRequestContext {
290    /// ID of the worker executing this request (0-based).
291    pub worker_id: usize,
292
293    /// Sequential request number for this worker (0-based).
294    pub request_number: usize,
295
296    /// Time elapsed since benchmark start.
297    pub elapsed: Duration,
298
299    /// Total requests completed so far (success + failure).
300    pub total_requests: usize,
301
302    /// Successful requests so far (HTTP status 2xx).
303    pub successful_requests: usize,
304
305    /// Failed requests so far (non-2xx status or connection errors).
306    pub failed_requests: usize,
307}
308
309/// Context passed to after-request hook functions.
310///
311/// Provides detailed information about a completed request, including latency and status code.
312/// After-request hooks can use this for custom metrics collection, retry logic based on
313/// response status, or conditional behavior based on performance.
314///
315/// # Examples
316///
317/// ```no_run
318/// # use httpress::{Benchmark, AfterRequestContext, HookAction};
319/// # use std::sync::{Arc, Mutex};
320/// # use std::time::Duration;
321/// # #[tokio::main]
322/// # async fn main() -> httpress::Result<()> {
323/// let slow_request_count = Arc::new(Mutex::new(0));
324/// let slow_count_clone = slow_request_count.clone();
325///
326/// Benchmark::builder()
327///     .url("http://localhost:3000")
328///     .after_request(move |ctx: AfterRequestContext| {
329///         // Track slow requests (> 100ms)
330///         if ctx.latency > Duration::from_millis(100) {
331///             let mut count = slow_count_clone.lock().unwrap();
332///             *count += 1;
333///         }
334///
335///         // Retry on 5xx errors
336///         if let Some(status) = ctx.status {
337///             if status >= 500 {
338///                 return HookAction::Retry;
339///             }
340///         }
341///
342///         HookAction::Continue
343///     })
344///     .max_retries(3)
345///     .requests(1000)
346///     .build()?;
347/// # Ok(())
348/// # }
349/// ```
350#[derive(Debug, Clone, Copy)]
351pub struct AfterRequestContext {
352    /// ID of the worker that executed this request (0-based).
353    pub worker_id: usize,
354
355    /// Sequential request number for this worker (0-based).
356    pub request_number: usize,
357
358    /// Time elapsed since benchmark start.
359    pub elapsed: Duration,
360
361    /// Total requests completed so far (success + failure).
362    pub total_requests: usize,
363
364    /// Successful requests so far (HTTP status 2xx).
365    pub successful_requests: usize,
366
367    /// Failed requests so far (non-2xx status or connection errors).
368    pub failed_requests: usize,
369
370    /// Time taken for this request (latency).
371    pub latency: Duration,
372
373    /// HTTP status code if the request succeeded, `None` if it failed.
374    pub status: Option<u16>,
375}
376
377/// Action returned by hook functions to control request execution.
378///
379/// Hook functions (both before-request and after-request) return this enum to signal
380/// what action the benchmark executor should take for the current request.
381///
382/// # Examples
383///
384/// ```no_run
385/// # use httpress::{Benchmark, AfterRequestContext, HookAction};
386/// # #[tokio::main]
387/// # async fn main() -> httpress::Result<()> {
388/// Benchmark::builder()
389///     .url("http://localhost:3000")
390///     .after_request(|ctx: AfterRequestContext| {
391///         match ctx.status {
392///             Some(status) if status >= 500 => {
393///                 // Retry server errors
394///                 HookAction::Retry
395///             }
396///             Some(status) if status == 429 => {
397///                 // Abort on rate limiting
398///                 HookAction::Abort
399///             }
400///             _ => {
401///                 // Continue normally
402///                 HookAction::Continue
403///             }
404///         }
405///     })
406///     .max_retries(3)
407///     .requests(1000)
408///     .build()?;
409/// # Ok(())
410/// # }
411/// ```
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413pub enum HookAction {
414    /// Continue with normal execution.
415    ///
416    /// The request proceeds normally. This is the typical return value.
417    Continue,
418
419    /// Abort this request without retrying.
420    ///
421    /// The request is counted as failed, but the benchmark continues with other requests.
422    /// Use this for requests that should be skipped (e.g., circuit breaker triggered).
423    Abort,
424
425    /// Retry this request.
426    ///
427    /// The request will be retried up to the configured `max_retries` limit.
428    /// Use this for transient errors that might succeed on retry (e.g., 5xx errors).
429    Retry,
430}
431
432/// Type alias for before-request hook functions.
433///
434/// Before-request hooks are called before sending each HTTP request. They receive
435/// [`BeforeRequestContext`] and return [`HookAction`] to control execution flow.
436///
437/// # Type Signature
438///
439/// ```text
440/// Fn(BeforeRequestContext) -> HookAction + Send + Sync + 'static
441/// ```
442///
443/// # Examples
444///
445/// See [`BenchmarkBuilder::before_request`](crate::BenchmarkBuilder::before_request) for usage examples.
446pub type BeforeRequestHook = Arc<dyn Fn(BeforeRequestContext) -> HookAction + Send + Sync>;
447
448/// Type alias for after-request hook functions.
449///
450/// After-request hooks are called after each HTTP request completes (or fails).
451/// They receive [`AfterRequestContext`] with request latency and status code,
452/// and return [`HookAction`] to control execution flow.
453///
454/// # Type Signature
455///
456/// ```text
457/// Fn(AfterRequestContext) -> HookAction + Send + Sync + 'static
458/// ```
459///
460/// # Examples
461///
462/// See [`BenchmarkBuilder::after_request`](crate::BenchmarkBuilder::after_request) for usage examples.
463pub type AfterRequestHook = Arc<dyn Fn(AfterRequestContext) -> HookAction + Send + Sync>;
464
465/// Source of request configuration - either static or dynamically generated.
466///
467/// This enum represents how requests are configured in a benchmark. It is used
468/// internally by the builder and executor, but is exposed publicly as part of
469/// the configuration API.
470///
471/// You typically don't construct this directly; instead use
472/// [`BenchmarkBuilder::url`](crate::BenchmarkBuilder::url) for static configuration or
473/// [`BenchmarkBuilder::request_fn`](crate::BenchmarkBuilder::request_fn) for dynamic generation.
474#[derive(Clone)]
475pub enum RequestSource {
476    /// Static configuration used for all requests.
477    ///
478    /// Created when using [`BenchmarkBuilder::url`](crate::BenchmarkBuilder::url).
479    Static(RequestConfig),
480
481    /// Dynamic generator function called for each request.
482    ///
483    /// Created when using [`BenchmarkBuilder::request_fn`](crate::BenchmarkBuilder::request_fn).
484    Dynamic(RequestGenerator),
485}
486
487/// Benchmark configuration
488#[derive(Clone)]
489pub struct BenchConfig {
490    pub request_source: RequestSource,
491    pub concurrency: usize,
492    pub stop_condition: StopCondition,
493    pub timeout: Duration,
494    pub rate: Option<u64>,
495    pub rate_fn: Option<RateFunction>,
496    pub before_request_hooks: Vec<BeforeRequestHook>,
497    pub after_request_hooks: Vec<AfterRequestHook>,
498    pub max_retries: usize,
499    pub progress_fn: Option<ProgressFn>,
500}
501
502impl BenchConfig {
503    /// Create config from CLI arguments
504    pub fn from_args(args: Args) -> Result<Self, Error> {
505        let stop_condition = match (args.requests, args.duration) {
506            (Some(n), None) => StopCondition::Requests(n),
507            (None, Some(d)) => StopCondition::Duration(parse_duration(&d)?),
508            (None, None) => StopCondition::Infinite,
509            (Some(_), Some(_)) => unreachable!("clap prevents this"),
510        };
511
512        let headers = parse_headers(&args.headers)?;
513
514        let request_config = RequestConfig {
515            url: args.url,
516            method: args.method,
517            headers,
518            body: args.body.map(Bytes::from),
519        };
520
521        Ok(BenchConfig {
522            request_source: RequestSource::Static(request_config),
523            concurrency: args.concurrency,
524            stop_condition,
525            timeout: Duration::from_secs(args.timeout),
526            rate: args.rate,
527            rate_fn: None,
528            before_request_hooks: Vec::new(),
529            after_request_hooks: Vec::new(),
530            max_retries: 3,
531            progress_fn: None,
532        })
533    }
534
535    /// Attach a built-in terminal progress bar and return the updated config
536    /// alongside the bar handle (used to call `finish_and_clear` after the run).
537    pub fn with_progress(mut self) -> (Self, Arc<ProgressBar>) {
538        let pb = Arc::new(create_progress_bar(&self.stop_condition));
539        let pb_fn = Arc::clone(&pb);
540        self.progress_fn = Some(Arc::new(move |snap| update_progress_bar(&pb_fn, &snap)));
541        (self, pb)
542    }
543}
544
545/// Parse duration string like "10s", "1m", "500ms"
546fn parse_duration(s: &str) -> Result<Duration, Error> {
547    let s = s.trim();
548
549    if let Some(ms) = s.strip_suffix("ms") {
550        let ms: u64 = ms
551            .parse()
552            .map_err(|_| Error::InvalidDuration(s.to_string()))?;
553        return Ok(Duration::from_millis(ms));
554    }
555
556    if let Some(secs) = s.strip_suffix('s') {
557        let secs: u64 = secs
558            .parse()
559            .map_err(|_| Error::InvalidDuration(s.to_string()))?;
560        return Ok(Duration::from_secs(secs));
561    }
562
563    if let Some(mins) = s.strip_suffix('m') {
564        let mins: u64 = mins
565            .parse()
566            .map_err(|_| Error::InvalidDuration(s.to_string()))?;
567        return Ok(Duration::from_secs(mins * 60));
568    }
569
570    // Try parsing as plain seconds
571    if let Ok(secs) = s.parse::<u64>() {
572        return Ok(Duration::from_secs(secs));
573    }
574
575    Err(Error::InvalidDuration(s.to_string()))
576}
577
578/// Parse header strings like "Content-Type: application/json"
579fn parse_headers(headers: &[String]) -> Result<HashMap<String, String>, Error> {
580    let mut map = HashMap::new();
581
582    for h in headers {
583        let (key, value) = h
584            .split_once(':')
585            .ok_or_else(|| Error::InvalidHeader(h.clone()))?;
586
587        map.insert(key.trim().to_string(), value.trim().to_string());
588    }
589
590    Ok(map)
591}