Skip to content

fix(worker): the cron could rebuild a repository the button never could - #237

Merged
sshlg merged 2 commits into
mainfrom
fix/repo-index-ceiling
Aug 27, 2026
Merged

fix(worker): the cron could rebuild a repository the button never could#237
sshlg merged 2 commits into
mainfrom
fix/repo-index-ceiling

Conversation

@sshlg

@sshlg sshlg commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What was measured

run_repo_index_task is the repo-index pipeline. Two ARQ jobs call it, each carrying its own ceiling:

Entry ARQ job Ceiling
nightly cron run_daily_project_knowledge_sync 7200 s
"Re-index repository", POST /api/projects/{id}/index run_repo_index 1800 s

Both numbers from indexing_runs, same repository (9 981 files):

08-25 22:00   completed      42.4 min    nightly
08-27 09:30   TimeoutError   30.0 min    manual

1800.02s ! 33f44e65d24b48bd9795717bc21b0285:run_repo_index failed, TimeoutError
  pipeline_runner.py:1468 in _run_code_symbol_embed

2 544 s of work fits under one ceiling and can never fit under the other. The repository rebuilt itself unattended at 3 a.m. and failed every time a person asked — the worse half, because the button is what an operator presses after shipping a fix.

Why it survived a previous diagnosis

AUD-0819-20 gave the job its own knob on 2026-08-19 for this exact failure, then left the default at 1800 — the value just measured as too small — with a note that a repository needing longer could say so without a code edit. Nothing in production said so; neither REPO_INDEX_JOB_TIMEOUT_SECONDS nor its sibling is set. A knob defaulting to the known-bad value moves a defect rather than closing it, and a test pinning {"timeout": 1800} as a literal made that default look deliberate.

The same day's memory fix had made it worse and the link was never drawn: EMBEDDING_UPSERT_BATCH_SIZE went 200 → 8, buying ~552 MiB with ~17 % more wall clock — spent inside the step the ceiling was cutting off. That fix's caution (raising the ceiling helps only a worker that is not swapping) was right, and its precondition is now met and measured: Standard-2X, a full rebuild with zero R14 and zero R15 (CB-OPS1, closed on that evidence).

The change

repo_index_job_timeout_seconds defaults to 3600. The invariants are tests, not comments:

  • it must clear the measured rebuild with 25 % headroom;
  • it must stay below daily_knowledge_sync_job_timeout_seconds, which contains it plus a DB index plus a code↔DB sync — asserted together with the fact that the containment still exists, so the ordering cannot quietly become arbitrary;
  • no long job may be registered without its own knob again — the shape that produced AUD-0819-20;
  • the non-positive guard still refuses to boot.

The existing registration test now compares against the setting rather than a literal.

Checked rather than assumed

The killed run's checkpoint recorded all five completed steps in indexing_checkpoint_step, so a resume skips them — the empty completed_steps column on the row is legacy and no longer written (checkpoint_service.py:10-15). No second defect there.

Verification

  • pytest tests/ — full backend suite green (6 952 passed / 4 skipped / 1 xfailed locally).
  • ruff format --check, ruff check, mypy app/ — clean.
  • Post-deploy, per CB-OPS2: force one manual full re-index and require it to reach pipeline_end. The row is struck on that evidence, not on this merge.

NGHTBOY and others added 2 commits August 27, 2026 12:14
`run_repo_index_task` is the repo-index pipeline, and two ARQ jobs call it with
two different ceilings — the nightly cron's `run_daily_project_knowledge_sync`
at 7200 s, and `run_repo_index`, which the "Re-index repository" button and
`POST /api/projects/{id}/index` enqueue, at 1800 s.

Both measured against the same repository, from `indexing_runs`:

    08-25 22:00   completed      42.4 min    nightly
    08-27 09:30   TimeoutError   30.0 min    manual

    1800.02s ! run_repo_index failed, TimeoutError
      pipeline_runner.py:1468 in _run_code_symbol_embed

2 544 s of work fits under one ceiling and can never fit under the other. So the
repository rebuilt itself unattended at 3 a.m. and failed every time a person
asked — the worse half, because the button is what an operator presses after
shipping a fix.

It had been diagnosed once already. AUD-0819-20 gave the job its own knob on
2026-08-19 for exactly this failure, then left the default at 1800 — the value
just measured as too small — noting that a repository needing longer could say
so without a code edit. Nothing in production said so. A knob defaulting to the
known-bad value moves a defect rather than closing it.

The same day's memory fix had made it worse and the link was never drawn:
`EMBEDDING_UPSERT_BATCH_SIZE` went 200 -> 8, buying ~552 MiB with ~17% more wall
clock, spent inside the step the ceiling was cutting off. That fix's caution —
raising the ceiling helps only a worker that is not swapping — was right, and
its precondition is now met and measured: Standard-2X, a full rebuild with zero
R14 and zero R15 (CB-OPS1, closed on that evidence).

`repo_index_job_timeout_seconds` now defaults to 3600, and the invariants are
tests rather than comments: it must clear the measured rebuild with 25% headroom,
it must stay below the daily sync's ceiling (which contains it plus a DB index
plus a code<->DB sync), and no long job may be registered without its own knob
again — the shape that produced AUD-0819-20.

Checked rather than assumed: the killed run's checkpoint recorded all five
completed steps in `indexing_checkpoint_step`, so a resume skips them. The empty
`completed_steps` column on the row is legacy and no longer written
(`checkpoint_service.py:10-15`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…wiring

`test_repo_index_registered_with_its_own_configurable_timeout` asserted
`{"timeout": 1800}` as a literal. That is how a default measured as too small
came to look deliberate: AUD-0819-20 added the knob, left the value, and a green
test asserted the bad number — which then failed in production again eight days
later.

It now compares the registration against `settings.repo_index_job_timeout_seconds`,
which is the wiring the test is named for. The sibling
`test_repo_index_timeout_reads_the_setting` already proves the value is read from
config, and the value itself is argued where its measurement lives, in
`tests/unit/services/test_repo_index_ceiling.py`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sshlg
sshlg merged commit 15ea2b9 into main Aug 27, 2026
2 checks passed
@sshlg
sshlg deleted the fix/repo-index-ceiling branch August 27, 2026 10:40
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