Skip to content

The test_invalid_json CI job spends 48 of its 51 minutes sleeping off rate limits #3540

Description

@adamtheturtle

What happens

The ci-tests matrix in .github/workflows/test.yml has 104 ci_pattern entries. One of them dominates the whole workflow:

pattern duration
test_invalid_json.py::TestInvalidJSON::test_invalid_json 51 min
test_vumark_generation_api.py 10 min
test_add_target.py::TestImage 9 min
61 of the remaining 101 patterns under 2 min

The Test workflow is serialised on concurrency: vuforia_credentials, so this single job sets how long the shared lock is held: a run takes ~52 min, of which one job accounts for ~51.

What I checked

I pulled the log of that job from run 33862902163 and measured the gaps between consecutive test results:

tests:  71
span:   48.3 min
gaps > 5s:  55 gaps, totalling 48.2 min
gaps <= 5s: 0.1 min
gap sizes:  60s x33, 40s x12, 50s x4, 20s x2, 10s x2, 80s x1, 70s x1

Six seconds of that job is test work. Forty-eight minutes is sleeping.

The gaps are all multiples of ten seconds because of RETRY_ON_TRANSIENT_VWS_FAILURE in tests/mock_vws/utils/retries.py:

wait=wait_fixed(wait=10),
stop=stop_after_attempt(max_attempt_number=10),

handle_server_errors raises TooManyRequestsError on a 429, and each retry costs a flat ten seconds. A 60-second gap is six 429s in a row for one test.

The job is slow because it issues its requests as fast as it can — 71 real requests in about six seconds of actual work — and then waits out the rate limiter it just tripped. It is the worst offender because it is the widest cross-product in the suite: every content value x every endpoint x every backend.

Why it matters

The job is the critical path of a workflow which can only run one at a time, and holding the lock for ~52 minutes is what makes queued runs get evicted (GitHub allows only one pending run per concurrency group, so a third run is cancelled before it starts). Shortening this job shortens the lock and reduces how often that happens.

Suggested resolution

Split the pattern into the three cases the single test currently conflates, which are genuinely different inputs rather than an arbitrary shard:

  • bytes which are not JSON at all (b"a")
  • valid JSON which is not an object (b"[]", b'"hello"', b"5", b"null", b"true")
  • a JSON object encoded as latin-1 rather than UTF-8

That is a 1 / 5 / 1 split, so the critical path becomes the five-value job at roughly 36 minutes, and a run drops from ~52 to ~37 minutes.

Two things to weigh before assuming that is the fix:

The gain depends on where Vuforia rate limits. Each matrix job gets its own credentials file and therefore its own database, so if the limit is per database, sharding really does divide the waiting. If it is per account, the 429s move rather than disappear and the total wait across the matrix is unchanged. I have not established which it is.

Sharding pushes credentialed jobs into the secrets wrap. There are only 100 credentials files, and the workflow already wraps with SECRET_INDEX=$((JOB_INDEX % 100)). Today indices 100-103 are test_healthcheck.py, test_docker.py, README.rst and docs/, which is benign because those barely touch real Vuforia. Adding two patterns pushes test_vumark_generation_api.py and test_vumark_generation_failure.py into the wrap, where they would share a database with the test_query.py jobs at indices 0 and 1. Whether that bites depends on runner scheduling — with 104 jobs and a much smaller concurrent-runner limit, index 0 has usually finished long before index 100 starts — but it is a hazard that grows with every pattern added, and the pool needs to grow past 100 or the tail needs to be reserved for patterns which do not use credentials.

The larger fix is probably not sharding at all. Six seconds of work should not take 48 minutes. Pacing the requests, or replacing wait_fixed(10) with an exponential backoff with jitter, would attack the cause rather than divide it across jobs. That is a change to shared retry behaviour with a much wider blast radius, so it is worth doing separately and deliberately.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions