Skip to content

Commit de21596

Browse files
committed
Add explicit source cache populate mode
1 parent cb01ac5 commit de21596

4 files changed

Lines changed: 219 additions & 0 deletions

File tree

scripts/streaming_conveyor.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,38 @@ def stream_lock_names(streams: str) -> tuple[str, ...]:
12141214
return (streams,)
12151215

12161216

1217+
def populate_code_source_cache(
1218+
work_root: Path,
1219+
source_cache_dir: Path,
1220+
should_process,
1221+
progress: ProgressWriter,
1222+
*,
1223+
max_repos: int | None = None,
1224+
) -> dict:
1225+
"""Populate the code source cache without running index/tokenize stages."""
1226+
started = time.monotonic()
1227+
report = sr.populate_source_cache(
1228+
work_root,
1229+
should_process,
1230+
source_cache_dir,
1231+
max_repos=max_repos,
1232+
)
1233+
for idx, item in enumerate(report["repos"], start=1):
1234+
progress.emit(
1235+
"source_cache_repo_ready",
1236+
repo=item["repo"],
1237+
repo_dir=item["path"],
1238+
source_cache_dir=str(source_cache_dir),
1239+
repo_count=idx,
1240+
)
1241+
report = {
1242+
**report,
1243+
"elapsed_s": round(time.monotonic() - started, 6),
1244+
}
1245+
progress.emit("source_cache_populated", **report)
1246+
return report
1247+
1248+
12171249
# --------------------------------------------------------------------------- #
12181250
# CODE half: orchestrate streaming_reindex.process_one_repo (no reimplementation).
12191251
# Its append_output already lands outputs/reindexed/<L>/<repo>.parquet; we then
@@ -2402,6 +2434,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
24022434
help="For --streams code, only process complete repos already "
24032435
"in --source-cache-dir; do not open/decompress the source "
24042436
"tarball.")
2437+
p.add_argument("--source-cache-populate-only", action="store_true",
2438+
help="For --streams code, populate --source-cache-dir from "
2439+
"the source tarball and exit without indexing/tokenizing. "
2440+
"Follow with --source-cache-only for hot code runs.")
24052441
p.add_argument("--source-dir-root", action="append", default=[],
24062442
help="For --streams code, process already-extracted repo dirs "
24072443
"directly without opening the source tarball. May be passed "
@@ -2469,6 +2505,12 @@ def main(argv: list[str]) -> int:
24692505
source_dir_roots = [Path(p) for p in args.source_dir_root]
24702506
if args.source_cache_only and source_cache_dir is None:
24712507
raise SystemExit("--source-cache-only requires --source-cache-dir")
2508+
if args.source_cache_populate_only and source_cache_dir is None:
2509+
raise SystemExit("--source-cache-populate-only requires --source-cache-dir")
2510+
if args.source_cache_populate_only and args.source_cache_only:
2511+
raise SystemExit("--source-cache-populate-only cannot be combined with --source-cache-only")
2512+
if args.source_cache_populate_only and args.streams != "code":
2513+
raise SystemExit("--source-cache-populate-only is only valid with --streams code")
24722514
if args.source_cache_only and args.streams != "code":
24732515
raise SystemExit("--source-cache-only is only valid with --streams code")
24742516
if source_cache_dir is not None and args.streams != "code":
@@ -2659,6 +2701,7 @@ def _on_signal(signum, _frame):
26592701
code_recompress_workers=args.code_recompress_workers if code_recompressor else 0,
26602702
source_cache_dir=str(source_cache_dir) if source_cache_dir is not None else None,
26612703
source_cache_only=args.source_cache_only,
2704+
source_cache_populate_only=args.source_cache_populate_only,
26622705
source_dir_roots=[str(p) for p in source_dir_roots],
26632706
reservation_file=str(args.reservation_file)
26642707
if reservations is not None else None,
@@ -2737,6 +2780,42 @@ def should_process(repo: str) -> bool:
27372780
)
27382781
return should_stage
27392782

