Skip to content

Commit 1990843

Browse files
authored
perf(ci): speed up webapp test execution (#4709)
## Summary Speeds up webapp test jobs by balancing measured work across runners, reducing repeated container setup, and ensuring test workers release shutdown resources promptly. Unit tests run across 24 duration-aware shards, while E2E tests run across two balanced shards. ## Design `RunEngine` shutdown now closes processing resources before support resources, continues cleanup if one close fails, and reuses one shutdown promise for concurrent callers. Redis workers clear completed shutdown deadlines so finished tests no longer wait on idle timers. Container-heavy suites are split only where it improves parallelism, and repeated replication and engine fixtures are consolidated where one end-to-end case provides coverage. Timing weights are refreshed for all affected files. Dependency installation overlaps container pulls, and both workflows use WarpBuild's Node setup action.
1 parent 4474718 commit 1990843

27 files changed

Lines changed: 5600 additions & 4593 deletions

.github/workflows/e2e-webapp.yml

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ jobs:
1616
name: "🧪 E2E Tests: Webapp"
1717
runs-on: warp-ubuntu-latest-x64-16x
1818
timeout-minutes: 30
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
shardIndex: [1, 2]
23+
shardTotal: [2]
1924
env:
2025
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
26+
SHARD_INDEX: ${{ matrix.shardIndex }}
27+
SHARD_TOTAL: ${{ matrix.shardTotal }}
2128
steps:
2229
- name: 🔧 Disable IPv6
2330
run: |
@@ -57,7 +64,7 @@ jobs:
5764
version: 10.33.2
5865

5966
- name: ⎔ Setup node
60-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
67+
uses: WarpBuilds/setup-node@bc639b444d583175926b588962199c247d23e8d3 # v6
6168
with:
6269
node-version: 24.18.0
6370
cache: "pnpm"
@@ -73,18 +80,52 @@ jobs:
7380
if: ${{ !env.DOCKERHUB_USERNAME }}
7481
run: echo "DockerHub login skipped because secrets are not available."
7582

76-
- name: 🐳 Pre-pull testcontainer images
83+
- name: 📥 Prepare deps and testcontainer images
7784
run: |
78-
echo "Pre-pulling Docker images with authenticated session..."
79-
docker pull postgres:14
80-
docker pull redis:7.2
81-
docker pull testcontainers/ryuk:0.14.0
82-
docker pull ghcr.io/s2-streamstore/s2:0.40.0@sha256:b26249e2ede0949755f5af8028185dc2bcfc3aa2db21eb9610543d144eb6ee9d
83-
docker pull minio/minio:latest
84-
echo "Image pre-pull complete"
85+
# Pull images concurrently with dependency installation. Retry each pull because
86+
# registry timeouts are a recurring transient CI flake.
87+
pull() {
88+
for attempt in 1 2 3; do
89+
docker pull "$1" && return 0
90+
echo "::warning::docker pull $1 failed (attempt ${attempt}/3); retrying in 10s"
91+
sleep 10
92+
done
93+
echo "::error::docker pull $1 failed after 3 attempts"
94+
return 1
95+
}
8596
86-
- name: 📥 Download deps
87-
run: pnpm install --frozen-lockfile
97+
pull_images() {
98+
local pids=()
99+
local failed=0
100+
for image in \
101+
postgres:14 \
102+
redis:7.2 \
103+
testcontainers/ryuk:0.14.0 \
104+
ghcr.io/s2-streamstore/s2:0.40.0@sha256:b26249e2ede0949755f5af8028185dc2bcfc3aa2db21eb9610543d144eb6ee9d \
105+
minio/minio:latest
106+
do
107+
pull "$image" &
108+
pids+=("$!")
109+
done
110+
for pid in "${pids[@]}"; do
111+
if ! wait "$pid"; then
112+
failed=1
113+
fi
114+
done
115+
return "$failed"
116+
}
117+
118+
echo "Installing dependencies and pre-pulling Docker images..."
119+
pull_images &
120+
pull_pid=$!
121+
install_status=0
122+
pnpm install --frozen-lockfile || install_status=$?
123+
pull_status=0
124+
wait "$pull_pid" || pull_status=$?
125+
if (( install_status != 0 || pull_status != 0 )); then
126+
exit 1
127+
fi
128+
echo "Dependency install and image pre-pull complete"
88129
89130
- name: 📀 Generate Prisma Client
90131
run: pnpm run generate
@@ -96,6 +137,6 @@ jobs:
96137
run: cd apps/webapp && pnpm exec playwright install chromium
97138

98139
- name: 🧪 Run Webapp E2E Tests
99-
run: cd apps/webapp && pnpm exec vitest run --config vitest.e2e.config.ts --reporter=default
140+
run: cd apps/webapp && pnpm exec vitest run --config vitest.e2e.config.ts --reporter=default --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
100141
env:
101142
WEBAPP_TEST_VERBOSE: "1"

.github/workflows/unit-tests-webapp.yml

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ on:
1414
jobs:
1515
unitTests:
1616
name: "🧪 Unit Tests: Webapp"
17-
# 10 shards on 16x machines: webapp test throughput is limited per-machine (one
18-
# docker daemon + disk absorbing all the per-file Postgres/ClickHouse container
19-
# spin-up), so many machines beats few big ones - fewer/bigger (3x32) measured
20-
# SLOWER than 10x8. The 16x (vs 8x) gives the fork pool the CPU headroom the 8x
21-
# runners lacked. Setup overhead per machine is ~1 min on warm runners.
17+
# Webapp test throughput is limited per-machine (one docker daemon + disk absorbing
18+
# all the per-file Postgres/ClickHouse container spin-up), so many machines beats
19+
# few big ones - fewer/bigger (3x32) measured slower than 10x8. The 16x (vs 8x)
20+
# gives the fork pool the CPU headroom the 8x runners lacked.
2221
runs-on: warp-ubuntu-latest-x64-16x
2322
strategy:
2423
# one flaky shard shouldn't cancel its siblings - lets us re-run only the failed shard
2524
fail-fast: false
2625
matrix:
27-
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
28-
shardTotal: [12]
26+
shardIndex:
27+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
28+
shardTotal: [24]
2929
env:
3030
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
3131
SHARD_INDEX: ${{ matrix.shardIndex }}
@@ -69,7 +69,7 @@ jobs:
6969
version: 10.33.2
7070

7171
- name: ⎔ Setup node
72-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
72+
uses: WarpBuilds/setup-node@bc639b444d583175926b588962199c247d23e8d3 # v6
7373
with:
7474
node-version: 24.18.0
7575
cache: "pnpm"
@@ -85,9 +85,10 @@ jobs:
8585
if: ${{ !env.DOCKERHUB_USERNAME }}
8686
run: echo "DockerHub login skipped because secrets are not available."
8787

88-
- name: 🐳 Pre-pull testcontainer images
88+
- name: 📥 Prepare deps and testcontainer images
8989
run: |
90-
# Retry each pull - DockerHub registry timeouts are a recurring transient CI flake.
90+
# Pull images concurrently with dependency installation. Retry each pull because
91+
# DockerHub registry timeouts are a recurring transient CI flake.
9192
pull() {
9293
for attempt in 1 2 3; do
9394
docker pull "$1" && return 0
@@ -97,18 +98,41 @@ jobs:
9798
echo "::error::docker pull $1 failed after 3 attempts"
9899
return 1
99100
}
100-
echo "Pre-pulling Docker images with authenticated session..."
101-
pull postgres:14
102-
pull postgres:17
103-
pull clickhouse/clickhouse-server:26.2.19.43-alpine@sha256:c6ad6a7eb2fb5999df3adfb8b69a0c7222c68fa9b8f6b04a088564ebbc959251
104-
pull redis:7.2
105-
pull testcontainers/ryuk:0.14.0
106-
pull electricsql/electric:1.2.4@sha256:20da3d0b0e74926c5623392db67fd56698b9e374c4aeb6cb5cadeb8fea171c36
107-
pull minio/minio:latest
108-
echo "Image pre-pull complete"
109-
110-
- name: 📥 Download deps
111-
run: pnpm install --frozen-lockfile
101+
102+
pull_images() {
103+
local pids=()
104+
local failed=0
105+
for image in \
106+
postgres:14 \
107+
postgres:17 \
108+
clickhouse/clickhouse-server:26.2.19.43-alpine@sha256:c6ad6a7eb2fb5999df3adfb8b69a0c7222c68fa9b8f6b04a088564ebbc959251 \
109+
redis:7.2 \
110+
testcontainers/ryuk:0.14.0 \
111+
electricsql/electric:1.2.4@sha256:20da3d0b0e74926c5623392db67fd56698b9e374c4aeb6cb5cadeb8fea171c36 \
112+
minio/minio:latest
113+
do
114+
pull "$image" &
115+
pids+=("$!")
116+
done
117+
for pid in "${pids[@]}"; do
118+
if ! wait "$pid"; then
119+
failed=1
120+
fi
121+
done
122+
return "$failed"
123+
}
124+
125+
echo "Installing dependencies and pre-pulling Docker images..."
126+
pull_images &
127+
pull_pid=$!
128+
install_status=0
129+
pnpm install --frozen-lockfile || install_status=$?
130+
pull_status=0
131+
wait "$pull_pid" || pull_status=$?
132+
if (( install_status != 0 || pull_status != 0 )); then
133+
exit 1
134+
fi
135+
echo "Dependency install and image pre-pull complete"
112136
113137
- name: 📀 Generate Prisma Client
114138
run: pnpm run generate

apps/webapp/app/runEngine/services/triggerTask.server.nullBytes.test.ts

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function buildService(engine: any, prisma: any) {
5050

5151
describe("RunEngineTriggerTaskService null-byte sanitization", () => {
5252
containerTest(
53-
"strips a NUL from idempotencyKeyOptions.key so the jsonb insert does not 22P05",
53+
"sanitizes NUL-containing idempotency and debounce keys before the jsonb insert",
5454
async ({ prisma, redisOptions }) => {
5555
const engine = buildEngine(prisma, redisOptions);
5656

@@ -59,49 +59,22 @@ describe("RunEngineTriggerTaskService null-byte sanitization", () => {
5959
const service = buildService(engine, prisma);
6060

6161
const result = await service.call({
62-
taskId: "nul-idem-task",
62+
taskId: "nul-keys-task",
6363
environment,
6464
body: {
65-
payload: { kind: "idem" },
65+
payload: { kind: "nul-keys" },
6666
options: {
6767
idempotencyKey: "a".repeat(64),
6868
idempotencyKeyOptions: { key: `acme${NUL}inc`, scope: "run" },
69-
},
70-
},
71-
});
72-
assertNonNullable(result);
73-
74-
const row = await prisma.taskRun.findUniqueOrThrow({ where: { id: result.run.id } });
75-
expect(row.idempotencyKeyOptions).toEqual({ key: "acmeinc", scope: "run" });
76-
} finally {
77-
await engine.quit();
78-
}
79-
}
80-
);
81-
82-
containerTest(
83-
"strips a NUL from debounce.key so the jsonb insert does not 22P05",
84-
async ({ prisma, redisOptions }) => {
85-
const engine = buildEngine(prisma, redisOptions);
86-
87-
try {
88-
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
89-
const service = buildService(engine, prisma);
90-
91-
const result = await service.call({
92-
taskId: "nul-debounce-task",
93-
environment,
94-
body: {
95-
payload: { kind: "debounce" },
96-
options: {
9769
debounce: { key: `grp${NUL}1`, delay: "1s" },
9870
},
9971
},
10072
});
10173
assertNonNullable(result);
10274

10375
const row = await prisma.taskRun.findUniqueOrThrow({ where: { id: result.run.id } });
104-
expect((row.debounce as { key: string }).key).toBe("grp1");
76+
expect(row.idempotencyKeyOptions).toEqual({ key: "acmeinc", scope: "run" });
77+
expect(row.debounce).toMatchObject({ key: "grp1", delay: "1s" });
10578
} finally {
10679
await engine.quit();
10780
}

0 commit comments

Comments
 (0)