Skip to content

test: add write and contention load tests - #169

Merged
Dejmenek merged 6 commits into
mainfrom
feat/165-loadtests-write-contention
Sep 29, 2026
Merged

Dejmenek merged 6 commits into
mainfrom
feat/165-loadtests-write-contention

Conversation

@Dejmenek

@Dejmenek Dejmenek commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

This is the third PR for #165 and it is stacked on #167. It adds load tests for the write paths: creating tournaments, joining them, playing and correcting matches, and generating brackets. The contention tests check that the concurrency handling described in ADRs 0002, 0004 and 0006 holds under load.

Scenarios

  • Ten users create open tournaments at 10 requests per second.
  • Eight users at a time try to take the two free slots of the same tournament. After the run, no tournament may hold more participants than MaxParticipants.
  • Six requests at a time play the same match, and six send different corrections against one stale version. Exactly one request per match may succeed and the rest must return Match.AlreadyPlayed or Match.VersionConflict.
  • Brackets are generated for 8, 16 and 32 participants, single elimination and round robin. Each tournament gets two calls, so the second must return Bracket.AlreadyExists.

Expected rejections count as OK responses with their domain code, so they do not use up the error budget. The contention tests also compare the number of successful requests with what ended up in the database.

Helpers

WriteScenarioProvisioner creates users, tournaments and matches through DbContext before the measured run, so the loop only exercises the mutation under test. Baselines for the new steps are the worst values from three runs, because run to run variance on this machine is large.

Fix to the scenario results

While checking the new numbers I found that scenario level status codes were doubled. Returning a step's Response from the scenario delegate makes NBomber count its status code and payload a second time. I confirmed this with a small probe outside the suite. Scenarios now go through GraphQLLoadScenario.Create, which returns a plain Ok or Fail from the last step, so a failing step still counts against the error budget. The status mix check also takes both sides of its ratio from status code counts. The older load tests are switched over too.

Things worth knowing

  • Losers of a slot race get Tournament.Full even when slots remain, as ADR 0004 describes. Only about 90 of roughly 150 free slots get filled in the join test.
  • The match tests run without warm-up, because NBomber warm-up runs the mutations but leaves them out of the stats.
  • Contenders are arrivals from an open model, about 25 ms apart, not perfectly simultaneous requests.
  • In one full suite run the authentication test failed on logout latency, then passed on its own with numbers better than its baseline. I read that as noise from a long back to back run and left its baselines alone.

Testing

All 15 load tests passed, apart from the one authentication run mentioned above. Unit tests (160) and integration tests (252) pass locally. This PR only changes the load test project, and CI runs the unit and integration tests. The load tests need Docker and run manually with dotnet test TournamentAPI.LoadTests --filter "Category=Load".

Part of #165.

Stack created with GitHub Stacks CLI • Give Feedback 💬

🤖 Generated with Claude Code

Dejmenek and others added 6 commits September 29, 2026 18:23
Returning a step's Response from the scenario delegate makes NBomber count its status code and payload a second time at scenario level, so scenario status codes were doubled. Scenarios now go through GraphQLLoadScenario.Create, which returns a plain Ok or Fail based on the last step. A failing step still counts against the error budget.

The status mix check now takes both sides of its ratio from status code counts. Multi-step scenarios aggregate the code of every step, while the request count only counts the scenario once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Write scenarios provision users, open tournaments, closed tournaments and matches through DbContext before the measured run, so the loop only exercises the mutation under test. The budgets hold the step names, rates and baselines for every write scenario. Baselines are the worst value over three runs, since run to run variance on this machine is large.

GraphQLLoadStep gets a TrueResult body check for mutations that return a boolean.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Ten users create open tournaments at 10 requests per second for 30 seconds. The test checks the error budget, tail latency and payload size of the create_tournament step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Eight users at a time try to take the two free slots of the same tournament. Tournament.Full and Tournament.UserAlreadyParticipant count as expected rejections. After the run the test checks that no tournament holds more participants than MaxParticipants and that at least one join succeeded.

Losers of a slot race get Tournament.Full even when slots remain, as ADR 0004 describes, so only part of the free slots get filled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Six requests at a time target the same match. Concurrent plays must produce one success and Match.AlreadyPlayed for the rest. Concurrent corrections that share one stale version and send different scores must produce one success and Match.VersionConflict for the rest. Both tests compare the number of successful requests with the number of matches that changed in the database. They run without warm-up because NBomber warm-up runs the mutations but leaves them out of the stats.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Single elimination and round robin brackets are generated for 8, 16 and 32 participants. Each tournament gets two calls, so the second one must return Bracket.AlreadyExists. After the run the number of successful requests must equal the number of tournaments that have a bracket.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Dejmenek
Dejmenek added this pull request to stack #168 September 29, 2026 16:25
@Dejmenek Dejmenek changed the title feat/165 loadtests write contention test: add write and contention load tests Sep 29, 2026
@Dejmenek
Dejmenek marked this pull request as ready for review September 29, 2026 19:43
Base automatically changed from feat/165-loadtests-dataset-auth to main September 29, 2026 19:51
@Dejmenek
Dejmenek merged commit d11b78e into main Sep 29, 2026
5 checks passed
@Dejmenek
Dejmenek deleted the feat/165-loadtests-write-contention branch September 29, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant