Skip to content

Commit c8c4e35

Browse files
committed
Add code index timeout guard
1 parent 3f678df commit c8c4e35

5 files changed

Lines changed: 85 additions & 11 deletions

File tree

scripts/streaming_conveyor.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,7 @@ def run_code_half(
11991199
global_symbol_index: Path | None = None,
12001200
memory_limit_gb: float = 10.0,
12011201
parse_workers: int = 2,
1202+
index_timeout_s: int | None = None,
12021203
) -> dict:
12031204
"""index+route+pack the repo's source via the EXISTING code stage, zstd-max.
12041205
@@ -1214,7 +1215,7 @@ def run_code_half(
12141215
try:
12151216
info = sr.process_one_repo(
12161217
repo, repo_dir, lengths_code, work_root, dedup_db, dedup_near,
1217-
global_symbol_index, memory_limit_gb, parse_workers,
1218+
global_symbol_index, memory_limit_gb, parse_workers, index_timeout_s,
12181219
promote_dedup_on_success=False,
12191220
)
12201221
# zstd-max the per-length code parquet files this repo just wrote.
@@ -1245,6 +1246,7 @@ def run_code_half(
12451246
Path | None,
12461247
float,
12471248
int,
1249+
int | None,
12481250
],
12491251
dict,
12501252
]
@@ -1309,6 +1311,7 @@ def run_code_half_adaptive(
13091311
global_symbol_index: Path | None = None,
13101312
memory_limit_gb: float = 10.0,
13111313
parse_workers: int = 2,
1314+
index_timeout_s: int | None = None,
13121315
*,
13131316
runner: CodeRunner | None = None,
13141317
isolate_dedup_on_retry: bool = True,
@@ -1331,6 +1334,7 @@ def run_code_half_adaptive(
13311334
global_symbol_index,
13321335
memory_limit_gb,
13331336
parse_workers,
1337+
index_timeout_s,
13341338
)
13351339
except RepoFailure as exc:
13361340
if parse_workers <= 1 or not is_index_project_memory_failure(exc):
@@ -1352,6 +1356,7 @@ def run_code_half_adaptive(
13521356
global_symbol_index,
13531357
memory_limit_gb,
13541358
1,
1359+
index_timeout_s,
13551360
)
13561361

13571362

@@ -2002,6 +2007,7 @@ def process_one_repo(
20022007
*,
20032008
code_memory_limit_gb: float | None = None,
20042009
commit_memory_limit_gb: float | None = None,
2010+
code_index_timeout_s: int | None = None,
20052011
reservations: UnitReservationLedger | None = None,
20062012
range_submit_window: int = 1,
20072013
analysis_cache_entries: int = 128,
@@ -2086,6 +2092,7 @@ def process_one_repo(
20862092
repo, repo_dir, lengths_code, work_root,
20872093
code_dedup_db, code_dedup_near,
20882094
global_symbol_index, code_limit, code_parse_workers,
2095+
code_index_timeout_s,
20892096
)
20902097
with manifest_lock:
20912098
manifest.mark_done(ck, cinfo)
@@ -2289,6 +2296,9 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
22892296
help="Parse workers passed to index_project for the CODE half "
22902297
"(default 2; avoid multiplying --repo-workers by the old "
22912298
"index_project default of 8 clang workers).")
2299+
p.add_argument("--code-index-timeout-s", type=int, default=0,
2300+
help="Optional fail-loud timeout for each CODE index_project "
2301+
"stage. 0 disables the timeout (default).")
22922302
p.add_argument("--memory-budget-gb", type=float, default=None,
22932303
help="Global conveyor memory budget for heavy subprocesses. "
22942304
"Default is 55%% of physical RAM (or 48 GiB if RAM size "
@@ -2667,6 +2677,7 @@ def mark_no_git_repo(repo: str) -> None:
26672677
progress, checkpoint,
26682678
code_memory_limit_gb=code_memory_limit_gb,
26692679
commit_memory_limit_gb=commit_memory_limit_gb,
2680+
code_index_timeout_s=args.code_index_timeout_s,
26702681
reservations=reservations,
26712682
range_submit_window=range_submit_window,
26722683
analysis_cache_entries=args.analysis_cache_entries,
@@ -2756,6 +2767,7 @@ def drain_one_or_more(block: bool = True) -> None:
27562767
progress, checkpoint,
27572768
code_memory_limit_gb=code_memory_limit_gb,
27582769
commit_memory_limit_gb=commit_memory_limit_gb,
2770+
code_index_timeout_s=args.code_index_timeout_s,
27592771
reservations=reservations,
27602772
range_submit_window=range_submit_window,
27612773
analysis_cache_entries=args.analysis_cache_entries,

scripts/streaming_reindex.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,27 +244,51 @@ def run_checked(
244244
printable = " ".join(str(c) for c in cmd)
245245
print(f" [{repo}] {stage}: {printable}", file=sys.stderr, flush=True)
246246
log_fh = open(log_path, "wb") if log_path else None
247+
stdout_data: bytes | None = None
248+
proc: subprocess.Popen[bytes] | None = None
247249
try:
248-
proc = subprocess.run(
250+
proc = subprocess.Popen(
249251
[str(c) for c in cmd],
250252
cwd=str(cwd) if cwd else None,
251253
env=_subprocess_env(),
252254
stdout=log_fh if log_fh else subprocess.PIPE,
253255
stderr=subprocess.STDOUT,
254-
timeout=timeout,
256+
start_new_session=bool(timeout),
255257
)
258+
stdout_data, _ = proc.communicate(timeout=timeout)
256259
except subprocess.TimeoutExpired as exc:
260+
if proc is not None:
261+
try:
262+
if timeout:
263+
os.killpg(proc.pid, signal.SIGTERM)
264+
else:
265+
proc.terminate()
266+
except ProcessLookupError:
267+
pass
268+
try:
269+
proc.wait(timeout=5)
270+
except subprocess.TimeoutExpired:
271+
try:
272+
if timeout:
273+
os.killpg(proc.pid, signal.SIGKILL)
274+
else:
275+
proc.kill()
276+
except ProcessLookupError:
277+
pass
278+
proc.wait()
257279
raise RepoFailure(repo, stage, f"timed out after {timeout}s: {exc}") from exc
258280
finally:
259281
if log_fh:
260282
log_fh.close()
283+
if proc is None:
284+
raise RepoFailure(repo, stage, "subprocess did not start")
261285
if proc.returncode != 0:
262286
tail = ""
263287
if log_path and log_path.exists():
264288
data = log_path.read_bytes()[-4000:]
265289
tail = data.decode("utf-8", errors="replace")
266-
elif proc.stdout:
267-
tail = proc.stdout.decode("utf-8", errors="replace")[-4000:]
290+
elif stdout_data:
291+
tail = stdout_data.decode("utf-8", errors="replace")[-4000:]
268292
raise RepoFailure(
269293
repo, stage, f"exit code {proc.returncode}\n--- last output ---\n{tail}"
270294
)
@@ -423,6 +447,7 @@ def stage_index_source(
423447
global_symbol_index: Path | None = None,
424448
memory_limit_gb: float = 10.0,
425449
parse_workers: int = 2,
450+
index_timeout_s: int | None = None,
426451
) -> Path:
427452
"""index_project.py --enriched -> <repo>.enriched.jsonl.
428453
@@ -461,6 +486,7 @@ def stage_index_source(
461486
"index_project",
462487
cmd,
463488
log_path=work / f"{repo}.index.log",
489+
timeout=index_timeout_s if index_timeout_s and index_timeout_s > 0 else None,
464490
)
465491
if not enriched.exists() or enriched.stat().st_size == 0:
466492
raise RepoFailure(repo, "index_project", f"empty enriched jsonl: {enriched}")
@@ -628,6 +654,7 @@ def process_one_repo(
628654
global_symbol_index: Path | None = None,
629655
memory_limit_gb: float = 10.0,
630656
parse_workers: int = 2,
657+
index_timeout_s: int | None = None,
631658
*,
632659
promote_dedup_on_success: bool = True,
633660
) -> dict:
@@ -655,7 +682,7 @@ def process_one_repo(
655682
enriched = stage_index_source(
656683
repo, repo_dir, work, dedup_db, dedup_near,
657684
stage_id, stage_db, global_symbol_index, memory_limit_gb,
658-
parse_workers,
685+
parse_workers, index_timeout_s,
659686
)
660687
timings["index_project_s"] = round(time.monotonic() - started, 6)
661688
started = time.monotonic()
@@ -795,6 +822,9 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
795822
p.add_argument("--parse-workers", type=int, default=2,
796823
help="Parse workers passed to index_project for the code stage "
797824
"(default 2; keep this low when multiple repos run).")
825+
p.add_argument("--code-index-timeout-s", type=int, default=0,
826+
help="Optional fail-loud timeout for each index_project code "
827+
"stage. 0 disables the timeout (default).")
798828
return p.parse_args(argv)
799829

800830

@@ -911,7 +941,8 @@ def should_process(repo: str) -> bool:
911941
dedup_db, dedup_near,
912942
global_symbol_index,
913943
args.memory_limit_gb,
914-
args.parse_workers)
944+
args.parse_workers,
945+
args.code_index_timeout_s)
915946
manifest.mark_done(repo, info)
916947
run_report[repo] = info
917948
processed += 1

