Repository: tag1consulting/goose (Rust)

Issue:
On Gaggle Worker it's defaulting to a hatch-rate of 1 causing error
Attempting to start Goose in Gaggle mode, on the Worker side we get an unexpected error:

```bash
$ cargo run --example drupal_loadtest --release --features gaggle -- --worker -v
    Finished release [optimized] target(s) in 0.09s
     Running `target/release/examples/drupal_loadtest --worker -v`
05:47:13 [ INFO] Output verbosity level: INFO
05:47:13 [ INFO] Logfile verbosity level: WARN
Error: InvalidOption { option: "--hatch-rate", value: "1", detail: "The --hatch-rate option can not be set together with the --worker flag." }
```

This regression can be worked around by setting `--hatch-rate` to `0`:

```bash
$ cargo run --example drupal_loadtest --release --features gaggle -- --worker -v --hatch-rate 0
    Finished release [optimized] target(s) in 0.10s
     Running `target/release/examples/drupal_loadtest --worker -v --hatch-rate 0`
05:49:32 [ INFO] Output verbosity level: INFO
05:49:32 [ INFO] Logfile verbosity level: WARN
05:49:32 [ INFO] worker connecting to manager at tcp://127.0.0.1:5115
05:49:32 [ INFO] waiting for instructions from manager
05:49:33 [ INFO] initializing user states...
05:49:33 [ INFO] [1] initialized 4 user states
05:49:33 [ INFO] [1] waiting for go-ahead from manager
```


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3c72d070..f203102b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 # Changelog
 
 ## 0.10.1-dev
+ - rework `hatch_rate` to be stored in an `Option<usize>` as it can be `None` on a Worker
 
 ## 0.10.0 Sep 13, 2020
  - default to resetting statistics, disable with `--no-reset-stats`, display spawning statistics before resetting
