Skip to content

Size CI test jobs on RunsOn runners to the parallelism they can use - #75833

Merged
potiuk merged 13 commits into
mainfrom
ci-runs-on-fewer-test-slices
Oct 29, 2026
Merged

potiuk merged 13 commits into
mainfrom
ci-runs-on-fewer-test-slices

Conversation

@potiuk

@potiuk potiuk commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Most test jobs on the self-hosted RunsOn runners can't use the CPUs they get. Jobs that run with --run-in-parallel (DB, lowest-dependencies and special tests) start one serial pytest per test type, so a job never uses more CPUs than it has test types. Logs from canary run 40517104168 show this:

  • Postgres core DB job (8 CPUs): runs 3 test types (Core, Other, Serialization). Serialization finishes in 2 min, Other in about 21 min and Core in 23 min, so for most of the job only 2 of the 8 CPUs are busy.
  • Lowest-dependencies provider jobs (32 CPUs): about 20 providers per job, so at most about 60% of the CPUs can be used.
  • Integration-core and system tests: one pytest each, on 32-CPU / 128 GB runners.

The core test types are split into two matrix jobs to keep the Non-DB xdist collection inside the 16 GB of a public runner. On RunsOn that job gets 128 GB. The DB halves each still last as long as their slowest test type, so on RunsOn the split only doubles the job count. It also ran the single system test once per slice.

Changes, for RunsOn only (public runners are unchanged):

  • Core test types run in one matrix job instead of 2.
  • Lowest-dependencies provider tests run in 3 jobs instead of 5, which is about one provider per CPU of the 32-CPU runner.
  • Integration-core and system tests move to the 8+16 / 32+64 size class, the same as the provider integration tests.
  • Provider tests run one provider per test type, in one matrix job on the 32-CPU size class, instead of the long-running providers plus one group of all the others. The group's single serial pytest left the other CPUs idle. The Non-DB (xdist) jobs read a new providers-xdist-test-types-list-as-strings-in-json output that never splits the providers, since xdist runs them all in one pytest anyway.
  • The longest-running providers (amazon, google, standard) are split into several test types such as Providers[amazon:hooks], with Providers[amazon:other] running the rest of the provider, so a provider job does not wait on one long serial pytest. Lowest-dependencies jobs on RunsOn are sized at about 40 test types each: 3 jobs for all providers, one for a change touching a few.
  • Self-hosted runners split Docker's default address range into /24 subnets before the tests start. Each parallel test type creates its own compose network, and the default pools fit only about 30, fewer than the large runners run at once.
  • Jobs with one process (end-to-end tests against one Airflow stack, a Gradle test run) or a minute or two of work move to the 8+16 or 4+8 size classes, and on RunsOn the Helm unit tests run all their packages in one job per Kubernetes version instead of one job per package.
  • The RunsOn cost summary step also charts each job's CPU and memory use (metrics: cpu,memory), so the size classes can be set from measurements.
  • Self-hosted runners raise fs.aio-max-nr, which a few dozen parallel MySQL servers exhaust.
  • The provider compatibility tests keep the grouped provider test types, since each of their test types installs an Airflow version first; the per-provider list is a separate providers-split-test-types-list-as-strings-in-json output.

The selective-checks doc covers the new sizing and slicing rules, and a test covers the slice counts on both runner types.

Checks run locally: the selective-checks test suite (passes), the fast static checks (pass, except the docker and network hooks the sandbox blocks), and a roborev review (no findings). This PR has the canary and full tests needed labels so the full matrix runs on RunsOn and the new job durations can be compared with the canary run above.


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Opus 5

Generated-by: Claude Opus 5 following the guidelines

🤖 Generated with Claude Code

Jobs that run tests with --run-in-parallel start one serial pytest per test type, so they never use more CPUs than they have test types. On RunsOn, splitting core test types into two jobs only doubles the jobs: each half still lasts as long as its slowest test type, and the out-of-memory risk the split guards against on public runners does not apply to the larger RunsOn runners. The lowest-dependencies provider jobs left their 32 CPUs partly idle, and the integration-core and system tests ran a single pytest on 32-CPU runners.

