From f82291ac66d4d9c20e418f7b38e5df6d5f60ea7f Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 15 Jul 2026 17:25:37 +0200 Subject: [PATCH 1/3] ci: split integration tests into per-provider jobs to fix container pressure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the whole solution in one CI job started every provider's Testcontainers at once (KurrentDB + Postgres + SQL Server + Mongo + Kafka + RabbitMQ + ...), saturating the GitHub-hosted runner and making container startup — and the integration tests — flaky. The old self-hosted runners masked this with far more CPU/RAM. - Replace the single whole-solution `dotnet test` with a {suite x framework} matrix. Each integration provider runs on its own runner, so only one container family starts per box. All container-less unit tests share a `core` suite. - Add the missing parallel limiter to the Postgres test project (it was the only integration project without one, so it could start unbounded concurrent containers). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pull-request.yml | 60 ++++++++++++++++--- .../test/Eventuous.Tests.Postgres/Limiter.cs | 10 ++++ 2 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/Postgres/test/Eventuous.Tests.Postgres/Limiter.cs diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index fec32a6db..5f0cdbe6e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -18,15 +18,54 @@ jobs: path: ${{ github.event_path }} build-and-test: - name: "Build and test" + # One job per test suite so a single runner only ever starts one container family. + # Running the whole solution on one runner stacked KurrentDB + Postgres + SQL Server + + # Mongo + Kafka + RabbitMQ containers at once, saturating the GitHub-hosted runner and + # making container startup (and thus the integration tests) flaky. Each integration + # provider now gets its own runner; all container-less unit tests share the `core` suite. + # When adding a new integration test project (one that uses Testcontainers), add a suite + # entry for it here — otherwise it won't run in CI. + name: "Build and test ${{ matrix.suite.name }} (${{ matrix.dotnet-version }})" runs-on: ubuntu-latest -# runs-on: [ self-hosted, type-cpx52, setup-docker, volume-cache-50GB ] strategy: + fail-fast: false matrix: dotnet-version: [ '8.0', '9.0', '10.0' ] -# env: -# NUGET_PACKAGES: "/mnt/cache/.nuget/packages" -# DOTNET_INSTALL_DIR: "/mnt/cache/.dotnet" + suite: + - name: core + projects: >- + src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj + src/Core/test/Eventuous.Tests.Application/Eventuous.Tests.Application.csproj + src/Core/test/Eventuous.Tests.Subscriptions/Eventuous.Tests.Subscriptions.csproj + src/Core/test/Eventuous.Tests.Shared.Analyzers/Eventuous.Tests.Shared.Analyzers.csproj + src/Extensions/test/Eventuous.Tests.DependencyInjection/Eventuous.Tests.DependencyInjection.csproj + src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/Eventuous.Tests.Extensions.AspNetCore.csproj + src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore.Analyzers/Eventuous.Tests.Extensions.AspNetCore.Analyzers.csproj + src/Gateway/test/Eventuous.Tests.Gateway/Eventuous.Tests.Gateway.csproj + src/Experimental/test/Eventuous.Tests.Spyglass/Eventuous.Tests.Spyglass.csproj + src/Experimental/test/Eventuous.Tests.Spyglass.Generators/Eventuous.Tests.Spyglass.Generators.csproj + src/Sqlite/test/Eventuous.Tests.Sqlite/Eventuous.Tests.Sqlite.csproj + src/SignalR/test/Eventuous.Tests.SignalR/Eventuous.Tests.SignalR.csproj + - name: kurrentdb + projects: src/KurrentDB/test/Eventuous.Tests.KurrentDB/Eventuous.Tests.KurrentDB.csproj + - name: postgres + projects: src/Postgres/test/Eventuous.Tests.Postgres/Eventuous.Tests.Postgres.csproj + - name: sqlserver + projects: src/SqlServer/test/Eventuous.Tests.SqlServer/Eventuous.Tests.SqlServer.csproj + - name: mongo + projects: src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Eventuous.Tests.Projections.MongoDB.csproj + - name: kafka + projects: src/Kafka/test/Eventuous.Tests.Kafka/Eventuous.Tests.Kafka.csproj + - name: rabbitmq + projects: src/RabbitMq/test/Eventuous.Tests.RabbitMq/Eventuous.Tests.RabbitMq.csproj + - name: redis + projects: src/Redis/test/Eventuous.Tests.Redis/Eventuous.Tests.Redis.csproj + - name: azure-servicebus + projects: src/Azure/test/Eventuous.Tests.Azure.ServiceBus/Eventuous.Tests.Azure.ServiceBus.csproj + - name: googlepubsub + projects: src/GooglePubSub/test/Eventuous.Tests.GooglePubSub/Eventuous.Tests.GooglePubSub.csproj + - name: signalr-integration + projects: src/SignalR/test/Eventuous.Tests.SignalR.Integration/Eventuous.Tests.SignalR.Integration.csproj steps: - name: Checkout @@ -49,14 +88,19 @@ jobs: - name: Run tests run: | - dotnet test -c "Debug CI" -f net${{ matrix.dotnet-version }} + set -euo pipefail + for proj in ${{ matrix.suite.projects }}; do + echo "::group::dotnet test $proj (net${{ matrix.dotnet-version }})" + dotnet test "$proj" -c "Debug CI" -f net${{ matrix.dotnet-version }} + echo "::endgroup::" + done - name: Upload Test Results if: always() uses: actions/upload-artifact@v7 with: - name: Test Results ${{ matrix.dotnet-version }} + name: Test Results ${{ matrix.suite.name }} ${{ matrix.dotnet-version }} path: | test-results/**/*.xml test-results/**/*.trx - test-results/**/*.json \ No newline at end of file + test-results/**/*.json diff --git a/src/Postgres/test/Eventuous.Tests.Postgres/Limiter.cs b/src/Postgres/test/Eventuous.Tests.Postgres/Limiter.cs new file mode 100644 index 000000000..450f26ec2 --- /dev/null +++ b/src/Postgres/test/Eventuous.Tests.Postgres/Limiter.cs @@ -0,0 +1,10 @@ +using Eventuous.Tests.Postgres; +using TUnit.Core.Interfaces; + +[assembly: ParallelLimiter] + +namespace Eventuous.Tests.Postgres; + +public class Limiter : IParallelLimit { + public int Limit => 4; +} From 2021460e01e40c8199d47932a14ef84385a31db9 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 15 Jul 2026 17:33:29 +0200 Subject: [PATCH 2/3] ci: run integration suites on one TFM, throttle Docker Hub pulls Splitting into 33 per-provider-per-framework jobs multiplied Docker Hub image pulls (each integration job pulls its DB image + the Testcontainers Ryuk reaper), tripping the pull rate limit ("toomanyrequests: unauthenticated pull rate limit") even though docker login succeeds. - Split into two jobs: `unit-tests` (container-less projects, all three TFMs) and `integration-tests` (one provider per runner, net10.0 only). DB adapters behave the same across runtimes, so multi-TFM integration runs mostly re-pull the same images for little extra signal. - Cap integration concurrency with max-parallel: 4 to stagger image pulls under the rate limit. - Add a concurrency group so superseded runs are cancelled. Cuts CI from 33 container-pulling jobs to 10 (staggered) + 3 unit jobs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pull-request.yml | 123 +++++++++++++++++++---------- 1 file changed, 82 insertions(+), 41 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5f0cdbe6e..045f93cd7 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -6,6 +6,10 @@ on: - docs/** - '*.md' +concurrency: + group: pr-build-and-test-${{ github.ref }} + cancel-in-progress: true + jobs: event_file: name: "Event File" @@ -17,55 +21,98 @@ jobs: name: Event File path: ${{ github.event_path }} - build-and-test: - # One job per test suite so a single runner only ever starts one container family. - # Running the whole solution on one runner stacked KurrentDB + Postgres + SQL Server + - # Mongo + Kafka + RabbitMQ containers at once, saturating the GitHub-hosted runner and - # making container startup (and thus the integration tests) flaky. Each integration - # provider now gets its own runner; all container-less unit tests share the `core` suite. - # When adding a new integration test project (one that uses Testcontainers), add a suite - # entry for it here — otherwise it won't run in CI. - name: "Build and test ${{ matrix.suite.name }} (${{ matrix.dotnet-version }})" + unit-tests: + # Container-less test projects. Run across all target frameworks — these exercise the + # library code paths, so multi-TFM coverage matters here. No Docker needed. + name: "Build and test core (${{ matrix.dotnet-version }})" runs-on: ubuntu-latest strategy: fail-fast: false matrix: dotnet-version: [ '8.0', '9.0', '10.0' ] + steps: + - + name: Checkout + uses: actions/checkout@v7 + - + name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - + name: Run tests + run: | + set -euo pipefail + projects=" + src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj + src/Core/test/Eventuous.Tests.Application/Eventuous.Tests.Application.csproj + src/Core/test/Eventuous.Tests.Subscriptions/Eventuous.Tests.Subscriptions.csproj + src/Core/test/Eventuous.Tests.Shared.Analyzers/Eventuous.Tests.Shared.Analyzers.csproj + src/Extensions/test/Eventuous.Tests.DependencyInjection/Eventuous.Tests.DependencyInjection.csproj + src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/Eventuous.Tests.Extensions.AspNetCore.csproj + src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore.Analyzers/Eventuous.Tests.Extensions.AspNetCore.Analyzers.csproj + src/Gateway/test/Eventuous.Tests.Gateway/Eventuous.Tests.Gateway.csproj + src/Experimental/test/Eventuous.Tests.Spyglass/Eventuous.Tests.Spyglass.csproj + src/Experimental/test/Eventuous.Tests.Spyglass.Generators/Eventuous.Tests.Spyglass.Generators.csproj + src/Sqlite/test/Eventuous.Tests.Sqlite/Eventuous.Tests.Sqlite.csproj + src/SignalR/test/Eventuous.Tests.SignalR/Eventuous.Tests.SignalR.csproj + " + for proj in $projects; do + echo "::group::dotnet test $proj (net${{ matrix.dotnet-version }})" + dotnet test "$proj" -c "Debug CI" -f net${{ matrix.dotnet-version }} + echo "::endgroup::" + done + - + name: Upload Test Results + if: always() + uses: actions/upload-artifact@v7 + with: + name: Test Results core ${{ matrix.dotnet-version }} + path: | + test-results/**/*.xml + test-results/**/*.trx + test-results/**/*.json + + integration-tests: + # One job per provider so a single runner only ever starts one container family — running + # the whole solution on one runner stacked KurrentDB + Postgres + SQL Server + Mongo + Kafka + # + RabbitMQ containers at once and made container startup (and the tests) flaky. + # + # Integration suites run on a single TFM: the DB adapter behaves the same across runtimes, so + # multi-TFM runs mostly re-pull the same images for little extra signal. max-parallel throttles + # concurrent Docker Hub image pulls to stay under the pull rate limit. + # + # When adding a new integration test project (one that uses Testcontainers), add a suite entry. + name: "Build and test ${{ matrix.suite.name }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 4 + matrix: suite: - - name: core - projects: >- - src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj - src/Core/test/Eventuous.Tests.Application/Eventuous.Tests.Application.csproj - src/Core/test/Eventuous.Tests.Subscriptions/Eventuous.Tests.Subscriptions.csproj - src/Core/test/Eventuous.Tests.Shared.Analyzers/Eventuous.Tests.Shared.Analyzers.csproj - src/Extensions/test/Eventuous.Tests.DependencyInjection/Eventuous.Tests.DependencyInjection.csproj - src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore/Eventuous.Tests.Extensions.AspNetCore.csproj - src/Extensions/test/Eventuous.Tests.Extensions.AspNetCore.Analyzers/Eventuous.Tests.Extensions.AspNetCore.Analyzers.csproj - src/Gateway/test/Eventuous.Tests.Gateway/Eventuous.Tests.Gateway.csproj - src/Experimental/test/Eventuous.Tests.Spyglass/Eventuous.Tests.Spyglass.csproj - src/Experimental/test/Eventuous.Tests.Spyglass.Generators/Eventuous.Tests.Spyglass.Generators.csproj - src/Sqlite/test/Eventuous.Tests.Sqlite/Eventuous.Tests.Sqlite.csproj - src/SignalR/test/Eventuous.Tests.SignalR/Eventuous.Tests.SignalR.csproj - name: kurrentdb - projects: src/KurrentDB/test/Eventuous.Tests.KurrentDB/Eventuous.Tests.KurrentDB.csproj + project: src/KurrentDB/test/Eventuous.Tests.KurrentDB/Eventuous.Tests.KurrentDB.csproj - name: postgres - projects: src/Postgres/test/Eventuous.Tests.Postgres/Eventuous.Tests.Postgres.csproj + project: src/Postgres/test/Eventuous.Tests.Postgres/Eventuous.Tests.Postgres.csproj - name: sqlserver - projects: src/SqlServer/test/Eventuous.Tests.SqlServer/Eventuous.Tests.SqlServer.csproj + project: src/SqlServer/test/Eventuous.Tests.SqlServer/Eventuous.Tests.SqlServer.csproj - name: mongo - projects: src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Eventuous.Tests.Projections.MongoDB.csproj + project: src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Eventuous.Tests.Projections.MongoDB.csproj - name: kafka - projects: src/Kafka/test/Eventuous.Tests.Kafka/Eventuous.Tests.Kafka.csproj + project: src/Kafka/test/Eventuous.Tests.Kafka/Eventuous.Tests.Kafka.csproj - name: rabbitmq - projects: src/RabbitMq/test/Eventuous.Tests.RabbitMq/Eventuous.Tests.RabbitMq.csproj + project: src/RabbitMq/test/Eventuous.Tests.RabbitMq/Eventuous.Tests.RabbitMq.csproj - name: redis - projects: src/Redis/test/Eventuous.Tests.Redis/Eventuous.Tests.Redis.csproj + project: src/Redis/test/Eventuous.Tests.Redis/Eventuous.Tests.Redis.csproj - name: azure-servicebus - projects: src/Azure/test/Eventuous.Tests.Azure.ServiceBus/Eventuous.Tests.Azure.ServiceBus.csproj + project: src/Azure/test/Eventuous.Tests.Azure.ServiceBus/Eventuous.Tests.Azure.ServiceBus.csproj - name: googlepubsub - projects: src/GooglePubSub/test/Eventuous.Tests.GooglePubSub/Eventuous.Tests.GooglePubSub.csproj + project: src/GooglePubSub/test/Eventuous.Tests.GooglePubSub/Eventuous.Tests.GooglePubSub.csproj - name: signalr-integration - projects: src/SignalR/test/Eventuous.Tests.SignalR.Integration/Eventuous.Tests.SignalR.Integration.csproj + project: src/SignalR/test/Eventuous.Tests.SignalR.Integration/Eventuous.Tests.SignalR.Integration.csproj steps: - name: Checkout @@ -87,19 +134,13 @@ jobs: password: ${{ secrets.DOCKER_TOKEN }} - name: Run tests - run: | - set -euo pipefail - for proj in ${{ matrix.suite.projects }}; do - echo "::group::dotnet test $proj (net${{ matrix.dotnet-version }})" - dotnet test "$proj" -c "Debug CI" -f net${{ matrix.dotnet-version }} - echo "::endgroup::" - done + run: dotnet test "${{ matrix.suite.project }}" -c "Debug CI" -f net10.0 - name: Upload Test Results if: always() uses: actions/upload-artifact@v7 with: - name: Test Results ${{ matrix.suite.name }} ${{ matrix.dotnet-version }} + name: Test Results ${{ matrix.suite.name }} path: | test-results/**/*.xml test-results/**/*.trx From 23e74bdb69d61bc63f20438387db2f584febe114 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 15 Jul 2026 17:39:42 +0200 Subject: [PATCH 3/3] ci: drop Docker Hub login from integration tests The login step reported "Login Succeeded!" but Testcontainers still pulled images anonymously ("unauthenticated pull rate limit"), so it never bought authenticated pulls. GitHub-hosted runners get a more generous anonymous Docker Hub allowance, and authenticating against a rate-limited free account can pull from a lower ceiling. Remove the login and rely on the runners' anonymous allowance, kept under the limit by the reduced, throttled job set. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pull-request.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 045f93cd7..bd3318b99 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -125,13 +125,6 @@ jobs: 8.0.x 9.0.x 10.0.x - - - name: Login to Docker Hub - if: ${{ github.event.pull_request.head.repo.fork == false }} - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_TOKEN }} - name: Run tests run: dotnet test "${{ matrix.suite.project }}" -c "Debug CI" -f net10.0