Skip to content

ci: split integration tests into per-provider jobs to fix container pressure#555

Merged
alexeyzimarev merged 3 commits into
devfrom
ci/split-integration-tests-by-provider
Jul 15, 2026
Merged

ci: split integration tests into per-provider jobs to fix container pressure#555
alexeyzimarev merged 3 commits into
devfrom
ci/split-integration-tests-by-provider

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Contributor

Problem

The KurrentDB (and other) integration tests fail randomly in CI. Root cause is container pressure: CI ran the whole solution in one dotnet test, so a single GitHub-hosted runner started every provider's Testcontainers at once — KurrentDB + Postgres + SQL Server (azure-sql-edge, ~2 GB) + Mongo + Kafka + Zookeeper + RabbitMQ + Redis. On a 4-vCPU/16 GB runner that saturates CPU/RAM, container startup slows past the Testcontainers wait-strategy timeouts, and tests flake. The previous self-hosted cpx52 runners masked this with far more headroom; the flakiness surfaced after moving to GitHub-hosted runners.

Contributing factor: the Postgres test project had no ParallelLimiter (KurrentDB and SQL Server do), so it could start unbounded concurrent Postgres containers.

Fix

  • Split CI into per-provider jobs. Replace the single whole-solution run with a { suite × framework } matrix (fail-fast: false). Each integration provider (kurrentdb, postgres, sqlserver, mongo, kafka, rabbitmq, redis, azure-servicebus, googlepubsub, signalr-integration) runs on its own runner, so only one container family starts per box. All container-less unit tests share a single core suite.
  • Add the missing Postgres parallel limiter (Limit => 4, mirroring the other providers).

Net effect: no single runner stacks multiple DB families, and wall-clock improves because providers run in parallel across runners.

Notes

  • Suite membership tracks Testcontainers usage. Adding a new integration test project requires adding a matrix suite entry, or it won't run in CI (noted in a workflow comment).
  • Excluded from core: Eventuous.Tests.OpenTelemetry (only a commented-out test), and the shared bases Eventuous.Tests.Subscriptions.Base / Eventuous.Tests.Persistence.Base — all produce zero runnable tests, so no coverage is lost.
  • test-results.yml needs no change: it globs all downloaded artifacts, and each job uploads a uniquely-named Test Results <suite> <framework> artifact.

🤖 Generated with Claude Code

…ressure

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) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

CI: split integration tests into per-provider matrix jobs; cap Postgres parallelism

🐞 Bug fix ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Split CI tests into a per-suite × .NET matrix to avoid container pressure flakes.
• Run only one provider’s Testcontainers per runner; keep unit tests in a shared core suite.
• Add a Postgres ParallelLimiter to prevent unbounded concurrent container starts.
Diagram

graph TD
  A["pull_request event"] --> B["build-and-test matrix"] --> C["suite job (core/provider)"] --> D["dotnet test per csproj"]
  D --> E["single provider containers"]
  D --> F["test-results files"] --> G["artifact: suite + framework"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Bigger / self-hosted runners
  • ➕ Reduces container pressure without changing workflow structure
  • ➕ Less risk of missing new integration projects in a suite list
  • ➖ Higher cost/maintenance burden
  • ➖ Doesn’t address unbounded container parallelism inside a test project
2. Single job but serialize integration providers
  • ➕ Keeps a single CI job and avoids matrix expansion
  • ➕ Prevents multiple provider stacks starting concurrently
  • ➖ Longer wall-clock time (no cross-runner parallelism)
  • ➖ More bespoke orchestration (manual ordering/conditional runs)
3. Tune Testcontainers + test parallelism only
  • ➕ Minimal CI workflow change
  • ➕ Can reduce flakiness by extending timeouts/reducing concurrency
  • ➖ Still risks multi-provider resource contention when all providers run together
  • ➖ Slower feedback and more brittle, provider-specific tuning

Recommendation: The PR’s approach (suite × framework matrix with fail-fast disabled) is the best trade-off: it eliminates the root cause (multi-provider container stacking on one runner) while improving wall-clock via parallel runners. Keep the workflow comment warning about adding new integration suites, and consider a lightweight guard (e.g., a script or CI check) to detect new test projects using Testcontainers that aren’t present in the suite list.

Files changed (2) +62 / -8

Bug fix (1) +10 / -0
Limiter.csAdd Postgres ParallelLimiter to cap concurrent test execution +10/-0

Add Postgres ParallelLimiter to cap concurrent test execution

• Introduces an assembly-level 'ParallelLimiter<Limiter>' for the Postgres integration tests. Caps parallel execution at 4 to prevent unbounded concurrent container startups and align with other provider test projects.

src/Postgres/test/Eventuous.Tests.Postgres/Limiter.cs

Other (1) +52 / -8
pull-request.ymlFan out CI tests into suite × framework matrix with per-project execution +52/-8

Fan out CI tests into suite × framework matrix with per-project execution

• Replaces a single whole-solution 'dotnet test' run with a matrix over .NET versions and test suites (core + per-provider integration suites), with 'fail-fast: false'. Runs 'dotnet test' per project in the selected suite and uploads uniquely named artifacts per suite/framework to support aggregation while avoiding container pressure on a single runner.

.github/workflows/pull-request.yml

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f82291ac66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/pull-request.yml Outdated
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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore full-solution build coverage

Because this now tests only matrix.suite.projects, the PR workflow no longer builds projects that are in Eventuous.slnx but not referenced by any listed test project. I checked the listed projects' ProjectReference closure and it misses shipped projects such as src/Extensions/src/Eventuous.Subscriptions.Polly/Eventuous.Subscriptions.Polly.csproj, src/GooglePubSub/src/Eventuous.GooglePubSub.CloudRun/Eventuous.GooglePubSub.CloudRun.csproj, and src/Experimental/src/Eventuous.ElasticSearch/Eventuous.ElasticSearch.csproj, all of which the previous root dotnet test built as part of the solution. A PR that breaks one of those projects can now go green until a later publish workflow, so please add a separate solution build or include those projects.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Test Results

 46 files  + 24   46 suites  +24   10m 39s ⏱️ - 2m 22s
357 tests +  4  357 ✅ +  4  0 💤 ±0  0 ❌ ±0 
672 runs  +308  672 ✅ +308  0 💤 ±0  0 ❌ ±0 

Results for commit 23e74bd. ± Comparison against base commit c55af3b.

This pull request removes 5 and adds 9 tests. Note that renamed tests count towards both.
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(07/15/2026 14:40:27 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(07/15/2026 14:40:27)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(63c18eb0-0dcf-4b73-9e5a-e0b9c763e6e0)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T14:41:36.3752178+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-07-15T14:41:36.3752178+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T14:41:36.3752178+00:00 })
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(07/15/2026 15:44:17 +00:00)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(07/15/2026 15:44:17)
Eventuous.Tests.Azure.ServiceBus.IsSerialisableByServiceBus ‑ Passes(a45b6385-0973-40ad-b97b-a7ef09120a57)
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:45.8572976+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-07-15T15:40:45.8572976+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:45.8572976+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:54.8335511+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-07-15T15:40:54.8335511+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:54.8335511+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 4, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:58.1844825+00:00 })
Eventuous.Tests.Subscriptions.SequenceTests ‑ ShouldReturnFirstBefore(CommitPosition { Position: 0, Sequence: 1, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 6, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 8, Timestamp: 2026-07-15T15:40:58.1844825+00:00 }, CommitPosition { Position: 0, Sequence: 2, Timestamp: 2026-07-15T15:40:58.1844825+00:00 })

♻️ This comment has been updated with latest results.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Redundant dotnet test runs 🐞 Bug ➹ Performance
Description
The new CI logic runs dotnet test once per project, multiplying CLI/MSBuild/test-host startup and
project-evaluation overhead, particularly for the core suite which expands to many projects. This
can noticeably increase CI runtime and runner cost compared to a single invocation per
suite/framework.
Code

.github/workflows/pull-request.yml[R91-96]

+          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
Evidence
The workflow now iterates over matrix.suite.projects and calls dotnet test for each entry; for
core, the matrix defines a long list of separate csproj paths, so this multiplies invocations per
job.

.github/workflows/pull-request.yml[35-48]
.github/workflows/pull-request.yml[89-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR changed the test step to loop over projects and invoke `dotnet test` for each one. This increases CI overhead (multiple `dotnet test` processes per job) and is most pronounced for the `core` suite.
### Issue Context
- The `core` suite contains many projects, so the loop causes many separate `dotnet test` invocations per framework.
### Fix Focus Areas
- .github/workflows/pull-request.yml[35-48]
- .github/workflows/pull-request.yml[89-96]
### Suggested fix approaches
Pick one (in order of robustness):
1) **Create suite-level solution filters** (e.g., `Eventuous.core.slnf`, `Eventuous.postgres.slnf`, etc.) and run a single `dotnet test <filter>.slnf -c "Debug CI" -f net${{ matrix.dotnet-version }}` per job.
2) If you keep the loop, **reduce repeated work** by doing one explicit restore/build first and then using `dotnet test --no-restore --no-build` inside the loop.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Brittle project list splitting 🐞 Bug ⚙ Maintainability
Description
The loop relies on bash word-splitting of matrix.suite.projects, which is safe for the current
whitespace-free paths but is brittle to maintain (e.g., accidental formatting changes or any future
path/token containing whitespace or glob characters). This increases the chance of a future CI break
where dotnet test receives malformed paths.
Code