diff --git a/src/lib.rs b/src/lib.rs
index 72bf76a6f..55857e562 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1366,44 +1366,44 @@ impl GooseAttack {
 
     // Configure how quickly to hatch Goose Users.
     fn set_hatch_rate(&mut self) -> Result<(), GooseError> {
-        // Setting --hatch-rate with --worker is not allowed.
-        if self.configuration.hatch_rate > 0 && self.attack_mode == GooseMode::Worker {
-            return Err(GooseError::InvalidOption {
-                option: "--hatch-rate".to_string(),
-                value: self.configuration.hatch_rate.to_string(),
-                detail: "The --hatch-rate option can not be set together with the --worker flag."
-                    .to_string(),
-            });
-        }
+        // Check if --hash-rate is set.
+        if let Some(hatch_rate) = self.configuration.hatch_rate {
+            // Setting --hatch-rate with --worker is not allowed.
+            if self.attack_mode == GooseMode::Worker {
+                return Err(GooseError::InvalidOption {
+                    option: "--hatch-rate".to_string(),
+                    value: hatch_rate.to_string(),
+                    detail:
+                        "The --hatch-rate option can not be set together with the --worker flag."
+                            .to_string(),
+                });
+            }
 
-        // Use default for hatch_rate if set and not on Worker.
-        if self.configuration.hatch_rate == 0 {
-            self.configuration.hatch_rate = if let Some(hatch_rate) = self.defaults.hatch_rate {
-                // On Worker hash_rate comes from the Manager.
-                if self.attack_mode == GooseMode::Worker {
-                    0
-                // Otherwise use default.
-                } else {
-                    hatch_rate
-                }
+            // Setting --hatch-rate of 0 is not allowed.
+            if hatch_rate == 0 {
+                return Err(GooseError::InvalidOption {
+                    option: "--hatch-rate".to_string(),
+                    value: "0".to_string(),
+                    detail: "The --hatch-rate option must be set to at least 1.".to_string(),
+                });
+            }
+        // If not, check if a default hatch_rate is set.
+        } else if let Some(default_hatch_rate) = self.defaults.hatch_rate {
+            // On Worker hatch_rate comes from the Manager.
+            if self.attack_mode == GooseMode::Worker {
+                self.configuration.hatch_rate = None;
+            // Otherwise use default.
             } else {
-                // On Worker hash_rate comes from the Manager.
-                if self.attack_mode == GooseMode::Worker {
-                    0
-                }
-                // Otherwise hash_rate is required.
-                else {
-                    return Err(GooseError::InvalidOption {
-                        option: "--hatch-rate".to_string(),
-                        value: self.configuration.hatch_rate.to_string(),
-                        detail: "The --hatch-rate option must be set to at least 1.".to_string(),
-                    });
-                }
+                self.configuration.hatch_rate = Some(default_hatch_rate);
             }
+        // If not and if not running on Worker, default to 1.
+        } else if self.attack_mode != GooseMode::Worker {
+            self.configuration.hatch_rate = Some(1);
         }
 
-        if self.configuration.hatch_rate > 0 {
-            info!("hatch_rate = {}", self.configuration.hatch_rate);
+        // Verbose output.
+        if let Some(hatch_rate) = self.configuration.hatch_rate {
+            info!("hatch_rate = {}", hatch_rate);
         }
 
         Ok(())
@@ -1942,7 +1942,8 @@ impl GooseAttack {
         // Hatch users at hatch_rate per second, or one every 1 / hatch_rate fraction of a second.
         let sleep_duration;
         if self.attack_mode != GooseMode::Worker {
-            let sleep_float = 1.0 / self.configuration.hatch_rate as f32;
+            // Hatch rate required to get here, so unwrap() is safe.
+            let sleep_float = 1.0 / self.configuration.hatch_rate.unwrap() as f32;
             sleep_duration = time::Duration::from_secs_f32(sleep_float);
         } else {
             sleep_duration = time::Duration::from_secs_f32(0.0);
@@ -2675,7 +2676,15 @@ impl GooseDefaultType<usize> for GooseAttack {
         match key {
             // Set valid defaults.
             GooseDefault::Users => self.defaults.users = Some(value),
-            GooseDefault::HatchRate => self.defaults.hatch_rate = Some(value),
+            GooseDefault::HatchRate => {
+                if value > 0 {
+                    self.defaults.hatch_rate = Some(value);
+                } else {
+                    panic!(
+                        "set_default(GooseDefault::HatchRate, 0) invalid, must be set to at least 1"
+                    );
+                }
+            }
             GooseDefault::RunTime => self.defaults.run_time = Some(value),
             GooseDefault::LogLevel => self.defaults.log_level = Some(value as u8),
             GooseDefault::Verbose => self.defaults.verbose = Some(value as u8),
@@ -2771,9 +2780,9 @@ pub struct GooseConfiguration {
     /// Sets concurrent users (default: number of CPUs)
     #[options(short = "u")]
     pub users: Option<usize>,
-    /// Sets per-second user hatch rate
-    #[options(short = "r", default = "1", meta = "RATE")]
-    pub hatch_rate: usize,
+    /// Sets per-second user hatch rate (default: 1)
+    #[options(short = "r", meta = "RATE")]
+    pub hatch_rate: Option<usize>,
     /// Stops after (30s, 20m, 3h, 1h30m, etc)
     #[options(short = "t", meta = "TIME")]
     pub run_time: String,
diff --git a/src/manager.rs b/src/manager.rs
index b4d9f6937..ddb1144f9 100644
--- a/src/manager.rs
+++ b/src/manager.rs
@@ -267,9 +267,9 @@ pub async fn manager_main(mut goose_attack: GooseAttack) -> GooseAttack {
         .metrics
         .initialize_task_metrics(&goose_attack.task_sets, &goose_attack.configuration);
 
-    // Update metrics, which doesn't happen automatically on the Master as we
-    // don't invoke launch_users.
-    let maximum_hatched = goose_attack.configuration.hatch_rate * goose_attack.run_time;
+    // Update metrics, which doesn't happen automatically on the Master as we don't
+    // invoke launch_users. Hatch rate is required here so unwrap() is safe.
+    let maximum_hatched = goose_attack.configuration.hatch_rate.unwrap() * goose_attack.run_time;
     if maximum_hatched < goose_attack.users {
         goose_attack.metrics.users = maximum_hatched;
     } else {
diff --git a/src/worker.rs b/src/worker.rs
index 1ccae97f0..0364d13aa 100644
--- a/src/worker.rs
+++ b/src/worker.rs
@@ -209,7 +209,8 @@ pub async fn worker_main(goose_attack: &GooseAttack) -> GooseAttack {
 
     // Divide hatch_rate amongst all workers.
     let hatch_rate = 1.0
-        / (worker_goose_attack.configuration.hatch_rate as f32 / (config.expect_workers as f32));
+        // Hatch rate is required here, so unwrap() is safe.
+        / (worker_goose_attack.configuration.hatch_rate.unwrap() as f32 / (config.expect_workers as f32));
     info!(
         "[{}] prepared to start 1 user every {:.2} seconds",
         worker_id, hatch_rate,


Test patch:
diff --git a/tests/defaults.rs b/tests/defaults.rs
index 61a14628b..2feb5115b 100644
--- a/tests/defaults.rs
+++ b/tests/defaults.rs
@@ -94,7 +94,7 @@ fn test_defaults() {
     // Unset options set in common.rs so set_default() is instead used.
     config.users = None;
     config.run_time = "".to_string();
-    config.hatch_rate = 0;
+    config.hatch_rate = None;
     let host = std::mem::take(&mut config.host);
 
     let goose_metrics = crate::GooseAttack::initialize_with_config(config)
@@ -220,7 +220,7 @@ fn test_gaggle_defaults() {
     // Unset options set in common.rs so set_default() is instead used.
     configuration.users = None;
     configuration.run_time = "".to_string();
-    configuration.hatch_rate = 0;
+    configuration.hatch_rate = None;
     let host = std::mem::take(&mut configuration.host);
 
     // Launch workers in their own threads, storing the thread handle.
@@ -310,7 +310,7 @@ fn test_defaults_no_metrics() {
     // Unset options set in common.rs so set_default() is instead used.
     config.users = None;
     config.run_time = "".to_string();
-    config.hatch_rate = 0;
+    config.hatch_rate = None;
 
     let goose_metrics = crate::GooseAttack::initialize_with_config(config)
         .unwrap()
diff --git a/tests/gaggle.rs b/tests/gaggle.rs
index e9eec716e..0860cc522 100644
--- a/tests/gaggle.rs
+++ b/tests/gaggle.rs
@@ -46,7 +46,7 @@ fn test_gaggle() {
     // Unset options set in common.rs as they can't be set on the Worker.
     worker_configuration.users = None;
     worker_configuration.run_time = "".to_string();
-    worker_configuration.hatch_rate = 0;
+    worker_configuration.hatch_rate = None;
 
     for _ in 0..2 {
         let configuration = worker_configuration.clone();