Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add more detail to errors #113

Merged
merged 14 commits into from
Jul 22, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- change `GooseAttack` method signatures where an error is possible
- where possible, passs error up the stack instead of calling `exit(1)`
- introduce `GooseAttack.display()` which consumes the load test state and displays statistics
- `panic!()` on unexpected errors instead of `exit(1)`

## 0.8.2 July 2, 2020
- `client.log_debug()` will write debug logs to file when specified with `--debug-log-file=`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ $ cargo run
Compiling loadtest v0.1.0 (/home/jandrews/devel/rust/loadtest)
Finished dev [unoptimized + debuginfo] target(s) in 3.56s
Running `target/debug/loadtest`
12:09:56 [ERROR] Host must be defined globally or per-TaskSet. No host defined for LoadtestTasks.
Error: InvalidOption { option: "--host", value: "", detail: Some("host must be defined via --host, GooseAttack.set_host() or GooseTaskSet.set_host() (no host defined for WebsiteUser)") }
```

Goose is unable to run, as it doesn't know the domain you want to load test. So,
Expand Down
174 changes: 114 additions & 60 deletions src/goose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,6 @@ use reqwest::{header, Client, ClientBuilder, RequestBuilder, Response};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};
use std::error::Error;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::atomic::AtomicUsize;
Expand Down Expand Up @@ -317,49 +316,66 @@ macro_rules! taskset {
};
}

/// Goose tasks return a result, which is empty on success, or contains a GooseTaskError
/// on error.
pub type GooseTaskResult = Result<(), GooseTaskError>;

/// Definition of all errors Goose Tasks can return.
#[derive(Debug)]
pub enum GooseTaskError {
/// Contains a reqwest::Error.
Reqwest(reqwest::Error),
/// Contains a url::ParseError.
Url(url::ParseError),
/// The request failed.
RequestFailed,
/// The request failed. The `GooseRawRequest` that failed can be found in
/// `.raw_request`.
RequestFailed { raw_request: GooseRawRequest },
/// The request was canceled (this happens when the throttle is enabled and
/// the load test finished).
RequestCanceled,
/// The request succeeded, but there was an error recording the statistics.
StatsFailed,
/// the load test finished). The mpsc SendError can be found in `.source`.
/// A `GooseRawRequest` has not yet been constructed, so is not available in
/// this error.
RequestCanceled {
source: mpsc::error::SendError<bool>,
},
/// There was an error sending the statistics for a request to the parent thread.
/// The `GooseRawRequest` that was not recorded can be extracted from the error
/// chain, available inside `.source`.
StatsFailed {
source: mpsc::error::SendError<GooseRawRequest>,
},
/// Attempt to send debug detail to logger failed.
LoggerFailed,
/// Attempted an unrecognized HTTP request method.
InvalidMethod,
/// There was an error sending debug information to the logger thread. The
/// `GooseDebug` that was not logged can be extracted from the error chain,
/// available inside `.source`.
LoggerFailed {
source: mpsc::error::SendError<Option<GooseDebug>>,
},
/// Attempted an unrecognized HTTP request method. The unrecognized method
/// is available in `.method`.
InvalidMethod { method: Method },
}

/// Goose tasks return a result, which is empty on success, or contains a GooseTaskError
/// on error.
pub type GooseTaskResult = Result<(), GooseTaskError>;

// Define how to display errors.
impl fmt::Display for GooseTaskError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self, f)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeremyandrews This will create an infinite loop as per rust-lang/rust#74892.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only works right now as we use the Debug and not the Display output for errors when returning from main.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #128

}
}

// Define the lower level source of this error, if any.
impl std::error::Error for GooseTaskError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
GooseTaskError::Reqwest(ref err) => err.fmt(f),
GooseTaskError::Url(ref err) => err.fmt(f),
GooseTaskError::RequestFailed => write!(
f,
"Request failed, usually because server returned a non-200 response code."
),
GooseTaskError::RequestCanceled => {
write!(f, "Request canceled because the load test ended.")
}
GooseTaskError::StatsFailed => write!(f, "Failed to send statistics to parent thread."),
GooseTaskError::LoggerFailed => write!(f, "Failed to send debug to logger thread."),
GooseTaskError::InvalidMethod => write!(f, "Unrecognized HTTP method."),
GooseTaskError::Reqwest(ref source) => Some(source),
GooseTaskError::Url(ref source) => Some(source),
GooseTaskError::RequestCanceled { ref source } => Some(source),
GooseTaskError::StatsFailed { ref source } => Some(source),
GooseTaskError::LoggerFailed { ref source } => Some(source),
_ => None,
}
}
}

impl Error for GooseTaskError {}

/// Auto-convert Reqwest errors.
impl From<reqwest::Error> for GooseTaskError {
fn from(err: reqwest::Error) -> GooseTaskError {
Expand All @@ -378,22 +394,22 @@ impl From<url::ParseError> for GooseTaskError {
/// shut down. This causes mpsc SendError, which gets automatically converted to
/// `RequestCanceled`.
impl From<mpsc::error::SendError<bool>> for GooseTaskError {
fn from(_err: mpsc::error::SendError<bool>) -> GooseTaskError {
GooseTaskError::RequestCanceled
fn from(source: mpsc::error::SendError<bool>) -> GooseTaskError {
GooseTaskError::RequestCanceled { source }
}
}

/// Attempt to send statistics to the parent thread failed.
impl From<mpsc::error::SendError<GooseRawRequest>> for GooseTaskError {
fn from(_err: mpsc::error::SendError<GooseRawRequest>) -> GooseTaskError {
GooseTaskError::StatsFailed
fn from(source: mpsc::error::SendError<GooseRawRequest>) -> GooseTaskError {
GooseTaskError::StatsFailed { source }
}
}

/// Attempt to send logs to the logger thread failed.
impl From<mpsc::error::SendError<Option<GooseDebug>>> for GooseTaskError {
fn from(_err: mpsc::error::SendError<Option<GooseDebug>>) -> GooseTaskError {
GooseTaskError::LoggerFailed
fn from(source: mpsc::error::SendError<Option<GooseDebug>>) -> GooseTaskError {
GooseTaskError::LoggerFailed { source }
}
}

Expand Down Expand Up @@ -488,12 +504,14 @@ impl GooseTaskSet {
/// ```
pub fn set_weight(mut self, weight: usize) -> Result<Self, GooseError> {
trace!("{} set_weight: {}", self.name, weight);
if weight < 1 {
error!("{} weight of {} not allowed", self.name, weight);
return Err(GooseError::InvalidWeight);
} else {
self.weight = weight;
if weight == 0 {
jeremyandrews marked this conversation as resolved.
Show resolved Hide resolved
return Err(GooseError::InvalidWeight {
weight,
detail: Some("weight of 0 not allowed".to_string()),
});
}
self.weight = weight;

Ok(self)
}

Expand Down Expand Up @@ -537,11 +555,11 @@ impl GooseTaskSet {
max_wait
);
if min_wait > max_wait {
error!(
"min_wait({}) can't be larger than max_wait({})",
min_wait, max_wait
);
return Err(GooseError::InvalidWaitTime);
return Err(GooseError::InvalidWaitTime {
min_wait,
max_wait,
detail: Some("min_wait can not be larger than max_wait".to_string()),
});
}
self.min_wait = min_wait;
self.max_wait = max_wait;
Expand Down Expand Up @@ -582,8 +600,7 @@ fn goose_method_from_method(method: Method) -> Result<GooseMethod, GooseTaskErro
Method::POST => GooseMethod::POST,
Method::PUT => GooseMethod::PUT,
_ => {
error!("unsupported method: {}", method);
return Err(GooseTaskError::InvalidMethod);
return Err(GooseTaskError::InvalidMethod { method });
}
})
}
Expand Down Expand Up @@ -1530,7 +1547,9 @@ impl GooseUser {
/// }
/// }
///
/// Err(GooseTaskError::RequestFailed)
/// Err(GooseTaskError::RequestFailed {
/// raw_request: goose.request.clone(),
/// })
/// }
/// ````
pub fn set_success(&self, request: &mut GooseRawRequest) -> GooseTaskResult {
Expand Down Expand Up @@ -1613,7 +1632,9 @@ impl GooseUser {
// Print log to stdout if `-v` is enabled.
info!("set_failure: {}", tag);

Err(GooseTaskError::RequestFailed)
Err(GooseTaskError::RequestFailed {
raw_request: request.clone(),
})
}

/// Write to debug_log_file if enabled.
Expand Down Expand Up @@ -1850,16 +1871,46 @@ pub fn get_base_url(
config_host: Option<String>,
task_set_host: Option<String>,
default_host: Option<String>,
) -> Url {
) -> Result<Url, GooseError> {
// If the `--host` CLI option is set, build the URL with it.
match config_host {
Some(host) => Url::parse(&host).unwrap(),
Some(host) => Ok(
Url::parse(&host).map_err(|parse_error| GooseError::InvalidHost {
host,
detail: Some("failure parsing host specified with --host".to_string()),
parse_error,
})?,
),
None => {
match task_set_host {
// Otherwise, if `GooseTaskSet.host` is defined, usee this
Some(host) => Url::parse(&host).unwrap(),
Some(host) => {
Ok(
Url::parse(&host).map_err(|parse_error| GooseError::InvalidHost {
host,
detail: Some(
"failure parsing host specified with GooseTaskSet.set_host()"
.to_string(),
),
parse_error,
jeremyandrews marked this conversation as resolved.
Show resolved Hide resolved
})?,
)
}
// Otherwise, use global `GooseAttack.host`. `unwrap` okay as host validation was done at startup.
None => Url::parse(&default_host.unwrap()).unwrap(),
None => {
// Host is required, if we get here it's safe to unwrap this variable.
let default_host = default_host.unwrap();
Ok(
Url::parse(&default_host).map_err(|parse_error| GooseError::InvalidHost {
host: default_host.to_string(),
detail: Some(
"failure parsing host specified globally with GooseAttack.set_host()"
.to_string(),
),
parse_error,
})?,
)
}
}
}
}
Expand Down Expand Up @@ -2010,12 +2061,14 @@ impl GooseTask {
self.tasks_index,
weight
);
if weight < 1 {
error!("{} weight of {} not allowed", self.name, weight);
return Err(GooseError::InvalidWeight);
} else {
self.weight = weight;
if weight == 0 {
return Err(GooseError::InvalidWeight {
weight,
detail: Some("weight of 0 not allowed".to_string()),
});
}
self.weight = weight;

Ok(self)
}

Expand Down Expand Up @@ -2125,7 +2178,7 @@ mod tests {

async fn setup_user(server: &MockServer) -> Result<GooseUser, GooseError> {
let configuration = GooseConfiguration::default();
let base_url = get_base_url(Some(server.url("/")), None, None);
let base_url = get_base_url(Some(server.url("/")), None, None).unwrap();
GooseUser::single(base_url, &configuration)
}

Expand Down Expand Up @@ -2553,7 +2606,7 @@ mod tests {
async fn goose_user() {
const HOST: &str = "http://example.com/";
let configuration = GooseConfiguration::default();
let base_url = get_base_url(Some(HOST.to_string()), None, None);
let base_url = get_base_url(Some(HOST.to_string()), None, None).unwrap();
let user = GooseUser::new(0, base_url, 0, 0, &configuration, 0).unwrap();
assert_eq!(user.task_sets_index, 0);
assert_eq!(user.min_wait, 0);
Expand Down Expand Up @@ -2588,7 +2641,8 @@ mod tests {
None,
Some("http://www2.example.com/".to_string()),
Some("http://www.example.com/".to_string()),
);
)
.unwrap();
let user2 = GooseUser::new(0, base_url, 1, 3, &configuration, 0).unwrap();
assert_eq!(user2.min_wait, 1);
assert_eq!(user2.max_wait, 3);
Expand Down
Loading