Skip to content

fix: run full local CI lanes in parallel and speed up Python codegen - #450

Merged
dmealing merged 5 commits into
mainfrom
fm/mo-ci-local-speed
Oct 23, 2026
Merged

dmealing merged 5 commits into
mainfrom
fm/mo-ci-local-speed

Conversation

@dmealing

@dmealing dmealing commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • scripts/ci-local.sh full (flagless) run now runs its lanes in parallel, capped by MO_CI_JOBS (default 8). Tree-writing steps (install, drift gates, workspace build) run first in a serial prelude. Output stays per-lane and ordered, and the exit code reflects every lane. --quick, --only and MO_CI_LIST_ONLY=1 still run and list the same selections as before.
  • Integration runners cut wall-clock time: pytest-xdist (MO_CI_PY_WORKERS, default 8), bun test --parallel for the TS integration suite (MO_CI_TS_WORKERS, 6), and surefire forkCount for JVM integration modules (MO_CI_JVM_FORKS, 3). Database containers use per-scenario databases, OS-assigned host ports and per-checkout container names. The Python tests/integration suite now runs once in the full run, not twice.
  • Python codegen runs ruff as its own binary (via find_ruff_bin, cached), falling back to python -m ruff. Output is unchanged. server/java/pom.xml wires the shared test logback config, so modules stop logging at DEBUG. CHANGELOG.md, CONTRIBUTING.md and docs/CONFORMANCE.md are updated to match.

Before / after (flagless scripts/ci-local.sh, same machine)

Total wall time: about 85m → 10m17s (my validation run). The gate's two back-to-back runs on the final tree took 10m38s and 8m55s, both green.

Step Before (serial) After (full run, 8 lanes at once)
conformance: python 16m49s 2m50s
integration-tests (ts) 15m52s 3m32s
integration-tests (kotlin) 8m31s 4m31s
integration-tests (java) 6m57s 2m52s
integration-tests (python) 5m51s 1m39s
integration-tests (csharp) 4m46s 5m02s
cube-model live check 1m43s 3m18s
java-reactor (install), Java/Kotlin conformance, mutation gate not printed 4m45s reactor; 10m30s mutation (the critical path)
Total about 85m 10m38s (lanes overlap)

"After" is from the gate's run #1. Some steps got slower in wall-clock terms because they now share the machine with seven other lanes. The total is set by the longest lane, which is mutation.

Test counts: nothing dropped or skipped

Suite Before After
ts unit suites 3757 / 382 / 156 / … identical
integration-tests (ts) 830 832
conformance: python 4131 3754 (see below)
integration-tests (python) 378 380 passed, 11 skipped
C# conformance / render / codegen / cli 2075 / 332 / 980 / 301 2075 / 332 / 984 / 301
integration-tests (csharp) 343 357
integration-tests (java), same tree 346 346
integration-tests (kotlin), same tree 376 376
  • The one count that fell is Python conformance, and no test was dropped. tests/integration (378 scenarios) used to run twice: once inside the conformance step and again as integration-tests (python). It now runs once, in the integration step: 4131 − 378 + 1 new test = 3754. Under --no-integration, or when docker is down, the conformance step still runs it.
  • Every other suite is equal or higher. The increases are tests that landed on main after the baseline log was taken. The Java and Kotlin rows compare the same tree before and after.
  • No test, gate or port was removed, skipped or sampled. --quick, --only and MO_CI_LIST_ONLY=1 list the same steps as before.