Generated-by: Claude Opus 5
@potiuk potiuk added full tests needed We need to run full set of tests for this PR to merge canary When set on PR running from apache repo - behave as canary run labels Oct 28, 2026
Grouping all providers but the long-running ones into one test type left the provider jobs waiting on that group's single serial pytest while the other CPUs idled. The Non-DB jobs run every provider in one xdist pytest, where splitting test types only adds entries, so they get the providers unsplit.

Generated-by: Claude Opus 5
@potiuk potiuk added the include success outputs Whether to include success outputs in test results label Oct 28, 2026
…runs

Each test type of a parallel test run is a docker compose project with its own network. Docker's default address pools fit about 30 networks, so provider jobs running a test type per CPU on 32-CPU and larger runners failed with "all predefined address pools have been fully subnetted".

Generated-by: Claude Opus 5
…unsOn

Once every provider runs as its own test type, a job lasts as long as its slowest provider, and amazon, google and standard each take far longer than the rest as a single serial pytest. Splitting them lets the large RunsOn runners work on their parts at once. Lowest-dependencies jobs are sized by test-type count, so a change touching a few providers keeps one job.

Generated-by: Claude Opus 5
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

The re-run of the jobs stopped by EC2 Spot interruptions finished: attempt 2 passed.

Docker Compose project names and GitHub artifact names reject the colon that names a part of a split provider.

Generated-by: Claude Opus 5
A split provider part selects a folder inside the provider's tests, and pytest runs an explicitly selected path even when its provider folder is ignored, so a skipped or excluded provider's parts would still run.

Generated-by: Claude Opus 5
…and memory use

Several jobs ran a single process, or a minute or two of work, on 32-CPU or 8-CPU runners, and fourteen Helm unit test jobs each spent more time preparing the image than running their tests. Charting each job's CPU and memory use lets the size classes be set from measurements rather than from how the jobs are expected to behave.

Generated-by: Claude Opus 5
The check for an empty run treated the first remaining argument, or any path-like argument, as a test selection, so option values such as the "8" of "-n 8" or a path passed to --rootdir decided whether tests ran. It now compares the ignored folders with the paths the test type itself selects, and passes paths the user names explicitly to pytest as they are.

Generated-by: Claude Opus 5
Under xdist every test type runs in one pytest process, where the --ignore arguments of a provider's other part would silently be dropped or would remove its sibling parts, so the wrong tests would run.

Generated-by: Claude Opus 5
Test suites such as All-Postgres add pytest options with values, which were taken for selected test paths, so a run whose every test folder was ignored still started pytest with nothing selected.

Generated-by: Claude Opus 5
Each worker kept the whole list of test types, so with --skip-db-tests every worker selected the folders of all of them.

Generated-by: Claude Opus 5
A few dozen MySQL servers running at once exhaust the default fs.aio-max-nr, and InnoDB then fails with "io_setup() failed with EAGAIN".

Generated-by: Claude Opus 5
Each test type of a compatibility test installs an Airflow version and the providers before running, so listing every provider as its own test type multiplied those installs and the jobs ran out of time.

Generated-by: Claude Opus 5
@potiuk
potiuk requested a review from shahar1 October 28, 2026 04:22
@potiuk
potiuk merged commit 083b82d into main Oct 29, 2026
293 of 296 checks passed
@potiuk
potiuk deleted the ci-runs-on-fewer-test-slices branch October 29, 2026 21:12
@github-actions

Copy link
Copy Markdown
Contributor

Backport successfully created: v3-4-test

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
✅ v3-4-test PR Link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:dev-tools backport-to-v3-4-test Backport to v3-4-test canary When set on PR running from apache repo - behave as canary run full tests needed We need to run full set of tests for this PR to merge include success outputs Whether to include success outputs in test results

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants