8000 ci: run the backend tests in parallel, start every CI job at once by adeelahmad · Pull Request #328 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

ci: run the backend tests in parallel, start every CI job at once - #328

Merged
adeelahmad merged 2 commits into
mainfrom
claude/parallel-ci-rmn4jv
Oct 10, 2026
Merged

adeelahmad merged 2 commits into
mainfrom
claude/parallel-ci-rmn4jv

Conversation

@adeelahmad
@adeelahmad adeelahmad commented Oct 10, 2026 •
Copy link
Copy Markdown
Owner

Requested by Adeel · project thread

Description

Before: a backend PR's CI took about 9 minutes of running after two waits for a free runner. The embedded-database tests ran in one job (8 min of tests) and the server-database tests in another (6.5 min), and since #303 every job also waited for an Affected projects job first, so it queued for a runner a second time. Runs of merged or closed PRs kept going.

After: every CI job starts at once. The backend's tests are split across two runners per database and are much faster on their own. Merging or closing a PR cancels its leftover runs.

  • The slow tests' cause (tests/conftest.py): each xdist worker runs its own tesseract, and tesseract's OpenMP threads fought over the cores. One thread each: the video and OCR tests went from 539 s to 23 s on 4 cores, and the whole backend suite from 320 s to 171 s locally (853 passed). Only tests are affected; the app is unchanged.
  • Shards: FastAPI (embedded SurrealDB) 1/2, 2/2 and FastAPI (SurrealDB server) 1/2, 2/2, split by recorded durations with pytest-split (MIT, pinned with uv run --with, so the lockfile is untouched). The durations are in fastapi_backend/tests/durations.json, and nx run backend:test-durations records them again. Locally the halves are 437 and 433 tests, disjoint and covering all 870. The first embedded shard also runs lint, mypy and the OpenAPI check.
  • Coverage uses Python's sys.monitoring (COVERAGE_CORE=sysmon): a shard took 117 s instead of 162 s locally. Each shard keeps its coverage data with the run. On main, Backend coverage combines them into the backend-coverage artifact and the summary; on PRs that job would hold the checks waiting for another runner, so it doesn't run there.
  • No second queue: each job asks Nx itself whether its project is affected (.github/actions/affected, the same nx-set-shas base as before). An unaffected job stops after that and passes. If Nx can't answer, the job runs. Other projects writes the affected list to the summary. .github/actions/** joins sharedGlobals in nx.json.
  • Cancelled leftovers: CI, pre-commit and Install also trigger on closed, with every job skipped. The per-PR concurrency group then cancels the PR's queued or running runs. PR title gets a concurrency group too.

Every check that ran before still runs. No test is skipped or disabled.

Motivation and Context

About 40 runs were queued, and jobs waited 4 to 23 minutes for a runner, longer than they ran (e.g. main's run at 20:53: Next.js started after 13 min, embedded tests after 23 min). Shorter jobs, no second queue and no leftover runs free runners for the open PRs.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality): CI only
  • Breaking change

Checklist:

  • The title is a Conventional Commit.
  • My code follows the code style of this project (actionlint with shellcheck, ruff).
  • I added or updated tests: no new tests. The split was checked locally (both halves pass, none lost), and this PR's own CI runs the new jobs.
  • My change requires documentation updates, and I have updated docs/ accordingly (docs/contributing.md, Monorepo and CI).
  • CHANGELOG: n/a, CI only.

Done questions (AGENTS.md)

  • Goal: parallel CI that clears the PR backlog faster; merged when green. On this PR's CI the four backend shards took 3.5 to 4.1 min each (job start to end, setup included), against about 9 min for the old single jobs.
  • Agents: n/a, CI only. Agents get CI results sooner.
  • Zero setup: no new prerequisite. pytest-split comes through uv run --with.
  • Fewest clicks: n/a.
  • Configurable: n/a, no new setting. Shard counts are the matrix lists.
  • Light: n/a, the app is unchanged.
  • Private and secure: n/a, no new surface. The workflows keep contents: read and actions: read.
  • Cost: free GitHub runners for a public repo. Fewer runner minutes per PR (no oversubscribed tests, no runs of closed PRs).
  • User decides: n/a. Defaults unchanged.
  • Upgrades: n/a, no stored data.
  • Honest: the per-test timings above were measured locally; the shard times in Goal come from this PR's CI run.
  • Tests, docs and generated files: durations recorded with the tool (--store-durations), docs updated.
  • Open: pytest-split is MIT.
  • Refine later: the app runs tesseract with all its OpenMP threads too, so parallel processing on a user's machine may oversubscribe the same way (worth a look on a Pi). Re-record durations.json now and then. Maybe drop the duplicated ruff/ESLint/tsc in pre-commit, since CI runs them too. Backend coverage's combine runs for the first time on main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AZk9aGmS4XYCbX5FtubfuF


Generated by Claude Code

The backend's tests ran in one job each for the embedded and the server
database (8 and 6.5 minutes on GitHub's runners), behind an Affected
projects job that every other job waited for, so each of them queued for a
free runner twice.

- tests/conftest.py: one OpenMP thread per tesseract. Each xdist worker runs
  its own, and their threads fighting over the cores made the video and OCR
  tests take minutes (539 s against 23 s for them on 4 cores; the whole
  suite 320 s against 171 s).
- Both backend jobs are split across two runners by recorded durations
  (pytest-split, fastapi_backend/tests/durations.json; nx run
  backend:test-durations records them again). Coverage uses Python's
  sys.monitoring (COVERAGE_CORE=sysmon), a third faster.
- Every CI job starts at once and asks Nx itself whether its project is
  affected (.github/actions/affected); one that isn't stops there and passes.
- On main, Backend coverage adds up the shards' coverage.
- Merging or closing a pull request cancels its CI, pre-commit and Install
  runs that are still waiting or going; PR title cancels its older checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZk9aGmS4XYCbX5FtubfuF
@adeelahmad adeelahmad self-assigned this Oct 10, 2026
@adeelahmad
adeelahmad merged commit 23550b2 into main Oct 10, 2026
17 checks passed
@adeelahmad
adeelahmad deleted the claude/parallel-ci-rmn4jv branch October 10, 2026 22:41
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.

2 participants

0