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

Stepped Burst Load Strategy #1

Merged
merged 4 commits into from
Aug 6, 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
8 changes: 8 additions & 0 deletions .idea/.idea.Rucksack/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/.idea.Rucksack/.idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ using Rucksack.LoadStrategies;
var options = new LoadTestOptions
{
LoadStrategy = new RepeatLoadStrategy(
CountPerStep: 10,
Interval: TimeSpan.FromSeconds(1),
TotalDuration: TimeSpan.FromSeconds(10)),
countPerInterval: 10,
interval: TimeSpan.FromSeconds(1),
totalDuration: TimeSpan.FromSeconds(10)),
};

await LoadTestRunner.Run(async () =>
Expand All @@ -31,3 +31,4 @@ There are a couple built-in strategies for generating load, or you can implement

* `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.
* `SteppedBurstLoadStrategy`: Enqueue an increasing (or decreasing) burst of tasks in a stepwise manner, regardless of how many are still running.
15 changes: 11 additions & 4 deletions Rucksack.Tests/Rucksack.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.4.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="xunit" Version="2.5.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
14 changes: 8 additions & 6 deletions Rucksack.Tests/Strategies/OneShotLoadStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ public async Task Step_WithCountOf1_CallsActionOnce()
// Arrange
var actionCalledCount = 0;
var strategy = new OneShotLoadStrategy(1);
var context = new LoadStrategyContext(PreviousResult: null);

// Act
var result = strategy.Step(() =>
var result = strategy.GenerateLoad(() =>
{
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
}, null);
}, context);

await StrategyTestHelper.ExecuteStrategyResultAndWait(result);

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

[Fact]
Expand All @@ -32,18 +33,19 @@ public async Task Step_WithCountOf3_CallsActionThreeTimes()
// Arrange
var actionCalledCount = 0;
var strategy = new OneShotLoadStrategy(3);
var context = new LoadStrategyContext(PreviousResult: null);

// Act
var result = strategy.Step(() =>
var result = strategy.GenerateLoad(() =>
{
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
}, null);
}, context);

await StrategyTestHelper.ExecuteStrategyResultAndWait(result);

// Assert
actionCalledCount.Should().Be(3);
result.NextStepDelay.Should().BeNull();
result.RepeatDelay.Should().BeNull();
}
}
18 changes: 10 additions & 8 deletions Rucksack.Tests/Strategies/RepeatLoadStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,25 @@ public async Task Step_WithCountOf1_CallsActionOnce()
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
};
var context = new LoadStrategyContext(PreviousResult: null);

// Act
var result = strategy.Step(action, null);
var result = strategy.GenerateLoad(action, context);

var tasks = await StrategyTestHelper.ExecuteStrategyResult(result);

// Assert
result.NextStepDelay.Should().Be(TimeSpan.FromSeconds(1));
result.RepeatDelay.Should().Be(TimeSpan.FromSeconds(1));

// Call again
result = strategy.Step(action, result);
result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result));

tasks.AddRange(await StrategyTestHelper.ExecuteStrategyResult(result));
await StrategyTestHelper.WhenAll(tasks);

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

[Fact]
Expand All @@ -47,23 +48,24 @@ public async Task Step_WithCountOf3_CallsActionThreeTimes()
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
};
var context = new LoadStrategyContext(PreviousResult: null);

// Act
var result = strategy.Step(action, null);
var result = strategy.GenerateLoad(action, context);

var tasks = await StrategyTestHelper.ExecuteStrategyResult(result);

// Assert
result.NextStepDelay.Should().Be(TimeSpan.FromSeconds(1));
result.RepeatDelay.Should().Be(TimeSpan.FromSeconds(1));

// Call again
result = strategy.Step(action, result);
result = strategy.GenerateLoad(action, new LoadStrategyContext(PreviousResult: result));

tasks.AddRange(await StrategyTestHelper.ExecuteStrategyResult(result));
await StrategyTestHelper.WhenAll(tasks);

// Assert
actionCalledCount.Should().Be(3);
result.NextStepDelay.Should().BeNull();
result.RepeatDelay.Should().BeNull();
}
}
72 changes: 72 additions & 0 deletions Rucksack.Tests/Strategies/SteppedBurstLoadStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using FluentAssertions;
using Rucksack.LoadStrategies;

namespace Rucksack.Tests.Strategies;

public class SteppedBurstLoadStrategyTests
{
[InlineData(1, 1, 2, 3)] // generated load = [1, 2] for a total of 3
[InlineData(2, 2, 4, 6)] // generated load = [2, 4] for a total of 6
[InlineData(10, 10, 50, 150)] // generated load = [10, 20, 30, 40, 50] for a total of 150
[InlineData(-10, 50, 10, 150)] // generated load = [50, 40, 30, 20, 10] for a total of 150
[InlineData(-1, 2, 1, 3)] // generated load = [2, 1] for a total of 3
[Theory]
public async Task SteppedBurstLoadStrategy_FullTests(int step, int from, int to, int expectedCount)
{
// Arrange
var actionCalledCount = 0;
var strategy = new SteppedBurstLoadStrategy(step, from, to, interval: TimeSpan.FromSeconds(1));
var action = () =>
{
Interlocked.Increment(ref actionCalledCount);
return ValueTask.FromResult(new LoadTaskResult(TimeSpan.Zero));
};

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

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

if (result.Tasks is { } resultTasks)
{
tasks.AddRange(resultTasks);
}

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

await StrategyTestHelper.WhenAll(tasks);

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

[InlineData(0, 1, 0, false)]
[InlineData(0, 1, 1, true)]
[InlineData(0, 1, 2, true)]
[InlineData(1, 0, 1, false)]
[InlineData(1, 0, 0, true)]
[InlineData(1, 0, -1, true)]
[InlineData(10, 100, 99, false)]
[InlineData(10, 100, 101, true)]
[InlineData(100, 10, 11, false)]
[InlineData(100, 10, 8, true)]
[Theory]
public void IsFinishedTests(int from, int to, int current, bool expected)
{
// Act
var result = SteppedBurstLoadStrategy.IsFinished(from, to, current);

// Assert
result.Should().Be(expected);
}
}
4 changes: 2 additions & 2 deletions Rucksack.Tests/Strategies/StrategyTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public static async ValueTask WhenAll(IEnumerable<ValueTask<LoadTaskResult>> tas

public static async ValueTask<List<ValueTask<LoadTaskResult>>> ExecuteStrategyResult(LoadStrategyResult result)
{
if (result.NextStepDelay.HasValue)
if (result.RepeatDelay.HasValue)
{
await Task.Delay(result.NextStepDelay.Value);
await Task.Delay(result.RepeatDelay.Value);
}

return [..result.Tasks ?? []];
Expand Down
2 changes: 1 addition & 1 deletion Rucksack/ILoadStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace Rucksack;

public interface ILoadStrategy
{
LoadStrategyResult Step(Func<ValueTask<LoadTaskResult>> action, LoadStrategyResult? previousResult);
LoadStrategyResult GenerateLoad(Func<ValueTask<LoadTaskResult>> action, LoadStrategyContext context);
}
10 changes: 4 additions & 6 deletions Rucksack/LoadStrategies/OneShotLoadStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ namespace Rucksack.LoadStrategies;

public class OneShotLoadStrategy(int count) : ILoadStrategy
{
public int Count { get; } = count;

public LoadStrategyResult Step(Func<ValueTask<LoadTaskResult>> action, LoadStrategyResult? previousResult)
public LoadStrategyResult GenerateLoad(Func<ValueTask<LoadTaskResult>> action, LoadStrategyContext context)
{
if (previousResult is not null)
if (context.PreviousResult is not null)
{
throw new InvalidOperationException("OneShotLoadStrategy does not support previous results.");
}

var tasks = Enumerable.Range(0, Count)
var tasks = Enumerable.Range(0, count)
.Select(_ => action())
.ToArray();

return new LoadStrategyResult(NextStepDelay: null, Tasks: tasks);
return new LoadStrategyResult(RepeatDelay: null, Tasks: tasks);
}
}
20 changes: 10 additions & 10 deletions Rucksack/LoadStrategies/RepeatLoadStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,34 @@

namespace Rucksack.LoadStrategies;

public class RepeatLoadStrategy(int CountPerStep, TimeSpan Interval, TimeSpan TotalDuration)
public class RepeatLoadStrategy(int countPerInterval, TimeSpan interval, TimeSpan totalDuration)
: ILoadStrategy
{
public LoadStrategyResult Step(Func<ValueTask<LoadTaskResult>> action, LoadStrategyResult? previousResult)
public LoadStrategyResult GenerateLoad(Func<ValueTask<LoadTaskResult>> action, LoadStrategyContext context)
{
RepeatLoadStrategyResult result;
int iteration = 1;

if (previousResult is null)
if (context.PreviousResult is null)
{
result = new RepeatLoadStrategyResult(Interval, Stopwatch.StartNew(), iteration, null);
result = new RepeatLoadStrategyResult(interval, Stopwatch.StartNew(), iteration, null);
}
else if (previousResult is not RepeatLoadStrategyResult previousRepeatResult)
else if (context.PreviousResult is not RepeatLoadStrategyResult previousRepeatResult)
{
throw new ArgumentException($"Expected {nameof(RepeatLoadStrategyResult)} but got {previousResult.GetType().Name}", nameof(previousResult));
throw new ArgumentException($"Expected previous result type {nameof(RepeatLoadStrategyResult)} but got {context.PreviousResult.GetType().Name}", nameof(context));
}
else
{
result = previousRepeatResult;
iteration = result.Iteration + 1;
}

if (result.Stopwatch.Elapsed >= TotalDuration)
if (result.Stopwatch.Elapsed >= totalDuration)
{
return LoadStrategyResult.Finished;
}

var tasks = Enumerable.Range(0, CountPerStep)
var tasks = Enumerable.Range(0, countPerInterval)
.Select(_ => action())
.ToArray();

Expand All @@ -40,6 +40,6 @@ public LoadStrategyResult Step(Func<ValueTask<LoadTaskResult>> action, LoadStrat
};
}

private record RepeatLoadStrategyResult(TimeSpan? NextStepDelay, Stopwatch Stopwatch, int Iteration, IReadOnlyList<ValueTask<LoadTaskResult>>? Tasks)
: LoadStrategyResult(NextStepDelay, Tasks);
private record RepeatLoadStrategyResult(TimeSpan? RepeatDelay, Stopwatch Stopwatch, int Iteration, IReadOnlyList<ValueTask<LoadTaskResult>>? Tasks)
: LoadStrategyResult(RepeatDelay, Tasks);
}
Loading