From 63e1f6bc0d36e95f8c8761ee85fe3897a0a28399 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Thu, 4 May 2023 17:59:59 +0200 Subject: [PATCH 1/7] restore gaggle configuration --- src/config.rs | 1270 +++++++++++++++++++++++++++++++++------------- src/lib.rs | 2 +- src/test_plan.rs | 2 +- 3 files changed, 917 insertions(+), 357 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1fe194a1..82b5f1fc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,6 +17,8 @@ use crate::test_plan::TestPlan; use crate::util; use crate::{GooseAttack, GooseError}; +use crate::DEFAULT_TELNET_PORT; + /// Runtime options available when launching a Goose load test. /// /// Custom defaults can be programmatically set for most of these options using the @@ -165,7 +167,7 @@ pub struct GooseConfiguration { /// Sets telnet Controller host (default: 0.0.0.0) #[options(no_short, meta = "HOST")] pub telnet_host: String, - /// Sets telnet Controller TCP port (default: 5116) + /// Sets telnet Controller TCP port (default: 5115) #[options(no_short, meta = "PORT")] pub telnet_port: u16, /// Doesn't enable WebSocket Controller @@ -193,8 +195,31 @@ pub struct GooseConfiguration { #[options(no_short, meta = "VALUE")] pub throttle_requests: usize, /// Follows base_url redirect with subsequent requests - #[options(no_short)] + // Add a blank line and then an Gaggle: header after this option + #[options( + no_short, + help = "Follows base_url redirect with subsequent requests\n\nGaggle:" + )] pub sticky_follow: bool, + + /// Enables distributed load test Manager mode + #[options(no_short)] + pub manager: bool, + /// Sets number of Workers to expect + #[options(no_short, meta = "VALUE")] + pub expect_workers: Option, + /// Tells Manager to ignore load test checksum + #[options(no_short)] + pub no_hash_check: bool, + /// Enables distributed load test Worker mode + #[options(no_short)] + pub worker: bool, + /// Sets host Worker connects to (default: 127.0.0.1) + #[options(no_short, meta = "HOST")] + pub manager_host: String, + /// Sets port Worker connects to (default: 5115) + #[options(no_short, meta = "PORT")] + pub manager_port: u16, } /// Optionally defines a subset of active Scenarios to run during a load test. @@ -240,98 +265,110 @@ impl FromStr for Scenarios { /// These custom defaults can be configured using [`GooseDefaultType::set_default()`]. #[derive(Clone, Debug, Default)] pub(crate) struct GooseDefaults { - /// An optional default host to run this load test against. - pub host: Option, - /// An optional default number of users to simulate. - pub users: Option, + /// An optional default for coordinated omission mitigation. + pub co_mitigation: Option, + /// An optional default for the debug log format. + pub debug_format: Option, + /// An optional default for the debug log file name. + pub debug_log: Option, + /// An optional default for the error log format. + pub error_format: Option, + /// An optional default for the error log file name. + pub error_log: Option, + /// An optional default for number of Workers to expect. + pub expect_workers: Option, + /// An optional default for the goose log file name. + pub goose_log: Option, /// An optional default number of clients to start per second. pub hatch_rate: Option, - /// An optional default number of seconds for the test to start. - pub startup_time: Option, - /// An optional default number of seconds for the test to run. - pub run_time: Option, - /// An optional default test plan. - pub test_plan: Option, + /// An optional default host to run this load test against. + pub host: Option, /// An optional default test plan. pub iterations: Option, - /// Optional default scenarios. - pub scenarios: Option, /// An optional default log level. pub log_level: Option, - /// An optional default for the goose log file name. - pub goose_log: Option, - /// An optional default value for quiet level. - pub quiet: Option, - /// An optional default value for verbosity level. - pub verbose: Option, - /// An optional default for printing running metrics. - pub running_metrics: Option, - /// An optional default for not resetting metrics after all users started. - pub no_reset_metrics: Option, + /// An optional default to enable Manager mode. + pub manager: Option, + /// An optional default for host Worker connects to. + pub manager_host: Option, + /// An optional default for port Worker connects to. + pub manager_port: Option, + /// An optional default for not auto-starting the load test. + pub no_autostart: Option, + /// An optional default for not logging response body in debug log. + pub no_debug_body: Option, + /// An optional default for not displaying an error summary. + pub no_error_summary: Option, + /// An optional default for the flag that disables granular data in HTML report graphs. + pub no_granular_report: Option, + /// An optional default for not setting the gzip Accept-Encoding header. + pub no_gzip: Option, + /// An optional default for Manager to ignore load test checksum. + pub no_hash_check: Option, /// An optional default for not tracking metrics. pub no_metrics: Option, - /// An optional default for not tracking transaction metrics. - pub no_transaction_metrics: Option, - /// An optional default for not tracking scenario metrics. - pub no_scenario_metrics: Option, /// An optional default for not displaying metrics at the end of the load test. pub no_print_metrics: Option, - /// An optional default for not displaying an error summary. - pub no_error_summary: Option, + /// An optional default for not resetting metrics after all users started. + pub no_reset_metrics: Option, + /// An optional default for not tracking scenario metrics. + pub no_scenario_metrics: Option, + /// An optional default to not track status code metrics. + pub no_status_codes: Option, + /// An optional default for not enabling telnet Controller thread. + pub no_telnet: Option, + /// An optional default for not tracking transaction metrics. + pub no_transaction_metrics: Option, + /// An optional default for not enabling WebSocket Controller thread. + pub no_websocket: Option, /// An optional default for the html-formatted report file name. pub report_file: Option, - /// An optional default for the flag that disables granular data in HTML report graphs. - pub no_granular_report: Option, - /// An optional default for the requests log file name. - pub request_log: Option, - /// An optional default for the requests log file format. - pub request_format: Option, /// An optional default for logging the request body. pub request_body: Option, - /// An optional default for the transaction log file name. - pub transaction_log: Option, - /// An optional default for the transaction log file format. - pub transaction_format: Option, - /// An optional default for the scenario log file name. - pub scenario_log: Option, + /// An optional default for the requests log file format. + pub request_format: Option, + /// An optional default for the requests log file name. + pub request_log: Option, + /// An optional default number of seconds for the test to run. + pub run_time: Option, + /// An optional default for printing running metrics. + pub running_metrics: Option, /// An optional default for the scenario log file format. pub scenario_format: Option, - /// An optional default for the error log file name. - pub error_log: Option, - /// An optional default for the error log format. - pub error_format: Option, - /// An optional default for the debug log file name. - pub debug_log: Option, - /// An optional default for the debug log format. - pub debug_format: Option, - /// An optional default for not logging response body in debug log. - pub no_debug_body: Option, - /// An optional default for not enabling telnet Controller thread. - pub no_telnet: Option, - /// An optional default for not enabling WebSocket Controller thread. - pub no_websocket: Option, - /// An optional default for not auto-starting the load test. - pub no_autostart: Option, - /// An optional default for not setting the gzip Accept-Encoding header. - pub no_gzip: Option, - /// An optional default number of seconds to timeout requests. - pub timeout: Option, - /// An optional default for coordinated omission mitigation. - pub co_mitigation: Option, - /// An optional default to not track status code metrics. - pub no_status_codes: Option, - /// An optional default maximum requests per second. - pub throttle_requests: Option, + /// An optional default for the scenario log file name. + pub scenario_log: Option, + /// Optional default scenarios. + pub scenarios: Option, + /// An optional default number of seconds for the test to start. + pub startup_time: Option, /// An optional default to follows base_url redirect with subsequent request. pub sticky_follow: Option, /// An optional default for host telnet Controller listens on. pub telnet_host: Option, /// An optional default for port telnet Controller listens on. pub telnet_port: Option, + /// An optional default test plan. + pub test_plan: Option, + /// An optional default maximum requests per second. + pub throttle_requests: Option, + /// An optional default number of seconds to timeout requests. + pub timeout: Option, + /// An optional default for the transaction log file format. + pub transaction_format: Option, + /// An optional default for the transaction log file name. + pub transaction_log: Option, + /// An optional default value for quiet level. + pub quiet: Option, + /// An optional default number of users to simulate. + pub users: Option, + /// An optional default value for verbosity level. + pub verbose: Option, /// An optional default for host WebSocket Controller listens on. pub websocket_host: Option, /// An optional default for port WebSocket Controller listens on. pub websocket_port: Option, + /// An optional default to enable Worker mode. + pub worker: Option, } /// Defines all [`GooseConfiguration`] options that can be programmatically configured with @@ -340,98 +377,110 @@ pub(crate) struct GooseDefaults { /// These custom defaults can be configured using [`GooseDefaultType::set_default()`]. #[derive(Debug)] pub enum GooseDefault { - /// An optional default host to run this load test against. - Host, - /// An optional default number of users to simulate. - Users, + /// An optional default for coordinated omission mitigation. + CoordinatedOmissionMitigation, + /// An optional default for the debug log format. + DebugFormat, + /// An optional default for the debug log file name. + DebugLog, + /// An optional default for the error log format. + ErrorFormat, + /// An optional default for the error log file name. + ErrorLog, + /// An optional default for numb er of Workers to expect. + ExpectWorkers, + /// An optional default for the log file name. + GooseLog, /// An optional default number of clients to start per second. HatchRate, - /// An optional default number of seconds for the test to start up. - StartupTime, - /// An optional default number of seconds for the test to run. - RunTime, - /// An optional default test plan. - TestPlan, + /// An optional default host to run this load test against. + Host, /// An optional default number of iterations to run scenarios then exit. Iterations, - /// Optional default list of scenarios to run. - Scenarios, /// An optional default log level. LogLevel, - /// An optional default for the log file name. - GooseLog, - /// An optional default value for quiet level. - Quiet, - /// An optional default value for verbosity level. - Verbose, - /// An optional default for printing running metrics. - RunningMetrics, - /// An optional default for not resetting metrics after all users started. - NoResetMetrics, + /// An optional default to enable Manager mode. + Manager, + /// An optional default for host Worker connects to. + ManagerHost, + /// An optional default for port Worker connects to. + ManagerPort, + /// An optional default for not automatically starting load test. + NoAutoStart, + /// An optional default for not logging the response body in the debug log. + NoDebugBody, + /// An optional default for not displaying an error summary. + NoErrorSummary, + /// An optional default for the flag that disables granular data in HTML report graphs. + NoGranularData, + /// An optional default for not setting the gzip Accept-Encoding header. + NoGzip, + /// An optional default for Manager to ignore load test checksum. + NoHashCheck, /// An optional default for not tracking metrics. NoMetrics, - /// An optional default for not tracking transaction metrics. - NoTransactionMetrics, - /// An optional default for not tracking scneario metrics. - NoScenarioMetrics, /// An optional default for not displaying metrics at end of load test. NoPrintMetrics, - /// An optional default for not displaying an error summary. - NoErrorSummary, + /// An optional default for not resetting metrics after all users started. + NoResetMetrics, + /// An optional default for not tracking scneario metrics. + NoScenarioMetrics, + /// An optional default to not track status code metrics. + NoStatusCodes, + /// An optional default for not tracking transaction metrics. + NoTransactionMetrics, + /// An optional default for not enabling telnet Controller thread. + NoTelnet, + /// An optional default for not enabling WebSocket Controller thread. + NoWebSocket, /// An optional default for the report file name. ReportFile, - /// An optional default for the flag that disables granular data in HTML report graphs. - NoGranularData, - /// An optional default for the request log file name. - RequestLog, - /// An optional default for the request log file format. - RequestFormat, /// An optional default for logging the request body. RequestBody, + /// An optional default for the request log file format. + RequestFormat, + /// An optional default for the request log file name. + RequestLog, + /// An optional default for printing running metrics. + RunningMetrics, + /// An optional default number of seconds for the test to run. + RunTime, + /// An optional default for the scenario log file format. + ScenarioFormat, + /// An optional default for the scenario log file name. + ScenarioLog, + /// Optional default list of scenarios to run. + Scenarios, + /// An optional default number of seconds for the test to start up. + StartupTime, + /// An optional default to follows base_url redirect with subsequent request. + StickyFollow, + /// An optional default for host telnet Controller listens on. + TelnetHost, + /// An optional default for port telnet Controller listens on. + TelnetPort, + /// An optional default test plan. + TestPlan, + /// An optional default maximum requests per second. + ThrottleRequests, + /// An optional default timeout for all requests, in seconds. + Timeout, /// An optional default for the transaction log file name. TransactionLog, /// An optional default for the transaction log file format. TransactionFormat, - /// An optional default for the scenario log file name. - ScenarioLog, - /// An optional default for the scenario log file format. - ScenarioFormat, - /// An optional default for the error log file name. - ErrorLog, - /// An optional default for the error log format. - ErrorFormat, - /// An optional default for the debug log file name. - DebugLog, - /// An optional default for the debug log format. - DebugFormat, - /// An optional default for not logging the response body in the debug log. - NoDebugBody, - /// An optional default for not enabling telnet Controller thread. - NoTelnet, - /// An optional default for not enabling WebSocket Controller thread. - NoWebSocket, - /// An optional default for coordinated omission mitigation. - CoordinatedOmissionMitigation, - /// An optional default for not automatically starting load test. - NoAutoStart, - /// An optional default timeout for all requests, in seconds. - Timeout, - /// An optional default for not setting the gzip Accept-Encoding header. - NoGzip, - /// An optional default to not track status code metrics. - NoStatusCodes, - /// An optional default maximum requests per second. - ThrottleRequests, - /// An optional default to follows base_url redirect with subsequent request. - StickyFollow, - /// An optional default for host telnet Controller listens on. - TelnetHost, - /// An optional default for port telnet Controller listens on. - TelnetPort, + /// An optional default value for quiet level. + Quiet, + /// An optional default number of users to simulate. + Users, + /// An optional default value for verbosity level. + Verbose, /// An optional default for host Websocket Controller listens on. WebSocketHost, /// An optional default for port WebSocket Controller listens on. WebSocketPort, + /// An optional default to enable Worker mode. + Worker, } /// Most run-time options can be programmatically configured with custom defaults. @@ -470,6 +519,7 @@ pub enum GooseDefault { /// - [`GooseDefault::GooseLog`] /// - [`GooseDefault::HatchRate`] /// - [`GooseDefault::Host`] +/// - [`GooseDefault::ManagerHost`] /// - [`GooseDefault::ReportFile`] /// - [`GooseDefault::RequestLog`] /// - [`GooseDefault::ScenarioLog`] @@ -482,43 +532,48 @@ pub enum GooseDefault { /// /// The following run-time options can be configured with a custom default using a /// [`usize`] integer: -/// - [`GooseDefault::Users`] -/// - [`GooseDefault::StartupTime`] -/// - [`GooseDefault::RunTime`] +/// - [`GooseDefault::ExpectWorkers`] /// - [`GooseDefault::Iterations`] -/// - [`GooseDefault::RunningMetrics`] /// - [`GooseDefault::LogLevel`] -/// - [`GooseDefault::Quiet`] -/// - [`GooseDefault::Verbose`] +/// - [`GooseDefault::ManagerPort`] +/// - [`GooseDefault::RunningMetrics`] +/// - [`GooseDefault::RunTime`] +/// - [`GooseDefault::StartupTime`] /// - [`GooseDefault::ThrottleRequests`] /// - [`GooseDefault::TelnetPort`] +/// - [`GooseDefault::Quiet`] +/// - [`GooseDefault::Users`] +/// - [`GooseDefault::Verbose`] /// - [`GooseDefault::WebSocketPort`] /// /// The following run-time flags can be configured with a custom default using a /// [`bool`] (and otherwise default to [`false`]). -/// - [`GooseDefault::NoResetMetrics`] -/// - [`GooseDefault::NoPrintMetrics`] -/// - [`GooseDefault::NoMetrics`] -/// - [`GooseDefault::NoTransactionMetrics`] -/// - [`GooseDefault::NoScenarioMetrics`] -/// - [`GooseDefault::RequestBody`] +/// - [`GooseDefault::Manager`] +/// - [`GooseDefault::NoAutoStart`] /// - [`GooseDefault::NoErrorSummary`] /// - [`GooseDefault::NoDebugBody`] -/// - [`GooseDefault::NoTelnet`] -/// - [`GooseDefault::NoWebSocket`] -/// - [`GooseDefault::NoAutoStart`] +/// - [`GooseDefault::NoGranularData`] /// - [`GooseDefault::NoGzip`] +/// - [`GooseDefault::NoHashCheck`] +/// - [`GooseDefault::NoMetrics`] +/// - [`GooseDefault::NoPrintMetrics`] +/// - [`GooseDefault::NoResetMetrics`] +/// - [`GooseDefault::NoScenarioMetrics`] /// - [`GooseDefault::NoStatusCodes`] +/// - [`GooseDefault::NoTelnet`] +/// - [`GooseDefault::NoTransactionMetrics`] +/// - [`GooseDefault::NoWebSocket`] +/// - [`GooseDefault::RequestBody`] /// - [`GooseDefault::StickyFollow`] -/// - [`GooseDefault::NoGranularData`] +/// - [`GooseDefault::Worker`] /// /// The following run-time flags can be configured with a custom default using a /// [`GooseLogFormat`]. +/// - [`GooseDefault::DebugFormat`] +/// - [`GooseDefault::ErrorFormat`] /// - [`GooseDefault::RequestFormat`] -/// - [`GooseDefault::TransactionFormat`] /// - [`GooseDefault::ScenarioFormat`] -/// - [`GooseDefault::ErrorFormat`] -/// - [`GooseDefault::DebugFormat`] +/// - [`GooseDefault::TransactionFormat`] /// /// The following run-time flags can be configured with a custom default using a /// [`GooseCoordinatedOmissionMitigation`]. @@ -562,6 +617,7 @@ impl GooseDefaultType<&str> for GooseAttack { Some(value.to_string()) } } + GooseDefault::ManagerHost => self.defaults.manager_host = Some(value.to_string()), GooseDefault::ReportFile => self.defaults.report_file = Some(value.to_string()), GooseDefault::RequestLog => self.defaults.request_log = Some(value.to_string()), GooseDefault::ScenarioLog => self.defaults.scenario_log = Some(value.to_string()), @@ -576,15 +632,17 @@ impl GooseDefaultType<&str> for GooseAttack { GooseDefault::TransactionLog => self.defaults.transaction_log = Some(value.to_string()), GooseDefault::WebSocketHost => self.defaults.websocket_host = Some(value.to_string()), // Otherwise display a helpful and explicit error. - GooseDefault::Users - | GooseDefault::StartupTime - | GooseDefault::RunTime + GooseDefault::ExpectWorkers | GooseDefault::Iterations | GooseDefault::LogLevel - | GooseDefault::Quiet - | GooseDefault::Verbose + | GooseDefault::ManagerPort + | GooseDefault::RunTime + | GooseDefault::StartupTime | GooseDefault::ThrottleRequests | GooseDefault::TelnetPort + | GooseDefault::Quiet + | GooseDefault::Users + | GooseDefault::Verbose | GooseDefault::WebSocketPort => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), @@ -595,22 +653,25 @@ impl GooseDefaultType<&str> for GooseAttack { ), }); } - GooseDefault::RunningMetrics - | GooseDefault::NoResetMetrics + GooseDefault::Manager + | GooseDefault::NoAutoStart + | GooseDefault::NoDebugBody + | GooseDefault::NoErrorSummary + | GooseDefault::NoGranularData + | GooseDefault::NoGzip + | GooseDefault::NoHashCheck | GooseDefault::NoMetrics - | GooseDefault::NoTransactionMetrics - | GooseDefault::NoScenarioMetrics - | GooseDefault::RequestBody | GooseDefault::NoPrintMetrics - | GooseDefault::NoErrorSummary - | GooseDefault::NoDebugBody + | GooseDefault::NoResetMetrics + | GooseDefault::NoScenarioMetrics + | GooseDefault::NoStatusCodes | GooseDefault::NoTelnet + | GooseDefault::NoTransactionMetrics | GooseDefault::NoWebSocket - | GooseDefault::NoAutoStart - | GooseDefault::NoGzip - | GooseDefault::NoStatusCodes + | GooseDefault::RequestBody + | GooseDefault::RunningMetrics | GooseDefault::StickyFollow - | GooseDefault::NoGranularData => { + | GooseDefault::Worker => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), value: value.to_string(), @@ -622,9 +683,9 @@ impl GooseDefaultType<&str> for GooseAttack { } GooseDefault::DebugFormat | GooseDefault::ErrorFormat - | GooseDefault::TransactionFormat | GooseDefault::ScenarioFormat - | GooseDefault::RequestFormat => { + | GooseDefault::RequestFormat + | GooseDefault::TransactionFormat => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), value: value.to_string(), @@ -652,16 +713,18 @@ impl GooseDefaultType for GooseAttack { /// Sets [`GooseDefault`] to a [`usize`] value. fn set_default(mut self, key: GooseDefault, value: usize) -> Result, GooseError> { match key { - GooseDefault::Users => self.defaults.users = Some(value), - GooseDefault::StartupTime => self.defaults.startup_time = Some(value), - GooseDefault::RunTime => self.defaults.run_time = Some(value), + GooseDefault::ExpectWorkers => self.defaults.expect_workers = Some(value), GooseDefault::Iterations => self.defaults.iterations = Some(value), - GooseDefault::RunningMetrics => self.defaults.running_metrics = Some(value), GooseDefault::LogLevel => self.defaults.log_level = Some(value as u8), - GooseDefault::Quiet => self.defaults.quiet = Some(value as u8), - GooseDefault::Verbose => self.defaults.verbose = Some(value as u8), + GooseDefault::ManagerPort => self.defaults.manager_port = Some(value as u16), + GooseDefault::RunningMetrics => self.defaults.running_metrics = Some(value), + GooseDefault::RunTime => self.defaults.run_time = Some(value), + GooseDefault::StartupTime => self.defaults.startup_time = Some(value), GooseDefault::ThrottleRequests => self.defaults.throttle_requests = Some(value), GooseDefault::TelnetPort => self.defaults.telnet_port = Some(value as u16), + GooseDefault::Quiet => self.defaults.quiet = Some(value as u8), + GooseDefault::Users => self.defaults.users = Some(value), + GooseDefault::Verbose => self.defaults.verbose = Some(value as u8), GooseDefault::WebSocketPort => self.defaults.websocket_port = Some(value as u16), // Otherwise display a helpful and explicit error. GooseDefault::DebugLog @@ -669,6 +732,7 @@ impl GooseDefaultType for GooseAttack { | GooseDefault::GooseLog | GooseDefault::HatchRate | GooseDefault::Host + | GooseDefault::ManagerHost | GooseDefault::ReportFile | GooseDefault::RequestLog | GooseDefault::ScenarioLog @@ -687,21 +751,24 @@ impl GooseDefaultType for GooseAttack { ), }) } - GooseDefault::NoResetMetrics + GooseDefault::Manager + | GooseDefault::NoAutoStart + | GooseDefault::NoDebugBody + | GooseDefault::NoErrorSummary + | GooseDefault::NoGranularData + | GooseDefault::NoGzip + | GooseDefault::NoHashCheck | GooseDefault::NoMetrics - | GooseDefault::NoTransactionMetrics - | GooseDefault::NoScenarioMetrics - | GooseDefault::RequestBody | GooseDefault::NoPrintMetrics - | GooseDefault::NoErrorSummary - | GooseDefault::NoDebugBody + | GooseDefault::NoResetMetrics + | GooseDefault::NoScenarioMetrics + | GooseDefault::NoStatusCodes + | GooseDefault::NoTransactionMetrics | GooseDefault::NoTelnet | GooseDefault::NoWebSocket - | GooseDefault::NoAutoStart - | GooseDefault::NoGzip - | GooseDefault::NoStatusCodes + | GooseDefault::RequestBody | GooseDefault::StickyFollow - | GooseDefault::NoGranularData => { + | GooseDefault::Worker => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), value: format!("{}", value), @@ -711,9 +778,9 @@ impl GooseDefaultType for GooseAttack { ), }) } - GooseDefault::RequestFormat - | GooseDefault::DebugFormat + GooseDefault::DebugFormat | GooseDefault::ErrorFormat + | GooseDefault::RequestFormat | GooseDefault::ScenarioFormat | GooseDefault::TransactionFormat => { return Err(GooseError::InvalidOption { @@ -743,29 +810,33 @@ impl GooseDefaultType for GooseAttack { /// Sets [`GooseDefault`] to a [`bool`] value. fn set_default(mut self, key: GooseDefault, value: bool) -> Result, GooseError> { match key { - GooseDefault::NoResetMetrics => self.defaults.no_reset_metrics = Some(value), + GooseDefault::Manager => self.defaults.manager = Some(value), + GooseDefault::NoAutoStart => self.defaults.no_autostart = Some(value), + GooseDefault::NoDebugBody => self.defaults.no_debug_body = Some(value), + GooseDefault::NoErrorSummary => self.defaults.no_error_summary = Some(value), + GooseDefault::NoGranularData => self.defaults.no_granular_report = Some(value), + GooseDefault::NoGzip => self.defaults.no_gzip = Some(value), + GooseDefault::NoHashCheck => self.defaults.no_hash_check = Some(value), GooseDefault::NoMetrics => self.defaults.no_metrics = Some(value), + GooseDefault::NoPrintMetrics => self.defaults.no_print_metrics = Some(value), + GooseDefault::NoResetMetrics => self.defaults.no_reset_metrics = Some(value), + GooseDefault::NoScenarioMetrics => self.defaults.no_scenario_metrics = Some(value), + GooseDefault::NoStatusCodes => self.defaults.no_status_codes = Some(value), + GooseDefault::NoTelnet => self.defaults.no_telnet = Some(value), GooseDefault::NoTransactionMetrics => { self.defaults.no_transaction_metrics = Some(value) } - GooseDefault::NoScenarioMetrics => self.defaults.no_scenario_metrics = Some(value), - GooseDefault::RequestBody => self.defaults.request_body = Some(value), - GooseDefault::NoPrintMetrics => self.defaults.no_print_metrics = Some(value), - GooseDefault::NoErrorSummary => self.defaults.no_error_summary = Some(value), - GooseDefault::NoDebugBody => self.defaults.no_debug_body = Some(value), - GooseDefault::NoTelnet => self.defaults.no_telnet = Some(value), GooseDefault::NoWebSocket => self.defaults.no_websocket = Some(value), - GooseDefault::NoAutoStart => self.defaults.no_autostart = Some(value), - GooseDefault::NoGzip => self.defaults.no_gzip = Some(value), - GooseDefault::NoStatusCodes => self.defaults.no_status_codes = Some(value), + GooseDefault::RequestBody => self.defaults.request_body = Some(value), GooseDefault::StickyFollow => self.defaults.sticky_follow = Some(value), - GooseDefault::NoGranularData => self.defaults.no_granular_report = Some(value), + GooseDefault::Worker => self.defaults.worker = Some(value), // Otherwise display a helpful and explicit error. GooseDefault::DebugLog | GooseDefault::ErrorLog | GooseDefault::GooseLog | GooseDefault::HatchRate | GooseDefault::Host + | GooseDefault::ManagerHost | GooseDefault::ReportFile | GooseDefault::RequestLog | GooseDefault::ScenarioLog @@ -784,16 +855,18 @@ impl GooseDefaultType for GooseAttack { ), }) } - GooseDefault::Users - | GooseDefault::StartupTime - | GooseDefault::RunTime - | GooseDefault::RunningMetrics + GooseDefault::ExpectWorkers | GooseDefault::Iterations | GooseDefault::LogLevel + | GooseDefault::ManagerPort + | GooseDefault::RunningMetrics + | GooseDefault::RunTime + | GooseDefault::StartupTime + | GooseDefault::TelnetPort + | GooseDefault::ThrottleRequests | GooseDefault::Quiet + | GooseDefault::Users | GooseDefault::Verbose - | GooseDefault::ThrottleRequests - | GooseDefault::TelnetPort | GooseDefault::WebSocketPort => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), @@ -804,9 +877,9 @@ impl GooseDefaultType for GooseAttack { ), }) } - GooseDefault::RequestFormat - | GooseDefault::DebugFormat + GooseDefault::DebugFormat | GooseDefault::ErrorFormat + | GooseDefault::RequestFormat | GooseDefault::ScenarioFormat | GooseDefault::TransactionFormat => { return Err(GooseError::InvalidOption { @@ -842,21 +915,24 @@ impl GooseDefaultType for GooseAttack { match key { GooseDefault::CoordinatedOmissionMitigation => self.defaults.co_mitigation = Some(value), // Otherwise display a helpful and explicit error. - GooseDefault::NoResetMetrics + GooseDefault::Manager + | GooseDefault::NoAutoStart + | GooseDefault::NoDebugBody + | GooseDefault::NoErrorSummary + | GooseDefault::NoGranularData + | GooseDefault::NoGzip + | GooseDefault::NoHashCheck | GooseDefault::NoMetrics - | GooseDefault::NoTransactionMetrics - | GooseDefault::NoScenarioMetrics - | GooseDefault::RequestBody | GooseDefault::NoPrintMetrics - | GooseDefault::NoErrorSummary - | GooseDefault::NoDebugBody + | GooseDefault::NoResetMetrics + | GooseDefault::NoScenarioMetrics + | GooseDefault::NoStatusCodes + | GooseDefault::NoTransactionMetrics | GooseDefault::NoTelnet | GooseDefault::NoWebSocket - | GooseDefault::NoAutoStart - | GooseDefault::NoGzip - | GooseDefault::NoStatusCodes + | GooseDefault::RequestBody | GooseDefault::StickyFollow - | GooseDefault::NoGranularData => { + | GooseDefault::Worker => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), value: format!("{:?}", value), @@ -872,6 +948,7 @@ impl GooseDefaultType for GooseAttack { | GooseDefault::GooseLog | GooseDefault::HatchRate | GooseDefault::Host + | GooseDefault::ManagerHost | GooseDefault::ReportFile | GooseDefault::RequestLog | GooseDefault::ScenarioLog @@ -890,16 +967,18 @@ impl GooseDefaultType for GooseAttack { ), }) } - GooseDefault::Users - | GooseDefault::StartupTime - | GooseDefault::RunTime - | GooseDefault::RunningMetrics + GooseDefault::ExpectWorkers | GooseDefault::Iterations | GooseDefault::LogLevel + | GooseDefault::ManagerPort + | GooseDefault::RunningMetrics + | GooseDefault::RunTime + | GooseDefault::StartupTime + | GooseDefault::TelnetPort + | GooseDefault::ThrottleRequests | GooseDefault::Quiet + | GooseDefault::Users | GooseDefault::Verbose - | GooseDefault::ThrottleRequests - | GooseDefault::TelnetPort | GooseDefault::WebSocketPort => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), @@ -910,9 +989,9 @@ impl GooseDefaultType for GooseAttack { ), }) } - GooseDefault::RequestFormat - | GooseDefault::DebugFormat + GooseDefault::DebugFormat | GooseDefault::ErrorFormat + | GooseDefault::RequestFormat | GooseDefault::ScenarioFormat | GooseDefault::TransactionFormat => { return Err(GooseError::InvalidOption { @@ -936,27 +1015,30 @@ impl GooseDefaultType for GooseAttack { value: GooseLogFormat, ) -> Result, GooseError> { match key { - GooseDefault::RequestFormat => self.defaults.request_format = Some(value), GooseDefault::DebugFormat => self.defaults.debug_format = Some(value), GooseDefault::ErrorFormat => self.defaults.error_format = Some(value), - GooseDefault::TransactionFormat => self.defaults.transaction_format = Some(value), + GooseDefault::RequestFormat => self.defaults.request_format = Some(value), GooseDefault::ScenarioFormat => self.defaults.scenario_format = Some(value), + GooseDefault::TransactionFormat => self.defaults.transaction_format = Some(value), // Otherwise display a helpful and explicit error. - GooseDefault::NoResetMetrics + GooseDefault::Manager + | GooseDefault::NoAutoStart + | GooseDefault::NoDebugBody + | GooseDefault::NoErrorSummary + | GooseDefault::NoGranularData + | GooseDefault::NoGzip + | GooseDefault::NoHashCheck | GooseDefault::NoMetrics - | GooseDefault::NoTransactionMetrics - | GooseDefault::NoScenarioMetrics - | GooseDefault::RequestBody | GooseDefault::NoPrintMetrics - | GooseDefault::NoErrorSummary - | GooseDefault::NoDebugBody + | GooseDefault::NoResetMetrics + | GooseDefault::NoScenarioMetrics + | GooseDefault::NoStatusCodes | GooseDefault::NoTelnet + | GooseDefault::NoTransactionMetrics | GooseDefault::NoWebSocket - | GooseDefault::NoAutoStart - | GooseDefault::NoGzip - | GooseDefault::NoStatusCodes + | GooseDefault::RequestBody | GooseDefault::StickyFollow - | GooseDefault::NoGranularData => { + | GooseDefault::Worker => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), value: format!("{:?}", value), @@ -972,6 +1054,7 @@ impl GooseDefaultType for GooseAttack { | GooseDefault::GooseLog | GooseDefault::HatchRate | GooseDefault::Host + | GooseDefault::ManagerHost | GooseDefault::ReportFile | GooseDefault::RequestLog | GooseDefault::ScenarioLog @@ -990,16 +1073,18 @@ impl GooseDefaultType for GooseAttack { ), }) } - GooseDefault::Users - | GooseDefault::StartupTime - | GooseDefault::RunTime - | GooseDefault::RunningMetrics + GooseDefault::ExpectWorkers | GooseDefault::Iterations | GooseDefault::LogLevel + | GooseDefault::ManagerPort + | GooseDefault::RunningMetrics + | GooseDefault::RunTime + | GooseDefault::StartupTime + | GooseDefault::TelnetPort + | GooseDefault::ThrottleRequests | GooseDefault::Quiet + | GooseDefault::Users | GooseDefault::Verbose - | GooseDefault::ThrottleRequests - | GooseDefault::TelnetPort | GooseDefault::WebSocketPort => { return Err(GooseError::InvalidOption { option: format!("GooseDefault::{:?}", key), @@ -1306,6 +1391,42 @@ impl GooseConfiguration { // Configure loggers. self.configure_loggers(defaults); + // Configure `manager`. + self.manager = self + .get_value(vec![ + // Use --manager if set. + GooseValue { + value: Some(self.manager), + filter: !self.manager, + message: "", + }, + // Otherwise use default. + GooseValue { + value: defaults.manager, + filter: defaults.manager.is_none(), + message: "", + }, + ]) + .unwrap_or(false); + + // Configure `worker`. + self.worker = self + .get_value(vec![ + // Use --worker if set. + GooseValue { + value: Some(self.worker), + filter: !self.worker, + message: "", + }, + // Otherwise use default. + GooseValue { + value: defaults.worker, + filter: defaults.worker.is_none(), + message: "", + }, + ]) + .unwrap_or(false); + // Configure `test_plan` before `users` so users doesn't get assigned a default when using a test plan. self.test_plan = self.get_value(vec![ // Use --test-plan if set. @@ -1317,7 +1438,7 @@ impl GooseConfiguration { // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.test_plan.clone(), - filter: defaults.test_plan.is_none(), + filter: defaults.test_plan.is_none() || self.worker, message: "test_plan", }, ]); @@ -1343,13 +1464,13 @@ impl GooseConfiguration { // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.users, - filter: defaults.users.is_none(), + filter: defaults.users.is_none() || self.worker, message: "users", }, // Otherwise use detected number of CPUs if not on Worker. GooseValue { value: default_users, - filter: self.test_plan.is_some(), + filter: self.test_plan.is_some() || self.worker, message: "users defaulted to number of CPUs", }, ]); @@ -1366,7 +1487,7 @@ impl GooseConfiguration { // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.startup_time, - filter: defaults.startup_time.is_none(), + filter: defaults.startup_time.is_none() | self.worker, message: "startup_time", }, ]) @@ -1384,7 +1505,7 @@ impl GooseConfiguration { // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.run_time, - filter: defaults.run_time.is_none(), + filter: defaults.run_time.is_none() || self.worker, message: "run_time", }, ]) @@ -1402,7 +1523,7 @@ impl GooseConfiguration { // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: Some(util::get_hatch_rate(defaults.hatch_rate.clone())), - filter: defaults.hatch_rate.is_none(), + filter: defaults.hatch_rate.is_none() | self.worker, message: "hatch_rate", }, ]) @@ -1420,7 +1541,7 @@ impl GooseConfiguration { // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: util::get_float_from_string(defaults.timeout.clone()), - filter: defaults.timeout.is_none(), + filter: defaults.timeout.is_none() || self.worker, message: "timeout", }, ]) @@ -1434,10 +1555,10 @@ impl GooseConfiguration { filter: self.running_metrics.is_none(), message: "running_metrics", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.running_metrics, - filter: defaults.running_metrics.is_none(), + filter: defaults.running_metrics.is_none() || self.worker, message: "running_metrics", }, ]); @@ -1451,10 +1572,10 @@ impl GooseConfiguration { filter: !self.no_reset_metrics, message: "no_reset_metrics", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.no_reset_metrics, - filter: defaults.no_reset_metrics.is_none(), + filter: defaults.no_reset_metrics.is_none() || self.worker, message: "no_reset_metrics", }, ]) @@ -1469,10 +1590,10 @@ impl GooseConfiguration { filter: !self.no_metrics, message: "no_metrics", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.no_metrics, - filter: defaults.no_metrics.is_none(), + filter: defaults.no_metrics.is_none() || self.worker, message: "no_metrics", }, ]) @@ -1487,10 +1608,10 @@ impl GooseConfiguration { filter: !self.no_transaction_metrics, message: "no_transaction_metrics", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.no_transaction_metrics, - filter: defaults.no_transaction_metrics.is_none(), + filter: defaults.no_transaction_metrics.is_none() || self.worker, message: "no_transaction_metrics", }, ]) @@ -1505,10 +1626,10 @@ impl GooseConfiguration { filter: !self.no_scenario_metrics, message: "no_scenario_metrics", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.no_scenario_metrics, - filter: defaults.no_scenario_metrics.is_none(), + filter: defaults.no_scenario_metrics.is_none() || self.worker, message: "no_scenario_metrics", }, ]) @@ -1523,10 +1644,10 @@ impl GooseConfiguration { filter: !self.no_print_metrics, message: "no_print_metrics", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.no_print_metrics, - filter: defaults.no_print_metrics.is_none(), + filter: defaults.no_print_metrics.is_none() || self.worker, message: "no_print_metrics", }, ]) @@ -1541,10 +1662,10 @@ impl GooseConfiguration { filter: !self.no_error_summary, message: "no_error_summary", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.no_error_summary, - filter: defaults.no_error_summary.is_none(), + filter: defaults.no_error_summary.is_none() || self.worker, message: "no_error_summary", }, ]) @@ -1558,10 +1679,10 @@ impl GooseConfiguration { filter: self.report_file.is_empty(), message: "report_file", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Manager. GooseValue { value: defaults.report_file.clone(), - filter: defaults.report_file.is_none(), + filter: defaults.report_file.is_none() || self.manager, message: "report_file", }, ]) { @@ -1578,10 +1699,10 @@ impl GooseConfiguration { filter: !self.no_granular_report, message: "no_granular_report", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Manager. GooseValue { value: defaults.no_debug_body, - filter: defaults.no_debug_body.is_none(), + filter: defaults.no_debug_body.is_none() || self.manager, message: "no_granular_report", }, ]) @@ -1599,7 +1720,7 @@ impl GooseConfiguration { // Use GooseDefault if not already set and not Worker. GooseValue { value: defaults.iterations, - filter: defaults.iterations.is_none(), + filter: defaults.iterations.is_none() || self.worker, message: "iterations", }, ]) @@ -1617,7 +1738,7 @@ impl GooseConfiguration { // Use GooseDefault if not already set and not Worker. GooseValue { value: defaults.scenarios.clone(), - filter: defaults.scenarios.is_none(), + filter: defaults.scenarios.is_none() || self.worker, message: "scenarios", }, ]) @@ -1632,10 +1753,10 @@ impl GooseConfiguration { filter: !self.no_debug_body, message: "no_debug_body", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Manager. GooseValue { value: defaults.no_debug_body, - filter: defaults.no_debug_body.is_none(), + filter: defaults.no_debug_body.is_none() || self.manager, message: "no_debug_body", }, ]) @@ -1650,10 +1771,10 @@ impl GooseConfiguration { filter: !self.no_status_codes, message: "no_status_codes", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Worker. GooseValue { value: defaults.no_status_codes, - filter: defaults.no_status_codes.is_none(), + filter: defaults.no_status_codes.is_none() || self.worker, message: "no_status_codes", }, ]) @@ -1707,7 +1828,7 @@ impl GooseConfiguration { // Use GooseDefault if not already set and not Worker. GooseValue { value: defaults.no_autostart, - filter: defaults.no_autostart.is_none(), + filter: defaults.no_autostart.is_none() || self.worker, message: "no_autostart", }, ]) @@ -1725,7 +1846,7 @@ impl GooseConfiguration { // Use GooseDefault if not already set and not Worker. GooseValue { value: defaults.no_gzip, - filter: defaults.no_gzip.is_none(), + filter: defaults.no_gzip.is_none() || self.worker, message: "no_gzip", }, ]) @@ -1741,7 +1862,7 @@ impl GooseConfiguration { // Otherwise use GooseDefault if set and not Worker. GooseValue { value: defaults.co_mitigation.clone(), - filter: defaults.co_mitigation.is_none(), + filter: defaults.co_mitigation.is_none() || self.worker, message: "co_mitigation", }, // Otherwise default to GooseCoordinaatedOmissionMitigation::Disabled. @@ -1761,10 +1882,10 @@ impl GooseConfiguration { filter: self.throttle_requests == 0, message: "throttle_requests", }, - // Otherwise use GooseDefault if set. + // Otherwise use GooseDefault if set and not on Manager. GooseValue { value: defaults.throttle_requests, - filter: defaults.throttle_requests.is_none(), + filter: defaults.throttle_requests.is_none() || self.manager, message: "throttle_requests", }, ]) @@ -1782,15 +1903,97 @@ impl GooseConfiguration { // Use GooseDefault if not already set and not Worker. GooseValue { value: defaults.sticky_follow, - filter: defaults.sticky_follow.is_none(), + filter: defaults.sticky_follow.is_none() || self.worker, message: "sticky_follow", }, ]) .unwrap_or(false); - } - /// Validate configured [`GooseConfiguration`] values. - pub(crate) fn validate(&self) -> Result<(), GooseError> { + // Configure `expect_workers`. + self.expect_workers = self.get_value(vec![ + // Use --expect-workers if configured. + GooseValue { + value: self.expect_workers, + filter: self.expect_workers.is_none(), + message: "expect_workers", + }, + // Use GooseDefault if not already set and not Worker. + GooseValue { + value: defaults.expect_workers, + filter: self.expect_workers.is_none() && self.worker, + message: "expect_workers", + }, + ]); + + // Configure `no_hash_check`. + self.no_hash_check = self + .get_value(vec![ + // Use --no-hash_check if set. + GooseValue { + value: Some(self.no_hash_check), + filter: !self.no_hash_check, + message: "no_hash_check", + }, + // Use GooseDefault if not already set and not Worker. + GooseValue { + value: defaults.no_hash_check, + filter: defaults.no_hash_check.is_none() || self.worker, + message: "no_hash_check", + }, + ]) + .unwrap_or(false); + + // Set `manager_host` on Worker. + self.manager_host = self + .get_value(vec![ + // Use --manager-host if configured. + GooseValue { + value: Some(self.manager_host.to_string()), + filter: self.manager_host.is_empty(), + message: "manager_host", + }, + // Otherwise use default if set and on Worker. + GooseValue { + value: defaults.manager_host.clone(), + filter: defaults.manager_host.is_none() || !self.worker, + message: "manager_host", + }, + // Otherwise default to 127.0.0.1 if on Worker. + GooseValue { + value: Some("127.0.0.1".to_string()), + filter: !self.worker, + message: "manager_host", + }, + ]) + .unwrap_or_default(); + + // Set `manager_port` on Worker. + self.manager_port = self + .get_value(vec![ + // Use --manager-port if configured. + GooseValue { + value: Some(self.manager_port), + filter: self.manager_port == 0, + message: "manager_port", + }, + // Otherwise use default if set and on Worker. + GooseValue { + value: defaults.manager_port, + filter: defaults.manager_port.is_none() || !self.worker, + message: "manager_port", + }, + // Otherwise default to DEFAULT_TELNET_PORT if on Worker. + GooseValue { + value: Some(DEFAULT_TELNET_PORT.to_string().parse().unwrap()), + filter: !self.worker, + message: "manager_port", + }, + ]) + .unwrap_or(0); + } + + /// Validate configured [`GooseConfiguration`] values. + pub(crate) fn validate(&self) -> Result<(), GooseError> { // Can't set both --verbose and --quiet. if self.verbose > 0 && self.quiet > 0 { return Err(GooseError::InvalidOption { @@ -2057,6 +2260,302 @@ impl GooseConfiguration { } } + // Validate nothing incompatible is enabled with --manager. + if self.manager { + // Don't allow --manager and --worker together. + if self.worker { + return Err(GooseError::InvalidOption { + option: "`configuration.manager` && `configuration.worker`".to_string(), + value: "true".to_string(), + detail: "Goose can not run as both Manager and Worker".to_string(), + }); + } else if !self.debug_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.debug_log`".to_string(), + value: self.debug_log.clone(), + detail: "`configuration.debug_log` can not be set on the Manager.".to_string(), + }); + } else if !self.error_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.error_log`".to_string(), + value: self.error_log.clone(), + detail: "`configuration.error_log` can not be set on the Manager.".to_string(), + }); + } else if !self.request_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.request_log`".to_string(), + value: self.request_log.clone(), + detail: "`configuration.request_log` can not be set on the Manager." + .to_string(), + }); + } else if !self.transaction_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.transaction_log`".to_string(), + value: self.transaction_log.clone(), + detail: "`configuration.transaction_log` can not be set on the Manager." + .to_string(), + }); + } else if !self.scenario_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.scenario_log`".to_string(), + value: self.scenario_log.clone(), + detail: "`configuration.scenario_log` can not be set on the Manager." + .to_string(), + }); + } else if self.no_autostart { + return Err(GooseError::InvalidOption { + option: "`configuration.no_autostart`".to_string(), + value: true.to_string(), + detail: "`configuration.no_autostart` can not be set on the Manager." + .to_string(), + }); + } else if !self.report_file.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.report_file`".to_string(), + value: self.report_file.to_string(), + detail: "`configuration.report_file` can not be set on the Manager." + .to_string(), + }); + } else if self.no_granular_report { + return Err(GooseError::InvalidOption { + option: "`configuration.no_granular_report`".to_string(), + value: true.to_string(), + detail: "`configuration.no_granular_report` can not be set on the Manager." + .to_string(), + }); + } else if self.no_debug_body { + return Err(GooseError::InvalidOption { + option: "`configuration.no_debug_body`".to_string(), + value: true.to_string(), + detail: "`configuration.no_debug_body` can not be set on the Manager." + .to_string(), + }); + // Can not set `throttle_requests` on Manager. + } else if self.throttle_requests > 0 { + return Err(GooseError::InvalidOption { + option: "`configuration.throttle_requests`".to_string(), + value: self.throttle_requests.to_string(), + detail: "`configuration.throttle_requests` can not be set on the Manager." + .to_string(), + }); + } + if let Some(expect_workers) = self.expect_workers.as_ref() { + // Must expect at least 1 Worker when running as Manager. + if expect_workers == &0 { + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers must be set to at least 1." + .to_string(), + }); + } + + // Must be at least 1 user per worker. + if let Some(users) = self.users.as_ref() { + if expect_workers > users { + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers can not be set to a value larger than `configuration.users`.".to_string(), + }); + } + } else { + // @TODO: Rework this to allow configuration with --test-plan as well. + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers can not be set without setting `configuration.users`.".to_string(), + }); + } + } else { + return Err(GooseError::InvalidOption { + option: "configuration.manager".to_string(), + value: true.to_string(), + detail: "Manager mode requires --expect-workers be configured".to_string(), + }); + } + } else { + // Don't allow `expect_workers` if not running as Manager. + if let Some(expect_workers) = self.expect_workers.as_ref() { + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers` can not be set unless on the Manager." + .to_string(), + }); + } + } + + // Validate nothing incompatible is enabled with --worker. + if self.worker { + // Can't set `users` on Worker. + if self.users.is_some() { + return Err(GooseError::InvalidOption { + option: "configuration.users".to_string(), + value: self.users.as_ref().unwrap().to_string(), + detail: "`configuration.users` can not be set together with the `configuration.worker`.".to_string(), + }); + // Can't set `startup_time` on Worker. + } else if self.startup_time != "0" { + return Err(GooseError::InvalidOption { + option: "`configuration.startup_time".to_string(), + value: self.startup_time.to_string(), + detail: "`configuration.startup_time` can not be set in Worker mode." + .to_string(), + }); + // Can't set `run_time` on Worker. + } else if self.run_time != "0" { + return Err(GooseError::InvalidOption { + option: "`configuration.run_time".to_string(), + value: self.run_time.to_string(), + detail: "`configuration.run_time` can not be set in Worker mode.".to_string(), + }); + // Can't set `hatch_rate` on Worker. + } else if self.hatch_rate.is_some() { + return Err(GooseError::InvalidOption { + option: "`configuration.hatch_rate`".to_string(), + value: self.hatch_rate.as_ref().unwrap().to_string(), + detail: "`configuration.hatch_rate` can not be set in Worker mode.".to_string(), + }); + // Can't set `timeout` on Worker. + } else if self.timeout.is_some() { + return Err(GooseError::InvalidOption { + option: "`configuration.timeout`".to_string(), + value: self.timeout.as_ref().unwrap().to_string(), + detail: "`configuration.timeout` can not be set in Worker mode.".to_string(), + }); + // Can't set `running_metrics` on Worker. + } else if self.running_metrics.is_some() { + return Err(GooseError::InvalidOption { + option: "`configuration.running_metrics".to_string(), + value: self.running_metrics.as_ref().unwrap().to_string(), + detail: "`configuration.running_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_reset_metrics` on Worker. + } else if self.no_reset_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_reset_metrics".to_string(), + value: self.no_reset_metrics.to_string(), + detail: "`configuration.no_reset_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_metrics` on Worker. + } else if self.no_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_metrics".to_string(), + value: self.no_metrics.to_string(), + detail: "`configuration.no_metrics` can not be set in Worker mode.".to_string(), + }); + // Can't set `no_transaction_metrics` on Worker. + } else if self.no_transaction_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_transaction_metrics".to_string(), + value: self.no_transaction_metrics.to_string(), + detail: "`configuration.no_transaction_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_scenario_metrics` on Worker. + } else if self.no_scenario_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_scenario_metrics".to_string(), + value: self.no_scenario_metrics.to_string(), + detail: "`configuration.no_scenario_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_print_metrics` on Worker. + } else if self.no_print_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_print_metrics".to_string(), + value: self.no_print_metrics.to_string(), + detail: "`configuration.no_print_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_error_summary` on Worker. + } else if self.no_error_summary { + return Err(GooseError::InvalidOption { + option: "`configuration.no_error_summary".to_string(), + value: self.no_error_summary.to_string(), + detail: "`configuration.no_error_summary` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_status_codes` on Worker. + } else if self.no_status_codes { + return Err(GooseError::InvalidOption { + option: "`configuration.no_status_codes".to_string(), + value: self.no_status_codes.to_string(), + detail: "`configuration.no_status_codes` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_autostart` on Worker. + } else if self.no_autostart { + return Err(GooseError::InvalidOption { + option: "`configuration.no_autostart`".to_string(), + value: true.to_string(), + detail: "`configuration.no_autostart` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_gzip` on Worker. + } else if self.no_gzip { + return Err(GooseError::InvalidOption { + option: "`configuration.no_gzip`".to_string(), + value: true.to_string(), + detail: "`configuration.no_gzip` can not be set in Worker mode.".to_string(), + }); + } else if self + .co_mitigation + .as_ref() + .unwrap_or(&GooseCoordinatedOmissionMitigation::Disabled) + != &GooseCoordinatedOmissionMitigation::Disabled + { + return Err(GooseError::InvalidOption { + option: "`configuration.co_mitigation`".to_string(), + value: format!("{:?}", self.co_mitigation.as_ref().unwrap()), + detail: "`configuration.co_mitigation` can not be set in Worker mode." + .to_string(), + }); + // Must set `manager_host` on Worker. + } else if self.manager_host.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.manager_host`".to_string(), + value: self.manager_host.clone(), + detail: "`configuration.manager_host` must be set when in Worker mode." + .to_string(), + }); + // Must set `manager_port` on Worker. + } else if self.manager_port == 0 { + return Err(GooseError::InvalidOption { + option: "`configuration.manager_port`".to_string(), + value: self.manager_port.to_string(), + detail: "`configuration.manager_port` must be set when in Worker mode." + .to_string(), + }); + // Can not set `sticky_follow` on Worker. + } else if self.sticky_follow { + return Err(GooseError::InvalidOption { + option: "`configuration.sticky_follow`".to_string(), + value: true.to_string(), + detail: "`configuration.sticky_follow` can not be set in Worker mode." + .to_string(), + }); + // Can not set `no_hash_check` on Worker. + } else if self.no_hash_check { + return Err(GooseError::InvalidOption { + option: "`configuration.no_hash_check`".to_string(), + value: true.to_string(), + detail: "`configuration.no_hash_check` can not be set in Worker mode." + .to_string(), + }); + // Can not set `test_plan` on Worker. + } else if self.test_plan.is_some() { + return Err(GooseError::InvalidOption { + option: "`configuration.test_plan`".to_string(), + value: true.to_string(), + detail: "`configuration.test_plan` can not be set in Worker mode.".to_string(), + }); + } + } + Ok(()) } @@ -2124,65 +2623,99 @@ mod test { #[test] fn set_defaults() { - let host = "http://example.com/".to_string(); - let users: usize = 10; - let run_time: usize = 10; + let debug_log = "custom-goose-debug.log".to_string(); + let error_log = "custom-goose-error.log".to_string(); + let expect_workers: usize = 7; + let goose_log = "custom-goose.log".to_string(); let hatch_rate = "2".to_string(); - let timeout = "45".to_string(); + let host = "http://example.com/".to_string(); + let iterations: usize = 123; let log_level: usize = 1; - let goose_log = "custom-goose.log".to_string(); - let quiet: usize = 0; - let verbose: usize = 0; + let manager_host = "10.11.12.13".to_string(); + let manager_port = 1234; let report_file = "custom-goose-report.html".to_string(); let request_log = "custom-goose-request.log".to_string(); - let transaction_log = "custom-goose-transaction.log".to_string(); + let run_time: usize = 10; let scenario_log = "custom-goose-scenario.log".to_string(); - let debug_log = "custom-goose-debug.log".to_string(); - let error_log = "custom-goose-error.log".to_string(); + let startup_time: usize = 30; + let telnet_host = "10.20.30.40".to_string(); + let telnet_port = 2468; let throttle_requests: usize = 25; + let test_plan = "60,60s;0,30s".to_string(); + // Compiles into TestPlan, in which steps are a vector of (users, milliseconds) tuples. + let test_plan_compiled = TestPlan { + steps: vec![(60, 60000), (0, 30000)], + current: 0, + }; + let timeout = "45".to_string(); + let transaction_log = "custom-goose-transaction.log".to_string(); + let quiet: usize = 0; + let users: usize = 10; + let verbose: usize = 0; + let websocket_host = "10.30.60.90".to_string(); + let websocket_port = 1369; let goose_attack = GooseAttack::initialize() .unwrap() - .set_default(GooseDefault::Host, host.as_str()) + .set_default( + GooseDefault::CoordinatedOmissionMitigation, + GooseCoordinatedOmissionMitigation::Disabled, + ) .unwrap() - .set_default(GooseDefault::Users, users) + .set_default(GooseDefault::DebugLog, debug_log.as_str()) .unwrap() - .set_default(GooseDefault::RunTime, run_time) + .set_default(GooseDefault::DebugFormat, GooseLogFormat::Csv) + .unwrap() + .set_default(GooseDefault::ErrorLog, error_log.as_str()) + .unwrap() + .set_default(GooseDefault::ErrorFormat, GooseLogFormat::Csv) + .unwrap() + .set_default(GooseDefault::ExpectWorkers, expect_workers) + .unwrap() + .set_default(GooseDefault::GooseLog, goose_log.as_str()) .unwrap() .set_default(GooseDefault::HatchRate, hatch_rate.as_str()) .unwrap() + .set_default(GooseDefault::Host, host.as_str()) + .unwrap() + .set_default(GooseDefault::Iterations, iterations) + .unwrap() .set_default(GooseDefault::LogLevel, log_level) .unwrap() - .set_default(GooseDefault::GooseLog, goose_log.as_str()) + .set_default(GooseDefault::Manager, true) .unwrap() - .set_default(GooseDefault::Quiet, quiet) + .set_default(GooseDefault::ManagerHost, manager_host.as_str()) .unwrap() - .set_default(GooseDefault::Verbose, verbose) + .set_default(GooseDefault::ManagerPort, manager_port) .unwrap() - .set_default(GooseDefault::Timeout, timeout.as_str()) + .set_default(GooseDefault::NoAutoStart, true) .unwrap() - .set_default(GooseDefault::RunningMetrics, 15) + .set_default(GooseDefault::NoDebugBody, true) .unwrap() - .set_default(GooseDefault::NoResetMetrics, true) + .set_default(GooseDefault::NoErrorSummary, true) + .unwrap() + .set_default(GooseDefault::NoGranularData, true) + .unwrap() + .set_default(GooseDefault::NoGzip, true) + .unwrap() + .set_default(GooseDefault::NoHashCheck, true) .unwrap() .set_default(GooseDefault::NoMetrics, true) .unwrap() - .set_default(GooseDefault::NoTransactionMetrics, true) + .set_default(GooseDefault::NoPrintMetrics, true) + .unwrap() + .set_default(GooseDefault::NoResetMetrics, true) .unwrap() .set_default(GooseDefault::NoScenarioMetrics, true) .unwrap() - .set_default(GooseDefault::NoPrintMetrics, true) + .set_default(GooseDefault::NoStatusCodes, true) .unwrap() - .set_default(GooseDefault::NoErrorSummary, true) + .set_default(GooseDefault::NoTransactionMetrics, true) .unwrap() .set_default(GooseDefault::NoTelnet, true) .unwrap() .set_default(GooseDefault::NoWebSocket, true) .unwrap() - .set_default(GooseDefault::NoAutoStart, true) - .unwrap() - .set_default(GooseDefault::NoGzip, true) - .unwrap() .set_default(GooseDefault::ReportFile, report_file.as_str()) .unwrap() .set_default(GooseDefault::RequestLog, request_log.as_str()) @@ -2191,71 +2724,98 @@ mod test { .unwrap() .set_default(GooseDefault::RequestBody, true) .unwrap() - .set_default(GooseDefault::TransactionLog, transaction_log.as_str()) + .set_default(GooseDefault::RunTime, run_time) .unwrap() - .set_default(GooseDefault::TransactionFormat, GooseLogFormat::Raw) + .set_default(GooseDefault::RunningMetrics, 15) + .unwrap() + .set_default(GooseDefault::ScenarioFormat, GooseLogFormat::Raw) .unwrap() .set_default(GooseDefault::ScenarioLog, scenario_log.as_str()) .unwrap() - .set_default(GooseDefault::ScenarioFormat, GooseLogFormat::Raw) + .set_default(GooseDefault::StartupTime, startup_time) .unwrap() - .set_default(GooseDefault::ErrorLog, error_log.as_str()) + .set_default(GooseDefault::StickyFollow, true) .unwrap() - .set_default(GooseDefault::ErrorFormat, GooseLogFormat::Csv) + .set_default(GooseDefault::TelnetHost, telnet_host.as_str()) .unwrap() - .set_default(GooseDefault::DebugLog, debug_log.as_str()) + .set_default(GooseDefault::TelnetPort, telnet_port) .unwrap() - .set_default(GooseDefault::DebugFormat, GooseLogFormat::Csv) + .set_default(GooseDefault::TestPlan, test_plan.as_str()) .unwrap() - .set_default(GooseDefault::NoDebugBody, true) + .set_default(GooseDefault::ThrottleRequests, throttle_requests) .unwrap() - .set_default(GooseDefault::NoStatusCodes, true) + .set_default(GooseDefault::Timeout, timeout.as_str()) .unwrap() - .set_default( - GooseDefault::CoordinatedOmissionMitigation, - GooseCoordinatedOmissionMitigation::Disabled, - ) + .set_default(GooseDefault::TransactionFormat, GooseLogFormat::Raw) .unwrap() - .set_default(GooseDefault::ThrottleRequests, throttle_requests) + .set_default(GooseDefault::TransactionLog, transaction_log.as_str()) .unwrap() - .set_default(GooseDefault::StickyFollow, true) + .set_default(GooseDefault::Quiet, quiet) + .unwrap() + .set_default(GooseDefault::Users, users) + .unwrap() + .set_default(GooseDefault::Verbose, verbose) + .unwrap() + .set_default(GooseDefault::WebSocketHost, websocket_host.as_str()) + .unwrap() + .set_default(GooseDefault::WebSocketPort, websocket_port) + .unwrap() + .set_default(GooseDefault::Worker, true) .unwrap(); - assert!(goose_attack.defaults.host == Some(host)); - assert!(goose_attack.defaults.users == Some(users)); - assert!(goose_attack.defaults.run_time == Some(run_time)); + assert!( + goose_attack.defaults.co_mitigation + == Some(GooseCoordinatedOmissionMitigation::Disabled) + ); + assert!(goose_attack.defaults.debug_log == Some(debug_log)); + assert!(goose_attack.defaults.debug_format == Some(GooseLogFormat::Csv)); + assert!(goose_attack.defaults.error_log == Some(error_log)); + assert!(goose_attack.defaults.error_format == Some(GooseLogFormat::Csv)); + assert!(goose_attack.defaults.expect_workers == Some(expect_workers)); + assert!(goose_attack.defaults.goose_log == Some(goose_log)); assert!(goose_attack.defaults.hatch_rate == Some(hatch_rate)); + assert!(goose_attack.defaults.host == Some(host)); + assert!(goose_attack.defaults.iterations == Some(iterations)); assert!(goose_attack.defaults.log_level == Some(log_level as u8)); - assert!(goose_attack.defaults.goose_log == Some(goose_log)); - assert!(goose_attack.defaults.request_body == Some(true)); + assert!(goose_attack.defaults.manager == Some(true)); + assert!(goose_attack.defaults.manager_host == Some(manager_host)); + assert!(goose_attack.defaults.manager_port == Some(manager_port as u16)); + assert!(goose_attack.defaults.no_autostart == Some(true)); assert!(goose_attack.defaults.no_debug_body == Some(true)); - assert!(goose_attack.defaults.quiet == Some(quiet as u8)); - assert!(goose_attack.defaults.verbose == Some(verbose as u8)); - assert!(goose_attack.defaults.running_metrics == Some(15)); - assert!(goose_attack.defaults.no_reset_metrics == Some(true)); + assert!(goose_attack.defaults.no_error_summary == Some(true)); + assert!(goose_attack.defaults.no_granular_report == Some(true)); + assert!(goose_attack.defaults.no_gzip == Some(true)); + assert!(goose_attack.defaults.no_hash_check == Some(true)); assert!(goose_attack.defaults.no_metrics == Some(true)); - assert!(goose_attack.defaults.no_transaction_metrics == Some(true)); - assert!(goose_attack.defaults.no_scenario_metrics == Some(true)); assert!(goose_attack.defaults.no_print_metrics == Some(true)); - assert!(goose_attack.defaults.no_error_summary == Some(true)); + assert!(goose_attack.defaults.no_reset_metrics == Some(true)); + assert!(goose_attack.defaults.no_scenario_metrics == Some(true)); + assert!(goose_attack.defaults.no_status_codes == Some(true)); + assert!(goose_attack.defaults.no_transaction_metrics == Some(true)); assert!(goose_attack.defaults.no_telnet == Some(true)); assert!(goose_attack.defaults.no_websocket == Some(true)); - assert!(goose_attack.defaults.no_autostart == Some(true)); - assert!(goose_attack.defaults.timeout == Some(timeout)); - assert!(goose_attack.defaults.no_gzip == Some(true)); assert!(goose_attack.defaults.report_file == Some(report_file)); - assert!(goose_attack.defaults.request_log == Some(request_log)); + assert!(goose_attack.defaults.request_body == Some(true)); assert!(goose_attack.defaults.request_format == Some(GooseLogFormat::Raw)); - assert!(goose_attack.defaults.error_log == Some(error_log)); - assert!(goose_attack.defaults.error_format == Some(GooseLogFormat::Csv)); - assert!(goose_attack.defaults.debug_log == Some(debug_log)); - assert!(goose_attack.defaults.debug_format == Some(GooseLogFormat::Csv)); - assert!(goose_attack.defaults.no_status_codes == Some(true)); - assert!( - goose_attack.defaults.co_mitigation - == Some(GooseCoordinatedOmissionMitigation::Disabled) - ); - assert!(goose_attack.defaults.throttle_requests == Some(throttle_requests)); + assert!(goose_attack.defaults.request_log == Some(request_log)); + assert!(goose_attack.defaults.run_time == Some(run_time)); + assert!(goose_attack.defaults.running_metrics == Some(15)); + assert!(goose_attack.defaults.scenario_format == Some(GooseLogFormat::Raw)); + assert!(goose_attack.defaults.scenario_log == Some(scenario_log)); + assert!(goose_attack.defaults.startup_time == Some(startup_time)); assert!(goose_attack.defaults.sticky_follow == Some(true)); + assert!(goose_attack.defaults.test_plan == Some(test_plan_compiled)); + assert!(goose_attack.defaults.telnet_host == Some(telnet_host)); + assert!(goose_attack.defaults.telnet_port == Some(telnet_port as u16)); + assert!(goose_attack.defaults.throttle_requests == Some(throttle_requests)); + assert!(goose_attack.defaults.timeout == Some(timeout)); + assert!(goose_attack.defaults.transaction_format == Some(GooseLogFormat::Raw)); + assert!(goose_attack.defaults.transaction_log == Some(transaction_log)); + assert!(goose_attack.defaults.quiet == Some(quiet as u8)); + assert!(goose_attack.defaults.users == Some(users)); + assert!(goose_attack.defaults.verbose == Some(verbose as u8)); + assert!(goose_attack.defaults.websocket_host == Some(websocket_host)); + assert!(goose_attack.defaults.websocket_port == Some(websocket_port as u16)); + assert!(goose_attack.defaults.worker == Some(true)); } } diff --git a/src/lib.rs b/src/lib.rs index 40afc6cc..672f70f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,7 +72,7 @@ use crate::metrics::{GooseMetric, GooseMetrics}; use crate::test_plan::{TestPlan, TestPlanHistory, TestPlanStepAction}; /// Constant defining Goose's default telnet Controller port. -const DEFAULT_TELNET_PORT: &str = "5116"; +const DEFAULT_TELNET_PORT: &str = "5115"; /// Constant defining Goose's default WebSocket Controller port. const DEFAULT_WEBSOCKET_PORT: &str = "5117"; diff --git a/src/test_plan.rs b/src/test_plan.rs index ad1cadd7..3e541362 100644 --- a/src/test_plan.rs +++ b/src/test_plan.rs @@ -15,7 +15,7 @@ use crate::util; use crate::{AttackPhase, GooseAttack, GooseAttackRunState, GooseError}; /// Internal data structure representing a test plan. -#[derive(Options, Debug, Clone, Serialize, Deserialize)] +#[derive(Options, Debug, Clone, Serialize, Deserialize, PartialEq)] 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)>, From ddd45cc4d4bf9a03e2cb92249595104c53131eb4 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Wed, 10 May 2023 10:12:56 +0200 Subject: [PATCH 2/7] begin restoring gaggle files and logic --- src/gaggle/manager.rs | 21 ++++++++++++++++++++ src/lib.rs | 45 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 src/gaggle/manager.rs diff --git a/src/gaggle/manager.rs b/src/gaggle/manager.rs new file mode 100644 index 00000000..6520daa7 --- /dev/null +++ b/src/gaggle/manager.rs @@ -0,0 +1,21 @@ +//use crate::{GooseAttack, GooseConfiguration, GooseUserCommand, CANCELED, SHUTDOWN_GAGGLE}; +use crate::{GooseAttack, GooseError}; + +impl GooseAttack { + /// Main manager loop. + pub(crate) async fn manager_main( + mut self, + ) -> Result { + // The GooseAttackRunState is used while spawning and running the + // GooseUser threads that generate the load test. + // @TODO: should this be replaced with a GooseAttackManagerState ? + let mut goose_attack_run_state = self + .initialize_attack() + .await + .expect("failed to initialize GooseAttackRunState"); + + assert!(goose_attack_run_state.controller_channel_rx.is_some()); + + Ok(self) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 672f70f5..b515b599 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,9 @@ mod test_plan; mod throttle; mod user; pub mod util; +pub mod gaggle { + pub mod manager; +} use gumdrop::Options; use lazy_static::lazy_static; @@ -58,7 +61,10 @@ use rand::seq::SliceRandom; use rand::thread_rng; use std::collections::{hash_map::DefaultHasher, BTreeMap, HashSet}; use std::hash::{Hash, Hasher}; -use std::sync::{atomic::AtomicUsize, Arc, RwLock}; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, +}; use std::time::{self, Duration}; use std::{fmt, io}; use tokio::fs::File; @@ -93,6 +99,15 @@ type UnsequencedTransactions = Vec; /// Internal representation of sequenced transactions. type SequencedTransactions = BTreeMap>; +/// Returns the unique identifier of the running Worker when running in Gaggle mode. +/// +/// The first Worker to connect to the Manager is assigned an ID of 1. For each +/// subsequent Worker to connect to the Manager the ID is incremented by 1. This +/// identifier is primarily an aid in tracing logs. +pub fn get_worker_id() -> usize { + WORKER_ID.load(Ordering::Relaxed) +} + /// An enumeration of all errors a [`GooseAttack`](./struct.GooseAttack.html) can return. #[derive(Debug)] pub enum GooseError { @@ -236,6 +251,10 @@ pub enum AttackMode { Undefined, /// A single standalone process performing a load test. StandAlone, + /// The controlling process in a Gaggle distributed load test. + Manager, + /// One of one or more working processes in a Gaggle distributed load test. + Worker, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -919,7 +938,13 @@ impl GooseAttack { self.test_plan = TestPlan::build(&self.configuration); // With a validated GooseConfiguration, enter a run mode. - self.attack_mode = AttackMode::StandAlone; + self.attack_mode = if self.configuration.manager { + AttackMode::Manager + } else if self.configuration.worker { + AttackMode::Worker + } else { + AttackMode::StandAlone + }; // Confirm there's either a global host, or each scenario has a host defined. if self.configuration.no_autostart && self.validate_host().is_err() { @@ -939,7 +964,21 @@ impl GooseAttack { self.metrics.hash = s.finish(); debug!("hash: {}", self.metrics.hash); - self = self.start_attack().await?; + self = match self.attack_mode { + AttackMode::Manager => { + self.manager_main().await?; + panic!("attempted to start in AttackMode::Manager"); + }, + AttackMode::Worker => { + panic!("attempted to start in AttackMode::Worker"); + }, + AttackMode::StandAlone => { + self.start_attack().await? + }, + AttackMode::Undefined => { + panic!("attempted to start in AttackMode::Undefined"); + }, + }; if self.metrics.display_metrics { info!( From 9fc78deb389ee3236245589c0b2ce558aab2e9db Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Sat, 13 May 2023 07:48:35 +0200 Subject: [PATCH 3/7] restore previous telnet gaggle work --- src/config.rs | 384 +---------------------------- src/controller.rs | 552 ++++++++++++++++++++++++++++++++++++------ src/gaggle/common.rs | 27 +++ src/gaggle/manager.rs | 373 ++++++++++++++++++++++++++-- src/gaggle/worker.rs | 515 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 223 ++++++++++++----- src/metrics.rs | 5 +- src/util.rs | 2 +- tests/controller.rs | 15 ++ 9 files changed, 1571 insertions(+), 525 deletions(-) create mode 100644 src/gaggle/common.rs create mode 100644 src/gaggle/worker.rs diff --git a/src/config.rs b/src/config.rs index 82b5f1fc..8deea809 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,8 +17,6 @@ use crate::test_plan::TestPlan; use crate::util; use crate::{GooseAttack, GooseError}; -use crate::DEFAULT_TELNET_PORT; - /// Runtime options available when launching a Goose load test. /// /// Custom defaults can be programmatically set for most of these options using the @@ -1909,87 +1907,11 @@ impl GooseConfiguration { ]) .unwrap_or(false); - // Configure `expect_workers`. - self.expect_workers = self.get_value(vec![ - // Use --expect-workers if configured. - GooseValue { - value: self.expect_workers, - filter: self.expect_workers.is_none(), - message: "expect_workers", - }, - // Use GooseDefault if not already set and not Worker. - GooseValue { - value: defaults.expect_workers, - filter: self.expect_workers.is_none() && self.worker, - message: "expect_workers", - }, - ]); - - // Configure `no_hash_check`. - self.no_hash_check = self - .get_value(vec![ - // Use --no-hash_check if set. - GooseValue { - value: Some(self.no_hash_check), - filter: !self.no_hash_check, - message: "no_hash_check", - }, - // Use GooseDefault if not already set and not Worker. - GooseValue { - value: defaults.no_hash_check, - filter: defaults.no_hash_check.is_none() || self.worker, - message: "no_hash_check", - }, - ]) - .unwrap_or(false); - - // Set `manager_host` on Worker. - self.manager_host = self - .get_value(vec![ - // Use --manager-host if configured. - GooseValue { - value: Some(self.manager_host.to_string()), - filter: self.manager_host.is_empty(), - message: "manager_host", - }, - // Otherwise use default if set and on Worker. - GooseValue { - value: defaults.manager_host.clone(), - filter: defaults.manager_host.is_none() || !self.worker, - message: "manager_host", - }, - // Otherwise default to 127.0.0.1 if on Worker. - GooseValue { - value: Some("127.0.0.1".to_string()), - filter: !self.worker, - message: "manager_host", - }, - ]) - .unwrap_or_default(); + // Manager configuration. + self.configure_manager(defaults); - // Set `manager_port` on Worker. - self.manager_port = self - .get_value(vec![ - // Use --manager-port if configured. - GooseValue { - value: Some(self.manager_port), - filter: self.manager_port == 0, - message: "manager_port", - }, - // Otherwise use default if set and on Worker. - GooseValue { - value: defaults.manager_port, - filter: defaults.manager_port.is_none() || !self.worker, - message: "manager_port", - }, - // Otherwise default to DEFAULT_TELNET_PORT if on Worker. - GooseValue { - value: Some(DEFAULT_TELNET_PORT.to_string().parse().unwrap()), - filter: !self.worker, - message: "manager_port", - }, - ]) - .unwrap_or(0); + // Worker configuration. + self.configure_worker(defaults); } /// Validate configured [`GooseConfiguration`] values. @@ -2260,301 +2182,11 @@ impl GooseConfiguration { } } - // Validate nothing incompatible is enabled with --manager. - if self.manager { - // Don't allow --manager and --worker together. - if self.worker { - return Err(GooseError::InvalidOption { - option: "`configuration.manager` && `configuration.worker`".to_string(), - value: "true".to_string(), - detail: "Goose can not run as both Manager and Worker".to_string(), - }); - } else if !self.debug_log.is_empty() { - return Err(GooseError::InvalidOption { - option: "`configuration.debug_log`".to_string(), - value: self.debug_log.clone(), - detail: "`configuration.debug_log` can not be set on the Manager.".to_string(), - }); - } else if !self.error_log.is_empty() { - return Err(GooseError::InvalidOption { - option: "`configuration.error_log`".to_string(), - value: self.error_log.clone(), - detail: "`configuration.error_log` can not be set on the Manager.".to_string(), - }); - } else if !self.request_log.is_empty() { - return Err(GooseError::InvalidOption { - option: "`configuration.request_log`".to_string(), - value: self.request_log.clone(), - detail: "`configuration.request_log` can not be set on the Manager." - .to_string(), - }); - } else if !self.transaction_log.is_empty() { - return Err(GooseError::InvalidOption { - option: "`configuration.transaction_log`".to_string(), - value: self.transaction_log.clone(), - detail: "`configuration.transaction_log` can not be set on the Manager." - .to_string(), - }); - } else if !self.scenario_log.is_empty() { - return Err(GooseError::InvalidOption { - option: "`configuration.scenario_log`".to_string(), - value: self.scenario_log.clone(), - detail: "`configuration.scenario_log` can not be set on the Manager." - .to_string(), - }); - } else if self.no_autostart { - return Err(GooseError::InvalidOption { - option: "`configuration.no_autostart`".to_string(), - value: true.to_string(), - detail: "`configuration.no_autostart` can not be set on the Manager." - .to_string(), - }); - } else if !self.report_file.is_empty() { - return Err(GooseError::InvalidOption { - option: "`configuration.report_file`".to_string(), - value: self.report_file.to_string(), - detail: "`configuration.report_file` can not be set on the Manager." - .to_string(), - }); - } else if self.no_granular_report { - return Err(GooseError::InvalidOption { - option: "`configuration.no_granular_report`".to_string(), - value: true.to_string(), - detail: "`configuration.no_granular_report` can not be set on the Manager." - .to_string(), - }); - } else if self.no_debug_body { - return Err(GooseError::InvalidOption { - option: "`configuration.no_debug_body`".to_string(), - value: true.to_string(), - detail: "`configuration.no_debug_body` can not be set on the Manager." - .to_string(), - }); - // Can not set `throttle_requests` on Manager. - } else if self.throttle_requests > 0 { - return Err(GooseError::InvalidOption { - option: "`configuration.throttle_requests`".to_string(), - value: self.throttle_requests.to_string(), - detail: "`configuration.throttle_requests` can not be set on the Manager." - .to_string(), - }); - } - if let Some(expect_workers) = self.expect_workers.as_ref() { - // Must expect at least 1 Worker when running as Manager. - if expect_workers == &0 { - return Err(GooseError::InvalidOption { - option: "`configuration.expect_workers`".to_string(), - value: expect_workers.to_string(), - detail: "`configuration.expect_workers must be set to at least 1." - .to_string(), - }); - } - - // Must be at least 1 user per worker. - if let Some(users) = self.users.as_ref() { - if expect_workers > users { - return Err(GooseError::InvalidOption { - option: "`configuration.expect_workers`".to_string(), - value: expect_workers.to_string(), - detail: "`configuration.expect_workers can not be set to a value larger than `configuration.users`.".to_string(), - }); - } - } else { - // @TODO: Rework this to allow configuration with --test-plan as well. - return Err(GooseError::InvalidOption { - option: "`configuration.expect_workers`".to_string(), - value: expect_workers.to_string(), - detail: "`configuration.expect_workers can not be set without setting `configuration.users`.".to_string(), - }); - } - } else { - return Err(GooseError::InvalidOption { - option: "configuration.manager".to_string(), - value: true.to_string(), - detail: "Manager mode requires --expect-workers be configured".to_string(), - }); - } - } else { - // Don't allow `expect_workers` if not running as Manager. - if let Some(expect_workers) = self.expect_workers.as_ref() { - return Err(GooseError::InvalidOption { - option: "`configuration.expect_workers`".to_string(), - value: expect_workers.to_string(), - detail: "`configuration.expect_workers` can not be set unless on the Manager." - .to_string(), - }); - } - } + // Validate Manager configuration. + self.validate_manager()?; - // Validate nothing incompatible is enabled with --worker. - if self.worker { - // Can't set `users` on Worker. - if self.users.is_some() { - return Err(GooseError::InvalidOption { - option: "configuration.users".to_string(), - value: self.users.as_ref().unwrap().to_string(), - detail: "`configuration.users` can not be set together with the `configuration.worker`.".to_string(), - }); - // Can't set `startup_time` on Worker. - } else if self.startup_time != "0" { - return Err(GooseError::InvalidOption { - option: "`configuration.startup_time".to_string(), - value: self.startup_time.to_string(), - detail: "`configuration.startup_time` can not be set in Worker mode." - .to_string(), - }); - // Can't set `run_time` on Worker. - } else if self.run_time != "0" { - return Err(GooseError::InvalidOption { - option: "`configuration.run_time".to_string(), - value: self.run_time.to_string(), - detail: "`configuration.run_time` can not be set in Worker mode.".to_string(), - }); - // Can't set `hatch_rate` on Worker. - } else if self.hatch_rate.is_some() { - return Err(GooseError::InvalidOption { - option: "`configuration.hatch_rate`".to_string(), - value: self.hatch_rate.as_ref().unwrap().to_string(), - detail: "`configuration.hatch_rate` can not be set in Worker mode.".to_string(), - }); - // Can't set `timeout` on Worker. - } else if self.timeout.is_some() { - return Err(GooseError::InvalidOption { - option: "`configuration.timeout`".to_string(), - value: self.timeout.as_ref().unwrap().to_string(), - detail: "`configuration.timeout` can not be set in Worker mode.".to_string(), - }); - // Can't set `running_metrics` on Worker. - } else if self.running_metrics.is_some() { - return Err(GooseError::InvalidOption { - option: "`configuration.running_metrics".to_string(), - value: self.running_metrics.as_ref().unwrap().to_string(), - detail: "`configuration.running_metrics` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_reset_metrics` on Worker. - } else if self.no_reset_metrics { - return Err(GooseError::InvalidOption { - option: "`configuration.no_reset_metrics".to_string(), - value: self.no_reset_metrics.to_string(), - detail: "`configuration.no_reset_metrics` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_metrics` on Worker. - } else if self.no_metrics { - return Err(GooseError::InvalidOption { - option: "`configuration.no_metrics".to_string(), - value: self.no_metrics.to_string(), - detail: "`configuration.no_metrics` can not be set in Worker mode.".to_string(), - }); - // Can't set `no_transaction_metrics` on Worker. - } else if self.no_transaction_metrics { - return Err(GooseError::InvalidOption { - option: "`configuration.no_transaction_metrics".to_string(), - value: self.no_transaction_metrics.to_string(), - detail: "`configuration.no_transaction_metrics` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_scenario_metrics` on Worker. - } else if self.no_scenario_metrics { - return Err(GooseError::InvalidOption { - option: "`configuration.no_scenario_metrics".to_string(), - value: self.no_scenario_metrics.to_string(), - detail: "`configuration.no_scenario_metrics` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_print_metrics` on Worker. - } else if self.no_print_metrics { - return Err(GooseError::InvalidOption { - option: "`configuration.no_print_metrics".to_string(), - value: self.no_print_metrics.to_string(), - detail: "`configuration.no_print_metrics` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_error_summary` on Worker. - } else if self.no_error_summary { - return Err(GooseError::InvalidOption { - option: "`configuration.no_error_summary".to_string(), - value: self.no_error_summary.to_string(), - detail: "`configuration.no_error_summary` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_status_codes` on Worker. - } else if self.no_status_codes { - return Err(GooseError::InvalidOption { - option: "`configuration.no_status_codes".to_string(), - value: self.no_status_codes.to_string(), - detail: "`configuration.no_status_codes` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_autostart` on Worker. - } else if self.no_autostart { - return Err(GooseError::InvalidOption { - option: "`configuration.no_autostart`".to_string(), - value: true.to_string(), - detail: "`configuration.no_autostart` can not be set in Worker mode." - .to_string(), - }); - // Can't set `no_gzip` on Worker. - } else if self.no_gzip { - return Err(GooseError::InvalidOption { - option: "`configuration.no_gzip`".to_string(), - value: true.to_string(), - detail: "`configuration.no_gzip` can not be set in Worker mode.".to_string(), - }); - } else if self - .co_mitigation - .as_ref() - .unwrap_or(&GooseCoordinatedOmissionMitigation::Disabled) - != &GooseCoordinatedOmissionMitigation::Disabled - { - return Err(GooseError::InvalidOption { - option: "`configuration.co_mitigation`".to_string(), - value: format!("{:?}", self.co_mitigation.as_ref().unwrap()), - detail: "`configuration.co_mitigation` can not be set in Worker mode." - .to_string(), - }); - // Must set `manager_host` on Worker. - } else if self.manager_host.is_empty() { - return Err(GooseError::InvalidOption { - option: "`configuration.manager_host`".to_string(), - value: self.manager_host.clone(), - detail: "`configuration.manager_host` must be set when in Worker mode." - .to_string(), - }); - // Must set `manager_port` on Worker. - } else if self.manager_port == 0 { - return Err(GooseError::InvalidOption { - option: "`configuration.manager_port`".to_string(), - value: self.manager_port.to_string(), - detail: "`configuration.manager_port` must be set when in Worker mode." - .to_string(), - }); - // Can not set `sticky_follow` on Worker. - } else if self.sticky_follow { - return Err(GooseError::InvalidOption { - option: "`configuration.sticky_follow`".to_string(), - value: true.to_string(), - detail: "`configuration.sticky_follow` can not be set in Worker mode." - .to_string(), - }); - // Can not set `no_hash_check` on Worker. - } else if self.no_hash_check { - return Err(GooseError::InvalidOption { - option: "`configuration.no_hash_check`".to_string(), - value: true.to_string(), - detail: "`configuration.no_hash_check` can not be set in Worker mode." - .to_string(), - }); - // Can not set `test_plan` on Worker. - } else if self.test_plan.is_some() { - return Err(GooseError::InvalidOption { - option: "`configuration.test_plan`".to_string(), - value: true.to_string(), - detail: "`configuration.test_plan` can not be set in Worker mode.".to_string(), - }); - } - } + // Validate Worker configuration. + self.validate_worker()?; Ok(()) } diff --git a/src/controller.rs b/src/controller.rs index 64ef52c1..7fc0a5d7 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -4,10 +4,12 @@ //! real-time control of the running load test. use crate::config::GooseConfiguration; +use crate::gaggle::manager::{ManagerCommand, ManagerMessage}; +use crate::gaggle::worker::{WorkerCommand, WorkerMessage}; use crate::metrics::GooseMetrics; use crate::test_plan::{TestPlan, TestPlanHistory, TestPlanStepAction}; use crate::util; -use crate::{AttackPhase, GooseAttack, GooseAttackRunState, GooseError}; +use crate::{AttackMode, AttackPhase, GooseAttack, GooseAttackRunState, GooseError}; use async_trait::async_trait; use futures::{SinkExt, StreamExt}; @@ -154,6 +156,38 @@ pub enum ControllerCommand { /// /// Can be configured on an idle or running load test. TestPlan, + /// Taggles Gaggle Manager-mode. + /// + /// # Example + /// Enables Gaggle Manager-mode. + /// ```notest + /// manager + /// ``` + Manager, + /// Configures the number of Gaggle Workers to expect before starting the load test. + /// + /// # Example + /// Tells Gaggle Manager to wait for 4 Workers to connect. + /// ```notest + /// expect-workers 4 + /// ``` + ExpectWorkers, + /// Configures the number of Gaggle Workers to expect before starting the load test. + /// + /// # Example + /// Tells Gaggle Manager to wait for 4 Workers to connect. + /// ```notest + /// expect-workers 4 + /// ``` + NoHashCheck, + /// Taggles no-hash-check when in Manager-mode. + /// + /// # Example + /// Enables no-hash-check. + /// ```notest + /// no-hash-check + /// ``` + Worker, /// Display the current [`GooseConfiguration`](../struct.GooseConfiguration.html)s. /// /// # Example @@ -192,6 +226,8 @@ pub enum ControllerCommand { /// /// This command can be run at any time. MetricsJson, + /// Used by Worker instances to connect to a Manager instance. + WorkerConnect, } /// Defines details around identifying and processing ControllerCommands. @@ -207,10 +243,10 @@ impl ControllerCommand { fn details(&self) -> ControllerCommandDetails { match self { ControllerCommand::Config => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "config", description: "display load test configuration\n", - }, + }), regex: r"(?i)^config$", process_response: Box::new(|response| { if let ControllerResponseMessage::Config(config) = response { @@ -221,10 +257,10 @@ impl ControllerCommand { }), }, ControllerCommand::ConfigJson => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "config-json", description: "display load test configuration in json format\n", - }, + }), regex: r"(?i)^(configjson|config-json)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Config(config) = response { @@ -235,10 +271,10 @@ impl ControllerCommand { }), }, ControllerCommand::Exit => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "exit", description: "exit controller\n\n", - }, + }), regex: r"(?i)^(exit|quit|q)$", process_response: Box::new(|_| { let e = "received an impossible EXIT command"; @@ -247,10 +283,10 @@ impl ControllerCommand { }), }, ControllerCommand::HatchRate => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "hatchrate FLOAT", description: "set per-second rate users hatch\n", - }, + }), regex: r"(?i)^(hatchrate|hatch_rate|hatch-rate) ([0-9]*(\.[0-9]*)?){1}$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -261,10 +297,10 @@ impl ControllerCommand { }), }, ControllerCommand::Help => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "help", description: "this help\n", - }, + }), regex: r"(?i)^(help|\?)$", process_response: Box::new(|_| { let e = "received an impossible HELP command"; @@ -273,10 +309,10 @@ impl ControllerCommand { }), }, ControllerCommand::Host => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "host HOST", description: "set host to load test, (ie https://web.site/)\n", - }, + }), regex: r"(?i)^(host|hostname|host_name|host-name) ((https?)://.+)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -286,11 +322,73 @@ impl ControllerCommand { } }), }, + ControllerCommand::Manager => ControllerCommandDetails { + help: Some(ControllerHelp { + name: "manager", + description: "toggle Manager mode\n", + }), + regex: r"(?i)^manager$", + process_response: Box::new(|response| { + if let ControllerResponseMessage::Bool(true) = response { + Ok("manager mode toggled".to_string()) + } else { + Err("failed to toggle manager mode, be sure load test is idle".to_string()) + } + }), + }, + ControllerCommand::ExpectWorkers => ControllerCommandDetails { + help: Some(ControllerHelp { + name: "expect-workers INT", + description: "set number of Workers to expect\n", + }), + regex: r"(?i)^(expect|expectworkers|expect_workers|expect-workers) ([0-9]+)$", + process_response: Box::new(|response| { + if let ControllerResponseMessage::Bool(true) = response { + Ok("expect-workers configured".to_string()) + } else { + Err( + "failed to configure expect-workers, be sure load test is idle and in Manager mode" + .to_string(), + ) + } + }), + }, + ControllerCommand::NoHashCheck => ControllerCommandDetails { + help: Some(ControllerHelp { + name: "no-hash-check", + description: "toggle no-hash-check\n", + }), + regex: r"(?i)^(no-hash-check|no_hash_check|nohashcheck)$", + process_response: Box::new(|response| { + if let ControllerResponseMessage::Bool(true) = response { + Ok("no-hash-check toggled".to_string()) + } else { + Err( + "failed to toggle no-hash-check, be sure load test is idle and in Manager mode" + .to_string(), + ) + } + }), + }, + ControllerCommand::Worker => ControllerCommandDetails { + help: Some(ControllerHelp { + name: "worker", + description: "toggle Worker mode\n\n", + }), + regex: r"(?i)^worker$", + process_response: Box::new(|response| { + if let ControllerResponseMessage::Bool(true) = response { + Ok("worker mode toggled".to_string()) + } else { + Err("failed to toggle worker mode, be sure load test is idle".to_string()) + } + }), + }, ControllerCommand::Metrics => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "metrics", description: "display metrics for current load test\n", - }, + }), regex: r"(?i)^(metrics|stats)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Metrics(metrics) = response { @@ -302,11 +400,11 @@ impl ControllerCommand { }, ControllerCommand::MetricsJson => { ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "metrics-json", // No new-line as this is the last line of the help screen. description: "display metrics for current load test in json format", - }, + }), regex: r"(?i)^(metricsjson|metrics-json|statsjson|stats-json)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Metrics(metrics) = response { @@ -318,10 +416,10 @@ impl ControllerCommand { } } ControllerCommand::RunTime => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "runtime TIME", description: "set how long to run test, (ie 1h30m5s)\n", - }, + }), regex: r"(?i)^(run|runtime|run_time|run-time|) (\d+|((\d+?)h)?((\d+?)m)?((\d+?)s)?)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -332,10 +430,10 @@ impl ControllerCommand { }), }, ControllerCommand::Shutdown => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "shutdown", description: "shutdown load test and exit controller\n\n", - }, + }), regex: r"(?i)^shutdown$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -347,10 +445,10 @@ impl ControllerCommand { }, ControllerCommand::Start => { ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "start", description: "start an idle load test\n", - }, + }), regex: r"(?i)^start$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -362,10 +460,10 @@ impl ControllerCommand { } } ControllerCommand::StartupTime => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "startup-time TIME", description: "set total time to take starting users\n", - }, + }), regex: r"(?i)^(starttime|start_time|start-time|startup|startuptime|startup_time|startup-time) (\d+|((\d+?)h)?((\d+?)m)?((\d+?)s)?)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -379,10 +477,10 @@ impl ControllerCommand { }), }, ControllerCommand::Stop => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "stop", description: "stop a running load test and return to idle state\n", - }, + }), regex: r"(?i)^stop$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -393,10 +491,10 @@ impl ControllerCommand { }), }, ControllerCommand::TestPlan => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "test-plan PLAN", description: "define or replace test-plan, (ie 10,5m;10,1h;0,30s)\n\n", - }, + }), regex: r"(?i)^(testplan|test_plan|test-plan|plan) (((\d+)\s*,\s*(\d+|((\d+?)h)?((\d+?)m)?((\d+?)s)?)*;*)+)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -407,10 +505,10 @@ impl ControllerCommand { }), }, ControllerCommand::Users => ControllerCommandDetails { - help: ControllerHelp { + help: Some(ControllerHelp { name: "users INT", description: "set number of simulated users\n", - }, + }), regex: r"(?i)^(users?) (\d+)$", process_response: Box::new(|response| { if let ControllerResponseMessage::Bool(true) = response { @@ -420,6 +518,17 @@ impl ControllerCommand { } }), }, + ControllerCommand::WorkerConnect => ControllerCommandDetails { + help: None, + regex: r"^(WORKER-CONNECT) (\d+)$", + process_response: Box::new(|response| { + if let ControllerResponseMessage::Bool(true) = response { + Ok("worker connected".to_string()) + } else { + Err("failed to connect worker".to_string()) + } + }), + }, } } @@ -447,12 +556,12 @@ impl ControllerCommand { // the value. // // Returns Some(value) if the value is valid, otherwise returns None. - fn get_value(&self, command_string: &str) -> Option { + fn get_value(&self, command_string: &str) -> Option { let regex = Regex::new(self.details().regex) .expect("ControllerCommand::details().regex returned invalid regex [2]"); let caps = regex.captures(command_string).unwrap(); let value = caps.get(2).map_or("", |m| m.as_str()); - self.validate_value(value) + self.validate_value(value).map(ControllerValue::Text) } // Builds a help screen displayed when a controller receives the `help` command. @@ -467,13 +576,15 @@ impl ControllerCommand { .expect("failed to write to buffer"); // Builds help screen in the order commands are defined in the ControllerCommand enum. for command in ControllerCommand::iter() { - write!( - &mut help_text, - "{:<18} {}", - command.details().help.name, - command.details().help.description - ) - .expect("failed to write to buffer"); + if command.details().help.is_some() { + write!( + &mut help_text, + "{:<19} {}", + command.details().help.unwrap().name, + command.details().help.unwrap().description, + ) + .expect("failed to write to buffer"); + } } String::from_utf8(help_text).expect("invalid utf-8 in help text") } @@ -486,8 +597,7 @@ impl GooseAttack { &mut self, goose_attack_run_state: &mut GooseAttackRunState, ) -> Result<(), GooseError> { - // If the controller is enabled, check if we've received any - // messages. + // If the controller is enabled, check if we've received any messages. if let Some(c) = goose_attack_run_state.controller_channel_rx.as_ref() { match c.try_recv() { Ok(message) => { @@ -517,29 +627,53 @@ impl GooseAttack { ControllerCommand::Start => { // We can only start an idle load test. if self.attack_phase == AttackPhase::Idle { - self.test_plan = TestPlan::build(&self.configuration); - if self.prepare_load_test().is_ok() { - // Rebuild test plan in case any parameters have been changed. - self.set_attack_phase( - goose_attack_run_state, - AttackPhase::Increase, - ); - self.reply_to_controller( - message, - ControllerResponseMessage::Bool(true), - ); - // Reset the run state when starting a new load test. - self.reset_run_state(goose_attack_run_state).await?; - self.metrics.history.push(TestPlanHistory::step( - TestPlanStepAction::Increasing, - 0, - )); - } else { - // Do not move to Starting phase if unable to prepare load test. - self.reply_to_controller( - message, - ControllerResponseMessage::Bool(false), - ); + // Validate GooseConfiguration. + match self.configuration.validate() { + Err(e) => { + println!("Failed to start: {:?}", e); + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + } + Ok(_) => { + // Update test plan in case configuration changed. + self.test_plan = TestPlan::build(&self.configuration); + + // Update attack_mode in case configuration changed. + self.attack_mode = if self.configuration.manager { + AttackMode::Manager + } else if self.configuration.worker { + AttackMode::Worker + } else { + AttackMode::StandAlone + }; + + // @TODO: Enforce waiting for Workers to connect. + if self.prepare_load_test().is_ok() { + // Rebuild test plan in case any parameters have been changed. + self.set_attack_phase( + goose_attack_run_state, + AttackPhase::Increase, + ); + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(true), + ); + // Reset the run state when starting a new load test. + self.reset_run_state(goose_attack_run_state).await?; + self.metrics.history.push(TestPlanHistory::step( + TestPlanStepAction::Increasing, + 0, + )); + } else { + // Do not move to Starting phase if unable to prepare load test. + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + } + } } } else { self.reply_to_controller( @@ -572,6 +706,21 @@ impl GooseAttack { } // Stop the load test, and acknowledge request. ControllerCommand::Shutdown => { + // @TODO: Properly implement shutdown logic, also for Manager. + if self.configuration.worker { + if let Some(worker_tx) = goose_attack_run_state.worker_tx.as_ref() { + info!("Telling Worker to stop.",); + let _ = worker_tx.tx.send(WorkerMessage { + command: WorkerCommand::Stop, + _value: None, + }); + } else { + warn!( + "Failed to unwrap worker_tx, unable to send Stop message." + ) + } + } + // If load test is Idle, there are no metrics to display. if self.attack_phase == AttackPhase::Idle { self.metrics.display_metrics = false; @@ -583,6 +732,9 @@ impl GooseAttack { self.cancel_attack(goose_attack_run_state).await?; } + // @TODO: Special handling for a running Gaggle? + self.gaggle_phase = None; + // Shutdown after stopping. goose_attack_run_state.shutdown_after_stop = true; // Confirm shut down to Controller. @@ -599,7 +751,7 @@ impl GooseAttack { // The controller uses a regular expression to validate that // this is a valid hostname, so simply use it with further // validation. - if let Some(host) = &message.request.value { + if let Some(ControllerValue::Text(host)) = &message.request.value { info!( "changing host from {:?} to {}", self.configuration.host, host @@ -630,7 +782,7 @@ impl GooseAttack { // The controller uses a regular expression to validate that // this is a valid integer, so simply use it with further // validation. - if let Some(users) = &message.request.value { + if let Some(ControllerValue::Text(users)) = &message.request.value { // Use expect() as Controller uses regex to validate this is an integer. let new_users = usize::from_str(users) .expect("failed to convert string to usize"); @@ -735,7 +887,8 @@ impl GooseAttack { // The controller uses a regular expression to validate that // this is a valid float, so simply use it with further // validation. - if let Some(hatch_rate) = &message.request.value { + if let Some(ControllerValue::Text(hatch_rate)) = &message.request.value + { // If startup_time was already set, unset it first. if !self.configuration.startup_time.is_empty() { info!( @@ -767,7 +920,9 @@ impl GooseAttack { // The controller uses a regular expression to validate that // this is a valid startup time, so simply use it with further // validation. - if let Some(startup_time) = &message.request.value { + if let Some(ControllerValue::Text(startup_time)) = + &message.request.value + { // If hatch_rate was already set, unset it first. if let Some(hatch_rate) = &self.configuration.hatch_rate { info!("resetting hatch_rate from {} to None", hatch_rate); @@ -805,7 +960,7 @@ impl GooseAttack { // The controller uses a regular expression to validate that // this is a valid run time, so simply use it with further // validation. - if let Some(run_time) = &message.request.value { + if let Some(ControllerValue::Text(run_time)) = &message.request.value { info!( "changing run_time from {:?} to {}", self.configuration.run_time, run_time @@ -825,7 +980,7 @@ impl GooseAttack { } } ControllerCommand::TestPlan => { - if let Some(value) = &message.request.value { + if let Some(ControllerValue::Text(value)) = &message.request.value { match value.parse::() { Ok(t) => { // Switch the configuration to use the test plan. @@ -896,6 +1051,208 @@ impl GooseAttack { ); } } + ControllerCommand::Manager => { + if self.attack_phase == AttackPhase::Idle { + // Toggle Manager mode. + self.configuration.manager = if self.configuration.manager { + info!("manager mode disabled"); + false + } else { + // Also disable Worker mode if enabled. + if self.configuration.worker { + info!("worker mode disabled"); + self.configuration.worker = false; + } + info!("manager mode enabled"); + true + }; + // Update Gaggle configuration options. + self.configuration.configure_manager(&self.defaults); + self.configuration.configure_worker(&self.defaults); + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(true), + ); + } else { + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + } + } + ControllerCommand::ExpectWorkers => { + // The controller uses a regular expression to validate that + // this is a valid run time, so simply use it with further + // validation. + if let Some(ControllerValue::Text(expect_workers)) = + &message.request.value + { + // Can only change expect_workers when the load test is idle and in Manager mode. + if self.attack_phase == AttackPhase::Idle + && self.configuration.manager + { + // Use expect() as Controller uses regex to validate this is an integer. + let expect_workers = usize::from_str(expect_workers) + .expect("failed to convert string to usize"); + if expect_workers == 0 { + info!( + "changing expect_workers from {:?} to None", + self.configuration.expect_workers + ); + self.configuration.expect_workers = None; + } else { + info!( + "changing expect_workers from {:?} to {}", + self.configuration.expect_workers, expect_workers + ); + self.configuration.expect_workers = Some(expect_workers); + } + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(true), + ); + } else { + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + if self.configuration.manager { + info!("unable to configure expect_workers when load test is not idle"); + } else { + info!("unable to configure expect_workers when not in manager mode"); + } + } + } else { + warn!( + "Controller didn't provide expect_workers: {:#?}", + &message.request + ); + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + } + } + ControllerCommand::NoHashCheck => { + if self.attack_phase == AttackPhase::Idle && self.configuration.manager + { + self.configuration.no_hash_check = + if self.configuration.no_hash_check { + info!("disabled no_hash_check"); + false + } else { + info!("enabled no_hash_check"); + true + }; + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(true), + ); + } else { + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + if self.configuration.manager { + info!("unable to configure expect_workers when load test is not idle"); + } else { + info!("unable to configure expect_workers when not in manager mode"); + } + } + } + ControllerCommand::Worker => { + if self.attack_phase == AttackPhase::Idle { + // Toggle Worker mode. + self.configuration.worker = if self.configuration.worker { + info!("worker mode disabled"); + false + } else { + // Disable Manager mode if enabled. + if self.configuration.manager { + info!("manager mode disabled"); + self.configuration.manager = false; + } + info!("worker mode enabled"); + true + }; + // Update Gaggle configuration options. + self.configuration.configure_manager(&self.defaults); + self.configuration.configure_worker(&self.defaults); + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(true), + ); + } else { + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + } + } + ControllerCommand::WorkerConnect => { + // Verify running in Manager mode. + if self.configuration.manager { + // Verify expecting more Workers to connect. + if goose_attack_run_state.gaggle_workers + < self.configuration.expect_workers.unwrap_or(0) + { + if let Some(manager_tx) = + goose_attack_run_state.manager_tx.as_ref() + { + if let Some(ControllerValue::Socket(worker_connection)) = + message.request.value + { + // Use expect() as Controller uses regex to validate this is an integer. + let worker_hash = + u64::from_str(&worker_connection.hash) + .expect("failed to convert string to usize"); + if worker_hash != self.metrics.hash + && !self.configuration.no_hash_check + { + /* + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + */ + warn!("WorkerConnect request ignored, Worker hash {} does not match Manager hash {}, enable --no-hash-check to ignore.", worker_hash, self.metrics.hash) + } else { + if worker_hash != self.metrics.hash { + warn!("Ignoring that Worker hash {} does not match Manager hash {} because --no-hash-check is enabled.", worker_hash, self.metrics.hash) + } else { + warn!("Valid hash: {}", worker_hash); + } + goose_attack_run_state.gaggle_workers += 1; + info!( + "Worker {} of {} connected.", + goose_attack_run_state.gaggle_workers, + self.configuration.expect_workers.unwrap_or(0) + ); + // Pass the Telnet socket to the Manager thread. + let _ = manager_tx.tx.send(ManagerMessage { + command: ManagerCommand::WorkerJoinRequest, + value: Some(worker_connection.socket), + }); + } + } else { + warn!("Whoops !?"); + //panic!("Whoops!?"); + } + } else { + panic!("WorkerConnect failure, failed to reference manager_tx.") + } + } else { + // @TODO: Can we return a helpful error? + self.reply_to_controller( + message, + ControllerResponseMessage::Bool(false), + ); + warn!("WorkerConnect request ignored, all expected Workers already connected.") + } + } else { + warn!("WorkerConnect request ignored, not in --manager mode.") + } + } // These messages shouldn't be received here. ControllerCommand::Help | ControllerCommand::Exit => { warn!("Unexpected command: {:?}", &message.request); @@ -1071,7 +1428,7 @@ pub(crate) struct ControllerHelp<'a> { /// recognized command worked correctly. pub(crate) struct ControllerCommandDetails<'a> { // The name and description of the controller command. - help: ControllerHelp<'a>, + help: Option>, // A [regular expression](https://docs.rs/regex/1.5.5/regex/struct.Regex.html) for // matching the command and option value. regex: &'a str, @@ -1086,7 +1443,20 @@ pub(crate) struct ControllerRequestMessage { /// The command that is being sent to the parent. pub command: ControllerCommand, /// An optional value that is being sent to the parent. - pub value: Option, + pub value: Option, +} + +#[derive(Debug)] +pub(crate) struct WorkerConnection { + hash: String, + socket: tokio::net::TcpStream, +} + +/// Allows multiple types to be sent to the parent process. +#[derive(Debug)] +pub(crate) enum ControllerValue { + Text(String), + Socket(WorkerConnection), } /// An enumeration of all messages the parent can reply back to the controller thread. @@ -1251,6 +1621,40 @@ impl ControllerState { if let Ok(command_string) = self.get_command_string(buf).await { // Extract the command and value in a generic way. if let Ok(request_message) = self.get_match(command_string.trim()).await { + let hash = if let Some(ControllerValue::Text(hash)) = + request_message.value.as_ref() + { + // Clone the value. + hash.to_string() + } else { + unreachable!("Hash must exist, enforced by regex"); + }; + // Workers using Telnet socket to connect to the Manager. + if request_message.command == ControllerCommand::WorkerConnect { + info!("Worker instance connecting ..."); + if request_message.command == ControllerCommand::WorkerConnect { + // A Worker is trying to connect, send the connection to the Parent. + if self + .channel_tx + .try_send(ControllerRequest { + response_channel: None, + client_id: self.thread_id, + request: ControllerRequestMessage { + command: ControllerCommand::WorkerConnect, + value: Some(ControllerValue::Socket( + WorkerConnection { hash, socket }, + )), + }, + }) + .is_err() + { + warn!("failed to send Worker socket to parent"); + }; + break; + // ELSE? + } + } + // Act on the commmand received. if self.execute_command(&mut socket, request_message).await { // If execute_command returns true, it's time to exit. @@ -1346,7 +1750,7 @@ impl ControllerState { // Use FromStr to convert &str to ControllerCommand. let command: ControllerCommand = ControllerCommand::from_str(command_string)?; // Extract value if there is one, otherwise will be None. - let value: Option = command.get_value(command_string); + let value: Option = command.get_value(command_string); Ok(ControllerRequestMessage { command, value }) } diff --git a/src/gaggle/common.rs b/src/gaggle/common.rs new file mode 100644 index 00000000..f12cd3d1 --- /dev/null +++ b/src/gaggle/common.rs @@ -0,0 +1,27 @@ +pub struct GaggleEcho { + _sequence: u32, + _acknowledge: Option, +} + +/// Commands sent to/from Works and Managers to control a Gaggle. +pub enum GaggleCommand { + ManagerShuttingDown, + Shutdown, + WorkerShuttingDown, + /// Notification that a Worker is standing by and ready to start the load test. + WorkerIsReady, +} + +pub enum GagglePhase { + WaitingForWorkers, +} + +pub enum GaggleCommands { + Control(GaggleCommand), + Echo(GaggleEcho), + // Not Gaggle-specific + //Error(GooseErrorMetrics), + //Request(GooseRequestMetrics), + //Scenario(ScenarioMetrics), + //Transaction(TransactionMetrics), +} diff --git a/src/gaggle/manager.rs b/src/gaggle/manager.rs index 6520daa7..a5b2674d 100644 --- a/src/gaggle/manager.rs +++ b/src/gaggle/manager.rs @@ -1,21 +1,358 @@ -//use crate::{GooseAttack, GooseConfiguration, GooseUserCommand, CANCELED, SHUTDOWN_GAGGLE}; -use crate::{GooseAttack, GooseError}; +/// Manager-specific code. +use std::time::Duration; +use tokio::io::AsyncWriteExt; -impl GooseAttack { - /// Main manager loop. +use crate::config::{GooseConfigure, GooseValue}; +use crate::util; +use crate::{GooseConfiguration, GooseDefaults, GooseError}; + +/// Optional join handle for manager thread, if enabled. +pub(crate) type ManagerJoinHandle = tokio::task::JoinHandle>; +/// Optional unbounded sender to manager thread, if enabled. +pub(crate) type ManagerTx = flume::Sender; + +// Tracks the join_handle and send socket for Worker instance. +#[derive(Debug)] +pub(crate) struct ManagerConnection { + pub(crate) _join_handle: ManagerJoinHandle, + pub(crate) tx: ManagerTx, +} + +#[derive(Debug)] +pub(crate) enum ManagerCommand { + // Gaggle is starting, wait for all Worker instances to connect. + WaitForWorkers, + // Worker is requesting to join the Gaggle. + WorkerJoinRequest, + // Exit + _Exit, +} + +/// This structure is used to control the Manager process. +#[derive(Debug)] +pub(crate) struct ManagerMessage { + /// The command that is being sent to the Manager. + pub(crate) command: ManagerCommand, + /// An optional socket if this is a Worker connecting to a Manager. + pub(crate) value: Option, +} + +struct ManagerRunState { + /// Workers + workers: Vec, + /// Whether or not a message has been displayed indicating the Manager is currently idle. + idle_status_displayed: bool, + /// Which phase the Manager is currently operating in. + phase: ManagerPhase, + /// This variable accounts for time spent doing things which is then subtracted from + /// the time sleeping to avoid an unintentional drift in events that are supposed to + /// happen regularly. + drift_timer: tokio::time::Instant, + /// Receive messages from the Controller. + controller_rx: flume::Receiver, +} +impl ManagerRunState { + fn new(controller_rx: flume::Receiver) -> ManagerRunState { + ManagerRunState { + workers: Vec::new(), + idle_status_displayed: false, + phase: ManagerPhase::Idle, + drift_timer: tokio::time::Instant::now(), + controller_rx, + } + } +} + +enum ManagerPhase { + /// No Workers are connected, Gaggle can be configured. + Idle, + /// Workers are connecting to the Manager, Gaggle can not be reconfigured. + WaitForWorkers, + /// All Workers are connected and the load test is ready. + Active, +} + +impl GooseConfiguration { + pub(crate) fn configure_manager(&mut self, defaults: &GooseDefaults) { + // Re-configure `users`, in case the AttackMode was changed. + self.users = self.get_value(vec![ + // Use --users if set and not on Worker. + GooseValue { + value: self.users, + filter: self.worker, + message: "--users", + }, + // Otherwise use GooseDefault if set and not on Worker. + GooseValue { + value: defaults.users, + filter: defaults.users.is_none() || self.worker, + message: "default users", + }, + // Otherwise use detected number of CPUs if not on Worker. + GooseValue { + value: Some(num_cpus::get()), + filter: self.worker || self.test_plan.is_some(), + message: "users defaulted to number of CPUs", + }, + ]); + // Configure `expect_workers`. + self.expect_workers = self.get_value(vec![ + // Use --expect-workers if configured. + GooseValue { + value: self.expect_workers, + filter: self.expect_workers.is_none(), + message: "expect_workers", + }, + // Use GooseDefault if not already set and not Worker. + GooseValue { + value: defaults.expect_workers, + filter: self.expect_workers.is_none() && self.worker, + message: "expect_workers", + }, + ]); + + // Configure `no_hash_check`. + self.no_hash_check = self + .get_value(vec![ + // Use --no-hash_check if set. + GooseValue { + value: Some(self.no_hash_check), + filter: !self.no_hash_check, + message: "no_hash_check", + }, + // Use GooseDefault if not already set and not Worker. + GooseValue { + value: defaults.no_hash_check, + filter: defaults.no_hash_check.is_none() || self.worker, + message: "no_hash_check", + }, + ]) + .unwrap_or(false); + } + + /// Validate configured [`GooseConfiguration`] values. + pub(crate) fn validate_manager(&self) -> Result<(), GooseError> { + // Validate nothing incompatible is enabled with --manager. + if self.manager { + // Don't allow --manager and --worker together. + // @TODO: Implement this! + if self.worker { + return Err(GooseError::InvalidOption { + option: "`configuration.manager` && `configuration.worker`".to_string(), + value: "true".to_string(), + detail: "Goose can not run as both Manager and Worker".to_string(), + }); + } else if !self.debug_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.debug_log`".to_string(), + value: self.debug_log.clone(), + detail: "`configuration.debug_log` can not be set on the Manager.".to_string(), + }); + } else if !self.error_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.error_log`".to_string(), + value: self.error_log.clone(), + detail: "`configuration.error_log` can not be set on the Manager.".to_string(), + }); + } else if !self.request_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.request_log`".to_string(), + value: self.request_log.clone(), + detail: "`configuration.request_log` can not be set on the Manager." + .to_string(), + }); + } else if !self.transaction_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.transaction_log`".to_string(), + value: self.transaction_log.clone(), + detail: "`configuration.transaction_log` can not be set on the Manager." + .to_string(), + }); + } else if !self.scenario_log.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.scenario_log`".to_string(), + value: self.scenario_log.clone(), + detail: "`configuration.scenario_log` can not be set on the Manager." + .to_string(), + }); + } else if !self.report_file.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.report_file`".to_string(), + value: self.report_file.to_string(), + detail: "`configuration.report_file` can not be set on the Manager." + .to_string(), + }); + } else if self.no_granular_report { + return Err(GooseError::InvalidOption { + option: "`configuration.no_granular_report`".to_string(), + value: true.to_string(), + detail: "`configuration.no_granular_report` can not be set on the Manager." + .to_string(), + }); + } else if self.no_debug_body { + return Err(GooseError::InvalidOption { + option: "`configuration.no_debug_body`".to_string(), + value: true.to_string(), + detail: "`configuration.no_debug_body` can not be set on the Manager." + .to_string(), + }); + // Can not set `throttle_requests` on Manager. + } else if self.throttle_requests > 0 { + return Err(GooseError::InvalidOption { + option: "`configuration.throttle_requests`".to_string(), + value: self.throttle_requests.to_string(), + detail: "`configuration.throttle_requests` can not be set on the Manager." + .to_string(), + }); + } + if let Some(expect_workers) = self.expect_workers.as_ref() { + // Must expect at least 1 Worker when running as Manager. + if expect_workers == &0 { + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers must be set to at least 1." + .to_string(), + }); + } + + // Must be at least 1 user per worker. + if let Some(users) = self.users.as_ref() { + if expect_workers > users { + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers can not be set to a value larger than `configuration.users`.".to_string(), + }); + } + } else { + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers can not be set without setting `configuration.users`.".to_string(), + }); + } + } else { + return Err(GooseError::InvalidOption { + option: "configuration.manager".to_string(), + value: true.to_string(), + detail: "Manager mode requires --expect-workers be configured".to_string(), + }); + } + } else { + // Don't allow `expect_workers` if not running as Manager. + if let Some(expect_workers) = self.expect_workers.as_ref() { + return Err(GooseError::InvalidOption { + option: "`configuration.expect_workers`".to_string(), + value: expect_workers.to_string(), + detail: "`configuration.expect_workers` can not be set unless on the Manager." + .to_string(), + }); + } + } + + Ok(()) + } + + // Spawn a Manager thread, provide a channel so it can be controlled by parent and/or Control;er thread. + pub(crate) async fn setup_manager(&mut self) -> Option<(ManagerJoinHandle, ManagerTx)> { + // There's no setup necessary if Manager mode is not enabled. + if !self.manager { + return None; + } + + // Create an unbounded channel to allow the controller to manage the Manager thread. + let (manager_tx, manager_rx): ( + flume::Sender, + flume::Receiver, + ) = flume::unbounded(); + + let configuration = self.clone(); + let manager_handle = + tokio::spawn(async move { configuration.manager_main(manager_rx).await }); + + // Return manager_tx thread for the (optional) controller thread. + Some((manager_handle, manager_tx)) + } + + /// Manager thread, coordinates Worker threads. pub(crate) async fn manager_main( - mut self, - ) -> Result { - // The GooseAttackRunState is used while spawning and running the - // GooseUser threads that generate the load test. - // @TODO: should this be replaced with a GooseAttackManagerState ? - let mut goose_attack_run_state = self - .initialize_attack() - .await - .expect("failed to initialize GooseAttackRunState"); - - assert!(goose_attack_run_state.controller_channel_rx.is_some()); - - Ok(self) + self: GooseConfiguration, + receiver: flume::Receiver, + ) -> Result<(), GooseError> { + // Initialze the Manager run state, used for the lifetime of this Manager instance. + let mut manager_run_state = ManagerRunState::new(receiver); + + loop { + debug!("top of manager loop..."); + + match manager_run_state.phase { + // Display message when entering ManagerPhase::Idle, otherwise sleep waiting for a + // message from Parent or Controller thread. + ManagerPhase::Idle => { + if !manager_run_state.idle_status_displayed { + info!("Gaggle mode enabled, Manager is idle."); + manager_run_state.idle_status_displayed = true; + } + } + ManagerPhase::WaitForWorkers => { + // @TODO: Keepalive? Timeout? + } + ManagerPhase::Active => { + // @TODO: Actually start the load test. + } + } + + // Process messages received from parent or Controller thread. + let sleep_duration = match manager_run_state.controller_rx.try_recv() { + Ok(message) => { + match message.command { + ManagerCommand::WaitForWorkers => { + let expect_workers = self.expect_workers.unwrap_or(0); + if expect_workers == 1 { + info!("Manager is waiting for 1 Worker."); + } else { + info!("Manager is waiting for {} Workers.", expect_workers); + } + manager_run_state.phase = ManagerPhase::WaitForWorkers; + } + ManagerCommand::WorkerJoinRequest => { + let mut socket = message.value.expect("failed to unwrap TcpSocket"); + if socket.write_all("OK\r\n".as_bytes()).await.is_err() { + warn!("failed to write data to socker"); + } + // Store Worker socket for ongoing communications. + manager_run_state.workers.push(socket); + + if let Some(expect_workers) = self.expect_workers { + if manager_run_state.workers.len() == self.expect_workers.unwrap() { + info!( + "All {} Workers have connected, starting the load test.", + expect_workers + ); + manager_run_state.phase = ManagerPhase::Active; + } + } + } + ManagerCommand::_Exit => { + info!("Manager is exiting."); + break; + } + } + // Message received, fall through but do not sleep. + Duration::ZERO + } + // No message, fall through and sleep to try again later. + Err(_) => Duration::from_millis(500), + }; + + // Wake up twice a second to handle messages and allow for a quick shutdown if the + // load test is canceled during startup. + debug!("sleeping {:?}...", sleep_duration); + manager_run_state.drift_timer = + util::sleep_minus_drift(sleep_duration, manager_run_state.drift_timer).await; + } + + Ok(()) } -} \ No newline at end of file +} diff --git a/src/gaggle/worker.rs b/src/gaggle/worker.rs new file mode 100644 index 00000000..dc533fe0 --- /dev/null +++ b/src/gaggle/worker.rs @@ -0,0 +1,515 @@ +/// Worker-specific code. +use std::io; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; +use tokio::time::{sleep, Duration}; + +use crate::config::{GooseConfigure, GooseValue}; +use crate::metrics::GooseCoordinatedOmissionMitigation; +use crate::util; +use crate::{GooseConfiguration, GooseDefaults, GooseError}; + +/// Optional join handle for worker thread, if enabled. +pub(crate) type WorkerJoinHandle = tokio::task::JoinHandle>; +/// Optional unbounded sender to worker thread, if enabled. +pub(crate) type WorkerTx = flume::Sender; + +type SocketBuffer = [u8; 1024]; + +const MAX_CONNECTION_ATTEMPTS: u8 = 25; + +#[derive(Debug)] +pub(crate) enum WorkerCommand { + ConnectToManager, + Stop, +} + +/// This structure is used to control the Worker process. +#[derive(Debug)] +pub(crate) struct WorkerMessage { + /// The command that is being sent to the Worker. + pub(crate) command: WorkerCommand, + /// An optional value that is being sent to the Worker. + pub(crate) _value: Option, +} + +// Tracks the join_handle and send socket for Worker instance. +#[derive(Debug)] +pub(crate) struct WorkerConnection { + pub(crate) _join_handle: WorkerJoinHandle, + pub(crate) tx: WorkerTx, +} + +enum ConnectionState { + WaitForPrompt, + WaitForOk, + Connected, +} + +struct WorkerRunState { + /// Whether or not a message has been displayed indicating the Worker is currently idle. + idle_status_displayed: bool, + /// Whether or Worker has successfully connected to Manager instance. + connected_to_manager: bool, + /// @TODO: Connection status + connection_state: Option, + /// A counter tracking how many times the Worker has attempted to connect to the Manager. + connection_attempts: u8, + /// Which phase the Worker is currently operating in. + phase: WorkerPhase, + /// Whether or not a message has been displayed indicating the Worker is ready and waiting. + waiting_status_displayed: bool, + /// This variable accounts for time spent doing things which is then subtracted from + /// the time sleeping to avoid an unintentional drift in events that are supposed to + /// happen regularly. + drift_timer: tokio::time::Instant, + /// Receive messages from the Controller. + controller_rx: flume::Receiver, + /// Connection to Manager. + stream: Option, +} +impl WorkerRunState { + fn new(controller_rx: flume::Receiver) -> WorkerRunState { + WorkerRunState { + idle_status_displayed: false, + connected_to_manager: false, + connection_state: None, + connection_attempts: 0, + phase: WorkerPhase::Idle, + waiting_status_displayed: false, + drift_timer: tokio::time::Instant::now(), + controller_rx, + stream: None, + } + } +} + +enum WorkerPhase { + /// Not connected to Manager, Worker instance is stand-alone and idle. + Idle, + /// Trying to connect to the Manager instance. + ConnectingToManager, + /// Connected to Manager instance, waiting for the go-ahead to start load test. + WaitingForManager, + /// Active load test. + _Active, + Exit, +} + +impl GooseConfiguration { + pub(crate) fn configure_worker(&mut self, defaults: &GooseDefaults) { + // Set `manager_host` on Worker. + self.manager_host = self + .get_value(vec![ + // Use --manager-host if configured. + GooseValue { + value: Some(self.manager_host.to_string()), + filter: self.manager_host.is_empty(), + message: "manager_host", + }, + // Otherwise use default if set and on Worker. + GooseValue { + value: defaults.manager_host.clone(), + filter: defaults.manager_host.is_none() || !self.worker, + message: "manager_host", + }, + // Otherwise default to 127.0.0.1 if on Worker. + GooseValue { + value: Some("127.0.0.1".to_string()), + filter: !self.worker, + message: "manager_host", + }, + ]) + .unwrap_or_else(|| "".to_string()); + + // Set `manager_port` on Worker. + self.manager_port = self + .get_value(vec![ + // Use --manager-port if configured. + GooseValue { + value: Some(self.manager_port), + filter: self.manager_port == 0, + message: "manager_port", + }, + // Otherwise use default if set and on Worker. + GooseValue { + value: defaults.manager_port, + filter: defaults.manager_port.is_none() || !self.worker, + message: "manager_port", + }, + // Otherwise default to DEFAULT_GAGGLE_PORT if on Worker. + GooseValue { + value: Some(crate::DEFAULT_TELNET_PORT.to_string().parse().unwrap()), + filter: !self.worker, + message: "manager_port", + }, + ]) + .unwrap_or(0); + } + + /// Validate configured [`GooseConfiguration`] values. + pub(crate) fn validate_worker(&self) -> Result<(), GooseError> { + // Validate nothing incompatible is enabled with --worker. + if self.worker { + // Can't set `users` on Worker. + if self.users.is_some() { + return Err(GooseError::InvalidOption { + option: "configuration.users".to_string(), + value: self.users.as_ref().unwrap().to_string(), + detail: "`configuration.users` can not be set together with the `configuration.worker`.".to_string(), + }); + // Can't set `startup_time` on Worker. + } else if self.startup_time != "0" { + return Err(GooseError::InvalidOption { + option: "`configuration.startup_time".to_string(), + value: self.startup_time.to_string(), + detail: "`configuration.startup_time` can not be set in Worker mode." + .to_string(), + }); + // Can't set `run_time` on Worker. + } else if self.run_time != "0" { + return Err(GooseError::InvalidOption { + option: "`configuration.run_time".to_string(), + value: self.run_time.to_string(), + detail: "`configuration.run_time` can not be set in Worker mode.".to_string(), + }); + // Can't set `hatch_rate` on Worker. + } else if self.hatch_rate.is_some() { + return Err(GooseError::InvalidOption { + option: "`configuration.hatch_rate`".to_string(), + value: self.hatch_rate.as_ref().unwrap().to_string(), + detail: "`configuration.hatch_rate` can not be set in Worker mode.".to_string(), + }); + // Can't set `timeout` on Worker. + } else if self.timeout.is_some() { + return Err(GooseError::InvalidOption { + option: "`configuration.timeout`".to_string(), + value: self.timeout.as_ref().unwrap().to_string(), + detail: "`configuration.timeout` can not be set in Worker mode.".to_string(), + }); + // Can't set `running_metrics` on Worker. + } else if self.running_metrics.is_some() { + return Err(GooseError::InvalidOption { + option: "`configuration.running_metrics".to_string(), + value: self.running_metrics.as_ref().unwrap().to_string(), + detail: "`configuration.running_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_reset_metrics` on Worker. + } else if self.no_reset_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_reset_metrics".to_string(), + value: self.no_reset_metrics.to_string(), + detail: "`configuration.no_reset_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_metrics` on Worker. + } else if self.no_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_metrics".to_string(), + value: self.no_metrics.to_string(), + detail: "`configuration.no_metrics` can not be set in Worker mode.".to_string(), + }); + // Can't set `no_transaction_metrics` on Worker. + } else if self.no_transaction_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_transaction_metrics".to_string(), + value: self.no_transaction_metrics.to_string(), + detail: "`configuration.no_transaction_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_scenario_metrics` on Worker. + } else if self.no_scenario_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_scenario_metrics".to_string(), + value: self.no_scenario_metrics.to_string(), + detail: "`configuration.no_scenario_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_print_metrics` on Worker. + } else if self.no_print_metrics { + return Err(GooseError::InvalidOption { + option: "`configuration.no_print_metrics".to_string(), + value: self.no_print_metrics.to_string(), + detail: "`configuration.no_print_metrics` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_error_summary` on Worker. + } else if self.no_error_summary { + return Err(GooseError::InvalidOption { + option: "`configuration.no_error_summary".to_string(), + value: self.no_error_summary.to_string(), + detail: "`configuration.no_error_summary` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_status_codes` on Worker. + } else if self.no_status_codes { + return Err(GooseError::InvalidOption { + option: "`configuration.no_status_codes".to_string(), + value: self.no_status_codes.to_string(), + detail: "`configuration.no_status_codes` can not be set in Worker mode." + .to_string(), + }); + // Can't set `no_gzip` on Worker. + } else if self.no_gzip { + return Err(GooseError::InvalidOption { + option: "`configuration.no_gzip`".to_string(), + value: true.to_string(), + detail: "`configuration.no_gzip` can not be set in Worker mode.".to_string(), + }); + } else if self + .co_mitigation + .as_ref() + .unwrap_or(&GooseCoordinatedOmissionMitigation::Disabled) + != &GooseCoordinatedOmissionMitigation::Disabled + { + return Err(GooseError::InvalidOption { + option: "`configuration.co_mitigation`".to_string(), + value: format!("{:?}", self.co_mitigation.as_ref().unwrap()), + detail: "`configuration.co_mitigation` can not be set in Worker mode." + .to_string(), + }); + // Must set `manager_host` on Worker. + } else if self.manager_host.is_empty() { + return Err(GooseError::InvalidOption { + option: "`configuration.manager_host`".to_string(), + value: self.manager_host.clone(), + detail: "`configuration.manager_host` must be set when in Worker mode." + .to_string(), + }); + // Must set `manager_port` on Worker. + } else if self.manager_port == 0 { + return Err(GooseError::InvalidOption { + option: "`configuration.manager_port`".to_string(), + value: self.manager_port.to_string(), + detail: "`configuration.manager_port` must be set when in Worker mode." + .to_string(), + }); + // Can not set `sticky_follow` on Worker. + } else if self.sticky_follow { + return Err(GooseError::InvalidOption { + option: "`configuration.sticky_follow`".to_string(), + value: true.to_string(), + detail: "`configuration.sticky_follow` can not be set in Worker mode." + .to_string(), + }); + // Can not set `no_hash_check` on Worker. + } else if self.no_hash_check { + return Err(GooseError::InvalidOption { + option: "`configuration.no_hash_check`".to_string(), + value: true.to_string(), + detail: "`configuration.no_hash_check` can not be set in Worker mode." + .to_string(), + }); + } + } + + Ok(()) + } + + // Spawn a Worker thread, provide a channel so it can be controlled by parent and/or Control;er thread. + pub(crate) async fn setup_worker(&mut self, hash: u64) -> Option<(WorkerJoinHandle, WorkerTx)> { + // There's no setup necessary if Worker mode is not enabled. + if !self.worker { + return None; + } + + // Create an unbounded channel to allow the controller to manage the Worker thread. + let (worker_tx, worker_rx): (flume::Sender, flume::Receiver) = + flume::unbounded(); + + let configuration = self.clone(); + let worker_handle = + tokio::spawn(async move { configuration.worker_main(worker_rx, hash).await }); + + // Return worker_tx thread for the (optional) controller thread. + Some((worker_handle, worker_tx)) + } + + /// Worker thread, coordiantes with Manager instanec. + pub(crate) async fn worker_main( + self: GooseConfiguration, + receiver: flume::Receiver, + hash: u64, + ) -> Result<(), GooseError> { + // Initialze the Worker run state, used for the lifetime of this Worker instance. + let mut worker_run_state = WorkerRunState::new(receiver); + + // Sleep 1 second to give Manager time to start, if started at the same time. + sleep(Duration::from_secs(1)).await; + + loop { + debug!("top of worker loop..."); + + // @TODO: How to detect that the socket is dropped? + // @TODO: Add a timeout. + + match worker_run_state.phase { + // Display message when entering WorkerPhase::Idle, otherwise sleep waiting for a + // message from Parent or Controller thread. + WorkerPhase::Idle => { + if !worker_run_state.idle_status_displayed { + info!("Gaggle mode enabled, Worker is idle."); + worker_run_state.idle_status_displayed = true; + } + } + WorkerPhase::ConnectingToManager => { + if !worker_run_state.connected_to_manager { + if worker_run_state.connection_attempts == 0 + || worker_run_state.connection_attempts % 5 == 0 + { + info!( + "Worker connecting to {}:{}.", + self.manager_host, self.manager_port + ); + } + + if worker_run_state.connection_attempts >= MAX_CONNECTION_ATTEMPTS { + // @TODO: If --no-autostart go back to idle mode. + warn!("failed to connect to Manager"); + break; + } + + // Only try so many times before giving up. + worker_run_state.connection_attempts += 1; + + // Actually try to connect. + worker_run_state.stream = match TcpStream::connect(format!( + "{}:{}", + self.manager_host, self.manager_port + )) + .await + { + Ok(s) => { + worker_run_state.connected_to_manager = true; + worker_run_state.connection_state = + Some(ConnectionState::WaitForPrompt); + Some(s) + } + Err(e) => { + if worker_run_state.connection_attempts % 5 == 0 { + warn!( + "Worker failed to connect to Manager ({} of {} attempts): {}", + worker_run_state.connection_attempts, + MAX_CONNECTION_ATTEMPTS, + e + ); + } + None + } + }; + } + if let Some(stream) = worker_run_state.stream.as_mut() { + if let Ok(Some(message)) = read_buffer(stream) { + if let Some(connection_state) = + worker_run_state.connection_state.as_ref() + { + match connection_state { + ConnectionState::WaitForPrompt => { + if message.starts_with("goose>") { + info!("Got `goose>` prompt."); + worker_run_state.connection_state = + Some(ConnectionState::WaitForOk); + stream + .write_all( + format!("WORKER-CONNECT {}\n", hash).as_bytes(), + ) + .await?; + } else { + panic!("Failed to get `goose>` prompt: @TODO: handle this more gracefully."); + } + } + ConnectionState::WaitForOk => { + if message.starts_with("OK") { + info!("Got OK."); + worker_run_state.connection_state = + Some(ConnectionState::Connected); + worker_run_state.phase = WorkerPhase::WaitingForManager; + } else { + panic!("Failed to get OK: @TODO: handle this more gracefully."); + } + } + _ => { + unreachable!("We should not be here."); + } + } + } + } + }; + } + WorkerPhase::WaitingForManager => { + if !worker_run_state.waiting_status_displayed { + info!("Standing by, waiting for Manager to start the load test..."); + worker_run_state.waiting_status_displayed = true; + } + } + WorkerPhase::_Active => { + info!("Let's get this party started!"); + } + WorkerPhase::Exit => { + info!("Worker is exiting."); + break; + } + } + + // Process messages received from parent or Controller thread. + let sleep_duration = match worker_run_state.controller_rx.try_recv() { + Ok(message) => { + match message.command { + WorkerCommand::ConnectToManager => { + worker_run_state.phase = WorkerPhase::ConnectingToManager; + } + WorkerCommand::Stop => { + worker_run_state.phase = WorkerPhase::Exit; + } + } + // Message received, fall through but do not sleep. + Duration::ZERO + } + // No message, fall through and sleep to try again later. + Err(_) => Duration::from_millis(500), + }; + + // Wake up twice a second to handle messages and allow for a quick shutdown if the + // load test is canceled during startup. + debug!("sleeping {:?}...", sleep_duration); + worker_run_state.drift_timer = + util::sleep_minus_drift(sleep_duration, worker_run_state.drift_timer).await; + } + + Ok(()) + } +} + +fn read_buffer(stream: &TcpStream) -> Result, &str> { + let mut socket_buffer: SocketBuffer = [0; 1024]; + + match stream.try_read(&mut socket_buffer) { + Ok(n) => { + if n == 0 { + return Err("Worker disconnected"); + } + let message = match std::str::from_utf8(&socket_buffer) { + Ok(m) => { + if let Some(c) = m.lines().next() { + c + } else { + "" + } + } + Err(e) => { + info!("ignoring unexpected input from Manager: {}", e); + "" + } + }; + Ok(Some(message.to_string())) + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + Err("io::ErrorKind::WouldBlock - @TODO wtf is this?") + } + Err(e) => { + warn!("unexpected read error: {}", e); + Ok(None) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index b515b599..efbcb255 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,9 @@ mod throttle; mod user; pub mod util; pub mod gaggle { + pub mod common; pub mod manager; + pub mod worker; } use gumdrop::Options; @@ -71,6 +73,9 @@ use tokio::fs::File; use crate::config::{GooseConfiguration, GooseDefaults}; use crate::controller::{ControllerProtocol, ControllerRequest}; +use crate::gaggle::common::GagglePhase; +use crate::gaggle::manager::{ManagerCommand, ManagerConnection, ManagerMessage}; +use crate::gaggle::worker::{WorkerCommand, WorkerConnection, WorkerMessage}; use crate::goose::{GooseUser, GooseUserCommand, Scenario, Transaction}; use crate::graph::GraphData; use crate::logger::{GooseLoggerJoinHandle, GooseLoggerTx}; @@ -319,8 +324,12 @@ struct GooseAttackRunState { /// Unbounded receiver used by [`GooseUser`](./goose.GooseUser.html) threads to notify /// the parent if they shut themselves down (for example if `--iterations` is reached). shutdown_rx: flume::Receiver, + /// Optional unbounded sender for Manager thread, if enabled. + manager_tx: Option, + /// Optional unbounded sender for Worker thread, if enabled. + worker_tx: Option, /// Optional unbounded receiver for logger thread, if enabled. - logger_handle: GooseLoggerJoinHandle, + logger_rx: GooseLoggerJoinHandle, /// Optional unbounded sender from all [`GooseUser`](./goose/struct.GooseUser.html)s /// to logger thread, if enabled. all_threads_logger_tx: GooseLoggerTx, @@ -355,6 +364,8 @@ struct GooseAttackRunState { shutdown_after_stop: bool, /// Whether or not the load test is currently canceling. canceling: bool, + /// How many Workers are connected, only non-zero in Manager mode. + gaggle_workers: usize, } /// Global internal state for the load test. @@ -377,6 +388,8 @@ pub struct GooseAttack { attack_mode: AttackMode, /// Which phase the load test is currently operating in. attack_phase: AttackPhase, + // If running in a Gaggle, which phase the load test is currently operating in. + gaggle_phase: Option, /// Defines the order [`Scenario`](./goose/struct.Scenario.html)s and /// [`Transaction`](./goose/struct.Transaction.html)s are allocated. scheduler: GooseScheduler, @@ -414,6 +427,7 @@ impl GooseAttack { configuration, attack_mode: AttackMode::Undefined, attack_phase: AttackPhase::Idle, + gaggle_phase: None, scheduler: GooseScheduler::RoundRobin, started: None, test_plan: TestPlan::new(), @@ -450,6 +464,7 @@ impl GooseAttack { configuration, attack_mode: AttackMode::Undefined, attack_phase: AttackPhase::Idle, + gaggle_phase: None, scheduler: GooseScheduler::RoundRobin, started: None, test_plan: TestPlan::new(), @@ -964,22 +979,60 @@ impl GooseAttack { self.metrics.hash = s.finish(); debug!("hash: {}", self.metrics.hash); - self = match self.attack_mode { - AttackMode::Manager => { - self.manager_main().await?; - panic!("attempted to start in AttackMode::Manager"); - }, - AttackMode::Worker => { - panic!("attempted to start in AttackMode::Worker"); - }, - AttackMode::StandAlone => { - self.start_attack().await? - }, - AttackMode::Undefined => { - panic!("attempted to start in AttackMode::Undefined"); - }, + // Launch Manager thread if enabled. + let manager = match self.configuration.setup_manager().await { + Some((h, t)) => { + self.gaggle_phase = Some(GagglePhase::WaitingForWorkers); + Some(ManagerConnection { + _join_handle: h, + tx: t, + }) + } + None => None, + }; + + // Launch Worker thread if enabled. + let worker = match self.configuration.setup_worker(self.metrics.hash).await { + Some((h, t)) => { + self.gaggle_phase = Some(GagglePhase::WaitingForWorkers); + Some(WorkerConnection { + _join_handle: h, + tx: t, + }) + } + None => None, }; + // When --no-autostart not enabled, automatically start ... + if !self.configuration.no_autostart { + // Autostart Manager. + if self.configuration.manager { + if let Some(manager) = manager.as_ref() { + let _ = manager.tx.send(ManagerMessage { + command: ManagerCommand::WaitForWorkers, + value: None, + }); + } else { + // @TODO: Review how this is possible, provide better error handling. + panic!("Failed to start in Manager mode.") + } + } + // Autostart Worker. + if self.configuration.worker { + if let Some(connection) = worker.as_ref() { + let _ = connection.tx.send(WorkerMessage { + command: WorkerCommand::ConnectToManager, + _value: None, + }); + } else { + // @TODO: Review how this is possible, provide better error handling. + panic!("Failed to start in Worker mode.") + } + } + } + + self = self.start_attack(manager, worker).await?; + if self.metrics.display_metrics { info!( "printing final metrics after {} seconds...", @@ -1257,7 +1310,11 @@ impl GooseAttack { // Create a GooseAttackRunState object and do all initialization required // to start a [`GooseAttack`](./struct.GooseAttack.html). - async fn initialize_attack(&mut self) -> Result { + async fn initialize_attack( + &mut self, + manager_tx: Option, + worker_tx: Option, + ) -> Result { trace!("initialize_attack"); // Create a single channel used to send metrics from GooseUser threads @@ -1289,7 +1346,9 @@ impl GooseAttack { all_threads_shutdown_tx, metrics_rx, shutdown_rx, - logger_handle: None, + manager_tx, + worker_tx, + logger_rx: None, all_threads_logger_tx: None, throttle_threads_tx: None, parent_to_throttle_tx: None, @@ -1304,6 +1363,7 @@ impl GooseAttack { all_users_spawned: false, shutdown_after_stop: !self.configuration.no_autostart, canceling: false, + gaggle_workers: 0, }; // Catch ctrl-c to allow clean shutdown to display metrics. @@ -1537,7 +1597,7 @@ impl GooseAttack { debug!("all users exited"); // If the logger thread is enabled, tell it to flush and exit. - if goose_attack_run_state.logger_handle.is_some() { + if goose_attack_run_state.logger_rx.is_some() { if let Err(e) = goose_attack_run_state .all_threads_logger_tx .clone() @@ -1548,7 +1608,7 @@ impl GooseAttack { }; // Take logger out of the GooseAttackRunState object so it can be // consumed by tokio::join!(). - let logger = std::mem::take(&mut goose_attack_run_state.logger_handle); + let logger = std::mem::take(&mut goose_attack_run_state.logger_rx); let _ = tokio::join!(logger.unwrap()); } @@ -1679,12 +1739,18 @@ impl GooseAttack { let elapsed = self.step_elapsed() as usize; // Reset the test_plan to stop all users quickly. - self.test_plan.steps = vec![ - // Record how many active users there are currently. - (goose_attack_run_state.active_users, elapsed), - // Record how long the attack ran in this step. - (0, 0), - ]; + self.test_plan.steps = if goose_attack_run_state.active_users > 0 { + // there is an active load test running. + vec![ + // Record how many active users there are currently. + (goose_attack_run_state.active_users, elapsed), + // Record how long the attack ran in this step. + (0, 0), + ] + } else { + // There is no active load test running. + vec![(0, 0)] + }; // Reset the current step to what was happening when canceled. self.test_plan.current = 0; @@ -1694,6 +1760,9 @@ impl GooseAttack { // Advance to the final decrease phase. self.advance_test_plan(goose_attack_run_state); + // @TODO: Special handling for a running Gaggle? + self.gaggle_phase = None; + // Load test isn't just decreasing, it's canceling. self.metrics .history @@ -1759,7 +1828,7 @@ impl GooseAttack { // If enabled, spawn a logger thread. let (logger_handle, all_threads_logger_tx) = self.configuration.setup_loggers(&self.defaults).await?; - goose_attack_run_state.logger_handle = logger_handle; + goose_attack_run_state.logger_rx = logger_handle; goose_attack_run_state.all_threads_logger_tx = all_threads_logger_tx; // If enabled, spawn a throttle thread. @@ -1786,17 +1855,47 @@ impl GooseAttack { } // Called internally in local-mode and gaggle-mode. - async fn start_attack(mut self) -> Result { + async fn start_attack( + mut self, + manager_tx: Option, + worker_tx: Option, + ) -> Result { // The GooseAttackRunState is used while spawning and running the // GooseUser threads that generate the load test. let mut goose_attack_run_state = self - .initialize_attack() + .initialize_attack(manager_tx, worker_tx) .await .expect("failed to initialize GooseAttackRunState"); // The Goose parent process GooseAttack loop runs until Goose shuts down. Goose enters // the loop in AttackPhase::Idle, and exits in AttackPhase::Shutdown. loop { + // Check if running in Gaggle mode. + if let Some(gaggle_phase) = self.gaggle_phase.as_ref() { + match gaggle_phase { + GagglePhase::WaitingForWorkers => { + debug!("Gaggle mode, waiting for workers..."); + + // Gracefully exit loop if ctrl-c is caught. + self.exit_gracefully(&mut goose_attack_run_state).await?; + + // Check if a Controller has made a request. + self.handle_controller_requests(&mut goose_attack_run_state) + .await?; + + // @TODO: determine if enough Workers have connected. + + // Wake up twice a second to check for events, and otherwise keep + // waiting for workers. + goose_attack_run_state.drift_timer = util::sleep_minus_drift( + time::Duration::from_millis(500), + goose_attack_run_state.drift_timer, + ) + .await; + continue; + } + } + } match self.attack_phase { // In the Idle phase the Goose configuration can be changed by a Controller, // and otherwise nothing happens but sleeping an checking for messages. @@ -1864,42 +1963,56 @@ impl GooseAttack { self.handle_controller_requests(&mut goose_attack_run_state) .await?; - let mut message = goose_attack_run_state.shutdown_rx.try_recv(); - while message.is_ok() { - goose_attack_run_state - .users_shutdown - .insert(message.expect("failed to wrap OK message")); + // Gracefully exit loop if ctrl-c is caught. + self.exit_gracefully(&mut goose_attack_run_state).await?; + } - // In Stand-alone mode, all users are started. - if goose_attack_run_state.users_shutdown.len() == self.test_plan.total_users() { - self.cancel_attack(&mut goose_attack_run_state).await?; - } + Ok(self) + } - message = goose_attack_run_state.shutdown_rx.try_recv(); + // Check if ctrl-c was caught or shutdown message was received, and if so exit gracefully. + async fn exit_gracefully( + &mut self, + goose_attack_run_state: &mut GooseAttackRunState, + ) -> Result<(), GooseError> { + let mut message = goose_attack_run_state.shutdown_rx.try_recv(); + while message.is_ok() { + goose_attack_run_state + .users_shutdown + .insert(message.expect("failed to wrap OK message")); + + // In Gaggle mode, the Worker starts a fraction of the users. + // @TODO + + // In Stand-alone mode, all users are started. + if goose_attack_run_state.users_shutdown.len() == self.test_plan.total_users() { + self.cancel_attack(goose_attack_run_state).await?; } - // Gracefully exit loop if ctrl-c is caught. - if self.attack_phase != AttackPhase::Shutdown - && !goose_attack_run_state.canceling - && *CANCELED.read().unwrap() - { - // Shutdown after stopping as the load test was canceled. - goose_attack_run_state.shutdown_after_stop = true; - - // No metrics to display when sitting idle, so disable. - if self.attack_phase == AttackPhase::Idle { - self.metrics.display_metrics = false; - } + message = goose_attack_run_state.shutdown_rx.try_recv(); + } - // Cleanly stop the load test. - self.cancel_attack(&mut goose_attack_run_state).await?; + // Gracefully exit loop if ctrl-c is caught. + if self.attack_phase != AttackPhase::Shutdown + && !goose_attack_run_state.canceling + && *CANCELED.read().unwrap() + { + // Shutdown after stopping as the load test was canceled. + goose_attack_run_state.shutdown_after_stop = true; - // Load test is actively canceling. - goose_attack_run_state.canceling = true; + // No metrics to display when sitting idle, so disable. + if self.attack_phase == AttackPhase::Idle { + self.metrics.display_metrics = false; } + + // Cleanly stop the load test. + self.cancel_attack(goose_attack_run_state).await?; + + // Load test is actively canceling. + goose_attack_run_state.canceling = true; } - Ok(self) + Ok(()) } } diff --git a/src/metrics.rs b/src/metrics.rs index 9e3c348e..e34c28ff 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -2650,7 +2650,7 @@ impl GooseAttack { pub(crate) async fn sync_metrics( &mut self, goose_attack_run_state: &mut GooseAttackRunState, - flush: bool, + _flush: bool, ) -> Result<(), GooseError> { if !self.configuration.no_metrics { // Update timers if displaying running metrics. @@ -2663,8 +2663,11 @@ impl GooseAttack { goose_attack_run_state.display_running_metrics = true; } }; + /* + * @TODO: Why was this disabled? // Load messages from user threads until the receiver queue is empty. self.receive_metrics(goose_attack_run_state, flush).await?; + */ } // If enabled, display running metrics after sync diff --git a/src/util.rs b/src/util.rs index 746321b0..7f1e215f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -101,7 +101,7 @@ pub async fn sleep_minus_drift( ) -> tokio::time::Instant { match duration.checked_sub(drift.elapsed()) { Some(delay) if delay.as_nanos() > 0 => tokio::time::sleep(delay).await, - _ => info!("sleep_minus_drift: drift was greater than or equal to duration, not sleeping"), + _ => debug!("sleep_minus_drift: drift was greater than or equal to duration, not sleeping"), }; tokio::time::Instant::now() } diff --git a/tests/controller.rs b/tests/controller.rs index a66ebc3b..e7801346 100644 --- a/tests/controller.rs +++ b/tests/controller.rs @@ -669,6 +669,21 @@ async fn run_standalone_test(test_type: TestType) { } } } + ControllerCommand::Manager => { + log::warn!("TODO: ControllerCommand::Manager"); + } + ControllerCommand::ExpectWorkers => { + log::warn!("TODO: ControllerCommand::ExpectWorkers"); + } + ControllerCommand::NoHashCheck => { + log::warn!("TODO: ControllerCommand::NoHashCheck"); + } + ControllerCommand::Worker => { + log::warn!("TODO: ControllerCommand::Worker"); + } + ControllerCommand::WorkerConnect => { + log::warn!("TODO: ControllerCommand::WorkerConnect"); + } } // Flush the buffer. test_state.buf = [0; 2048]; From b04588ee6cf2325c00125c7959e89ba2e43591f1 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 15 May 2023 14:44:39 +0200 Subject: [PATCH 4/7] only expect hash for worker messages --- src/controller.rs | 59 ++++++++++++++++++++++---------------------- src/gaggle/worker.rs | 2 +- tests/controller.rs | 8 +++--- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/controller.rs b/src/controller.rs index 7fc0a5d7..5da77225 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1621,38 +1621,39 @@ impl ControllerState { if let Ok(command_string) = self.get_command_string(buf).await { // Extract the command and value in a generic way. if let Ok(request_message) = self.get_match(command_string.trim()).await { - let hash = if let Some(ControllerValue::Text(hash)) = - request_message.value.as_ref() - { - // Clone the value. - hash.to_string() - } else { - unreachable!("Hash must exist, enforced by regex"); - }; // Workers using Telnet socket to connect to the Manager. if request_message.command == ControllerCommand::WorkerConnect { info!("Worker instance connecting ..."); - if request_message.command == ControllerCommand::WorkerConnect { - // A Worker is trying to connect, send the connection to the Parent. - if self - .channel_tx - .try_send(ControllerRequest { - response_channel: None, - client_id: self.thread_id, - request: ControllerRequestMessage { - command: ControllerCommand::WorkerConnect, - value: Some(ControllerValue::Socket( - WorkerConnection { hash, socket }, - )), - }, - }) - .is_err() - { - warn!("failed to send Worker socket to parent"); - }; - break; - // ELSE? - } + // Workers must include hash when connecting. + let hash = if let Some(ControllerValue::Text(hash)) = + request_message.value.as_ref() + { + // Clone the value. + hash.to_string() + } else { + // @TODO: Don't panic, instead cancel the connection ... + panic!("Hash must exist, enforced by regex"); + }; + + // A Worker is trying to connect, send the connection to the Parent. + if self + .channel_tx + .try_send(ControllerRequest { + response_channel: None, + client_id: self.thread_id, + request: ControllerRequestMessage { + command: ControllerCommand::WorkerConnect, + value: Some(ControllerValue::Socket( + WorkerConnection { hash, socket }, + )), + }, + }) + .is_err() + { + warn!("failed to send Worker socket to parent"); + }; + break; + // ELSE? } // Act on the commmand received. diff --git a/src/gaggle/worker.rs b/src/gaggle/worker.rs index dc533fe0..6d62635e 100644 --- a/src/gaggle/worker.rs +++ b/src/gaggle/worker.rs @@ -120,7 +120,7 @@ impl GooseConfiguration { message: "manager_host", }, ]) - .unwrap_or_else(|| "".to_string()); + .unwrap_or_default(); // Set `manager_port` on Worker. self.manager_port = self diff --git a/tests/controller.rs b/tests/controller.rs index e7801346..5a254a12 100644 --- a/tests/controller.rs +++ b/tests/controller.rs @@ -206,8 +206,8 @@ async fn run_standalone_test(test_type: TestType) { if let Some(stream) = test_state.telnet_stream.as_mut() { let _ = match stream.read(&mut test_state.buf).await { Ok(data) => data, - Err(_) => { - panic!("ERROR: server disconnected!"); + Err(e) => { + panic!("ERROR: server disconnected: {}", e); } }; response = str::from_utf8(&test_state.buf).unwrap(); @@ -233,7 +233,7 @@ async fn run_standalone_test(test_type: TestType) { unreachable!(); } - //println!("{:?}: {}", test_state.command, response); + println!("{:?}: {}", test_state.command, response); match test_state.command { ControllerCommand::Exit => { match test_state.step { @@ -751,7 +751,7 @@ async fn update_state(test_state: Option, test_type: &TestType) -> Te } else { // Connect to telnet controller. let telnet_stream = match test_type { - TestType::Telnet => Some(TcpStream::connect("127.0.0.1:5116").await.unwrap()), + TestType::Telnet => Some(TcpStream::connect("127.0.0.1:5115").await.unwrap()), _ => None, }; From 07e1af939bd53b30f25acac74c6506b9febe16c6 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 15 May 2023 15:29:13 +0200 Subject: [PATCH 5/7] fix bad merge --- src/gaggle/manager.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/gaggle/manager.rs b/src/gaggle/manager.rs index a5b2674d..22a77eba 100644 --- a/src/gaggle/manager.rs +++ b/src/gaggle/manager.rs @@ -74,6 +74,16 @@ enum ManagerPhase { impl GooseConfiguration { pub(crate) fn configure_manager(&mut self, defaults: &GooseDefaults) { + // Determine how many CPUs are available. + let default_users = match std::thread::available_parallelism() { + Ok(ap) => Some(ap.get()), + Err(e) => { + // Default to 1 user if unable to detect number of CPUs. + info!("failed to detect available_parallelism: {}", e); + Some(1) + } + }; + // Re-configure `users`, in case the AttackMode was changed. self.users = self.get_value(vec![ // Use --users if set and not on Worker. @@ -90,7 +100,7 @@ impl GooseConfiguration { }, // Otherwise use detected number of CPUs if not on Worker. GooseValue { - value: Some(num_cpus::get()), + value: default_users, filter: self.worker || self.test_plan.is_some(), message: "users defaulted to number of CPUs", }, From 18492216ab2be2efc516bf502e64f42216a7f536 Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Mon, 15 May 2023 16:21:58 +0200 Subject: [PATCH 6/7] fix tests --- src/metrics.rs | 5 +---- tests/controller.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/metrics.rs b/src/metrics.rs index e34c28ff..9e3c348e 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -2650,7 +2650,7 @@ impl GooseAttack { pub(crate) async fn sync_metrics( &mut self, goose_attack_run_state: &mut GooseAttackRunState, - _flush: bool, + flush: bool, ) -> Result<(), GooseError> { if !self.configuration.no_metrics { // Update timers if displaying running metrics. @@ -2663,11 +2663,8 @@ impl GooseAttack { goose_attack_run_state.display_running_metrics = true; } }; - /* - * @TODO: Why was this disabled? // Load messages from user threads until the receiver queue is empty. self.receive_metrics(goose_attack_run_state, flush).await?; - */ } // If enabled, display running metrics after sync diff --git a/tests/controller.rs b/tests/controller.rs index 5a254a12..78b3c634 100644 --- a/tests/controller.rs +++ b/tests/controller.rs @@ -233,7 +233,7 @@ async fn run_standalone_test(test_type: TestType) { unreachable!(); } - println!("{:?}: {}", test_state.command, response); + //println!("{:?}: {}", test_state.command, response); match test_state.command { ControllerCommand::Exit => { match test_state.step { From 2e25743365e43ccd1d40a7a0130e772f757c08ff Mon Sep 17 00:00:00 2001 From: Jeremy Andrews Date: Tue, 16 May 2023 16:36:00 +0200 Subject: [PATCH 7/7] initial manual testing of gaggle via telnet --- tests/gaggle.rs | 247 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 tests/gaggle.rs diff --git a/tests/gaggle.rs b/tests/gaggle.rs new file mode 100644 index 00000000..5f69db3d --- /dev/null +++ b/tests/gaggle.rs @@ -0,0 +1,247 @@ +use gumdrop::Options; +use httpmock::{Method::GET, Mock, MockServer}; +use std::{str, time}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; + +use goose::config::GooseConfiguration; +use goose::prelude::*; +//use goose::util::parse_timespan; + +mod common; + +// Paths used in load tests performed during these tests. +const INDEX_PATH: &str = "/"; +const ABOUT_PATH: &str = "/about.html"; + +// Indexes to the above paths. +const INDEX_KEY: usize = 0; +const ABOUT_KEY: usize = 1; + +// Used to build a test plan. +const STARTUP_TIME: &str = "2s"; +const USERS: usize = 100; +const RUNNING_TIME: &str = "2s"; + +// There are multiple test variations in this file. +#[derive(Clone, PartialEq)] +enum TestType { + // Expect a single worker. + Workers1, + // Expect multiple workers/ + _Workers2, +} + +// State machine for tracking Controller state during tests. +struct TestState { + // A buffer for the telnet Controller. + buf: [u8; 2048], + // A TCP socket for the telnet Controller. + telnet_stream: TcpStream, +} + +// Test transaction. +pub async fn get_index(user: &mut GooseUser) -> TransactionResult { + let _goose = user.get(INDEX_PATH).await?; + Ok(()) +} + +// Test transaction. +pub async fn get_about(user: &mut GooseUser) -> TransactionResult { + let _goose = user.get(ABOUT_PATH).await?; + Ok(()) +} + +// All tests in this file run against the following common endpoints. +fn setup_mock_server_endpoints(server: &MockServer) -> Vec { + vec![ + // First set up INDEX_PATH, store in vector at INDEX_KEY. + server.mock(|when, then| { + when.method(GET).path(INDEX_PATH); + then.status(200); + }), + // Next set up ABOUT_PATH, store in vector at ABOUT_KEY. + server.mock(|when, then| { + when.method(GET).path(ABOUT_PATH); + then.status(200); + }), + ] +} + +// Build appropriate configuration for these tests. Normally this also calls +// common::build_configuration() to get defaults most all tests needs, but +// for these tests we don't want a default configuration. We keep the signature +// the same to simplify reuse, accepting the MockServer but not using it. +fn common_build_configuration(_server: &MockServer, custom: &mut Vec<&str>) -> GooseConfiguration { + // Common elements in all our tests. + let mut configuration: Vec<&str> = + //vec!["--quiet", "--no-autostart", "--co-mitigation", "disabled"]; + vec!["--no-autostart", "--co-mitigation", "disabled"]; + + // Custom elements in some tests. + configuration.append(custom); + + // Parse these options to generate a GooseConfiguration. + GooseConfiguration::parse_args_default(&configuration) + .expect("failed to parse options and generate a configuration") +} + +// Helper to confirm all variations generate appropriate results. +fn validate_one_scenario( + _goose_metrics: &GooseMetrics, + mock_endpoints: &[Mock], + configuration: &GooseConfiguration, + _test_type: TestType, +) { + //println!("goose_metrics: {:#?}", goose_metrics); + //println!("configuration: {:#?}", configuration); + + assert!(configuration.manager); + assert!(!configuration.worker); + + // Confirm that we did not actually load the mock endpoints. + assert!(mock_endpoints[INDEX_KEY].hits() == 0); + assert!(mock_endpoints[ABOUT_KEY].hits() == 0); + + // Get index and about out of goose metrics. + /* + let index_metrics = goose_metrics + .requests + .get(&format!("GET {}", INDEX_PATH)) + .unwrap(); + let about_metrics = goose_metrics + .requests + .get(&format!("GET {}", ABOUT_PATH)) + .unwrap(); + + // There should not have been any failures during this test. + assert!(index_metrics.fail_count == 0); + assert!(about_metrics.fail_count == 0); + */ + + // Host was not configured at start time. + assert!(configuration.host.is_empty()); + + // The load test was manually shut down instead of running to completion. + //assert!(goose_metrics.duration >= parse_timespan(RUNNING_TIME)); + + // Increasing, Maintaining, Increasing, Maintaining, Decreasing, Maintaining, Canceling, + // Finished, Finished. + // Finished is logged twice because `stop` puts the test to Idle, and then `shutdown` + // actually shuts down the test, and both are logged as "Finished". + //assert!(goose_metrics.history.len() == 9); +} + +// Returns the appropriate scenario needed to build these tests. +fn get_transactions() -> Scenario { + scenario!("LoadTest") + .register_transaction(transaction!(get_index).set_weight(2).unwrap()) + .register_transaction(transaction!(get_about).set_weight(1).unwrap()) +} + +// Helper to run all standalone tests. +async fn run_standalone_test(test_type: TestType) { + // Start the mock server. + let server = MockServer::start(); + let server_url = server.base_url(); + + // Setup the endpoints needed for this test on the mock server. + let mock_endpoints = setup_mock_server_endpoints(&server); + + let mut configuration_flags = match &test_type { + // Manager with 1 Worker process. + TestType::Workers1 => vec!["--manager", "--expect-workers", "1"], + // Manager with multiple Worker processes. + TestType::_Workers2 => vec!["--manager", "--expect-workers", "2"], + }; + + // Keep a copy for validation. + let validate_test_type = test_type.clone(); + + // Build common configuration elements. + let configuration = common_build_configuration(&server, &mut configuration_flags); + + // Create a new thread from which to test the Controller. + let _controller_handle = tokio::spawn(async move { + // Sleep a half a second allowing the GooseAttack to start. + tokio::time::sleep(time::Duration::from_millis(500)).await; + + let commands = [ + // Run load test against mock server. + (format!("host {}\r\n", server_url), "host configured"), + // Start USERS in STARTUP_TIME, run USERS for RUNNING_TIME, then shut down as quickly as possible. + ( + format!( + "test_plan {},{};{},{};{},{}\r\n", + USERS, STARTUP_TIME, USERS, RUNNING_TIME, 0, 0 + ), + "test-plan configured", + ), + // Start the Manager process. + //("start\r\n".to_string(), "load test started"), + // All done, shut down. + ("shutdown\r\n".to_string(), "load test shut down"), + ]; + + // Maintain a test state for looping through commands. + let mut test_state = TestState { + buf: [0; 2048], + telnet_stream: TcpStream::connect("127.0.0.1:5115").await.unwrap(), + }; + let response = get_response(&mut test_state).await; + assert!(response.starts_with("goose> ")); + + for command in commands { + log::info!(">-> Sending request: `{}`", command.0); + let response = make_request(&mut test_state, &command.0).await; + log::info!("<-< Received response: `{}`", response); + assert!(response.starts_with(command.1)); + } + }); + + // Run the Goose Attack. Add timeout to be sure Goose exits even if the controller tests fail. + let goose_metrics = tokio::time::timeout( + tokio::time::Duration::from_secs(60), + common::run_load_test( + common::build_load_test(configuration.clone(), vec![get_transactions()], None, None), + None, + ), + ) + .await + .expect("load test timed out"); + + // Confirm that the load test ran correctly. + validate_one_scenario( + &goose_metrics, + &mock_endpoints, + &configuration, + validate_test_type, + ); +} + +// Helper to send request to Controller and get response back. +async fn make_request(test_state: &mut TestState, command: &str) -> String { + //println!("making request: {}", command); + match test_state.telnet_stream.write_all(command.as_bytes()).await { + Ok(_) => (), + Err(e) => panic!("failed to send {} command: {}", command, e), + }; + get_response(test_state).await.to_string() +} + +// Helper to read response from Controller. +async fn get_response(test_state: &mut TestState) -> &str { + let _ = match test_state.telnet_stream.read(&mut test_state.buf).await { + Ok(data) => data, + Err(e) => { + panic!("ERROR: server disconnected: {}", e); + } + }; + str::from_utf8(&test_state.buf).unwrap() +} + +// Test a load test simulating 1 Worker. +#[tokio::test(flavor = "multi_thread", worker_threads = 8)] +async fn test_worker1() { + run_standalone_test(TestType::Workers1).await; +}