2783+
if args.source_cache_populate_only:
2784+
try:
2785+
report = populate_code_source_cache(
2786+
work_root,
2787+
source_cache_dir,
2788+
should_process,
2789+
progress,
2790+
max_repos=args.max_repos,
2791+
)
2792+
summary = {
2793+
"repos_this_run": 0,
2794+
"code_halves_this_run": 0,
2795+
"commit_ranges_this_run": 0,
2796+
"commit_ranges_failed_this_run": 0,
2797+
"workers": workers,
2798+
"repo_workers": repo_workers,
2799+
"max_active_repos": max_active_repos,
2800+
"parse_workers": parse_workers,
2801+
"memory_plan": memory_plan,
2802+
"streams": args.streams,
2803+
"source_cache_populate_only": True,
2804+
"source_cache_report": report,
2805+
"manifest": str(CONVEYOR_MANIFEST),
2806+
"interrupted": STOP_EVENT.is_set(),
2807+
}
2808+
progress.emit("run_finished", **summary)
2809+
print(json.dumps(summary, indent=2))
2810+
return 130 if STOP_EVENT.is_set() else 0
2811+
finally:
2812+
if code_recompressor is not None:
2813+
code_recompressor.shutdown()
2814+
if own_work_root and not args.keep_temp:
2815+
remove_tree(work_root, reason="source-cache populate work_root")
2816+
for lock in reversed(run_locks):
2817+
lock.close()
2818+
27402819
range_pool = ThreadPoolExecutor(max_workers=workers)
27412820
repo_pool = ThreadPoolExecutor(max_workers=repo_workers) if repo_workers > 1 else None
27422821

scripts/streaming_reindex.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,43 @@ def stream_repo_dirs(source_roots: Sequence[Path], should_process):
659659
yield repo, repo_dir
660660

661661

