search: bound Python regex CPU per request (#38) - #100
Merged
IceRhymers merged 1 commit intoJul 24, 2026
Merged
Conversation
Switch grep's highlight rescan from stdlib re to the regex module and thread a per-request wall-clock match budget (default 2000ms, CODE_SEARCH_MATCH_BUDGET_MS) through grep_search. A trip stops scanning, keeps fully-scanned files, and surfaces truncated=True + truncation_reason="match_budget" instead of stalling the event loop on a catastrophic-backtracking pattern.
IceRhymers
merged commit Jul 24, 2026
f27565b
into
integration/search-hardening-and-ci-integrity
4 checks passed
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Grep's Python-side highlight rescan (
extract_line_matchesinapp/search/grep.py) previouslyran user-supplied
Substring/Regexpatterns through stdlibrewith no CPU/wall-clock bound.Because stdlib
renever releases the GIL and cannot be interrupted mid-call, acatastrophic-backtracking pattern on a single under-cap file could hang not just its own
request but the whole process's event loop (
/health,/ready, every concurrent request onboth the MCP app and webui) — reachable from the web-exposed
webui/api/searchendpoint andthe MCP
search_codetool since #35.This PR closes that gap per the Critic-approved plan for #38:
reto the third-partyregexmodule(
regex>=2021.11.10, re-compatible superset, still not RE2).regexsupports atimeout=kwarg that is checked periodically inside the match loop (genuinely interruptible, unlike
re) and releases the GIL while matchingstr, so a budgeted pathological match no longerstarves the event loop.
Settings.match_budget_ms(default2000, envCODE_SEARCH_MATCH_BUDGET_MS), threaded throughgrep_searchas a singledeadline covering all pattern × line × file matching in the request.
partial result:
truncated=True,truncation_reason="match_budget"— no envelope keysadded/removed, existing pinned-key test unchanged.
matches are discarded) so pagination steps past a pathological file instead of stalling
forever on it; a pre-file trip leaves the file unconsumed so a resumed page re-fetches it
fresh with a new budget.
pre-file trip on the very first row of a resumed page — reachable via content-fetch
latency, not just an undersized budget, since per-row
yield_per=1fetch time also fallsinside the deadline window — left
next_cursor=Nonewhiletruncated=True, indistinguishablefrom "exhausted" and silently dropping the rest of the corpus. Fixed by falling back to the
unchanged incoming
resumecursor in that case, with a dedicated deterministic regressiontest (
test_match_budget_pre_file_trip_on_first_row_of_resumed_page_falls_back_to_resume_cursor,using a negative
match_budget_msso the trip is guaranteed rather than timing-dependent).app/search/errors.py'sregex_invalid/RegexInvalidErrorhandling (Postgres-invalid regex,issue search: negated (and un-negated) Postgres-invalid regex surfaces as an unhandled 500-class fault #75, merged as part of this branch's rebase) is untouched — out of scope for search: bound Python regex CPU per request (web-exposed ReDoS) #38.
Review
cursor dead-end above) — fixed, with a regression test. A few LOW/informational notes (some
incidental
uv exporttransitive dependency churn inapp/requirements.txt; no floor onmatch_budget_ms, consistent with existing unvalidatedstatement_timeout_ms/max_content_bytes) were reviewed and left as-is, matching repo convention.gap, no exploitable regression found" — empirically verified against the installed
regexversion that the exemplar pattern actually catastrophically backtracks unbudgeted, that
timeout=interrupts it, and that the GIL is released during matching (event loop staysresponsive under a full-budget pathological match). One LOW suggestion (pin a
regexversionfloor) was applied (
regex>=2021.11.10, the point at whichtimeout=support is available).Residual CPU-cost risk (
match_budget_ms × CapacityLimiter(5)slots per volley, no ratelimiting) is accepted and documented — availability (not throughput) is what this fix
guarantees, matching the plan's stated scope.
Gates (all run fresh, locally)
make lint(ruff check + ruff format --check +mypy app indexer webui) — clean.make test(unit + observability) — 1082 passed.make test-integration(integration + e2e, against localcodesearch-pgPostgres) —190 passed, 8 failed, 41 errors, 3 xfailed, 2 xpassed. The 8 failures / 41 errors are
pre-existing and environmental, confirmed identical against the unmodified base branch:
they require Lakebase-only extensions (
lakebase_ann/lakebase_bm25/lakebase_tokenizer)this local vanilla-pgvector Postgres doesn't have (
test_semantic_rrf,test_migrations0004+,
test_reconcile,test_store_chunk_writer,test_webui_semantic), or needprovisioned PG roles not present locally (
test_commit_search,test_mcp_server). Allplain-Postgres grep/service suites this PR touches run and pass in full:
tests/integration/test_grep.py— 42 passed (all pre-existing tests + this PR's 3 newmatch-budget tests, including the review-driven regression test above), plus
#75'sregex_invalid-flavored grep tests, unaffected.ci-lakebase.yml) currently no-ops repo-wide(
CI_LAKEBASE_ENABLEDunprovisioned — known state since PR indexer: extend typed reference edges to JS/TS/TSX/Go/Java/Rust (#85) #92);ci.yml(lint/unit/webui) is the CI check expected to be green on this PR.
Refs
Refs #38