indexer: single-pass in-memory tarball ingestion (#106) - #115
Merged
IceRhymers merged 1 commit intoJul 25, 2026
Merged
Conversation
Replace extract-to-disk tarball ingestion with a streaming iterator (indexer/ingest.py:iter_tar_source_files) that reads directly off the tar stream via tarfile.next()/extractfile(). This drops the second full gzip decompression that extractall() used to pay and removes the extracted tree from disk entirely, so a worker's peak local disk is just the compressed tarball (0.5 GB) instead of the tarball plus its expansion (2.5 GB). Since filter="data"'s built-in protections are gone with extractall(), the new path reimplements them by hand over the raw stream: absolute names, ".." traversal (checked on the raw path component, before any normalisation, since normalising first hides two latent path-confusion bugs the old extract-to-disk path had), and absolute/escaping symlink and hardlink targets all now raise instead of silently dropping, silently indexing under the wrong path, or (for links) failing the branch as before. An incremental cap bounds decompressed bytes per branch, keyed off both accumulated regular-file size and the tar stream's cumulative offset so non-regular members can't bypass it, and a truncated download is caught by draining the stream's trailer rather than silently indexing a partial or empty corpus. indexer/parse.py is untouched and stays as the executable oracle a new parity test pins the streaming path against; INDEX_SEMANTICS_VERSION is not bumped. The #103 phase-timing line drops its now-nonexistent extract= field (nine fields to eight) since decompression is folded into parse= instead. Docs, config.yaml, and indexer/AGENTS.md are updated to the new disk/timing arithmetic. Refs #106
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.
Refs #106
What
Replaces extract-to-disk tarball ingestion (
indexer/fetch.py:extract_tarball+tarfile.extractall(filter="data")) with a single-pass streaming iterator,indexer/ingest.py:iter_tar_source_files, that reads members directly off thetar stream (
tf.next()/tf.extractfile()). Nogetmembers(), noextractall(), nothing written to disk beyond the compressed tarball itself.This is PR A of a two-PR sequence for #106 (per the Critic-approved plan in
.omc/plans/issue-106/approved-plan.md). PR B — addingindexer/ingest.pytotests/unit/test_semantics_version_tripwire.py'sSEMANTICS_PATHS— isdeliberately not in this diff; it must land as a separate, immediate
follow-up once this merges (see "Why two PRs" below).
Why it matters
extracted tree) to ~0.5 GB (the tarball alone).
config.yaml,indexer/repo_config.py, and the runbook's disk-guard section are updated tothe new arithmetic; the concurrency default (4) is intentionally left as-is,
with a note that infra: re-derive worker, disk, and memory limits after streaming + delta #109 owns re-deriving it.
indexer/parse.pyis untouched (verify:git diff --stat origin/integration/indexer-performance...HEAD -- indexer/parse.pyis empty) and stays as the executable oracle a new parity test
(
tests/unit/test_ingest_parity.py) pins the streaming path against via liveset-equality, not a golden fixture.
INDEX_SEMANTICS_VERSIONis not bumped.config.yaml,indexer/repo_config.py, andindexer/AGENTS.mdupdated to the new disk/timing numbers.Security-relevant behavior change
filter="data"'s built-in protections don't exist for a raw stream, so this PRhand-implements the equivalent checks and, in the process, closes two latent
path-confusion bugs the old extract-to-disk path had (probe-verified against
Python 3.12's
tarfile, not inferred from its docs):/etc/passwdValueError/{TOP}/evil.pyValueError../evil.txtValueError{TOP}/../evil.txtValueError{TOP}/../{TOP}/evil.pyValueErrorValueError(hardlinks now explicitly in scope for the same rule as symlinks)The member-name
..check is deliberately evaluated on the raw, unnormalizedpath (rejecting any literal
".."component) before any normalisation —normalising first would silently defeat two of the rows above. The link-target
escape check is separately implemented in pure
posixpathstring arithmetic(not
os.path.realpath, which would resolve against the real filesystem/cwd —silently wrong in a stream context with no destination directory).
The decompression-bomb cap (
MAX_EXTRACTED_BYTES, moved fromfetch.pytoingest.pyand re-scoped from a disk cap to a work cap) is enforced two ways:accumulated regular-file size (matching the old accounting exactly) and the tar
stream's cumulative
member.offset, so archives made mostly of non-regularmembers can't bypass the cap by carrying no counted data.
tarfile.openispinned to
mode="r:gz"(not"r:*") since GitHub only ever serves gzip, andaccepting bzip2/LZMA would otherwise raise the disk-to-decompressed
amplification ceiling substantially.
This diff went through an independent code-reviewer pass (verdict: APPROVE)
and an independent security-reviewer pass (verdict: LOW risk, all
probe-verified hardening rows above independently reproduced) in separate
contexts from implementation; findings from both were fixed before this PR was
opened.
Operator-visible changes
0.5 GB peakinstead of2.5 GB peak.total resolve download extract parse embed db sweep other)drops
extract=— decompression cost now lands insideparse=instead, sincethere's no longer a separate extraction phase. Nine fields → eight, at every
site: code, tests (
_TIMING_RE, and the timing tests, including a rewrite —not just a re-tune — of the test that bound the now-deleted
job.extract_tarball), the runbook, andindexer/AGENTS.md.other=shrinks materially — no morerm -rfof a multi-GB extracted tree atteardown.
actually rewrote — never for the corpus as a whole, and not at all on a
branch that re-indexes as fully unchanged (the common case, since indexer: file-level delta indexing keyed on (path, content_sha) #104's
delta gate classifies an already-indexed, unchanged file with zero writes and
a preserved
files.id). A dedicated integration test(
tests/integration/test_job_ingest_delta.py) proves this end-to-end: indexa branch via the old code path, then re-index the identical tarball through
the new streaming path with the delta gate open, and confirm zero writes.
Gates (all run fresh in this worktree, not relayed from CI)
(Baseline at this branch's base commit,
17aeb4f, was1187 passed, 260 deselected.)(Confirmed green and not skipped — both CI workflows checkout at depth 1, so
this test likely does skip in CI; the
git diff --statcheck above is gate 5'sprimary AC3 evidence for that reason.)
make test-integration— local-environment limitation, documented for thereviewer: this repo's CI has no provisioned Postgres gate
(
ci-lakebase.ymlis inert repo-wide —gh variable listis empty), so thiscan't be deferred to CI. Run locally against a local
pgvector/pgvector:pg16container (
codesearch-pg) withPGUSER=codesearch PGPASSWORD=codesearch PGDATABASE=codesearch PGHOST=localhostexported (theMakefiletarget itselfdoesn't set these). Result:
205 passed, 8 failed, 43 errors, 3 xfailed, 2 xpassed. The failures/errors are a pre-existing local-environment gap, not aregression: this container is vanilla pgvector/pg16, not a real Lakebase
branch, so it lacks the
lakebase_tokenizer/lakebase_ann/lakebase_bm25extensions that
test_migrations.py(rev 0004+),test_reconcile.py,test_semantic_rrf.py,test_store_chunk_writer.py, andtest_webui_semantic.pyneed — reproduced identically with this branch's changes stashed. Three of the
eight
FAILED(test_commit_search.pyx2,test_mcp_server.py) alsoreproduce byte-identically on the unmodified base branch. Every suite that
can run locally passes, including the new
tests/integration/test_job_ingest_delta.py,test_job_reconcile.py, andtest_store_delta.py.Definition of done (PR A)
integration/indexer-performance— branched from17aeb4fgit diff --statempty onindexer/parse.py,indexer/store.py,app/db/models.py,tests/unit/test_semantics_version_tripwire.pyINDEX_SEMANTICS_VERSIONunchangedindexer/ingest.pyadded;extract_tarballdeleted; nogetmembers/extractallanywhere inindexer/MAX_EXTRACTED_BYTESmoved toingest.py, re-scoped as a work cap, and bounded against non-regular-member bypassgetmembers()callsfilter="data"would itself rejectposixpatharithmetic, not anos.path.realpathtransliterationtf.members.clear()is the first statement of the loop bodyREQUIRED_FREE_BYTES == MAX_TARBALL_BYTES; disk-guard message rewordedconfig.yaml,repo_config.py,indexer/AGENTS.md,fetch.py,job.pydocstrings updatedmake lint/make testgreen with pasted output above; count above the1187/260baselineextract=retirement complete across code, tests, runbook,AGENTS.md—grep -rn 'extract=' indexer/ tests/ docs/ | grep -v 'extract_file\|extract symbols'returns only the two prose sentences explaining the retirementlang/sizefield-by-field (including an uppercase-suffix case, since(path, content_sha)-keyed delta indexing can't self-heal alang/sizedivergence)code-reviewer(APPROVE) +security-reviewer(LOW risk) passes complete, findings fixedintegration/indexer-performance, body saysRefs #106(neverCloses)Not in this PR, by design: arming
SEMANTICS_PATHSonindexer/ingest.py(PR B, immediately after this merges — adding a new file to that list is itself
a tripwire offender in any diff whose base predates it, including
master...integration/indexer-performance's eventual arc merge, so it'ssequenced deliberately rather than bundled here).