.github/workflows/pull-request.yml[92]

+          for proj in ${{ matrix.suite.projects }}; do
Evidence
The workflow uses for proj in ... over a YAML-provided scalar containing multiple paths, which
depends on whitespace tokenization.

.github/workflows/pull-request.yml[35-48]
.github/workflows/pull-request.yml[91-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`for proj in ${{ matrix.suite.projects }}` relies on shell tokenization. While current paths work, the representation is fragile and easy to break with future edits.
### Issue Context
The `core` suite uses a folded YAML scalar to hold multiple project paths, which are then consumed via bash word-splitting.
### Fix Focus Areas
- .github/workflows/pull-request.yml[35-48]
- .github/workflows/pull-request.yml[91-93]
### Suggested fix
Represent `projects` as a structured list and iterate safely, e.g.:
- Store `projects` as a JSON array string in the matrix and iterate with `fromJSON(...)` + a step matrix, or
- Keep it line-delimited and iterate with a `while IFS= read -r proj; do ...; done` loop fed by a heredoc.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread .github/workflows/pull-request.yml Outdated
Comment on lines +91 to +96
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Redundant dotnet test runs 🐞 Bug ➹ Performance

The new CI logic runs dotnet test once per project, multiplying CLI/MSBuild/test-host startup and
project-evaluation overhead, particularly for the core suite which expands to many projects. This
can noticeably increase CI runtime and runner cost compared to a single invocation per
suite/framework.
Agent Prompt
### Issue description
The PR changed the test step to loop over projects and invoke `dotnet test` for each one. This increases CI overhead (multiple `dotnet test` processes per job) and is most pronounced for the `core` suite.

### Issue Context
- The `core` suite contains many projects, so the loop causes many separate `dotnet test` invocations per framework.

### Fix Focus Areas
- .github/workflows/pull-request.yml[35-48]
- .github/workflows/pull-request.yml[89-96]

### Suggested fix approaches
Pick one (in order of robustness):
1) **Create suite-level solution filters** (e.g., `Eventuous.core.slnf`, `Eventuous.postgres.slnf`, etc.) and run a single `dotnet test <filter>.slnf -c "Debug CI" -f net${{ matrix.dotnet-version }}` per job.
2) If you keep the loop, **reduce repeated work** by doing one explicit restore/build first and then using `dotnet test --no-restore --no-build` inside the loop.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread .github/workflows/pull-request.yml Outdated
run: |
dotnet test -c "Debug CI" -f net${{ matrix.dotnet-version }}
set -euo pipefail
for proj in ${{ matrix.suite.projects }}; do

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Brittle project list splitting 🐞 Bug ⚙ Maintainability

The loop relies on bash word-splitting of matrix.suite.projects, which is safe for the current
whitespace-free paths but is brittle to maintain (e.g., accidental formatting changes or any future
path/token containing whitespace or glob characters). This increases the chance of a future CI break
where dotnet test receives malformed paths.
Agent Prompt
### Issue description
`for proj in ${{ matrix.suite.projects }}` relies on shell tokenization. While current paths work, the representation is fragile and easy to break with future edits.

### Issue Context
The `core` suite uses a folded YAML scalar to hold multiple project paths, which are then consumed via bash word-splitting.

### Fix Focus Areas
- .github/workflows/pull-request.yml[35-48]
- .github/workflows/pull-request.yml[91-93]

### Suggested fix
Represent `projects` as a structured list and iterate safely, e.g.:
- Store `projects` as a JSON array string in the matrix and iterate with `fromJSON(...)` + a step matrix, or
- Keep it line-delimited and iterate with a `while IFS= read -r proj; do ...; done` loop fed by a heredoc.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

alexeyzimarev and others added 2 commits July 15, 2026 17:33
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@alexeyzimarev alexeyzimarev merged commit e676950 into dev Jul 15, 2026
16 checks passed
@alexeyzimarev alexeyzimarev deleted the ci/split-integration-tests-by-provider branch July 15, 2026 16:00
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