CI'da Testleri Parallel Çalıştırmak
Matrix yeşil, paylaşılan veritabanı değil

Tek job 14 dakikaydı. Matrix ile dört parçaya böldüm, duvar saati 5 dakikaya indi. Bir hafta sonra main'de rastgele kırmızı. Aynı test lokal'de geçiyordu. Log'da unique constraint. İki shard, aynı şema, aynı satır. Parallel test değil, parallel vandalizm.
Neyi paylaşmamalı
Veritabanı, Redis, disk üstü upload klasörü, sabit port. Matrix her hücrede ayrı runner verir; servis konteyneri services: altında hücreye özeldir — ta ki sen dışarıdaki staging DB'ye URL basana kadar. O URL'i 'hızlı olsun' diye koymak, izolasyonu satmaktır.
name: test
on: [push]
jobs:
unit:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
services:
postgres:
image: postgres:16.2-bookworm
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: app_test
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Test shard
env:
TEST_SHARD: ${{ matrix.shard }}
TEST_SHARD_COUNT: "4"
ConnectionStrings__App: Host=localhost;Port=5432;Database=app_test;Username=postgres;Password=test
run: |
dotnet test src/Api.Tests/Api.Tests.csproj \
--filter "Shard=${{ matrix.shard }}" \
--logger trx \
--results-directory ./trx
- uses: actions/upload-artifact@v4
if: always()
with:
name: trx-${{ matrix.shard }}
path: trx/
fail-fast: true ilk kırmızıda diğer shard'ları keser. Hızlıdır, teşhisi keser. false dört raporu da bırakır. Flaky ararken ikincisini istiyorum. upload-artifact shard adıyla; aynı isim üstüne yazmak, kazananın log'unu siler.
Filter yalan söyleyebilir
Trait ile shard'a bölmek, test sayısını eşitlamaz. Ağır entegrasyon tek hücrede toplanırsa duvar saati yine o hücredir. Süreye göre bölmek daha dürüst, kurulumu daha çirkin. Önce eşit sayı, ölç, sonra ağırları ayır.
Paylaşılan staging'e parallel test basmam. Bir job, bir ömürlük Postgres. Seed data çakışmasın diye şema adı veya veritabanı adı shard numarası taşır. Unique key'ler UUID. Sabit user@test.local dört shard'da dört kez insert edilir — etmez, çakışır. Sabit fixture, serial job'un kalıntısı.
Kazanç nerede biter
Raporu birleştirmek
Dört trx, dört coverage. dotnet-coverage merge veya ReportGenerator olmadan 'yüzde 82' hangi shard'ın yüzdesidir? Merge job'u needs: unit ile dört artifact'i indirir, birleşir, tek satır basar. Merge kırmızıysa test yeşil olsa da iş kırmızı. Coverage kapısını ayrı tutmuyorum; ayrı tutunca kimse bakmaz.
merge:
needs: unit
if: always()
runs-on: ubuntu-24.04
steps:
- uses: actions/download-artifact@v4
with:
path: trx
pattern: trx-*
merge-multiple: true
- name: Merge trx
run: |
find trx -name '*.trx' | wc -l
# fail if a shard uploaded nothing
test "$(find trx -name '*.trx' | wc -l)" -eq 4
Boş artifact, sessizce düşen shard'dır. if: always() upload'u kırmızı job'da da bırakır. if: success() kırmızı shard'ın trx'ini siler, teşhisi keser. Timeout olan shard da boş bırakır; timeout-minutes hücrede durur, 20 dakika sonra job ölür, merge 3 dosya görür, test ... -eq 4 patlar. İstediğim bu.
Testcontainers kullanan hücre, Docker socket ister. Dört hücre, dört daemon, dört çekilen postgres. Image pinli değilse (ilk yazı) dört latest. Cache yoksa (cache yazısı) dört kez 400 MB. Parallel'in faturası test süresinden önce pull'dadır. Servis imajını da pinle.
Paylaşılan dosya ve port
Testlerin /tmp/app-test yazması, dört shard'da dört değil, tek dizin olur — eğer aynı runner'ı paylaşıyorlarsa. GitHub'ın her matrix hücresi ayrı VM. Self-hosted tek makinede dört hücre aynı anda koşarsa dizin ve port çarpışır. TEST_SHARD 'ı path ve port'a göm. 5432 + shard veya soket dosyası. Sabit 5432, self-hosted'te regressiyon.
dotnet test --filter trait yazmayan testi hiçbir shard almaz. O testler lokal'de geçer, CI'da hiç koşmaz. Kapı: test sayıları toplamı == discovered. Discover tek job, shard'lar filter. Toplam eşleşmezse iş kırmızı. 'Yeşil çünkü koşmadı' en pahalı yeşil.
- name: Count vs filter
run: |
total=$(dotnet test --list-tests --nologo | grep -c '^ ')
chunk=$(dotnet test --list-tests --filter "Shard=${{ matrix.shard }}" --nologo | grep -c '^ ')
echo "total=$total chunk=$chunk"
test "$chunk" -gt 0
Liste komutu suite'e göre kırılır; grep kaba. Yine de sıfır chunk'ı yakalar. Sıfır chunk, yanlış trait veya boş hücre. Boş hücreyi yeşil saymak, 14 dakikayı 5'e indirmenin bedelini gizler.
name: test
on: [push]
jobs:
unit:
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
services:
postgres:
image: postgres:16.2-bookworm
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: app_${{ matrix.shard }}
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- run: |
dotnet test --filter "Shard=${{ matrix.shard }}" --logger trx --results-directory ./trx
env:
TEST_SHARD: ${{ matrix.shard }}
ConnectionStrings__App: Host=localhost;Database=app_${{ matrix.shard }};Username=postgres;Password=test
- uses: actions/upload-artifact@v4
if: always()
with:
name: trx-${{ matrix.shard }}
path: trx/
Ne zaman seri bırakıyorum
Testler paylaşılan bir dış sisteme yazıyorsa — gerçek ödeme sandbox'ı, gerçek S3, tek tenant'lı bir staging — matrix'i kapatıyorum. İzolasyon yoksa parallel vandalizmdir. O sistemin ikinci kopyası yoksa hız, doğru yeşilden pahalıdır. Bir job, bir ömür, bir temizlik.
Flaky'yi shard'a yüklemek de ayrı bir kaçış. Dört kez koşup bir kez kırmızı olan test, race'tir. retry eklemek, vandalizmi gizler. Önce izolasyon, sonra tekrar. Tekrar, ağ titremesi içindir; unique constraint için değildir.
Self-hosted kuyruk dolunca matrix 4, aslında 1+1+1+1 bekler. Duvar saati inmez. Önce runner sayısı, sonra shard sayısı. Runner 1 iken matrix süs. Bunu dashboard'dan değil, job Queued süresinden okuyorum. Kuyruk 8 dakikaysa shard kazancı çoktan gitmiştir. O zaman seri job, daha dürüst bir yeşildir.
Runner ücreti × shard. 4× makine, 3× hız, fatura 4×'e yaklaşır. 14 dakikayı 5'e indirmek PR döngüsü için değer. 5'i 4'e indirmek için sekiz shard, gürültü. Parallel bir hedef değil, kuyruk süresine karşı bir takas. Takası yaz, matrix'i süs diye açma.
Yorumlar
Yorumlar (0)
Yorumlar üyelere açık. Üye ol · Giriş yap