no-mistakes runs: 1. It had two fix rounds: review, plus test (the C# port race, fixed by binding Kestrel and the containers to OS-assigned ports with no retries, and the MySQL native-AIO limit).

Validation

Risk Assessment

✅ Low: The fix round's one change in gate_conf_python matches the probe run_integration_for uses (docker info) and restores the old behavior when docker is down; the rest of the diff was reviewed in round 1 with no new defects found.

Testing

Two consecutive full flagless ci-local runs on the final tree both passed, in 10m38s and 8m55s, against roughly 85m before the change. The final tree includes the OS-assigned-port fix and the MySQL native-AIO fix. The runs are the complete regression suite: all ports, conformance, integration, cube and the mutation gate. The C# integration lane passed 357/357 each time with no port-bind errors.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Full flagless ci-local run #1 is green with the OS-assigned-port fix ✅ pass live exit 0, LOCAL CI PASSED, 10m38s
Full flagless ci-local run #2, back to back, is green again (no intermittent port race) ✅ pass live exit 0, LOCAL CI PASSED, 8m55s
C# integration lane (357 tests) shows no port-bind collision under parallel lanes ✅ pass live 357/357 passed in both runs; no address already in use in either log
Parallel wall time is far below the ~85m serial baseline ✅ pass live 10m38s and 8m55s, with 8 lanes running at once
  • Outcome: ⚠️ 2 infos across 2 runs (1h43m37s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ⚠️ scripts/ci-local.sh:737 - gate_conf_python now drops tests/integration whenever --no-integration is NOT set. It assumes integration-tests (python) will run that directory instead, but that only happens when docker is up. Concrete path: scripts/ci-local.sh --only python with docker down and no --strict-toolchains. The conformance step runs pytest tests/ --ignore=tests/integration. run_integration_for then records only a SKIP (ci-local.sh:921-923). The docker-free half of tests/integration (the generated-lane and normalization tests) runs nowhere, and the run reports 'PASSED (with 1 skipped)'. Before this change, the same run executed tests/integration in the conformance step. postgres_container.py does not skip without docker, so the old run went red. Fix: only add --ignore=tests/integration when the integration step will actually run. Use the same docker info check that run_integration_for uses, or integ_ready in the full run. In the full run with docker down, the FAIL 'integration-tests (docker down)' already makes the run red, so only the serial --only path is affected.
  • ℹ️ scripts/ci-local.sh:58 - Fixed the stale help text for --only cube. It said the full run adds cube 'after the integration suite'. It now says the full run runs cube as its own parallel lane.
  • ℹ️ scripts/ci-local.sh:783 - Fixed the stale gate_cube comment. It said 'the full run after the integration suite'. It now says 'the full run's cube lane'.
  • ℹ️ scripts/ci-local.sh:459 - Fixed the gate_showcase_all_ports comment. It said the workspace is built by gate_ts_build_typecheck earlier in the same run. The full run now builds the workspace in its serial prelude (gate_ts_build), so the comment now says that.

🔧 Fix applied.

✅ The docker-down Python integration finding is fixed in the pipeline (commit b732c18). The conformance step now drops tests/integration only when the integration step will run it, meaning integration is enabled and docker info succeeds. Otherwise conformance runs it as before.

3 infos still open:

  • ℹ️ scripts/ci-local.sh:58 - Fixed the stale help text for --only cube. It said the full run adds cube 'after the integration suite'. It now says the full run runs cube as its own parallel lane.
  • ℹ️ scripts/ci-local.sh:783 - Fixed the stale gate_cube comment. It said 'the full run after the integration suite'. It now says 'the full run's cube lane'.
  • ℹ️ scripts/ci-local.sh:459 - Fixed the gate_showcase_all_ports comment. It said the workspace is built by gate_ts_build_typecheck earlier in the same run. The full run now builds the workspace in its serial prelude (gate_ts_build), so the comment now says that.
✅ **Test** - passed after one fix round

Final state: the first full run found an intermittent C# integration failure (address already in use). The cause was a port race: a free port was picked, released, then bound again. It is fixed at the root by binding Kestrel to http://127.0.0.1:0, so the OS assigns the port, and PickFreePort is deleted. The same sweep moved the other servers and containers to OS-assigned ports. There are no retries and nothing was serialised. Parallel MySQL containers also exhausted the kernel fs.aio-max-nr limit, so all five MySQL helpers now start InnoDB with --innodb-use-native-aio=0. After those fixes, two full flagless runs passed back to back, in 10m38s and 8m55s.

  • ℹ️ scripts/ci-local.sh - --integration-only now runs through the parallel full-run scheduler, where before it ran as one serial step. This is intended.
  • ℹ️ The per-checkout Postgres sidecar container keeps running on purpose so later runs can reuse it. docker rm -f <name> stops it.
Scenario Result Live Evidence
Full flagless ci-local run #1 is green with the OS-assigned-port fix ✅ pass live exit 0, LOCAL CI PASSED, 10m38s
Full flagless ci-local run #2, back to back, is green again (no intermittent port race) ✅ pass live exit 0, LOCAL CI PASSED, 8m55s
C# integration lane (357 tests) shows no port-bind collision under parallel lanes ✅ pass live 357/357 passed in both runs; no address already in use in either log
Parallel wall time is far below the ~85m serial baseline ✅ pass live 10m38s and 8m55s, with 8 lanes running at once
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

… the root

The flagless scripts/ci-local.sh took about 85 minutes, every lane one after
another. Measured on the same 24-core machine it now takes 10m17s, green, with
the same tests: every suite reports the same counts, less the duplicate below.

- Parallel lanes. The tree-writing steps (install, drift gates, one workspace
  build) run first; the rest run as independent lanes, at most MO_CI_JOBS (8)
  at once, each into its own log printed as one block when it ends. The
  summary lists every step's time, the slowest steps and each lane's wall
  time. --quick and --only stay serial and select exactly what they did.
- Each test runs once. tests/integration ran inside "conformance: python" and
  again as "integration-tests (python)"; the java and kotlin conformance
  classes ran inside the reactor and again on their own; the workspace was
  rebuilt and reinstalled by several lanes; mvn install ran three times.
- Python codegen ran ruff as `python -m ruff`, a second interpreter per call,
  twice per emitted file: ~90% of codegen time. It now runs ruff's binary
  (find_ruff_bin), same version, same output. Python conformance 16m49s ->
  1m20s alone (with xdist).
- The shared Java test logback config was never wired, so every module but
  metadata logged at DEBUG (1.86M lines a run). Surefire now points every
  module at it.
- Runners in parallel: pytest-xdist, bun test --parallel for the TS
  integration suite, surefire forkCount=3 for the JVM integration modules.
  TS integration 15m52s -> 2m49s, Java 6m57s -> 1m55s, Kotlin 4m52s -> 2m33s,
  Python integration 5m51s -> 56s, each measured alone.
- The Postgres sidecar is named per checkout; each scenario still gets its own
  database.
@dmealing
dmealing merged commit 492decb into main Oct 23, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-ci-local-speed branch October 23, 2026 07:44
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