Skip to content

Commit

Permalink
Merge pull request #264 from jeremyandrews/scheduler
Browse files Browse the repository at this point in the history
respect GooseScheduler when allocating GooseTasks
  • Loading branch information
jeremyandrews authored May 15, 2021
2 parents 79f0ff2 + d101aac commit 0e043d3
Show file tree
Hide file tree
Showing 8 changed files with 688 additions and 396 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- update dependencies: `itertools` to `0.10`, `simplelog` to `0.10`, `url` to `2`
- update `nng` dependency for optional `gaggle` feature
- simplify `examples/umami` regex when parsing form
- allow configuration of algorithm for allocating `GooseTask`s the same as `GooseTaskSet`s; `GooseTaskSetScheduler` becomes more generically `GooseScheduler`

## 0.11.0 April 9, 2021
- capture errors and count frequency for each, including summary in metrics report; optionally disable with `--no-error-summary`
Expand Down
69 changes: 57 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,36 +352,81 @@ All 1024 users hatched.
## Scheduling GooseTaskSets
When starting a load test, Goose assigns one `GooseTaskSet` to each `GooseUser` thread. By default, it assigns `GooseTaskSets` in a round robin order. As new `GooseUser` threads are launched, the first will be assigned the first defined `GooseTaskSet`, the next will be assigned the next defined `GooseTaskSet`, and so on, looping through all available `GooseTaskSet`s. Weighting is respected during this process, so if one `GooseTaskSet` is weighted heavier than others, that `GooseTaskSet` will get assigned more at the end of the launching process.
When starting a load test, Goose assigns one `GooseTaskSet` to each `GooseUser` thread. By default, it assigns `GooseTaskSet`s (and then `GooseTask`s within the task set) in a round robin order. As new `GooseUser` threads are launched, the first will be assigned the first defined `GooseTaskSet`, the next will be assigned the next defined `GooseTaskSet`, and so on, looping through all available `GooseTaskSet`s. Weighting is respected during this process, so if one `GooseTaskSet` is weighted heavier than others, that `GooseTaskSet` will get assigned to `GooseUser`s more at the end of the launching process.
It is also possible to allocate `GooseTaskSet`s in a serial or random order. When allocating `GooseTaskSet`s serially, they are launched in the exact order and weighting as they are defined in the load test. When allocating randomly, running the same load test multiple times can generate different amounts of load.
The `GooseScheduler` can be configured to instead launch `GooseTaskSet`s and `GooseTask`s in a `Serial` or a `Random order`. When configured to allocate in a `Serial` order, `GooseTaskSet`s and `GooseTask`s are launched in the extact order they are defined in the load test (see below for more detail on how this works). When configured to allocate in a `Random` order, running the same load test multiple times can lead to different amounts of load being generated.
Prior to Goose `0.10.6` `GooseTaskSet`s were allocated in a serial order. To restore this behavior, you can use the `.set_scheduler()` function as follows:
Prior to Goose `0.10.6` `GooseTaskSet`s were allocated in a serial order. Prior to Goose `0.11.1` `GooseTask`s were allocated in a serial order. To restore the old behavior, you can use the `GooseAttack::set_scheduler()` method as follows:
```
```rust
GooseAttack::initialize()?
.set_scheduler(GooseTaskSetScheduler::Serial)
.set_scheduler(GooseScheduler::Serial)
```
Or, to randomize the order `GooseTaskSet`s are allocated to newly launched users, you can instead configure your `GooseAttack` as follows:
To instead randomize the order that `GooseTaskSet`s and `GooseTask`s are allocated, you can instead configure as follows:
```
```rust
GooseAttack::initialize()?
.set_scheduler(GooseTaskSetScheduler::Random)
.set_scheduler(GooseScheduler::Random)
```
The following configuration is possible but superfluous because it is the scheduling default:
The following configuration is possible but superfluous because it is the scheduling default, and is therefor how Goose behaves even if the `.set_scheduler()` method is not called at all:
```rust
GooseAttack::initialize()?
.set_scheduler(GooseScheduler::RoundRobin)
```
### Scheduling Example
The following simple example helps illustrate how the different schedulers work.
```rust
GooseAttack::initialize()?
.set_scheduler(GooseTaskSetScheduler::RoundRobin)
.register_taskset(taskset!("TaskSet1")
.register_task(task!(task1).set_weight(2)?)
.register_task(task!(task2))
.set_weight(2)?
)
.register_taskset(taskset!("TaskSet2")
.register_task(task!(task1))
.register_task(task!(task2).set_weight(2)?)
)
.execute()?
.print();
Ok(())
```
### Round Robin
This first example assumes the default of `.set_scheduler(GooseScheduler::RoundRobin)`.
If Goose is told to launch only two users, the first GooseUser will run `TaskSet1` and the second user will run `TaskSet2`. Even though `TaskSet1` has a weight of 2 `GooseUser`s are allocated round-robin so with only two users the second instance of `TaskSet1` is never launched.
The `GooseUser` running `TaskSet1` will then launch tasks repeatedly in the following order: `task1`, `task2`, `task1`. If it runs through twice, then it runs all of the following tasks in the following order: `task1`, `task2`, `task1`, `task1`, `task2`, `task1`.
### Serial
This second example assumes the manual configuration of `.set_scheduler(GooseScheduler::Serial)`.
If Goose is told to launch only two users, then both `GooseUser`s will launch `TaskSet1` as it has a weight of 2. `TaskSet2` will not get assigned to either of the users.
Both `GooseUser`s running `TaskSet1` will then launch tasks repeatedly in the following order: `task1`, `task1`, `task2`. If it runs through twice, then it runs all of the following tasks in the following order: `task1`, `task1`, `task2`, `task1`, `task1`, `task2`.
### Random
This third example assumes the manual configuration of `.set_scheduler(GooseScheduler::Random)`.
If Goose is told to launch only two users, the first will be randomly assigned either `TaskSet1` or `TaskSet2`. Regardless of which is assigned to the first user, the second will again be randomly assigned either `TaskSet1` or `TaskSet2`. If the load test is stopped and run again, there users are randomly re-assigned, there is no consistency between load test runs.
Each `GooseUser` will run tasks in a random order. The random order will be determined at start time and then will run repeatedly in this random order as long as the user runs.
## Defaults
All run-time options can be configured with custom defaults. For example, you may want to default to the the host name of your local development environment, only requiring that `--host` be set when running against a production environment. Assuming your local development environment is at "http://local.dev/" you can do this as follows:
```
```rust
GooseAttack::initialize()?
.register_taskset(taskset!("LoadtestTasks")
.register_task(task!(loadtest_index))
Expand Down Expand Up @@ -428,7 +473,7 @@ The following defaults can be configured with a `bool`:
For example, without any run-time options the following load test would automatically run against `local.dev`, logging metrics to `goose-metrics.log` and debug to `goose-debug.log`. It will automatically launch 20 users in 4 seconds, and run the load test for 15 minutes. Metrics will be displayed every minute during the test and will include additional status code metrics. The order the defaults are set is not important.
```
```rust
GooseAttack::initialize()?
.register_taskset(taskset!("LoadtestTasks")
.register_task(task!(loadtest_index))
Expand Down
23 changes: 6 additions & 17 deletions src/goose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ pub struct GooseTaskSet {
pub max_wait: usize,
/// A vector containing one copy of each GooseTask that will run by users running this task set.
pub tasks: Vec<GooseTask>,
/// A vector of vectors of integers, controlling the sequence and order GooseTasks are run.
/// A fully scheduled and weighted vector of integers (pointing to GooseTasks) and GooseTask names.
pub weighted_tasks: WeightedGooseTasks,
/// A vector of vectors of integers, controlling the sequence and order on_start GooseTasks are run when the user first starts.
pub weighted_on_start_tasks: WeightedGooseTasks,
Expand Down Expand Up @@ -946,10 +946,8 @@ pub struct GooseUser {
pub task_sets_index: usize,
/// Client used to make requests, managing sessions and cookies.
pub client: Arc<Mutex<Client>>,
/// Integer value tracking the sequenced bucket user is running tasks from.
pub weighted_bucket: Arc<AtomicUsize>,
/// Integer value tracking the current task user is running.
pub weighted_bucket_position: Arc<AtomicUsize>,
pub position: Arc<AtomicUsize>,
/// The base URL to prepend to all relative paths.
pub base_url: Arc<RwLock<Url>>,
/// Minimum amount of time to sleep after running a task.
Expand Down Expand Up @@ -997,8 +995,7 @@ impl GooseUser {
started: Instant::now(),
task_sets_index,
client: Arc::new(Mutex::new(client)),
weighted_bucket: Arc::new(AtomicUsize::new(0)),
weighted_bucket_position: Arc::new(AtomicUsize::new(0)),
position: Arc::new(AtomicUsize::new(0)),
base_url: Arc::new(RwLock::new(base_url)),
min_wait,
max_wait,
Expand Down Expand Up @@ -1605,17 +1602,9 @@ impl GooseUser {
None => {
// Otherwise determine if the current GooseTask is named, and if so return
// a copy of it.
let weighted_bucket = self.weighted_bucket.load(atomic::Ordering::SeqCst);
let weighted_bucket_position =
self.weighted_bucket_position.load(atomic::Ordering::SeqCst);
if !self.weighted_tasks.is_empty()
&& !self.weighted_tasks[weighted_bucket][weighted_bucket_position]
.1
.is_empty()
{
self.weighted_tasks[weighted_bucket][weighted_bucket_position]
.1
.clone()
let position = self.position.load(atomic::Ordering::SeqCst);
if !self.weighted_tasks.is_empty() && !self.weighted_tasks[position].1.is_empty() {
self.weighted_tasks[position].1.clone()
} else {
// Otherwise return a copy of the the path.
path.to_string()
Expand Down
Loading

0 comments on commit 0e043d3

Please sign in to comment.