Skip to content

UN-3636 [PERF] Stop the test rig wasting CPU and hashing time - #2195

Merged
chandrasekharan-zipstack merged 13 commits into
mainfrom
feat/rig-extra-manifests
Jul 23, 2026
Merged

UN-3636 [PERF] Stop the test rig wasting CPU and hashing time#2195
chandrasekharan-zipstack merged 13 commits into
mainfrom
feat/rig-extra-manifests

Conversation

@chandrasekharan-zipstack

@chandrasekharan-zipstack chandrasekharan-zipstack commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

What

Three independent, test-only changes to how the rig runs the backend suites. No product code touched.

Change Where
Resolve the xdist worker count in the rig instead of deferring to -n auto tests/rig/cli.py
--no-migrations backend/pyproject.toml
MD5 password hasher backend/backend/settings/test.py

Why

Worker count. -n auto calls xdist's pytest_xdist_auto_num_workers, which prefers psutil.cpu_count(logical=False) and only falls back to sched_getaffinity when psutil is absent. psutil is present in the backend and workers venvs, so on a hyperthreaded runner those groups see the physical core count — often 1 — and collapse to a single worker, while groups without psutil get the full thread count. The three largest suites (unit-workers, unit-backend, integration-backend) are exactly the ones that lose their parallelism. Only gw0/gw1 ever appear in the logs.

-n logical would fix that but oversubscribe developer machines. Measured on a 12-core box, integration-backend: 2w=95.1s, 4w=79.4s, 8w=89.4s, 12w=121.4s — past ~4 workers the suites contend on the test DB more than they parallelise. So the rig counts usable CPUs itself (sched_getaffinity, honouring pinning) and caps at 8.

