Skip to content

Commit

Permalink
move test_plan logic into contained file
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyandrews committed Apr 19, 2022
1 parent 5817b06 commit ca46062
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 215 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
- [#379](https://github.com/tag1consulting/goose/pull/379) **API change**: default to `INFO` level verbosity, introduce `-q` to reduce Goose verbosity
o **note**: `-v` now sets Goose to `DEBUG` level verbosity which when enabled will negatively impact load test performance; set `-q` to restore previous level of verbosity
- [#379](https://github.com/tag1consulting/goose/pull/379) **API change**: remove `.print()` which is no longer required to display metrics after a load test, disable with `--no-print-metrics` or `GooseDefault::NoPrintMetrics`
- @TODO: introduce `--test-plan` and `GooseDefault::TestPlan`
- [#422](https://github.com/tag1consulting/goose/pull/422) **API change**: introduce `--test-plan` and `GooseDefault::TestPlan`
o internally represent all load tests as `Vec<(usize, usize)>`l test plan
o use [FromStr] to auto convert --test-plan "{users},{timespan};{users},{timespan}", where {users} must be an integer, ie "100", and {timespan} can be integer seconds or "30s", "20m", "3h", "1h30m", etc, to internal Vec<(usize, usize)> representation
o don't allow `--test-plan` together with `--users`, `--startup-time`, `--hatch-rate`, `--run-time`, `--no-reset-metrics`, `--manager` and `--worker`

## 0.15.2 December 13, 2021
- [#391](https://github.com/tag1consulting/goose/pull/391) properly sleep for configured `set_wait_time()` walking regularly to exit quickly if the load test ends
Expand Down
123 changes: 1 addition & 122 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
//! Goose can be configured programmatically with [`GooseDefaultType::set_default`].
use gumdrop::Options;
use regex::Regex;
use serde::{Deserialize, Serialize};
use simplelog::*;
use std::path::PathBuf;
use std::str::FromStr;

use crate::logger::GooseLogFormat;
use crate::metrics::GooseCoordinatedOmissionMitigation;
use crate::test_plan::TestPlan;
use crate::util;
use crate::{GooseAttack, GooseError};

Expand Down Expand Up @@ -375,126 +374,6 @@ pub(crate) struct GooseDefaults {
pub manager_port: Option<u16>,
}

/// Internal data structure representing a test plan.
//#[derive(Options, Debug, Clone, Serialize, Deserialize)]
#[derive(Options, Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TestPlan {
// A test plan is a vector of tuples each indicating a # of users and milliseconds.
pub(crate) steps: Vec<(usize, usize)>,
// Which step of the test_plan is currently running.
pub(crate) current: usize,
}

/// Automatically represent all load tests internally as a test plan.
///
/// Load tests launched using `--users`, `--startup-time`, `--hatch-rate`, and/or `--run-time` are
/// automatically converted to a `Vec<(usize, usize)>` test plan.
impl TestPlan {
pub(crate) fn new() -> TestPlan {
TestPlan {
steps: Vec::new(),
current: 0,
}
}

pub(crate) fn build(configuration: &GooseConfiguration) -> TestPlan {
if let Some(test_plan) = configuration.test_plan.as_ref() {
// Test plan was manually defined, clone and return as is.
test_plan.clone()
} else {
let mut steps: Vec<(usize, usize)> = Vec::new();

// Build a simple test plan from configured options if possible.
if let Some(users) = configuration.users {
if configuration.startup_time != "0" {
// Load test is configured with --startup-time.
steps.push((
users,
util::parse_timespan(&configuration.startup_time) * 1_000,
));
} else {
// Load test is configured with --hatch-rate.
let hatch_rate = if let Some(hatch_rate) = configuration.hatch_rate.as_ref() {
util::get_hatch_rate(Some(hatch_rate.to_string()))
} else {
util::get_hatch_rate(None)
};
// Convert hatch_rate to milliseconds.
let ms_hatch_rate = 1.0 / hatch_rate * 1_000.0;
// Finally, multiply the hatch rate by the number of users to hatch.
let total_time = ms_hatch_rate * users as f32;
steps.push((users, total_time as usize));
}

// A run-time is set, configure the load plan to run for the specified time then shut down.
if configuration.run_time != "0" {
// Maintain the configured number of users for the configured run-time.
steps.push((users, util::parse_timespan(&configuration.run_time) * 1_000));
// Then shut down the load test as quickly as possible.
steps.push((0, 0));
}
}

// Define test plan from options.
TestPlan { steps, current: 0 }
}
}

// Determine the maximum number of users configured during the test plan.
pub(crate) fn max_users(&self) -> usize {
let mut max_users = 0;
for step in &self.steps {
if step.0 > max_users {
max_users = step.0;
}
}
max_users
}
}

/// Implement [`FromStr`] to convert `"users,timespan"` string formatted test plans to Goose's
/// internal representation of Vec<(usize, usize)>.
///
/// Users are represented simply as an integer.
///
/// Time span can be specified as an integer, indicating seconds. Or can use integers together
/// with one or more of "h", "m", and "s", in that order, indicating "hours", "minutes", and
/// "seconds". Valid formats include: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.
impl FromStr for TestPlan {
type Err = GooseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// Convert string into a TestPlan.
let mut steps: Vec<(usize, usize)> = Vec::new();
// Each line of the test plan must be in the format "{users},{timespan}", white space is ignored
let re = Regex::new(r"^\s*(\d+)\s*,\s*(\d+|((\d+?)h)?((\d+?)m)?((\d+?)s)?)\s*$").unwrap();
// A test plan can have multiple lines split by the semicolon ";".
let lines = s.split(';');
for line in lines {
if let Some(cap) = re.captures(line) {
let left = cap[1]
.parse::<usize>()
.expect("failed to convert \\d to usize");
let right = util::parse_timespan(&cap[2]) * 1_000;
steps.push((left, right));
} else {
// Logger isn't initialized yet, provide helpful debug output.
eprintln!("ERROR: invalid `configuration.test_plan` value: '{}'", line);
eprintln!(" Expected format: --test-plan \"{{users}},{{timespan}};{{users}},{{timespan}}\"");
eprintln!(" {{users}} must be an integer, ie \"100\"");
eprintln!(" {{timespan}} can be integer seconds or \"30s\", \"20m\", \"3h\", \"1h30m\", etc");
return Err(GooseError::InvalidOption {
option: "`configuration.test_plan".to_string(),
value: line.to_string(),
detail: "invalid `configuration.test_plan` value.".to_string(),
});
}
}
// The steps are only valid if the logic gets this far.
Ok(TestPlan { steps, current: 0 })
}
}

/// Defines all [`GooseConfiguration`] options that can be programmatically configured with
/// a custom default.
///
Expand Down
5 changes: 3 additions & 2 deletions src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
//! By default, Goose launches both a telnet Controller and a WebSocket Controller, allowing
//! real-time control of the running load test.
use crate::config::{GooseConfiguration, TestPlan};
use crate::metrics::{GooseMetrics, TestPlanHistory, TestPlanStepAction};
use crate::config::GooseConfiguration;
use crate::metrics::GooseMetrics;
use crate::test_plan::{TestPlan, TestPlanHistory, TestPlanStepAction};
use crate::util;
use crate::{AttackPhase, GooseAttack, GooseAttackRunState, GooseError};

Expand Down
57 changes: 4 additions & 53 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mod manager;
pub mod metrics;
pub mod prelude;
//mod report;
mod test_plan;
mod throttle;
mod user;
pub mod util;
Expand All @@ -63,7 +64,6 @@ use lazy_static::lazy_static;
use nng::Socket;
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::cmp;
use std::collections::hash_map::DefaultHasher;
use std::collections::BTreeMap;
use std::hash::{Hash, Hasher};
Expand All @@ -75,12 +75,13 @@ use std::time::{self, Duration};
use std::{fmt, io};
use tokio::fs::File;

use crate::config::{GooseConfiguration, GooseDefaults, TestPlan};
use crate::config::{GooseConfiguration, GooseDefaults};
use crate::controller::{GooseControllerProtocol, GooseControllerRequest};
use crate::goose::{GaggleUser, GooseTask, GooseTaskSet, GooseUser, GooseUserCommand};
//use crate::graph::GraphData;
use crate::logger::{GooseLoggerJoinHandle, GooseLoggerTx};
use crate::metrics::{GooseMetric, GooseMetrics, TestPlanHistory, TestPlanStepAction};
use crate::metrics::{GooseMetric, GooseMetrics};
use crate::test_plan::{TestPlan, TestPlanHistory, TestPlanStepAction};
#[cfg(feature = "gaggle")]
use crate::worker::{register_shutdown_pipe_handler, GaggleMetrics};

Expand Down Expand Up @@ -1301,56 +1302,6 @@ impl GooseAttack {
Ok(goose_attack_run_state)
}

// Advance the active [`GooseAttack`](./struct.GooseAttack.html) to the next TestPlan step.
fn advance_test_plan(&mut self, goose_attack_run_state: &mut GooseAttackRunState) {
// Record the instant this new step starts, for use with timers.
self.step_started = Some(time::Instant::now());

let action = if self.test_plan.current == self.test_plan.steps.len() - 1 {
// If this is the last TestPlan step and there are 0 users, shut down.
if self.test_plan.steps[self.test_plan.current].0 == 0 {
// @TODO: don't shut down if stopped by a controller...
self.set_attack_phase(goose_attack_run_state, AttackPhase::Shutdown);
TestPlanStepAction::Finished
}
// Otherwise maintain the number of GooseUser threads until canceled.
else {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Maintain);
TestPlanStepAction::Maintaining
}
// If this is not the last TestPlan step, determine what happens next.
} else if self.test_plan.current < self.test_plan.steps.len() {
match self.test_plan.steps[self.test_plan.current]
.0
.cmp(&self.test_plan.steps[self.test_plan.current + 1].0)
{
cmp::Ordering::Less => {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Increase);
TestPlanStepAction::Increasing
}
cmp::Ordering::Greater => {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Decrease);
TestPlanStepAction::Decreasing
}
cmp::Ordering::Equal => {
self.set_attack_phase(goose_attack_run_state, AttackPhase::Maintain);
TestPlanStepAction::Maintaining
}
}
} else {
unreachable!("Advanced 2 steps beyond the end of the TestPlan.")
};

// Record details about new new TestPlan step that is starting.
self.metrics.history.push(TestPlanHistory::step(
action,
self.test_plan.steps[self.test_plan.current].0,
));

// Always advance the TestPlan step
self.test_plan.current += 1;
}

// Increase the number of active [`GooseUser`](./goose/struct.GooseUser.html) threads in the
// active [`GooseAttack`](./struct.GooseAttack.html).
async fn increase_attack(
Expand Down
37 changes: 1 addition & 36 deletions src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
//! contained [`GooseTaskMetrics`], [`GooseRequestMetrics`], and
//! [`GooseErrorMetrics`] are displayed in tables.
use chrono::prelude::*;
use http::StatusCode;
use itertools::Itertools;
use num_format::{Locale, ToFormattedString};
Expand All @@ -25,6 +24,7 @@ use crate::config::GooseDefaults;
use crate::goose::{get_base_url, GooseMethod, GooseTaskSet};
use crate::logger::GooseLog;
//use crate::report;
use crate::test_plan::{TestPlanHistory, TestPlanStepAction};
use crate::util;
#[cfg(feature = "gaggle")]
use crate::worker::{self, GaggleMetrics};
Expand Down Expand Up @@ -730,41 +730,6 @@ impl GooseTaskMetricAggregate {
debug!("incremented {} counter: {}", rounded_time, counter);
}
}

/// A test plan is a series of steps performing one of the following actions.
#[derive(Clone, Debug)]
pub enum TestPlanStepAction {
/// A test plan step that is increasing the number of GooseUser threads.
Increasing,
/// A test plan step that is maintaining the number of GooseUser threads.
Maintaining,
/// A test plan step that is increasing the number of GooseUser threads.
Decreasing,
/// The final step indicating that the load test is finished.
Finished,
}

/// A historical record of a single test plan step, used to generate reports from the metrics.
#[derive(Clone, Debug)]
pub struct TestPlanHistory {
/// What action happend in this step.
pub action: TestPlanStepAction,
/// A timestamp of when the step started.
pub timestamp: DateTime<Utc>,
/// The number of users when the step started.
pub users: usize,
}
impl TestPlanHistory {
/// A helper to record a new test plan step in the historical record.
pub(crate) fn step(action: TestPlanStepAction, users: usize) -> TestPlanHistory {
TestPlanHistory {
action,
timestamp: Utc::now(),
users,
}
}
}

/// All metrics optionally collected during a Goose load test.
///
/// By default, Goose collects metrics during a load test in a `GooseMetrics` object
Expand Down
Loading

0 comments on commit ca46062

Please sign in to comment.