Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

User and Sequential load strategies #3

Merged
merged 5 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ await LoadTestRunner.Run(async () =>

There are a couple built-in strategies for generating load, or you can implement `ILoadStrategy` yourself for custom logic.

* `OneShotLoadStrategy`: Enqueue the given number of tasks all at once. Does not repeat.
* `RepeatLoadStrategy`: Repeat enqueueing the given count of tasks at each given interval until the given duration has passed.
* `ConstantUserLoadStrategy`: Attempts to maintain a constant concurrent user load at each given check interval until the given duration has passed.
* `OneShotBurstLoadStrategy`: Enqueue the given number of tasks all at once. Does not repeat.
* `RepeatBurstLoadStrategy`: Repeat enqueueing the given count of tasks at each given interval until the given duration has passed.
* `SequentialLoadStrategy`: An aggregate strategy that executes multiple load strategies in order. Great for creating scenarios like step up, then hold steady, then step down.
* `SteppedBurstLoadStrategy`: Enqueue an increasing (or decreasing) burst of tasks in a stepwise manner, regardless of how many are still running.
* `SteppedUserLoadStrategy`: Attempts to maintain an increasing (or decreasing) concurrent user load in a stepwise manner.
150 changes: 148 additions & 2 deletions Rucksack.Tests/BasicIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ await LoadTestRunner.Run(() =>
return Task.CompletedTask;
}, new LoadTestOptions
{
LoadStrategy = new RepeatLoadStrategy(count, interval, duration),
LoadStrategy = new RepeatBurstLoadStrategy(count, interval, duration),
LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXUnit(testOutputHelper);
Expand Down Expand Up @@ -82,6 +82,152 @@ await LoadTestRunner.Run(() =>
executionCount.Should().Be(expected);
}

[Fact]
public async Task BasicConstantUserLoadIntegrationTest()
{
// NOTE: these tests should all finish in under a second,
// so we can expect this strategy to spawn 10 each second

int executionCount = 0;
const int count = 10;
const int expected = 50;

await LoadTestRunner.Run(() =>
{
Interlocked.Increment(ref executionCount);
return Task.CompletedTask;
}, new LoadTestOptions
{
LoadStrategy = new ConstantUserLoadStrategy(count, FromSeconds(1), FromSeconds(5)),
LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXUnit(testOutputHelper);
}),
});

executionCount.Should().Be(expected);
}

[Fact]
public async Task BasicConstantUserLoadIntegrationTest_WithLongRunningTask()
{
// Only the first task will take a while to run, so we would
// expect it to launch in a pattern of [10, 9, 9, 9, 9] for a total of 46

int executionCount = 0;
const int count = 10;
const int expected = 46;

await LoadTestRunner.Run(async () =>
{
int value = Interlocked.Increment(ref executionCount);

if (value == 1)
{
await Task.Delay(5000);
}
}, new LoadTestOptions
{
LoadStrategy = new ConstantUserLoadStrategy(count, FromSeconds(1), FromSeconds(5)),
LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXUnit(testOutputHelper);
}),
});

executionCount.Should().Be(expected);
}

[Fact]
public async Task BasicSteppedUserLoadIntegrationTest()
{
// NOTE: these tests should all finish in under a second,
// so we can expect this strategy to spawn 10-50 each second

int executionCount = 0;
const int step = 10;
const int from = 10;
const int to = 50;
const int expected = 150;

await LoadTestRunner.Run(() =>
{
Interlocked.Increment(ref executionCount);
return Task.CompletedTask;
}, new LoadTestOptions
{
LoadStrategy = new SteppedUserLoadStrategy(step, from, to, FromSeconds(1), FromSeconds(1), FromSeconds(5)),
LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXUnit(testOutputHelper);
}),
});

executionCount.Should().Be(expected);
}

[Fact]
public async Task BasicSteppedUserLoadIntegrationTest_WithLongRunningTask()
{
// Only the first task will take a while to run, so we would
// expect it to launch in a pattern of [10, 19, 29, 39, 49] for a total of 146

int executionCount = 0;
const int step = 10;
const int from = 10;
const int to = 50;
const int expected = 146;

await LoadTestRunner.Run(async () =>
{
int value = Interlocked.Increment(ref executionCount);

if (value == 1)
{
await Task.Delay(5000);
}
}, new LoadTestOptions
{
LoadStrategy = new SteppedUserLoadStrategy(step, from, to, FromSeconds(1), FromSeconds(1), FromSeconds(5)),
LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXUnit(testOutputHelper);
}),
});