Migrations. pytest-django replays the full migration history into a fresh database per xdist worker. --no-migrations builds the schema straight from the current models instead. The final schema is identical to the post-migration schema (Django's makemigrations --check keeps models and migrations in sync), so tests see the same tables and columns — they just skip the historical replay. The one thing it drops is data migrations (RunPython seed rows); nothing in these suites depends on migration-seeded data (verified: identical pass set), and tests that need rows create them via fixtures/factories. Payoff scales with the number of migrations, so it grows on larger app trees.

Hasher. No PASSWORD_HASHERS override existed anywhere under backend/settings/, so every create_user() in a fixture paid Django's default 600k-iteration PBKDF2 — ~120ms a call, and the permissions/owner-management suites seed several users per test.

Measured

integration-backend, 4 workers, 104 passed identical in every arm:

Config pytest time
baseline (migrations + PBKDF2) 52.8s
+ --no-migrations 49.4s
+ MD5 hasher 20.9s

The hasher dominates on this tree because the integration suites seed many users; --no-migrations contributes more as the migration count grows. The xdist change is orthogonal — it recovers the lost workers on CI runners that report a single physical core.

Verification

On this branch:

  • tox -e groups -- integration-backend104 passed, 26 skipped, 1 xfailed
  • tox -e groups -- unit-backend163 passed
  • tox -e groups -- unit-rig86 passed

🤖 Generated with Claude Code

https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz

chandrasekharan-zipstack and others added 10 commits July 21, 2026 16:58
load_groups() now merges extra group manifests listed in the env var
(os.pathsep-separated, REPO_ROOT-relative) onto the base tests/groups.yaml
before validation, so cross-manifest depends_on and the platform-gate
invariant are checked over the union. Name collisions are an error.

Lets a downstream repo (the cloud build) contribute its own test groups by
copying a groups.cloud.yaml into the merged tree, without editing the OSS
manifest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz
UserContext.get_organization() ran Organization.objects.get() even with no
org id in StateStore (import time, or management commands with no request),
catching only DoesNotExist/ProgrammingError — a DB-less/unmigrated setup hit
an uncaught OperationalError. Short-circuit when there's no org id: no query,
so serializers/managers that reference org-scoped querysets at class-def can
be imported during DB-free test collection.
Address review feedback on the UNSTRACT_RIG_EXTRA_MANIFESTS overlay:

- Overlays now apply only when loading the default manifest, so an
  explicit `load_groups(path)` (test fixture, ad-hoc manifest) can no
  longer absorb a downstream repo's ambient overlay.
- `_merge_manifest` returns the merged defaults so an overlay can rename
  `platform_gate_group` instead of having it silently ignored.
- A bad path in the env var raises a ValueError naming the variable
  rather than a bare FileNotFoundError/IsADirectoryError.
- Extract `_load_manifest_dict` to single-source manifest parsing and its
  error message.
- Tests: drive the real default-manifest path; cover overlay isolation,
  overlay defaults, malformed overlay, and a missing overlay path.
- Pin the truthy branch of UserContext.get_organization so inverting the
  guard can't pass unnoticed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz
`--tier X` expanded each selected group's `depends_on` transitively, with
no tier bound. `integration-workflow-execution` and `e2e-smoke` both
declare `depends_on: [unit-sdk1, unit-workers]`, so those two unit groups
ran again in the integration leg and a third time in the e2e leg.

Tiers run as separate CI legs and the unit leg already covers them, so
dep expansion is now bounded to the requested tier. Explicitly named
groups are never dropped, and intra-tier deps (e2e-smoke -> e2e-login)
still expand and order as before. Unrun deps do not weaken gating:
`blocked_by` intersects with groups that failed in the same run.

Measured on the last main run: ~88s of unit-workers and ~48s of
unit-sdk1 re-executed per run. On the cloud CI runner the same
duplication costs ~350s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz
* UN-3636 [FIX] Drop the ENVIRONMENT gate on the LLM mock

It did not defend the case it was added for. The threat was a worker env block
copied out of the test overlay into a real deployment, but the gate was written
into that same block, so a copy carries it. Base compose also sets
ENVIRONMENT=development on both workers that run the injection, so any
deployment derived from it satisfied the gate regardless. That left one real
case -- the mock var set alone somewhere that sets no ENVIRONMENT at all --
which holds by accident rather than design, in exchange for depending on a
variable nothing else in the codebase reads.

What actually guards the hatch is unchanged: it is off unless someone sets
UNSTRACT_LLM_MOCK_RESPONSE, and it warns once per process while active. Making
mocked spend distinguishable downstream is the defence worth having, and it
belongs on the usage record rather than in a config check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* UN-3636 [MISC] Drop the unread ENVIRONMENT variable from compose

Nothing reads it: no service, worker, frontend or plugin looks the variable
up, and the one consumer it ever had — the LLM mock gate — was removed in the
previous commit. Dropping it everywhere keeps a dead knob from looking
load-bearing to the next reader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ryg9chVDJQggCybpq3YoY3

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Drop lines that restate the code, trim session-specific detail, and merge
comments that duplicated each other across a module and its test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz
`load_groups(DEFAULT_MANIFEST)` merged overlays even though the caller named a
manifest explicitly, because the check compared path values. Path equality is
also spelling-sensitive, so the same file relative and absolute behaved
differently. Key on `path is None` instead: an explicit path loads exactly what
it names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ryg9chVDJQggCybpq3YoY3
TestIdePromptComplete drives the full success path, which reaches
client_plugin_registry.get_client_plugin("subscription_usage"). In OSS no
such plugin is installed, so the lookup returns None instantly. In a tree
with the cloud plugins copied in, it resolves to a real plugin that POSTs
to the backend; with no backend running the call only fails after a
multi-second connect timeout, and _track_subscription_usage swallows the
error so the tests still pass. That accounted for ~190s of the cloud
unit-workers run.

The file already declared _PATCH_GET_PLUGIN but never applied it outside
the dedicated subscription-usage classes. Apply it as a class-scoped
fixture so the lookup is pinned to the OSS answer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz
Three independent wins measured on the cloud-merged tree:

- `-n auto` collapsed to a single worker on any group shipping psutil,
  because xdist prefers physical cores there. Resolve the count in the
  rig instead, capped at 8 to avoid contending on the test database.
- `--no-migrations` builds the schema from the models rather than
  replaying the full migration history once per xdist worker.
- Test fixtures were paying 600k-iteration PBKDF2 per seeded user.

integration-backend fell from 163s to ~52s with an identical result set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c8724284-293a-454e-bfa7-f0eb68ac8e6e

📥 Commits

Reviewing files that changed from the base of the PR and between c715fc3 and 49c65c9.

📒 Files selected for processing (2)
  • .github/workflows/ci-test.yaml
  • backend/backend/settings/test.py
💤 Files with no reviewable changes (1)
  • .github/workflows/ci-test.yaml

Summary by CodeRabbit

  • Test Improvements
    • Accelerated test data creation by using a faster password hasher in the test settings.
    • Reduced test runtime overhead by building test databases directly from models (without replaying migrations).
    • Improved retry/backoff unit tests by eliminating real sleep delays.
    • Updated dashboard metrics Celery task tests to use lighter-weight Django isolation.
  • Developer Experience
    • Enhanced test runner defaults for parallelism and improved import reliability by preserving the test plugin path.
    • Added support for overlaying additional test manifests via an environment setting.
  • CI
    • Enabled persistent package caching in CI to speed up repeated runs.

Walkthrough

Test execution settings, CI caching, rig manifest loading, worker defaults, and per-group PYTHONPATH handling are updated. Django cleanup tests and retry utilities use faster test behavior.

Changes

Test infrastructure

Layer / File(s) Summary
Test runtime and CI configuration
backend/backend/settings/test.py, backend/pyproject.toml, backend/dashboard_metrics/tests/test_tasks.py, unstract/sdk1/tests/utils/test_retry_utils.py, .github/workflows/ci-test.yaml
Tests use MD5 password hashing, build schemas without migrations, use TestCase, skip retry sleeps, and enable uv caching in three CI jobs.
Rig manifest overlays
tests/rig/groups.py
Optional environment-selected manifests are loaded, merged with defaults, and validated for duplicate groups, paths, dependencies, cycles, and platform gates.
Rig worker and environment execution
tests/rig/cli.py
Default workers are derived from usable CPUs and capped at eight; group execution preserves plugin, base, and group PYTHONPATH entries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Environment
  participant RigCLI
  participant load_groups
  participant Pytest
  Environment->>RigCLI: provide worker and group environment settings
  RigCLI->>load_groups: load base and optional manifests
  load_groups-->>RigCLI: return validated groups
  RigCLI->>Pytest: execute groups with merged PYTHONPATH and worker count
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers What/Why/Testing, but it omits required template sections like How, breakage analysis, migrations, env config, and checklist. Add the missing template sections, especially How, breakage analysis, database migrations, env config, related issues, dependencies, testing notes, screenshots, and checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 58.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the PR's main performance-focused test rig changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rig-extra-manifests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chandrasekharan-zipstack
chandrasekharan-zipstack marked this pull request as ready for review July 23, 2026 08:11
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR optimizes test execution without changing product behavior.

  • Resolves and caps pytest-xdist workers based on usable CPUs.
  • Disables Django migration replay and selects the MD5 password hasher in tests.
  • Preserves group-specific PYTHONPATH values and tightens manifest validation.
  • Removes real retry sleeps and unnecessary transaction-test overhead.
  • Enables uv caching in CI.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure related to the prior inline review thread remains.

No blocking failure remains.

Important Files Changed

Filename Overview
tests/rig/cli.py Resolves a capped worker count and preserves group-specific import paths during test execution.
backend/pyproject.toml Configures pytest-django to construct test schemas without replaying migrations.
backend/backend/settings/test.py Uses Django's fast MD5 password hasher exclusively in test settings.
tests/rig/groups.py Rejects manifests whose top-level groups value is not a mapping.
backend/dashboard_metrics/tests/test_tasks.py Runs cleanup-task tests using Django TestCase instead of TransactionTestCase.
unstract/sdk1/tests/utils/test_retry_utils.py Replaces real retry sleeps with an autouse test fixture.
.github/workflows/ci-test.yaml Enables uv dependency caching across CI test and report jobs.

Reviews (4): Last reviewed commit: "UN-3636 [MISC] Drop stale-prone CI comme..." | Re-trigger Greptile

Comment thread unstract/sdk1/src/unstract/sdk1/llm.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/rig/groups.py`:
- Around line 171-178: Update _load_manifest_dict to validate that raw["groups"]
is a mapping before returning the manifest. Raise the existing intended
ValueError for missing or non-mapping groups values, while preserving valid
manifest handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03694465-9bfb-4e49-9956-f296f71d424b

📥 Commits

Reviewing files that changed from the base of the PR and between a03fcf8 and 4d06f2c.

📒 Files selected for processing (15)
  • backend/backend/settings/test.py
  • backend/pyproject.toml
  • backend/utils/tests/test_user_context.py
  • backend/utils/user_context.py
  • docker/docker-compose.yaml
  • tests/README.md
  • tests/compose/docker-compose.test.yaml
  • tests/rig/cli.py
  • tests/rig/groups.py
  • tests/rig/selection.py
  • tests/rig/tests/test_groups.py
  • tests/rig/tests/test_selection.py
  • unstract/sdk1/src/unstract/sdk1/llm.py
  • unstract/sdk1/tests/test_mock_response.py
  • workers/tests/test_ide_callback.py
💤 Files with no reviewable changes (3)
  • docker/docker-compose.yaml
  • unstract/sdk1/src/unstract/sdk1/llm.py
  • unstract/sdk1/tests/test_mock_response.py

Comment thread tests/rig/groups.py
chandrasekharan-zipstack and others added 3 commits July 23, 2026 14:04
Second performance pass on the test rig:

- unit-workers-cloud ran zero tests: its group `PYTHONPATH` overwrote the
  rig-injected plugin dir, so `-p rig_critical_path` failed to import. Merge
  the two instead of letting env.update clobber it.
- Drop `-s` from backend addopts — it disabled capture and flooded the log.
- Persist uv's cache across runs via setup-uv enable-cache, so per-group
  `uv sync` links from cache instead of refetching.
- No-op the real backoff sleeps in the sdk retry tests (~7s -> ~1s); no test
  asserts on elapsed time.
- TestCleanupTasks needs no transaction semantics; TestCase over
  TransactionTestCase drops the per-test truncate-and-reseed.
- Reject a non-mapping `groups:` manifest instead of crashing later.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnLaN45ShBbcCXWZkqCThz
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 16.7
e2e-coowners e2e 1 0 0 0 1.2
e2e-etl e2e 1 0 0 0 8.1
e2e-login e2e 2 0 0 0 1.2
e2e-prompt-studio e2e 1 0 0 0 4.9
e2e-smoke e2e 2 0 0 0 0.8
e2e-workflow e2e 1 0 0 0 21.2
integration-backend integration 161 0 0 27 41.7
integration-connectors integration 1 0 0 7 8.2
integration-workers integration 0 0 0 141 101.2
unit-backend unit 277 0 0 1 37.1
unit-connectors unit 63 0 0 0 9.7
unit-core unit 27 0 0 0 1.3
unit-platform-service unit 15 0 0 0 2.7
unit-rig unit 86 0 0 0 4.4
unit-sdk1 unit 480 0 0 0 23.6
unit-workers unit 1312 0 0 0 98.8
TOTAL 2433 0 0 176 382.9

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@athul-rs
athul-rs self-requested a review July 23, 2026 12:10
@chandrasekharan-zipstack
chandrasekharan-zipstack merged commit e2485a8 into main Jul 23, 2026
13 checks passed
@chandrasekharan-zipstack
chandrasekharan-zipstack deleted the feat/rig-extra-manifests branch July 23, 2026 13:11
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