unseen: parallelize extraction + batch per-repo blob fetch

Serial extraction at ~147s/repo made a 300-repo pool take days, and
~626 lazy per-file blob faults per clone pushed slow repos past the
240s deadline (a draw reached 21 entries from 347 attempts, 160 of them
extraction_timeout). Both fixed.

Parallel: a bounded ThreadPoolExecutor (--extract-workers, default 6)
runs clones concurrently, each into its own clone with a private counter.
The main thread alone owns the journal, REST (license via finish_license),
counters, and acceptance. Entries are accepted in STRICT SEED ORDER with
the fork-network dedup applied at acceptance, so the pool is byte-
identical to the serial draw (tested: workers=6 == workers=1, including
fork collisions, out-of-order completion, and resume). The systemic guard
and resilient skips are unchanged; a graceful stop drains started work to
the journal for resume.

Batched blobs: repository_changes fetches the fixes together and
Git.materialize fetches all missing blobs for the read revisions in one
`fetch --no-filter --stdin` (rev-list --missing=print), instead of per-
file promisor faults. Content is byte-identical (tested; >=12 fetches
collapsed to 1).

202 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-10-09 17:37:05 +02:00
co-authored by Claude Opus 4.8
parent 4f7eedddf1
commit 65714405eb
4 changed files with 501 additions and 73 deletions
+72
View File
@@ -372,3 +372,75 @@ def test_disk_bytes_skips_files_that_vanish_mid_walk(tmp_path):
git.path = type("P", (), {"rglob": lambda self, pattern: iter([*real, Vanished()])})()
assert git.disk_bytes() == 30
def test_partial_clone_batch_matches_lazy_bytes_without_per_file_fetches(history, tmp_path):
"""Real promisor packs over file:// only: count lazy fetches via Git's trace."""
import json
import os
git, commit, candidate, _, _, _, _ = history
for revision in range(12):
commit(
f"def handler(value):\n return int(value) + {revision}\n",
extra={f"file{revision * 5 + i}.py": f"x = {revision * 5 + i}\n" for i in range(5)},
)
git.run("config", "uploadpack.allowFilter", "true")
git.run("config", "uploadpack.allowAnySHA1InWant", "true")
baseline = [e.introducing(git, candidate), e.ordinary_history(git, "HEAD")]
results, fetches = [], []
for batch in (False, True):
path = tmp_path / ("batch" if batch else "lazy")
trace = tmp_path / (path.name + ".trace")
# Local file transport only, no network. No checkout: all blobs start missing.
subprocess.run(
["git", "clone", "--quiet", "--filter=blob:none", "--no-checkout", tmp_path.as_uri(), str(path)],
check=True,
capture_output=True,
)
commands = []
def runner(argv, commands=commands, trace=trace, **kwargs):
commands.append(argv)
return subprocess.run(argv, **kwargs, env={**os.environ, "GIT_TRACE": str(trace), "GIT_ALLOW_PROTOCOL": "file"})
partial = e.Git(path, runner=runner)
missing = partial.run("rev-list", "--objects", "--missing=print", "HEAD")
assert sum(line.startswith("?") for line in missing.splitlines()) >= 60
if batch:
partial.materialize(["HEAD", candidate.fix])
results.append([e.introducing(partial, candidate), e.ordinary_history(partial, "HEAD")])
assert partial.files_read > 0 and partial.bytes_read > 0
fetches.append(sum("built-in: git fetch " in line for line in trace.read_text().splitlines()))
if batch:
assert sum("fetch" in argv for argv in commands) == 1
partial.materialize(["HEAD", candidate.fix]) # Already hydrated: no second fetch.
assert sum("fetch" in argv for argv in commands) == 1
assert json.dumps(results[0], sort_keys=True).encode() == json.dumps(results[1], sort_keys=True).encode()
assert results[1] == baseline
assert fetches[0] >= 12 and fetches[1] == 1
def test_materialize_timeout_uses_repository_deadline_and_checks_completeness(tmp_path):
oid = "a" * 40
calls = []
now = [0.0]
def runner(argv, **kwargs):
calls.append((argv, kwargs))
if "fetch" in argv:
assert kwargs["timeout"] == 3
assert kwargs["input"] == (oid + "\n").encode()
raise subprocess.TimeoutExpired(argv, kwargs["timeout"])
now[0] += 7
return subprocess.CompletedProcess(argv, 0, ("?" + oid + "\n").encode(), b"")
with pytest.raises(e.ExtractionTimeout, match="extraction_timeout"):
e.Git(tmp_path, runner=runner, deadline_seconds=10, _now=lambda: now[0]).materialize(["HEAD"])
assert len(calls) == 2
def incomplete(argv, **kwargs):
return subprocess.CompletedProcess(argv, 0, ("?" + oid + "\n").encode(), b"")
with pytest.raises(e.InstrumentFailure, match="missing_blobs_after_fetch"):
e.Git(tmp_path, runner=incomplete).materialize(["HEAD"])