executionCount.Should().Be(expected);
}

[Fact]
public async Task BasicSequentialUserLoadIntegrationTest_OneShot()
{
int executionCount = 0;
const int count = 10;
const int expected = 50;

await LoadTestRunner.Run(() =>
{
Interlocked.Increment(ref executionCount);
return Task.CompletedTask;
}, new LoadTestOptions
{
LoadStrategy = new SequentialLoadStrategy(FromSeconds(1))
{
new OneShotLoadStrategy(count),
new OneShotLoadStrategy(count),
new OneShotLoadStrategy(count),
new OneShotLoadStrategy(count),
new OneShotLoadStrategy(count),
},
LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXUnit(testOutputHelper);
}),
});

executionCount.Should().Be(expected);
}

[Fact]
public async Task RandomDelayRepeatIntegrationTest()
{
Expand All @@ -101,7 +247,7 @@ await LoadTestRunner.Run(async () =>
Interlocked.Increment(ref executionCount);
}, new LoadTestOptions
{
LoadStrategy = new RepeatLoadStrategy(count, interval, duration),
LoadStrategy = new RepeatBurstLoadStrategy(count, interval, duration),
LoggerFactory = LoggerFactory.Create(builder =>
{
builder.AddXUnit(testOutputHelper);
Expand Down
33 changes: 33 additions & 0 deletions Rucksack.Tests/Strategies/ConstantUserLoadStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using FluentAssertions;
using Rucksack.LoadStrategies;

namespace Rucksack.Tests.Strategies;

public class ConstantUserLoadStrategyTests
{
[Fact]
public async Task ConstantLoadStrategy_BasicTest()
{
// Arrange
const int expectedCount = 46; // [10, 9, 9, 9, 9] since the first call runs for 5 seconds
var actionCalledCount = 0;
var strategy = new ConstantUserLoadStrategy(count: 10, checkInterval: TimeSpan.FromSeconds(1), totalDuration: TimeSpan.FromSeconds(5));
LoadTask action = async () =>
{
int newCount = Interlocked.Increment(ref actionCalledCount);

if (newCount == 1)
{
await Task.Delay(5000);
}

return new LoadTaskResult(TimeSpan.Zero);
};

// Act
await StrategyTestHelper.RunFullStrategyTest(strategy, action);

// Assert
actionCalledCount.Should().Be(expectedCount);
}
}
4 changes: 2 additions & 2 deletions Rucksack.Tests/Strategies/OneShotLoadStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public async Task Step_WithCountOf1_CallsActionOnce()
// Arrange
var actionCalledCount = 0;
var strategy = new OneShotLoadStrategy(1);
var context = new LoadStrategyContext(PreviousResult: null);
var context = new LoadStrategyContext(PreviousResult: null, CurrentRunningTasks: 0);

// Act
var result = strategy.GenerateLoad(() =>
Expand All @@ -33,7 +33,7 @@ public async Task Step_WithCountOf3_CallsActionThreeTimes()
// Arrange
var actionCalledCount = 0;
var strategy = new OneShotLoadStrategy(3);
var context = new LoadStrategyContext(PreviousResult: null);
var context = new LoadStrategyContext(PreviousResult: null, CurrentRunningTasks: 0);

// Act
var result = strategy.GenerateLoad(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@

namespace Rucksack.Tests.Strategies;

public class RepeatLoadStrategyTests
public class RepeatBurstLoadStrategyTests
{
[Fact]
public async Task Step_WithCountOf1_CallsActionOnce()
{
// Arrange
var actionCalledCount = 0;
var strategy = new RepeatLoadStrategy(1, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
var strategy = new RepeatBurstLoadStrategy(1, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
LoadTask action = () =>
{
Interlocked.Increment(ref actionCalledCount);
return Task.FromResult(new LoadTaskResult(TimeSpan.Zero));
};
var context = new LoadStrategyContext(PreviousResult: null);
var context = new LoadStrategyContext(PreviousResult: null, CurrentRunningTasks: 0);

// Act
var result = strategy.GenerateLoad(action, context);
Expand All @@ -27,7 +27,8 @@ public async Task Step_WithCountOf1_CallsActionOnce()
result.RepeatDelay.Should().Be(TimeSpan.FromSeconds(1));

// Call again
result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result));
var currentRunningCount = tasks.Count(t => !t.IsCompleted);
result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result, CurrentRunningTasks: currentRunningCount));

tasks.AddRange(await StrategyTestHelper.ExecuteStrategyResult(result));
await Task.WhenAll(tasks);
Expand All @@ -42,13 +43,13 @@ public async Task Step_WithCountOf3_CallsActionThreeTimes()
{
// Arrange
var actionCalledCount = 0;
var strategy = new RepeatLoadStrategy(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
var strategy = new RepeatBurstLoadStrategy(3, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
LoadTask action = () =>
{
Interlocked.Increment(ref actionCalledCount);
return Task.FromResult(new LoadTaskResult(TimeSpan.Zero));
};
var context = new LoadStrategyContext(PreviousResult: null);
var context = new LoadStrategyContext(PreviousResult: null, CurrentRunningTasks: 0);

// Act
var result = strategy.GenerateLoad(action, context);
Expand All @@ -59,7 +60,8 @@ public async Task Step_WithCountOf3_CallsActionThreeTimes()
result.RepeatDelay.Should().Be(TimeSpan.FromSeconds(1));

// Call again
result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result));
var currentRunningCount = tasks.Count(t => !t.IsCompleted);
result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result, CurrentRunningTasks: currentRunningCount));

tasks.AddRange(await StrategyTestHelper.ExecuteStrategyResult(result));
await Task.WhenAll(tasks);
Expand Down
59 changes: 59 additions & 0 deletions Rucksack.Tests/Strategies/SequentialLoadStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using FluentAssertions;
using Rucksack.LoadStrategies;

namespace Rucksack.Tests.Strategies;

public class SequentialLoadStrategyTests
{
[Fact]
public async Task SequentialLoadStrategy_BasicOneShotTest()
{
// Arrange
const int expectedCount = 60; // [10, 20, 30]
var actionCalledCount = 0;
var strategy = new SequentialLoadStrategy(intervalBetweenStrategies: TimeSpan.FromSeconds(1))
{
new OneShotLoadStrategy(count: 10),
new OneShotLoadStrategy(count: 20),
new OneShotLoadStrategy(count: 30),
};

LoadTask action = () =>
{
Interlocked.Increment(ref actionCalledCount);
return Task.FromResult(new LoadTaskResult(TimeSpan.Zero));
};

// Act
await StrategyTestHelper.RunFullStrategyTest(strategy, action);

// Assert
actionCalledCount.Should().Be(expectedCount);
}

[Fact]
public async Task SequentialLoadStrategy_BasicSteppedBurstTest()
{
// Arrange
const int expectedCount = 450; // [10, 20, 30, 40, 50, 50, 50, 50, 50, 40, 30, 20, 10]
var actionCalledCount = 0;
var strategy = new SequentialLoadStrategy(intervalBetweenStrategies: TimeSpan.FromSeconds(1))
{
new SteppedBurstLoadStrategy(step: 10, from: 10, to: 50, interval: TimeSpan.FromSeconds(1)), // [10, 20, 30, 40, 50]
new RepeatBurstLoadStrategy(countPerInterval: 50, interval: TimeSpan.FromSeconds(1), totalDuration: TimeSpan.FromSeconds(3)), // [50, 50, 50]
new SteppedBurstLoadStrategy(step: -10, from: 50, to: 10, interval: TimeSpan.FromSeconds(1)), // [50, 40, 30, 20, 10]
};

LoadTask action = () =>
{
Interlocked.Increment(ref actionCalledCount);
return Task.FromResult(new LoadTaskResult(TimeSpan.Zero));
};

// Act
await StrategyTestHelper.RunFullStrategyTest(strategy, action);

// Assert
actionCalledCount.Should().Be(expectedCount);
}
}
23 changes: 1 addition & 22 deletions Rucksack.Tests/Strategies/SteppedBurstLoadStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,9 @@ public async Task SteppedBurstLoadStrategy_FullTests(int step, int from, int to,
};

// Act
LoadStrategyResult? result = null;
List<Task<LoadTaskResult>> tasks = [];

do
{
var context = new LoadStrategyContext(PreviousResult: result);
result = strategy.GenerateLoad(action, context);

if (result.Tasks is { } resultTasks)
{
tasks.AddRange(resultTasks.Select(i => Task.Run(() => i())));
}

if (result.RepeatDelay.HasValue)
{
await Task.Delay(result.RepeatDelay.Value);
}
}
while (result.RepeatDelay.HasValue);

await Task.WhenAll(tasks);
await StrategyTestHelper.RunFullStrategyTest(strategy, action);

// Assert
result.RepeatDelay.Should().BeNull();
actionCalledCount.Should().Be(expectedCount);
}

Expand Down
Loading