662+
def populate_source_cache(
663+
work_root: Path,
664+
should_process,
665+
source_cache_dir: Path,
666+
*,
667+
max_repos: int | None = None,
668+
) -> dict:
669+
"""Materialize source-cache repos without running the tokenization pipeline.
670+
671+
This is the explicit "cold source-store build" phase. It may read the
672+
monolithic source tar once, but callers can then run the hot code conveyor
673+
with --source-cache-only and avoid any sequential tar drain in production.
674+
"""
675+
if source_cache_dir is None:
676+
raise ValueError("source_cache_dir is required")
677+
repos: list[dict[str, str]] = []
678+
gen = stream_repo_subtrees(
679+
work_root,
680+
should_process,
681+
source_cache_dir=source_cache_dir,
682+
source_cache_only=False,
683+
)
684+
try:
685+
for repo, repo_dir in gen:
686+
repos.append({"repo": repo, "path": str(repo_dir)})
687+
if max_repos is not None and len(repos) >= max_repos:
688+
break
689+
finally:
690+
if hasattr(gen, "close"):
691+
gen.close()
692+
return {
693+
"source_cache_dir": str(source_cache_dir),
694+
"repos": repos,
695+
"repo_count": len(repos),
696+
}
697+
698+
662699
# --------------------------------------------------------------------------- #
663700
# Per-repo pipeline stages.
664701
# --------------------------------------------------------------------------- #
@@ -1070,6 +1107,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
10701107
p.add_argument("--source-cache-only", action="store_true",
10711108
help="Only process repos already complete in --source-cache-dir; "
10721109
"do not open/decompress the source tarball.")
1110+
p.add_argument("--source-cache-populate-only", action="store_true",
1111+
help="Populate --source-cache-dir from the source tarball and "
1112+
"exit without indexing/tokenizing. Use the resulting cache "
1113+
"with --source-cache-only for hot code runs.")
10731114
p.add_argument("--source-dir-root", action="append", default=[],
10741115
help="Already-extracted repo root to process directly, without "
10751116
"opening the source tarball. May be passed multiple times; "
@@ -1186,8 +1227,25 @@ def should_process(repo: str) -> bool:
11861227
source_dir_roots = [Path(p) for p in args.source_dir_root]
11871228
if args.source_cache_only and source_cache_dir is None:
11881229
raise SystemExit("--source-cache-only requires --source-cache-dir")
1230+
if args.source_cache_populate_only and source_cache_dir is None:
1231+
raise SystemExit("--source-cache-populate-only requires --source-cache-dir")
1232+
if args.source_cache_populate_only and args.source_cache_only:
1233+
raise SystemExit("--source-cache-populate-only cannot be combined with --source-cache-only")
1234+
if args.source_cache_populate_only and args.commit_source:
1235+
raise SystemExit("--source-cache-populate-only is code-only; remove --commit-source")
11891236
if source_dir_roots and (source_cache_dir is not None or args.source_cache_only):
11901237
raise SystemExit("--source-dir-root cannot be combined with source cache flags")
1238+
if args.source_cache_populate_only:
1239+
report = populate_source_cache(
1240+
work_root,
1241+
should_process,
1242+
source_cache_dir,
1243+
max_repos=args.max_repos,
1244+
)
1245+
print(json.dumps(report, indent=2, sort_keys=True))
1246+
if own_work_root and not args.keep_temp:
1247+
shutil.rmtree(work_root, ignore_errors=True)
1248+
return 0
11911249
gen = (
11921250
stream_repo_dirs(source_dir_roots, should_process)
11931251
if source_dir_roots

tests/test_streaming_conveyor_progress.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,59 @@ def test_progress_writer_tracks_extract_cache_rates(tmp_path):
5858
assert writer.extract_cache_metrics()["status_counts"]["hit"] == 1
5959

6060

61+
def test_populate_code_source_cache_emits_ready_and_summary(tmp_path, monkeypatch):
62+
import streaming_conveyor
63+
64+
progress_path = tmp_path / "progress.jsonl"
65+
progress = streaming_conveyor.ProgressWriter(progress_path)
66+
cache = tmp_path / "source_cache"
67+
observed = {}
68+
69+
def fake_populate(work_root, should_process, source_cache_dir, *, max_repos):
70+
observed["work_root"] = work_root
71+
observed["source_cache_dir"] = source_cache_dir
72+
observed["max_repos"] = max_repos
73+
repos = []
74+
for repo in ("repo-a", "repo.bare", "repo-b"):
75+
if should_process(repo):
76+
repos.append({"repo": repo, "path": str(source_cache_dir / repo)})
77+
repos = repos[:max_repos]
78+
return {
79+
"source_cache_dir": str(source_cache_dir),
80+
"repos": repos,
81+
"repo_count": len(repos),
82+
}
83+
84+
monkeypatch.setattr(streaming_conveyor.sr, "populate_source_cache", fake_populate)
85+
86+
report = streaming_conveyor.populate_code_source_cache(
87+
tmp_path / "work",
88+
cache,
89+
streaming_conveyor.sr.is_code_worktree_repo,
90+
progress,
91+
max_repos=1,
92+
)
93+
94+
assert observed == {
95+
"work_root": tmp_path / "work",
96+
"source_cache_dir": cache,
97+
"max_repos": 1,
98+
}
99+
assert report["repo_count"] == 1
100+
rows = [
101+
json.loads(line)
102+
for line in progress_path.read_text(encoding="utf-8").splitlines()
103+
]
104+
assert [row["event"] for row in rows] == [
105+
"source_cache_repo_ready",
106+
"source_cache_populated",
107+
]
108+
assert rows[0]["repo"] == "repo-a"
109+
assert rows[0]["repo_dir"] == str(cache / "repo-a")
110+
assert rows[1]["repo_count"] == 1
111+
assert rows[1]["repos"] == [{"repo": "repo-a", "path": str(cache / "repo-a")}]
112+
113+
61114
def test_background_recompressor_runs_and_surfaces_completion(tmp_path, monkeypatch):
62115
import streaming_conveyor
63116

tests/test_streaming_reindex_run_checked.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,32 @@ def test_stream_repo_dirs_yields_extracted_src_without_tar(tmp_path) -> None:
154154
)
155155

156156
assert yielded == [("repo", root / "repo" / "_src")]
157+
158+
159+
def test_populate_source_cache_only_materializes_cache(tmp_path, monkeypatch) -> None:
160+
import streaming_reindex
161+
162+
cache = tmp_path / "cache"
163+
calls = []
164+
165+
def fake_stream(work_root, should_process, *, source_cache_dir, source_cache_only):
166+
calls.append((work_root, source_cache_dir, source_cache_only))
167+
for repo in ("repo-a", "repo.bare", "repo-b"):
168+
if should_process(repo):
169+
yield repo, source_cache_dir / repo
170+
171+
monkeypatch.setattr(streaming_reindex, "stream_repo_subtrees", fake_stream)
172+
173+
report = streaming_reindex.populate_source_cache(
174+
tmp_path / "work",
175+
streaming_reindex.is_code_worktree_repo,
176+
cache,
177+
max_repos=1,
178+
)
179+
180+
assert calls == [(tmp_path / "work", cache, False)]
181+
assert report == {
182+
"source_cache_dir": str(cache),
183+
"repos": [{"repo": "repo-a", "path": str(cache / "repo-a")}],
184+
"repo_count": 1,
185+
}

0 commit comments

Comments
 (0)