Skip to content

test: cut the integration suite from 101s to 15s - #101

Merged
cnmaia merged 6 commits into
mainfrom
perf/integration-test-speed
Sep 26, 2026
Merged

cnmaia merged 6 commits into
mainfrom
perf/integration-test-speed

Conversation

@cnmaia

@cnmaia cnmaia commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

🤔 Problem

The CI takes ~5 min, and almost all of it is the integration job: ~3m15s running the tests and 47–71s starting services. The unit job takes ~30s.

🧐 Solution

  • bcrypt cost 4 on the seeded client. Each authenticated request spent ~180ms in checkpw. This alone took the suite from 101s to 35s locally.
  • Random DOI suffix in the WireMock mock. The identifier used now at one-second resolution. With fast requests, two tests collided with 409 doi_already_exists. This bug was being hidden by the slowness.
  • Polling instead of time.sleep(1) in the log tests, via the new tests/integration/utils/container_log.py:
    • wait_for_log for "was logged" assertions;
    • flush_log (a marker request) for "was not logged" assertions.
  • Docker layer cache (type=gha) for the image build in the integration job.

🤨 Rationale

  • I measured before changing anything (--durations=0). The time was not in the tests themselves, so concurrency (xdist) would have attacked the wrong thing. It would also be risky here: object_storage_down stops MinIO for everyone, and the log tests read a shared docker logs.
  • Absence assertions after flush_log still catch leaks: in 20/20 attempts, a line written just before the barrier was already in the log.
  • The cost is stored inside the hash, so production and clients created through the API are unaffected.

Locally: 233 passed on a clean stack, 4 runs, 14.5–15.6s (baseline 101s). The CI time is still to be confirmed by this run; the build cache only takes effect from the second run.

Side finding, not addressed here: authenticate is async def and calls bcrypt synchronously. It blocks the event loop for ~180ms per request in production too. With 10 concurrent requests, even the health-check waited 1.7s.

🤖 Generated with Claude Code

cnmaia and others added 2 commits September 25, 2026 22:57
Two thirds of the run was bcrypt. Every authenticated request verifies the
seeded client's secret, and at cost 12 that is ~180ms of CPU on a single
uvicorn worker. The seeded hash is now cost 4; the cost travels inside the
hash, so nothing outside this fixture changes, and clients created through the
API still get 12. 101s -> 35s.

That exposed a collision the slow requests had been hiding. The DOI mock built
its identifier from the current time to the second, so two tests creating a
DOI in the same second got 409 doi_already_exists. The suffix is now random,
generated once per response so id, doi and suffix agree. ALPHANUMERIC_UPPER,
used by the other mock bodies, is not a WireMock type and silently produced
symbols; this uses ALPHANUMERIC with uppercase=true.

Most of what remained was 21 fixed one-second sleeps before reading the
container log. tests/integration/utils/container_log.py replaces them:
wait_for_log polls until the expected line is there, and flush_log makes a
marker request and waits for it, so an assertion that something was NOT
logged runs only once everything before it has been written. The degradation
test now matches the line of its own request rather than any line from the
last minute, which the previous test's outage could also satisfy.

233 pass on a clean stack, four runs, 14.5-15.6s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Start services spent 47-71s, most of it compiling dependencies on alpine in
the builder stage. Compose only builds an image it cannot find, so building
it first with the gha cache makes `up` reuse it, and the builder stage is
rebuilt only when requirements.txt changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cnmaia
cnmaia requested a review from andrenmaia as a code owner September 26, 2026 01:58
cnmaia and others added 4 commits September 26, 2026 17:16
Timestamps on pytest's progress lines mark when a file finishes, not when it
starts, which pointed the last investigation at the wrong file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cnmaia
cnmaia merged commit 382ba08 into main Sep 26, 2026
4 checks passed
@cnmaia
cnmaia deleted the perf/integration-test-speed branch September 26, 2026 20:48
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