use downcast_rs::{impl_downcast, Downcast};
use regex::Regex;
use reqwest::{header, Client, ClientBuilder, Method, RequestBuilder, Response};
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::time::Duration;
use std::{fmt, str};
use std::{future::Future, pin::Pin, time::Instant};
use url::Url;
use crate::logger::GooseLog;
use crate::metrics::{
GooseCoordinatedOmissionMitigation, GooseMetric, GooseRawRequest, GooseRequestMetric,
TransactionDetail,
};
use crate::{GooseConfiguration, GooseError, WeightedTransactions};
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
static GOOSE_REQUEST_TIMEOUT: u64 = 60_000;
#[macro_export]
macro_rules! transaction {
($transaction_func:ident) => {
Transaction::new(std::sync::Arc::new(move |s| {
std::boxed::Box::pin($transaction_func(s))
}))
};
}
#[macro_export]
macro_rules! scenario {
($name:tt) => {
Scenario::new($name)
};
}
pub type TransactionResult = Result<(), Box<TransactionError>>;
#[derive(Debug)]
pub enum TransactionError {
Reqwest(reqwest::Error),
Url(url::ParseError),
RequestFailed {
raw_request: GooseRequestMetric,
},
RequestCanceled {
source: flume::SendError<bool>,
},
MetricsFailed {
source: flume::SendError<GooseMetric>,
},
LoggerFailed {
source: flume::SendError<Option<GooseLog>>,
},
InvalidMethod {
method: Method,
},
}
impl TransactionError {
fn describe(&self) -> &str {
match *self {
TransactionError::Reqwest(_) => "reqwest::Error",
TransactionError::Url(_) => "url::ParseError",
TransactionError::RequestFailed { .. } => "request failed",
TransactionError::RequestCanceled { .. } => {
"request canceled because throttled load test ended"
}
TransactionError::MetricsFailed { .. } => "failed to send metrics to parent thread",
TransactionError::LoggerFailed { .. } => "failed to send log message to logger thread",
TransactionError::InvalidMethod { .. } => "unrecognized HTTP request method",
}
}
}
impl fmt::Display for TransactionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
TransactionError::Reqwest(ref source) => {
write!(f, "TransactionError: {} ({})", self.describe(), source)
}
TransactionError::Url(ref source) => {
write!(f, "TransactionError: {} ({})", self.describe(), source)
}
TransactionError::RequestCanceled { ref source } => {
write!(f, "TransactionError: {} ({})", self.describe(), source)
}
TransactionError::MetricsFailed { ref source } => {
write!(f, "TransactionError: {} ({})", self.describe(), source)
}
TransactionError::LoggerFailed { ref source } => {
write!(f, "TransactionError: {} ({})", self.describe(), source)
}
_ => write!(f, "TransactionError: {}", self.describe()),
}
}
}
impl std::error::Error for TransactionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
TransactionError::Reqwest(ref source) => Some(source),
TransactionError::Url(ref source) => Some(source),
TransactionError::RequestCanceled { ref source } => Some(source),
TransactionError::MetricsFailed { ref source } => Some(source),
TransactionError::LoggerFailed { ref source } => Some(source),
_ => None,
}
}
}
impl From<reqwest::Error> for TransactionError {
fn from(err: reqwest::Error) -> TransactionError {
TransactionError::Reqwest(err)
}
}
impl From<reqwest::Error> for Box<TransactionError> {
fn from(value: reqwest::Error) -> Self {
Box::new(TransactionError::Reqwest(value))
}
}
impl From<url::ParseError> for TransactionError {
fn from(err: url::ParseError) -> TransactionError {
TransactionError::Url(err)
}
}
impl From<flume::SendError<bool>> for TransactionError {
fn from(source: flume::SendError<bool>) -> TransactionError {
TransactionError::RequestCanceled { source }
}
}
impl From<flume::SendError<GooseMetric>> for TransactionError {
fn from(source: flume::SendError<GooseMetric>) -> TransactionError {
TransactionError::MetricsFailed { source }
}
}
impl From<flume::SendError<Option<GooseLog>>> for TransactionError {
fn from(source: flume::SendError<Option<GooseLog>>) -> TransactionError {
TransactionError::LoggerFailed { source }
}
}
#[derive(Clone, Hash)]
pub struct Scenario {
pub name: String,
pub machine_name: String,
pub scenarios_index: usize,
pub weight: usize,
pub transaction_wait: Option<(Duration, Duration)>,
pub transactions: Vec<Transaction>,
pub weighted_transactions: WeightedTransactions,
pub weighted_on_start_transactions: WeightedTransactions,
pub weighted_on_stop_transactions: WeightedTransactions,
pub host: Option<String>,
}
impl Scenario {
pub fn new(name: &str) -> Self {
trace!("new scenario: name: {}", &name);
Scenario {
name: name.to_string(),
machine_name: Scenario::get_machine_name(name),
scenarios_index: usize::MAX,
weight: 1,
transaction_wait: None,
transactions: Vec::new(),
weighted_transactions: Vec::new(),
weighted_on_start_transactions: Vec::new(),
weighted_on_stop_transactions: Vec::new(),
host: None,
}
}
fn get_machine_name(name: &str) -> String {
let re = Regex::new("[^a-zA-Z0-9]+").unwrap();
let alphanumeric = re.replace_all(name, "");
alphanumeric.to_lowercase()
}
pub fn register_transaction(mut self, mut transaction: Transaction) -> Self {
trace!("{} register_transaction: {}", self.name, transaction.name);
transaction.transactions_index = self.transactions.len();
self.transactions.push(transaction);
self
}
pub fn set_weight(mut self, weight: usize) -> Result<Self, GooseError> {
trace!("{} set_weight: {}", self.name, weight);
if weight == 0 {
return Err(GooseError::InvalidWeight {
weight,
detail: ("Weight must be set to at least 1.".to_string()),
});
}
self.weight = weight;
Ok(self)
}
pub fn set_host(mut self, host: &str) -> Self {
trace!("{} set_host: {}", self.name, host);
self.host = Some(host.to_string());
self
}
pub fn set_wait_time(
mut self,
min_wait: Duration,
max_wait: Duration,
) -> Result<Self, GooseError> {
trace!(
"{} set_wait time: min: {:?} max: {:?}",
self.name,
min_wait,
max_wait
);
if min_wait.as_millis() > max_wait.as_millis() {
return Err(GooseError::InvalidWaitTime {
min_wait,
max_wait,
detail:
"The min_wait option can not be set to a larger value than the max_wait option."
.to_string(),
});
}
self.transaction_wait = Some((min_wait, max_wait));
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GooseUserCommand {
Wait,
Run,
Exit,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)]
pub enum GooseMethod {
Delete,
Get,
Head,
Patch,
Post,
Put,
}
impl fmt::Display for GooseMethod {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GooseMethod::Delete => write!(f, "DELETE"),
GooseMethod::Get => write!(f, "GET"),
GooseMethod::Head => write!(f, "HEAD"),
GooseMethod::Patch => write!(f, "PATCH"),
GooseMethod::Post => write!(f, "POST"),
GooseMethod::Put => write!(f, "PUT"),
}
}
}
pub fn goose_method_from_method(method: Method) -> Result<GooseMethod, Box<TransactionError>> {
Ok(match method {
Method::DELETE => GooseMethod::Delete,
Method::GET => GooseMethod::Get,
Method::HEAD => GooseMethod::Head,
Method::PATCH => GooseMethod::Patch,
Method::POST => GooseMethod::Post,
Method::PUT => GooseMethod::Put,
_ => {
return Err(Box::new(TransactionError::InvalidMethod { method }));
}
})
}
#[derive(Debug)]
pub struct GooseResponse {
pub request: GooseRequestMetric,
pub response: Result<Response, reqwest::Error>,
}
impl GooseResponse {
pub fn new(request: GooseRequestMetric, response: Result<Response, reqwest::Error>) -> Self {
GooseResponse { request, response }
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct GooseDebug {
pub tag: String,
pub request: Option<GooseRequestMetric>,
pub header: Option<String>,
pub body: Option<String>,
}
impl GooseDebug {
fn new(
tag: &str,
request: Option<&GooseRequestMetric>,
header: Option<&header::HeaderMap>,
body: Option<&str>,
) -> Self {
GooseDebug {
tag: tag.to_string(),
request: request.cloned(),
header: header.map(|h| format!("{h:?}")),
body: body.map(|b| b.to_string()),
}
}
}
#[derive(Debug, Clone)]
struct GooseRequestCadence {
last_time: std::time::Instant,
delays_since_last_time: u64,
counter: u64,
minimum_cadence: u64,
maximum_cadence: u64,
average_cadence: u64,
total_elapsed: u64,
coordinated_omission_mitigation: u64,
user_cadence: u64,
coordinated_omission_counter: isize,
}
impl GooseRequestCadence {
fn new() -> GooseRequestCadence {
GooseRequestCadence {
last_time: std::time::Instant::now(),
delays_since_last_time: 0,
counter: 0,
minimum_cadence: 0,
maximum_cadence: 0,
average_cadence: 0,
total_elapsed: 0,
coordinated_omission_mitigation: 0,
user_cadence: 0,
coordinated_omission_counter: -1,
}
}
}
pub trait GooseUserData: Downcast + Send + Sync + 'static {}
impl_downcast!(GooseUserData);
impl<T: Send + Sync + 'static> GooseUserData for T {}
trait CloneGooseUserData {
fn clone_goose_user_data(&self) -> Box<dyn GooseUserData>;
}
impl<T> CloneGooseUserData for T
where
T: GooseUserData + Clone + 'static,
{
fn clone_goose_user_data(&self) -> Box<dyn GooseUserData> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn GooseUserData> {
fn clone(&self) -> Self {
self.clone_goose_user_data()
}
}
#[derive(Debug)]
pub struct GooseUser {
pub started: Instant,
pub(crate) iterations: usize,
pub(crate) scenarios_index: usize,
pub(crate) scenario_name: String,
pub(crate) transaction_index: Option<String>,
pub(crate) transaction_name: Option<String>,
pub client: Client,
pub base_url: Url,
pub config: GooseConfiguration,
pub logger: Option<flume::Sender<Option<GooseLog>>>,
pub throttle: Option<flume::Sender<bool>>,
pub is_throttled: bool,
pub metrics_channel: Option<flume::Sender<GooseMetric>>,
pub shutdown_channel: Option<flume::Sender<usize>>,
pub weighted_users_index: usize,
pub load_test_hash: u64,
request_cadence: GooseRequestCadence,
pub(crate) slept: u64,
session_data: Option<Box<dyn GooseUserData>>,
}
impl Clone for GooseUser {
fn clone(&self) -> Self {
Self {
started: self.started,
iterations: self.iterations,
scenarios_index: self.scenarios_index,
scenario_name: self.scenario_name.clone(),
transaction_index: self.transaction_index.clone(),
transaction_name: self.transaction_name.clone(),
client: self.client.clone(),
base_url: self.base_url.clone(),
config: self.config.clone(),
logger: self.logger.clone(),
throttle: self.throttle.clone(),
is_throttled: self.is_throttled,
metrics_channel: self.metrics_channel.clone(),
shutdown_channel: self.shutdown_channel.clone(),
weighted_users_index: self.weighted_users_index,
load_test_hash: self.load_test_hash,
request_cadence: self.request_cadence.clone(),
slept: self.slept,
session_data: if self.session_data.is_some() {
Option::from(self.session_data.clone_goose_user_data())
} else {
None
},
}
}
}
impl Debug for dyn GooseUserData {
fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
write!(fmt, "GooseUserData")
}
}
impl GooseUser {
pub fn new(
scenarios_index: usize,
scenario_name: String,
base_url: Url,
configuration: &GooseConfiguration,
load_test_hash: u64,
reqwest_client: Option<Client>,
) -> Result<Self, GooseError> {
trace!("new GooseUser");
let client = match reqwest_client {
Some(c) => c,
None => create_reqwest_client(configuration)?,
};
Ok(GooseUser {
started: Instant::now(),
iterations: 0,
scenarios_index,
scenario_name,
transaction_index: None,
transaction_name: None,
client,
base_url,
config: configuration.clone(),
logger: None,
throttle: None,
is_throttled: true,
metrics_channel: None,
shutdown_channel: None,
weighted_users_index: usize::MAX,
load_test_hash,
request_cadence: GooseRequestCadence::new(),
slept: 0,
session_data: None,
})
}
pub fn single(base_url: Url, configuration: &GooseConfiguration) -> Result<Self, GooseError> {
let mut single_user = GooseUser::new(0, "".to_string(), base_url, configuration, 0, None)?;
single_user.weighted_users_index = 0;
single_user.is_throttled = false;
Ok(single_user)
}
pub fn get_iterations(&self) -> usize {
self.iterations
}
pub fn get_session_data<T: GooseUserData>(&self) -> Option<&T> {
match &self.session_data {
Some(data) => data.downcast_ref::<T>(),
None => None,
}
}
pub fn get_session_data_unchecked<T: GooseUserData>(&self) -> &T {
let session_data = self.session_data.as_deref().expect("Missing session data!");
session_data
.downcast_ref::<T>()
.expect("Invalid session data!")
}
pub fn get_session_data_mut<T: GooseUserData>(&mut self) -> Option<&mut T> {
match &mut self.session_data {
Some(data) => data.downcast_mut::<T>(),
None => None,
}
}
pub fn get_session_data_unchecked_mut<T: GooseUserData>(&mut self) -> &mut T {
let session_data = self
.session_data
.as_deref_mut()
.expect("Missing session data!");
session_data
.downcast_mut::<T>()
.expect("Invalid session data!")
}
pub fn set_session_data<T: GooseUserData>(&mut self, data: T) {
self.session_data.replace(Box::new(data));
}
pub fn build_url(&self, path: &str) -> Result<String, Box<TransactionError>> {
if let Ok(parsed_path) = Url::parse(path) {
if let Some(_host) = parsed_path.host() {
return Ok(path.to_string());
}
}
match self.base_url.join(path) {
Ok(u) => Ok(u.to_string()),
Err(e) => Err(Box::new(e.into())),
}
}
pub async fn get(&mut self, path: &str) -> Result<GooseResponse, Box<TransactionError>> {
let goose_request = GooseRequest::builder()
.method(GooseMethod::Get)
.path(path)
.build();
self.request(goose_request).await
}
pub async fn get_named(
&mut self,
path: &str,
name: &str,
) -> Result<GooseResponse, Box<TransactionError>> {
let goose_request = GooseRequest::builder()
.method(GooseMethod::Get)
.path(path)
.name(name)
.build();
self.request(goose_request).await
}
pub async fn post<T: Into<reqwest::Body>>(
&mut self,
path: &str,
body: T,
) -> Result<GooseResponse, Box<TransactionError>> {
let url = self.build_url(path)?;
let reqwest_request_builder = self.client.post(url);
let goose_request = GooseRequest::builder()
.method(GooseMethod::Post)
.path(path)
.set_request_builder(reqwest_request_builder.body(body))
.build();
self.request(goose_request).await
}
pub async fn post_form<T: Serialize + ?Sized>(
&mut self,
path: &str,
form: &T,
) -> Result<GooseResponse, Box<TransactionError>> {
let url = self.build_url(path)?;
let reqwest_request_builder = self.client.post(url);
let goose_request = GooseRequest::builder()
.method(GooseMethod::Post)
.path(path)
.set_request_builder(reqwest_request_builder.form(&form))
.build();
self.request(goose_request).await
}
pub async fn post_json<T: Serialize + ?Sized>(
&mut self,
path: &str,
json: &T,
) -> Result<GooseResponse, Box<TransactionError>> {
let url = self.build_url(path)?;
let reqwest_request_builder = self.client.post(url);
let goose_request = GooseRequest::builder()
.method(GooseMethod::Post)
.path(path)
.set_request_builder(reqwest_request_builder.json(&json))
.build();
self.request(goose_request).await
}
pub async fn head(&mut self, path: &str) -> Result<GooseResponse, Box<TransactionError>> {
let goose_request = GooseRequest::builder()
.method(GooseMethod::Head)
.path(path)
.build();
self.request(goose_request).await
}
pub async fn delete(&mut self, path: &str) -> Result<GooseResponse, Box<TransactionError>> {
let goose_request = GooseRequest::builder()
.method(GooseMethod::Delete)
.path(path)
.build();
self.request(goose_request).await
}
pub fn get_request_builder(
&self,
method: &GooseMethod,
path: &str,
) -> Result<RequestBuilder, Box<TransactionError>> {
let url = self.build_url(path)?;
Ok(match method {
GooseMethod::Delete => self.client.delete(&url),
GooseMethod::Get => self.client.get(&url),
GooseMethod::Head => self.client.head(&url),
GooseMethod::Patch => self.client.patch(&url),
GooseMethod::Post => self.client.post(&url),
GooseMethod::Put => self.client.put(&url),
})
}
pub async fn request(
&mut self,
mut request: GooseRequest<'_>,
) -> Result<GooseResponse, Box<TransactionError>> {
let request_builder = if request.request_builder.is_some() {
request.request_builder.take().unwrap()
} else {
self.get_request_builder(&request.method, request.path)?
};
let request_name = self.get_request_name(&request);
if self.is_throttled && self.throttle.is_some() {
debug!(
"[user {}]: waiting on throttle",
self.weighted_users_index + 1
);
if let Err(e) = self.throttle.clone().unwrap().send_async(true).await {
return Err(Box::new(e.into()));
}
};
let started = Instant::now();
let built_request = match request_builder.build() {
Ok(r) => r,
Err(e) => return Err(Box::new(e.into())),
};
let path = match Url::parse(built_request.url().as_ref()) {
Ok(u) => u.path().to_string(),
Err(e) => {
error!("failed to parse url: {e}");
"".to_string()
}
};
let mut headers: Vec<String> = Vec::new();
for header in built_request.headers() {
headers.push(format!("{header:?}"));
}
let body = if self.config.request_body {
let body_bytes = match built_request.body() {
Some(b) => b.as_bytes().unwrap_or(b""),
None => b"",
};
str::from_utf8(body_bytes).unwrap_or("")
} else {
""
};
let raw_request = GooseRawRequest::new(
goose_method_from_method(built_request.method().clone())?,
built_request.url().as_str(),
headers,
body,
);
let transaction_detail = TransactionDetail {
scenario_index: self.scenarios_index,
scenario_name: self.scenario_name.as_str(),
transaction_index: self
.transaction_index
.as_ref()
.map_or_else(|| "", |v| v.as_ref()),
transaction_name: self
.transaction_name
.as_ref()
.map_or_else(|| "", |v| v.as_ref()),
};
let mut request_metric = GooseRequestMetric::new(
raw_request,
transaction_detail,
request_name,
self.started.elapsed().as_millis(),
self.weighted_users_index,
);
let response = self.client.execute(built_request).await;
request_metric.set_response_time(started.elapsed().as_millis());
match &response {
Ok(r) => {
let status_code = r.status();
debug!("{:?}: status_code {}", &path, status_code);
request_metric.set_status_code(Some(status_code));
request_metric.set_final_url(r.url().as_str());
if let Some(expect_status_code) = request.expect_status_code {
if status_code != expect_status_code {
request_metric.success = false;
request_metric.error = format!("{status_code}: {request_name}");
}
} else if !status_code.is_success() {
request_metric.success = false;
request_metric.error = format!("{status_code}: {request_name}");
}
if self.config.sticky_follow && request_metric.raw.url != request_metric.final_url {
let base_url = self.base_url.to_string();
if !request_metric.final_url.starts_with(&base_url) {
let redirected_url = match Url::parse(&request_metric.final_url) {
Ok(u) => u,
Err(e) => return Err(Box::new(e.into())),
};
let redirected_base_url =
redirected_url[..url::Position::BeforePath].to_string();
info!(
"base_url for user {} redirected from {} to {}",
self.weighted_users_index + 1,
&base_url,
&redirected_base_url
);
let _ = self.set_base_url(&redirected_base_url);
}
}
}
Err(e) => {
warn!("{:?}: {}", &path, e);
request_metric.success = false;
request_metric.set_status_code(None);
request_metric.error = clean_reqwest_error(e, request_name);
}
};
request_metric.user_cadence = self
.coordinated_omission_mitigation(&request_metric)
.await?;
if !self.config.no_metrics {
self.send_request_metric_to_parent(request_metric.clone())?;
}
if request.error_on_fail && !request_metric.success {
error!("{:?} {}", &path, &request_metric.error);
return Err(Box::new(TransactionError::RequestFailed {
raw_request: request_metric,
}));
}
Ok(GooseResponse::new(request_metric, response))
}
pub(crate) async fn update_request_cadence(&mut self, thread_number: usize) {
if let Some(co_mitigation) = self.config.co_mitigation.as_ref() {
if co_mitigation == &GooseCoordinatedOmissionMitigation::Disabled {
return;
}
let now = std::time::Instant::now();
self.request_cadence.delays_since_last_time = self.slept;
self.slept = 0;
let elapsed = (now - self.request_cadence.last_time).as_millis() as u64
- self.request_cadence.delays_since_last_time;
if elapsed < self.request_cadence.minimum_cadence
|| self.request_cadence.minimum_cadence == 0
{
self.request_cadence.minimum_cadence = elapsed;
} else if elapsed > self.request_cadence.maximum_cadence {
self.request_cadence.maximum_cadence = elapsed;
}
self.request_cadence.counter += 1;
self.request_cadence.total_elapsed += elapsed;
self.request_cadence.last_time = now;
self.request_cadence.average_cadence =
self.request_cadence.total_elapsed / self.request_cadence.counter;
if self.request_cadence.counter > 3 {
if self.request_cadence.coordinated_omission_counter < 0 {
debug!("user {thread_number} enabled coordinated omission mitigation");
self.request_cadence.coordinated_omission_counter += 1;
}
let cadence = match co_mitigation {
GooseCoordinatedOmissionMitigation::Average => {
self.request_cadence.average_cadence
}
GooseCoordinatedOmissionMitigation::Maximum => {
self.request_cadence.maximum_cadence
}
GooseCoordinatedOmissionMitigation::Minimum => {
self.request_cadence.minimum_cadence
}
GooseCoordinatedOmissionMitigation::Disabled => unreachable!(),
};
if elapsed > (cadence * 2) {
debug!(
"user {thread_number}: coordinated_omission_mitigation: elapsed({elapsed}) > cadence({cadence})"
);
self.request_cadence.coordinated_omission_counter += 1;
self.request_cadence.coordinated_omission_mitigation = elapsed;
} else {
self.request_cadence.coordinated_omission_mitigation = 0;
}
self.request_cadence.user_cadence = cadence;
}
} else {
unreachable!();
}
}
async fn coordinated_omission_mitigation(
&self,
request_metric: &GooseRequestMetric,
) -> Result<u64, Box<TransactionError>> {
if let Some(co_mitigation) = self.config.co_mitigation.as_ref() {
if co_mitigation == &GooseCoordinatedOmissionMitigation::Disabled {
return Ok(0);
}
if self.request_cadence.counter > 3
&& request_metric.response_time > self.request_cadence.user_cadence
{
let transaction_name = if let Some(transaction_name) = &self.transaction_name {
format!(", transaction name: \"{transaction_name}\"")
} else {
"".to_string()
};
info!(
"{:.3}s into goose attack: \"{} {}\" [{}] took abnormally long ({} ms){}",
request_metric.elapsed as f64 / 1_000.0,
request_metric.raw.method,
request_metric.raw.url,
request_metric.status_code,
request_metric.response_time,
transaction_name,
);
}
if self.request_cadence.coordinated_omission_mitigation > 0 {
let mut coordinated_omission_request_metric = request_metric.clone();
coordinated_omission_request_metric.coordinated_omission_elapsed =
self.request_cadence.coordinated_omission_mitigation;
coordinated_omission_request_metric.user_cadence =
self.request_cadence.user_cadence;
self.send_request_metric_to_parent(coordinated_omission_request_metric)?;
}
Ok(self.request_cadence.user_cadence)
} else {
unreachable!();
}
}
fn send_request_metric_to_parent(
&self,
request_metric: GooseRequestMetric,
) -> TransactionResult {
if !self.config.request_log.is_empty() {
if let Some(logger) = self.logger.as_ref() {
if let Err(e) = logger.send(Some(GooseLog::Request(request_metric.clone()))) {
return Err(Box::new(e.into()));
}
}
}
if let Some(metrics_channel) = self.metrics_channel.clone() {
if let Err(e) = metrics_channel.send(GooseMetric::Request(Box::new(request_metric))) {
return Err(Box::new(e.into()));
}
}
Ok(())
}
fn get_request_name<'a>(&'a self, request: &'a GooseRequest) -> &'a str {
match request.name {
Some(rn) => rn,
None => {
if let Some(transaction_name) = &self.transaction_name {
transaction_name
} else {
request.path
}
}
}
}
pub fn set_success(&self, request: &mut GooseRequestMetric) -> TransactionResult {
if !request.success {
request.success = true;
request.update = true;
self.send_request_metric_to_parent(request.clone())?;
}
Ok(())
}
pub fn set_failure(
&self,
tag: &str,
request: &mut GooseRequestMetric,
headers: Option<&header::HeaderMap>,
body: Option<&str>,
) -> TransactionResult {
if request.success {
request.success = false;
request.update = true;
request.error = tag.to_string();
self.send_request_metric_to_parent(request.clone())?;
}
self.log_debug(tag, Some(&*request), headers, body)?;
info!("set_failure: {tag}");
Err(Box::new(TransactionError::RequestFailed {
raw_request: request.clone(),
}))
}
pub fn log_debug(
&self,
tag: &str,
request: Option<&GooseRequestMetric>,
headers: Option<&header::HeaderMap>,
body: Option<&str>,
) -> TransactionResult {
if !self.config.debug_log.is_empty() {
if let Some(logger) = self.logger.clone() {
if self.config.no_debug_body {
if let Err(e) = logger.send(Some(GooseLog::Debug(GooseDebug::new(
tag, request, headers, None,
)))) {
return Err(Box::new(e.into()));
}
} else if let Err(e) = logger.send(Some(GooseLog::Debug(GooseDebug::new(
tag, request, headers, body,
)))) {
return Err(Box::new(e.into()));
}
}
}
Ok(())
}
pub async fn set_client_builder(
&mut self,
builder: ClientBuilder,
) -> Result<(), TransactionError> {
self.client = builder.build()?;
Ok(())
}
pub fn set_base_url(&mut self, host: &str) -> Result<(), Box<TransactionError>> {
self.base_url = match Url::parse(host) {
Ok(u) => u,
Err(e) => return Err(Box::new(e.into())),
};
Ok(())
}
}
pub(crate) fn create_reqwest_client(
configuration: &GooseConfiguration,
) -> Result<Client, reqwest::Error> {
let timeout = if configuration.timeout.is_some() {
match crate::util::get_float_from_string(configuration.timeout.clone()) {
Some(f) => f as u64 * 1_000,
None => GOOSE_REQUEST_TIMEOUT,
}
} else {
GOOSE_REQUEST_TIMEOUT
};
Client::builder()
.user_agent(APP_USER_AGENT)
.cookie_store(true)
.timeout(Duration::from_millis(timeout))
.gzip(!configuration.no_gzip)
.danger_accept_invalid_certs(configuration.accept_invalid_certs)
.build()
}
#[derive(Debug)]
pub struct GooseRequest<'a> {
path: &'a str,
method: GooseMethod,
name: Option<&'a str>,
expect_status_code: Option<u16>,
error_on_fail: bool,
request_builder: Option<RequestBuilder>,
}
impl<'a> GooseRequest<'a> {
pub fn builder() -> GooseRequestBuilder<'a> {
GooseRequestBuilder::new()
}
}
pub struct GooseRequestBuilder<'a> {
path: &'a str,
method: GooseMethod,
name: Option<&'a str>,
expect_status_code: Option<u16>,
error_on_fail: bool,
request_builder: Option<RequestBuilder>,
}
impl<'a> GooseRequestBuilder<'a> {
fn new() -> Self {
Self {
path: "",
method: GooseMethod::Get,
name: None,
expect_status_code: None,
error_on_fail: false,
request_builder: None,
}
}
pub fn path(mut self, path: impl Into<&'a str>) -> Self {
self.path = path.into();
self
}
pub fn method(mut self, method: GooseMethod) -> Self {
self.method = method;
self
}
pub fn name(mut self, name: impl Into<&'a str>) -> Self {
self.name = Some(name.into());
self
}
pub fn expect_status_code(mut self, status_code: u16) -> Self {
self.expect_status_code = Some(status_code);
self
}
pub fn error_on_fail(mut self) -> Self {
self.error_on_fail = true;
self
}
pub fn set_request_builder(mut self, request_builder: RequestBuilder) -> Self {
self.request_builder = Some(request_builder);
self
}
pub fn build(self) -> GooseRequest<'a> {
let Self {
path,
method,
name,
expect_status_code,
error_on_fail,
request_builder,
} = self;
GooseRequest {
path,
method,
name,
expect_status_code,
error_on_fail,
request_builder,
}
}
}
fn clean_reqwest_error(e: &reqwest::Error, request_name: &str) -> String {
let kind = if e.is_builder() {
"builder error"
} else if e.is_request() {
"error sending request"
} else if e.is_body() {
"request or response body error"
} else if e.is_decode() {
"error decoding response body"
} else if e.is_redirect() {
"error following redirect"
} else {
"Http status"
};
if let Some(ref e) = std::error::Error::source(e) {
format!("{kind} {request_name}: {e}")
} else {
format!("{kind} {request_name}")
}
}
pub fn get_base_url(
config_host: Option<String>,
scenario_host: Option<String>,
default_host: Option<String>,
) -> Result<Url, GooseError> {
match config_host {
Some(host) => Ok(
Url::parse(&host).map_err(|parse_error| GooseError::InvalidHost {
host,
detail: "There was a failure parsing the host specified with --host.".to_string(),
parse_error,
})?,
),
None => {
match scenario_host {
Some(host) => {
Ok(
Url::parse(&host).map_err(|parse_error| GooseError::InvalidHost {
host,
detail: "There was a failure parsing the host specified with the Scenario.set_host() function.".to_string(),
parse_error,
})?,
)
}
None => {
let default_host = default_host.unwrap();
Ok(
Url::parse(&default_host).map_err(|parse_error| GooseError::InvalidHost {
host: default_host.to_string(),
detail: "There was a failure parsing the host specified globally with the GooseAttack.set_default() function.".to_string(),
parse_error,
})?,
)
}
}
}
}
}
pub type TransactionFunction = Arc<
dyn for<'r> Fn(
&'r mut GooseUser,
) -> Pin<Box<dyn Future<Output = TransactionResult> + Send + 'r>>
+ Send
+ Sync,
>;
#[derive(Clone)]
pub struct Transaction {
pub transactions_index: usize,
pub name: String,
pub weight: usize,
pub sequence: usize,
pub on_start: bool,
pub on_stop: bool,
pub function: TransactionFunction,
}
impl Transaction {
pub fn new(function: TransactionFunction) -> Self {
trace!("new transaction");
Transaction {
transactions_index: usize::MAX,
name: "".to_string(),
weight: 1,
sequence: 0,
on_start: false,
on_stop: false,
function,
}
}
pub fn set_name(mut self, name: &str) -> Self {
trace!("[{}] set_name: {}", self.transactions_index, self.name);
self.name = name.to_string();
self
}
pub fn set_on_start(mut self) -> Self {
trace!(
"{} [{}] set_on_start transaction",
self.name,
self.transactions_index
);
self.on_start = true;
self
}
pub fn set_on_stop(mut self) -> Self {
trace!(
"{} [{}] set_on_stop transaction",
self.name,
self.transactions_index
);
self.on_stop = true;
self
}
pub fn set_weight(mut self, weight: usize) -> Result<Self, GooseError> {
trace!(
"{} [{}] set_weight: {}",
self.name,
self.transactions_index,
weight
);
if weight == 0 {
return Err(GooseError::InvalidWeight {
weight,
detail: "Weight must be set to at least 1.".to_string(),
});
}
self.weight = weight;
Ok(self)
}
pub fn set_sequence(mut self, sequence: usize) -> Self {
trace!(
"{} [{}] set_sequence: {}",
self.name,
self.transactions_index,
sequence
);
if sequence < 1 {
info!(
"setting sequence to 0 for transaction {} is unnecessary, sequence disabled",
self.name
);
}
self.sequence = sequence;
self
}
}
impl Hash for Transaction {
fn hash<H: Hasher>(&self, state: &mut H) {
self.transactions_index.hash(state);
self.name.hash(state);
self.weight.hash(state);
self.sequence.hash(state);
self.on_start.hash(state);
self.on_stop.hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use gumdrop::Options;
use httpmock::{
Method::{GET, POST},
MockServer,
};
const EMPTY_ARGS: Vec<&str> = vec![];
fn setup_user(server: &MockServer) -> Result<GooseUser, GooseError> {
let mut configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
configuration.co_mitigation = Some(GooseCoordinatedOmissionMitigation::Average);
let base_url = get_base_url(Some(server.url("/")), None, None).unwrap();
GooseUser::single(base_url, &configuration)
}
#[test]
fn goose_scenario() {
async fn test_function_a(user: &mut GooseUser) -> TransactionResult {
let _goose = user.get("/a/").await?;
Ok(())
}
async fn test_function_b(user: &mut GooseUser) -> TransactionResult {
let _goose = user.get("/b/").await?;
Ok(())
}
let mut scenario = scenario!("foo");
assert_eq!(scenario.name, "foo");
assert_eq!(scenario.scenarios_index, usize::MAX);
assert_eq!(scenario.weight, 1);
assert_eq!(scenario.transaction_wait, None);
assert!(scenario.host.is_none());
assert_eq!(scenario.transactions.len(), 0);
assert_eq!(scenario.weighted_transactions.len(), 0);
assert_eq!(scenario.weighted_on_start_transactions.len(), 0);
assert_eq!(scenario.weighted_on_stop_transactions.len(), 0);
scenario = scenario.register_transaction(transaction!(test_function_a));
assert_eq!(scenario.transactions.len(), 1);
assert_eq!(scenario.weighted_transactions.len(), 0);
assert_eq!(scenario.scenarios_index, usize::MAX);
assert_eq!(scenario.weight, 1);
assert_eq!(scenario.transaction_wait, None);
assert!(scenario.host.is_none());
scenario = scenario.register_transaction(transaction!(test_function_b));
assert_eq!(scenario.transactions.len(), 2);
assert_eq!(scenario.weighted_transactions.len(), 0);
assert_eq!(scenario.scenarios_index, usize::MAX);
assert_eq!(scenario.weight, 1);
assert_eq!(scenario.transaction_wait, None);
assert!(scenario.host.is_none());
scenario = scenario.register_transaction(transaction!(test_function_a));
assert_eq!(scenario.transactions.len(), 3);
assert_eq!(scenario.weighted_transactions.len(), 0);
assert_eq!(scenario.scenarios_index, usize::MAX);
assert_eq!(scenario.weight, 1);
assert_eq!(scenario.transaction_wait, None);
assert!(scenario.host.is_none());
scenario = scenario.set_weight(50).unwrap();
assert_eq!(scenario.weight, 50);
assert_eq!(scenario.transactions.len(), 3);
assert_eq!(scenario.weighted_transactions.len(), 0);
assert_eq!(scenario.scenarios_index, usize::MAX);
assert_eq!(scenario.transaction_wait, None);
assert!(scenario.host.is_none());
scenario = scenario.set_weight(5).unwrap();
assert_eq!(scenario.weight, 5);
scenario = scenario.set_host("http://foo.example.com/");
assert_eq!(scenario.host, Some("http://foo.example.com/".to_string()));
assert_eq!(scenario.weight, 5);
assert_eq!(scenario.transactions.len(), 3);
assert_eq!(scenario.weighted_transactions.len(), 0);
assert_eq!(scenario.scenarios_index, usize::MAX);
assert_eq!(scenario.transaction_wait, None);
scenario = scenario.set_host("https://bar.example.com/");
assert_eq!(scenario.host, Some("https://bar.example.com/".to_string()));
scenario = scenario
.set_wait_time(Duration::from_secs(1), Duration::from_secs(10))
.unwrap();
assert_eq!(
scenario.transaction_wait,
Some((Duration::from_secs(1), Duration::from_secs(10)))
);
assert_eq!(scenario.host, Some("https://bar.example.com/".to_string()));
assert_eq!(scenario.weight, 5);
assert_eq!(scenario.transactions.len(), 3);
assert_eq!(scenario.weighted_transactions.len(), 0);
assert_eq!(scenario.scenarios_index, usize::MAX);
scenario = scenario
.set_wait_time(Duration::from_secs(3), Duration::from_secs(9))
.unwrap();
assert_eq!(
scenario.transaction_wait,
Some((Duration::from_secs(3), Duration::from_secs(9)))
);
}
#[test]
fn goose_transaction() {
async fn test_function_a(user: &mut GooseUser) -> TransactionResult {
let _goose = user.get("/a/").await?;
Ok(())
}
let mut transaction = transaction!(test_function_a);
assert_eq!(transaction.transactions_index, usize::MAX);
assert_eq!(transaction.name, "".to_string());
assert_eq!(transaction.weight, 1);
assert_eq!(transaction.sequence, 0);
assert!(!transaction.on_start);
assert!(!transaction.on_stop);
transaction = transaction.set_name("foo");
assert_eq!(transaction.name, "foo".to_string());
assert_eq!(transaction.weight, 1);
assert_eq!(transaction.sequence, 0);
assert!(!transaction.on_start);
assert!(!transaction.on_stop);
transaction = transaction.set_name("bar");
assert_eq!(transaction.name, "bar".to_string());
transaction = transaction.set_on_start();
assert!(transaction.on_start);
assert_eq!(transaction.name, "bar".to_string());
assert_eq!(transaction.weight, 1);
assert_eq!(transaction.sequence, 0);
assert!(!transaction.on_stop);
transaction = transaction.set_on_start();
assert!(transaction.on_start);
transaction = transaction.set_on_stop();
assert!(transaction.on_stop);
assert!(transaction.on_start);
assert_eq!(transaction.name, "bar".to_string());
assert_eq!(transaction.weight, 1);
assert_eq!(transaction.sequence, 0);
transaction = transaction.set_on_stop();
assert!(transaction.on_stop);
transaction = transaction.set_weight(2).unwrap();
assert_eq!(transaction.weight, 2);
assert!(transaction.on_stop);
assert!(transaction.on_start);
assert_eq!(transaction.name, "bar".to_string());
assert_eq!(transaction.sequence, 0);
transaction = transaction.set_weight(3).unwrap();
assert_eq!(transaction.weight, 3);
transaction = transaction.set_sequence(4);
assert_eq!(transaction.sequence, 4);
assert_eq!(transaction.weight, 3);
assert!(transaction.on_stop);
assert!(transaction.on_start);
assert_eq!(transaction.name, "bar".to_string());
transaction = transaction.set_sequence(8);
assert_eq!(transaction.sequence, 8);
}
#[tokio::test]
async fn goose_user() {
const HOST: &str = "http://example.com/";
let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
let base_url = get_base_url(Some(HOST.to_string()), None, None).unwrap();
let user = GooseUser::new(0, "".to_string(), base_url, &configuration, 0, None).unwrap();
assert_eq!(user.scenarios_index, 0);
assert_eq!(user.weighted_users_index, usize::MAX);
let url = user.build_url("/foo").unwrap();
assert_eq!(&url, &[HOST, "foo"].concat());
let url = user.build_url("bar/").unwrap();
assert_eq!(&url, &[HOST, "bar/"].concat());
let url = user.build_url("/foo/bar").unwrap();
assert_eq!(&url, &[HOST, "foo/bar"].concat());
let url = user.build_url("https://example.com/foo").unwrap();
assert_eq!(url, "https://example.com/foo");
let url = user
.build_url("https://www.example.com/path/to/resource")
.unwrap();
assert_eq!(url, "https://www.example.com/path/to/resource");
let base_url = get_base_url(
None,
Some("http://www2.example.com/".to_string()),
Some("http://www.example.com/".to_string()),
)
.unwrap();
let user2 = GooseUser::new(0, "".to_string(), base_url, &configuration, 0, None).unwrap();
let url = user2.build_url("/foo").unwrap();
assert_eq!(url, "http://www2.example.com/foo");
let url = user2.build_url("https://example.com/foo").unwrap();
assert_eq!(url, "https://example.com/foo");
const HOST_WITH_PATH: &str = "http://example.com/with/path/";
let base_url = get_base_url(Some(HOST_WITH_PATH.to_string()), None, None).unwrap();
let user = GooseUser::new(0, "".to_string(), base_url, &configuration, 0, None).unwrap();
let url = user.build_url("foo").unwrap();
assert_eq!(&url, &[HOST_WITH_PATH, "foo"].concat());
let url = user.build_url("bar/").unwrap();
assert_eq!(&url, &[HOST_WITH_PATH, "bar/"].concat());
let url = user.build_url("foo/bar").unwrap();
assert_eq!(&url, &[HOST_WITH_PATH, "foo/bar"].concat());
let url = user.build_url("/foo").unwrap();
assert_eq!(&url, &[HOST, "foo"].concat());
}
#[tokio::test]
async fn manual_requests() {
let server = MockServer::start();
let mut user = setup_user(&server).unwrap();
const INDEX_PATH: &str = "/";
let index = server.mock(|when, then| {
when.method(GET).path(INDEX_PATH);
then.status(200);
});
assert_eq!(index.hits(), 0);
let goose = user
.get(INDEX_PATH)
.await
.expect("get returned unexpected error");
let status = goose.response.unwrap().status();
assert_eq!(status, 200);
assert_eq!(goose.request.raw.method, GooseMethod::Get);
assert_eq!(goose.request.name, INDEX_PATH);
assert!(goose.request.success);
assert!(!goose.request.update);
assert_eq!(goose.request.status_code, 200);
assert_eq!(index.hits(), 1);
const NO_SUCH_PATH: &str = "/no/such/path";
let not_found = server.mock(|when, then| {
when.method(GET).path(NO_SUCH_PATH);
then.status(404);
});
assert_eq!(not_found.hits(), 0);
let goose = user
.get(NO_SUCH_PATH)
.await
.expect("get returned unexpected error");
let status = goose.response.unwrap().status();
assert_eq!(status, 404);
assert_eq!(goose.request.raw.method, GooseMethod::Get);
assert_eq!(goose.request.name, NO_SUCH_PATH);
assert!(!goose.request.success);
assert!(!goose.request.update);
assert_eq!(goose.request.status_code, 404,);
not_found.assert_hits(1);
const COMMENT_PATH: &str = "/comment";
let comment = server.mock(|when, then| {
when.method(POST).path(COMMENT_PATH).body("foo");
then.status(200).body("foo");
});
assert_eq!(comment.hits(), 0);
let goose = user
.post(COMMENT_PATH, "foo")
.await
.expect("post returned unexpected error");
let unwrapped_response = goose.response.unwrap();
let status = unwrapped_response.status();
assert_eq!(status, 200);
let body = unwrapped_response.text().await.unwrap();
assert_eq!(body, "foo");
assert_eq!(goose.request.raw.method, GooseMethod::Post);
assert!(goose.request.success);
assert!(!goose.request.update);
assert_eq!(goose.request.status_code, 200);
comment.assert_hits(1);
}
#[test]
fn test_set_session_data() {
#[derive(Debug, PartialEq, Eq, Clone)]
struct CustomSessionData {
data: String,
}
let session_data = CustomSessionData {
data: "foo".to_owned(),
};
let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
let mut user =
GooseUser::single("http://localhost:8080".parse().unwrap(), &configuration).unwrap();
user.set_session_data(session_data.clone());
let session = user.get_session_data::<CustomSessionData>();
assert!(session.is_some());
assert_eq!(session.unwrap(), &session_data);
let session = user.get_session_data_unchecked::<CustomSessionData>();
assert_eq!(session, &session_data);
}
#[test]
fn test_get_mut_session_data() {
#[derive(Debug, Clone)]
struct CustomSessionData {
data: String,
}
let session_data = CustomSessionData {
data: "foo".to_owned(),
};
let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
let mut user =
GooseUser::single("http://localhost:8080".parse().unwrap(), &configuration).unwrap();
user.set_session_data(session_data);
if let Some(session) = user.get_session_data_mut::<CustomSessionData>() {
"bar".clone_into(&mut session.data);
}
let session = user.get_session_data_unchecked::<CustomSessionData>();
assert_eq!(session.data, "bar".to_string());
let session = user.get_session_data_unchecked_mut::<CustomSessionData>();
"foo".clone_into(&mut session.data);
let session = user.get_session_data_unchecked::<CustomSessionData>();
assert_eq!(session.data, "foo".to_string());
}
#[test]
fn test_set_session_data_override() {
#[derive(Debug, Clone)]
struct CustomSessionData {
data: String,
}
let mut session_data = CustomSessionData {
data: "foo".to_owned(),
};
let configuration = GooseConfiguration::parse_args_default(&EMPTY_ARGS).unwrap();
let mut user =
GooseUser::single("http://localhost:8080".parse().unwrap(), &configuration).unwrap();
user.set_session_data(session_data.clone());
"bar".clone_into(&mut session_data.data);
user.set_session_data(session_data);
let session = user.get_session_data_unchecked::<CustomSessionData>();
assert_eq!(session.data, "bar".to_string());
}
}