tests/conveyor_ckpt_harness.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def _lengths_info(lengths, valid):
165165

166166
def fake_run_code_half(repo, repo_dir, lengths_code, work_root, dedup_db,
167167
dedup_near, global_symbol_index=None, memory_limit_gb=10.0,
168-
parse_workers=2):
168+
parse_workers=2, index_timeout_s=None):
169169
return {"source": f"{repo}::code", "lengths": _lengths_info(lengths_code, 10)}
170170

171171

tests/test_streaming_conveyor_progress.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,9 @@ def runner(
284284
global_symbol_index,
285285
memory_limit_gb,
286286
parse_workers,
287+
index_timeout_s,
287288
):
288-
calls.append((parse_workers, dedup_db, dedup_near))
289+
calls.append((parse_workers, dedup_db, dedup_near, index_timeout_s))
289290
if parse_workers > 1:
290291
raise streaming_conveyor.RepoFailure(
291292
repo,
@@ -308,12 +309,13 @@ def runner(
308309
None,
309310
8.0,
310311
2,
312+
7200,
311313
runner=runner,
312314
)
313315

314316
assert calls == [
315-
(2, tmp_path / "global.sqlite", True),
316-
(1, None, False),
317+
(2, tmp_path / "global.sqlite", True, 7200),
318+
(1, None, False, 7200),
317319
]
318320
assert info["lengths"]["1024"]["valid_tokens"] == 1024
319321

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
5+
6+
def test_run_checked_times_out_fail_loud(tmp_path) -> None:
7+
import streaming_reindex
8+
9+
log_path = tmp_path / "sleep.log"
10+
try:
11+
streaming_reindex.run_checked(
12+
"repo",
13+
"index_project",
14+
[
15+
sys.executable,
16+
"-c",
17+
"import time; print('started', flush=True); time.sleep(30)",
18+
],
19+
log_path=log_path,
20+
timeout=1,
21+
)
22+
except streaming_reindex.RepoFailure as exc:
23+
assert exc.repo == "repo"
24+
assert exc.stage == "index_project"
25+
assert "timed out after 1s" in exc.detail
26+
else: # pragma: no cover
27+
raise AssertionError("expected run_checked to fail loud on timeout")
28+
29+
assert "started" in log_path.read_text(encoding="utf-8")

0 commit comments

Comments
 (0)