fix(integration-tests): run the integration suite in parallel - #15694
Conversation
c6f5ac4 to
533ac47
Compare
The `integration tests` Jenkins stage on this branch runs single-threaded and always exceeds its 45 minute timeout, so Jenkins ABORTs the build before pytest can write integration-tests-junit.xml. Nothing is published and the jenkins/integration-tests check never reports. pytest-xdist is only a declared dependency from branch-2026.1 upward, so `sct.py integration-tests` had no way to fan out. Add the dependency and mirror branch-2026.1: a `-n` option (default 4) and `--dist loadgroup`. Two xdist groups keep the tests that genuinely share mutable state pinned to a single worker each, while the rest spread freely: - docker_ssl: modules whose tests run the docker_scylla fixture with ssl=True. That path regenerates the *shared* CA and certificates under data_dir/ssl_conf, so two workers doing it concurrently overwrite each other's certs. - docker_heavy: the resource-intensive modules already grouped under this name on branch-2026.1, plus the modules that reuse a fixed docker network name (ycsb_net, kafka-stack-docker-compose_default). Using two independent groups rather than one keeps the critical path at roughly half of what a single all-Docker group would give, since loadgroup runs different groups on different workers in parallel. Refs SCT-804.
d57bc6a to
412b2b6
Compare
|
CI now proves the premise of this PR. Build PR-15694/5 against the freshly published
The 22 remaining failures are not what this PR set out to fix — as stated under "This makes the stage finish, not pass". They are the branch's pre-existing environment and fixture rot, now finally visible instead of hidden behind an abort:
One caveat I would rather flag than paper over: |
|
I'll fight the failing integration test in other PRs |
a2ce699 ("feat(argus): add always-on replay log and replay-only client") removed uv.lock along with its other changes. Nothing broke loudly, because docker/env/build_n_push.sh runs `uv lock` immediately before `docker build`, so the lock the Dockerfile consumes via `ADD uv.lock .` + `uv sync --frozen` is regenerated on every build. That is precisely the problem: with no committed lock, `--frozen` is frozen against a file resolved seconds earlier, so two builds of the same commit can resolve different transitive versions. On a branch whose whole purpose is performance measurement, the toolchain underneath the numbers should not drift silently between images. Restored from a2ce699^ and re-resolved against the current pyproject.toml. The only content changes are five pins that moved in the meantime — pyzmq, questionary, requests, rich and scylla-driver — each already declared in pyproject.toml, so the lock now simply agrees with it. Verified with `uv lock --check` and a real `uv sync --frozen`. Confirmed byte-identical to what the CI runner's own `uv lock` produces: the build_image run on #15694 (which had this lock committed) committed only docker/env/version, leaving the lock untouched.
Refs SCT-804, blocks SCT-714. Sibling of #15691 (same change for
branch-2025.1).Problem
The
integration testsstage onbranch-perf-v17runs single-threaded and therefore always exceeds its 45-minute timeout. Jenkins ABORTs the build before pytest can writeintegration-tests-junit.xml, so nothing is published and thejenkins/integration-testscheck never reports a result at all.Measured:
branch-2026.1runs 4 xdist workers and finishes the stage in 21.6 min.branch-perf-v17produces zero[gwworker lines and had only reached 65% of ~181 selected tests when the timeout fired. 83% of its wall time is 18 stalls of ~116-146 s each, one per Docker-backed test.Root cause:
pytest-xdistis only a declared dependency frombranch-2026.1upward. On this branch the stringpytest-xdistappears only insideunit_tests/unit/test_keystore.py, never as a dependency, sosct.py integration-testshas no-noption and no way to fan out.What changed
1.
sct.py— added-n(default 4) and pass--dist loadgroup -n{n}to pytest, mirroringupstream/branch-2026.1:sct.py.-p no:warningsis retained (2026.1 dropped it, but changing warning behaviour is out of scope here), and-t/--testis left single-valued to keep the diff focused.2.
xdist_groupmarkers. This branch had zero. Rather than one blanket group, the tests are split into two groups, each a genuine serialization domain, because--dist loadgroupruns different groups on different workers in parallel — so two balanced groups halve the critical path relative to putting every Docker test in one group.docker_ssl(18 selected tests) — every module whose tests drivedocker_scyllawithssl=True. That path callscreate_ca()/create_certificate(), which write fixed shared paths underdata_dir/ssl_conf(sdcm/provision/helpers/certificate.py:CA_CERT_FILE,CA_KEY_FILE,CLIENT_CERT_FILE,CLIENT_FACING_CERTFILE, …), and itos.chdir()s the process. Two workers doing this concurrently overwrite each other's certificates, so the client ends up presenting a cert minted for the other worker's container IP. Modules:test_cassandra_stress_thread,test_latte_thread,test_python_driver,test_scylla_bench_thread.Note this branch does not have the
configure_scylla_node(..., ssl_dir: Path | None = None)per-test SSL directory thatbranch-2026.1has, which is exactly why the group is needed here and not there.docker_heavy(21 selected tests) — the resource-intensive modules already grouped under this same name onbranch-2026.1(introduced byfaf581538b"fix(integration tests): group resource-intensive Docker based tests" — YCSB / vector store / cassandra-stress were failing intermittently in parallel), plus the modules that reuse a fixed docker network name (ycsb_netintest_ycsb_thread,kafka-stack-docker-compose_defaultintest_kafka). Modules:test_cql_stress_cassandra_stress_thread,test_gemini_thread,test_kafka,test_ndbench_thread,test_run_cqlsh,test_vector_store,test_ycsb_thread.test_latte_threadandtest_scylla_bench_threadmix unit and integration tests in one module, so they get the marker on the individual integration tests rather than viapytestmark— otherwise the marker would leak onto 58 and 21 non-integration tests respectively.docker_scyllais function-scoped and each test gets its own container on ephemeral host ports (-p <port>with no host port), so Docker-backed tests do not share a container and the remaining 17 ungrouped Docker tests (test_cluster14,test_alternator_streams_kcl,test_cassandra_harry,test_utils_database_query_utils) parallelise safely alongside the 126 pure-Python config/version tests.3.
pyproject.toml— addedpytest-xdist==3.8.0, the same pin asbranch-2026.1.4.
docker/env/version— not touched by hand. TheNew Hydra Versionlabel drives thebuild_imageworkflow, which builds and pushes the image and then commits the bump itself (see below).The hydra image rebuild — done, by the bot
pytest-xdistis NOT present in the currently pinned hydra image. Verified empirically against the exact tag this branch pins (docker/env/version=1.107-PR13751-82c347a):python/python3are the same/usr/local/bin/python3.13interpreter (the Dockerfile setsUV_PROJECT_ENVIRONMENT=/usr/local/, so there is no separate venv), and nothing namedxdistexists anywhere on the filesystem.Which Dockerfile builds the image was confirmed before touching anything:
docker/env/build_n_push.shrunsuv lockand thendocker build -t scylladb/hydra:${VERSION} .from the repo root, so the repo-rootDockerfileis the one (there is nodocker/env/Dockerfileon this branch). ItsADD uv.lock .+uv sync --frozenis not inconsistent withuv.lockbeing absent from git —build_n_push.shgenerates the lock immediately before the build, so it is a build artefact rather than a committed file. (#15703 proposes committing it anyway, so--frozenreproduces a pinned resolution instead of one invented seconds earlier — that is independent of this PR and not required by it.)pyproject.tomlis therefore the only dependency file to edit, and nouv.lockis committed here.The image now exists and the bot pinned it:
scylla-sct[bot]commitd57bc6ae53"chore(hydra): create image 1.108-PR15694-82b64e2" setdocker/env/version=1.108-PR15694-82b64e2, andscylladb/hydra:v1.108-PR15694-82b64e2is published (568 MB, 2026-08-05T18:26:07Z). Confirmed to contain what this PR needs:Why the first attempt produced no image
The version file was originally bumped by hand, in a commit whose subject copied the bot's own format —
chore(hydra): create image 1.108-PR15694-d3ec10a. The workflow guards against rebuilding with:so that hand-written subject made the guard match, every build step was skipped, and the run still reported success — while
1.108-PR15694-d3ec10awas never pushed. Everyhydra.shstage then failed on the pull (failed to resolve reference … not found), which is whyprecommit,unittests,lint_test_casesandintegration-testsall reported failure with no test output.Dropping that commit and re-applying the label was enough. The guard is not otherwise fragile here:
actions/checkoutuses the defaultfetch-depth: 1, soorigin/master..HEADonly ever sees the tip commit, and the branch's 8 inheritedchore(hydra): create image …commits are invisible to it.Verification
The rebuilt image was built locally from this branch's Dockerfile (not pushed) to prove the dependency actually reaches it:
--dist loadgrouppins same-group tests to a single worker, and — importantly — runs the two groups concurrently on different workers:All 6
docker_ssltests landed ongw1, all 6docker_heavytests ongw0, and the ungrouped ones spread overgw1/gw2/gw3— note the@docker_ssl/@docker_heavynodeid suffixes xdist appends for grouped tests.Group assignment audited across the whole selected suite inside the hydra image (temporary reporting plugin, not committed):
No marker leaks onto a non-integration test, and every module that regenerates the shared certificates is inside
docker_ssl.Collection works with the real argument list:
CLI wiring:
Pre-commit over the diff — all hooks pass (
uv-sortin particular confirms the new dependency is correctly placed):This makes the stage finish, not pass
Please do not read still-failing Docker integration tests as "the PR didn't work". There is a separate, independent bug on this branch:
unit_tests/conftest.py::configure_scylla_nodebuildsscylla = RemoteDocker(...)and then ends on its twowait.wait_for(...)readiness calls without ever returning it — everyreturnin that function belongs to the nesteddb_up/db_alternator_uphelpers. Sofixture_docker_scylladoesscylla = configure_scylla_node(...)→yield None→scylla.kill()→AttributeError: 'NoneType' object has no attribute 'kill'.return scyllais present onmaster,branch-2026.3,branch-2026.2andbranch-2026.1, and absent on exactlybranch-perf-v17andbranch-2025.1— the two branches whose Docker integration tests mass-ERROR. It is deliberately not fixed here so this PR stays reviewable in isolation (same call as #15691); it is recorded on SCT-714 and will land as its own per-branch change.So the expected outcome of merging this PR alone is: the stage completes within its timeout and JUnit finally gets published, with the Docker tests still ERRORing — just ~4× faster. It goes green once the
return scyllafix lands.Because the ~2 min per Docker-backed test is genuine container boot rather than an expiring timeout, the critical path is now bounded by the larger group: roughly
max(21, 18) × ~2 min. That is what motivated two balanced groups instead of one, and if the stage still runs tight, splittingdocker_heavyfurther (e.g. liftingtest_ndbench_threadandtest_run_cqlsh, which share no named resource and run ungrouped and green onmaster) is a one-line change.unit-testsdeliberately left serialsct.py unit-testsis also serial on this branch andbranch-2026.1runs it with-n2, so parallelising it is desirable. It is not done here: theunit-testsjob is currently green, and a local serial-vs-parallel comparison over its ~1481 tests could not be completed (the serial baseline was OOM-killed at 58% in my container), so there is no evidence that parallelising it is safe. Rather than risk a working job on an unverified change, it is left alone and can be done as a follow-up with a proper before/after run.Out of scope (tracked in SCT-714)
Deliberately not touched: the missing
return scylla(above), the container-readiness ERRORs, the missingunit_tests/test_configs/directory, and the JUnit-on-abort gap.