Skip to content

Commit e70c220

Browse files
committed
fix(conveyor): quarantine unmapped archives and preflight libclang
1 parent d595a45 commit e70c220

2 files changed

Lines changed: 196 additions & 26 deletions

File tree

scripts/streaming_conveyor.py

Lines changed: 112 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@
167167
EXTRACT_CACHE_MODE = EXTRACT_CACHE_MODE_RUN_LOCAL
168168

169169
_PRINT_LOCK = threading.Lock()
170+
_LIBCLANG_PREFLIGHT_CODE = """
171+
import clang.cindex as cindex
172+
173+
cindex.Index.create()
174+
print(cindex.__file__)
175+
"""
170176

171177

172178
class CodeRevisionError(RuntimeError):
@@ -181,6 +187,39 @@ class CodeRevisionDriftError(CodeRevisionError):
181187
"""The checkout changed after the conveyor captured its revision."""
182188

183189

190+
def verify_libclang_preflight(python: Path = VENV_PYTHON) -> str:
191+
"""Prove the stage interpreter can load and initialize libclang."""
192+
193+
try:
194+
completed = subprocess.run(
195+
[str(python), "-c", _LIBCLANG_PREFLIGHT_CODE],
196+
check=False,
197+
capture_output=True,
198+
text=True,
199+
timeout=30,
200+
)
201+
except subprocess.TimeoutExpired as exc:
202+
raise SystemExit(
203+
f"libclang preflight timed out after 30s: python={python}"
204+
) from exc
205+
except OSError as exc:
206+
raise SystemExit(
207+
f"libclang preflight could not start: python={python}: {exc}"
208+
) from exc
209+
if completed.returncode != 0:
210+
detail = (completed.stderr or completed.stdout).strip()
211+
raise SystemExit(
212+
"libclang preflight failed: "
213+
f"python={python} exit={completed.returncode}: {detail}"
214+
)
215+
binding_path = completed.stdout.strip()
216+
if not binding_path:
217+
raise SystemExit(
218+
f"libclang preflight returned no binding path: python={python}"
219+
)
220+
return binding_path
221+
222+
184223
def _sha256_bytes(payload: bytes) -> str:
185224
return hashlib.sha256(payload).hexdigest()
186225

@@ -2511,6 +2550,67 @@ def claim_code_project_identity(
25112550
return previous
25122551

25132552

2553+
def claim_code_repo_for_submission(
2554+
*,
2555+
repo: str,
2556+
repo_list: Path,
2557+
project_identity_claims: dict[str, str],
2558+
manifest: Manifest,
2559+
manifest_lock: threading.Lock,
2560+
progress: ProgressWriter,
2561+
) -> bool:
2562+
"""Claim a code repo without letting one unresolved identity stop the run.
2563+
2564+
Missing canonical identity is a fail-closed, repo-local data rejection: no
2565+
indexing subprocess may run, but unrelated repositories must continue.
2566+
Corrupt repo-list files and other unexpected failures still propagate.
2567+
"""
2568+
2569+
try:
2570+
project_id = sr.resolve_project_identity(repo, repo_list)
2571+
except SymbolIdentityError as exc:
2572+
detail = str(exc)
2573+
unit = code_key(repo)
2574+
with manifest_lock:
2575+
manifest.mark_failed(unit, "project_identity", detail)
2576+
progress.emit(
2577+
"unit_failed",
2578+
stream="code",
2579+
repo=repo,
2580+
unit=unit,
2581+
stage="project_identity",
2582+
detail=detail[:2000],
2583+
)
2584+
_log(f"QUARANTINE {unit}: unresolved project identity: {detail}")
2585+
return False
2586+
2587+
previous = claim_code_project_identity(
2588+
project_identity_claims,
2589+
repo,
2590+
project_id,
2591+
)
2592+
if previous is None:
2593+
return True
2594+
2595+
info = {
2596+
"source": "code",
2597+
"skipped": True,
2598+
"skip_reason": "duplicate_project_identity",
2599+
"project_identity": project_id,
2600+
"canonical_repo": previous,
2601+
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
2602+
}
2603+
with manifest_lock:
2604+
manifest.mark_done(code_key(repo), info)
2605+
progress.emit(
2606+
"duplicate_project_identity_skipped",
2607+
repo=repo,
2608+
stream="code",
2609+
**info,
2610+
)
2611+
return False
2612+
2613+
25142614
def no_git_key(repo: str) -> str:
25152615
"""Checkpoint key proving the .git-preserving stream saw no git metadata."""
25162616
return f"{repo}::no_git"
@@ -4336,6 +4436,12 @@ def _on_signal(signum, _frame):
43364436
for path in required_paths:
43374437
if not Path(path).exists():
43384438
raise SystemExit(f"required path missing: {path}")
4439+
if not args.source_cache_populate_only:
4440+
libclang_binding = verify_libclang_preflight(VENV_PYTHON)
4441+
_log(
4442+
"libclang preflight: OK "
4443+
f"python={VENV_PYTHON} binding={libclang_binding}"
4444+
)
43394445

43404446
# Pre-create output trees for BOTH streams.
43414447
CONVEYOR_ROOT.mkdir(parents=True, exist_ok=True)
@@ -4599,34 +4705,14 @@ def claim_repo_once(repo: str) -> bool:
45994705
submitted_repo_names.add(repo)
46004706
if args.streams != "code" or repo_list is None:
46014707
return True
4602-
try:
4603-
project_id = sr.resolve_project_identity(repo, repo_list)
4604-
except Exception as exc:
4605-
raise RepoFailure(repo, "project_identity", str(exc)) from exc
4606-
previous = claim_code_project_identity(
4607-
submitted_code_project_identities,
4608-
repo,
4609-
project_id,
4610-
)
4611-
if previous is None:
4612-
return True
4613-
info = {
4614-
"source": "code",
4615-
"skipped": True,
4616-
"skip_reason": "duplicate_project_identity",
4617-
"project_identity": project_id,
4618-
"canonical_repo": previous,
4619-
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
4620-
}
4621-
with manifest_lock:
4622-
manifest.mark_done(code_key(repo), info)
4623-
progress.emit(
4624-
"duplicate_project_identity_skipped",
4708+
return claim_code_repo_for_submission(
46254709
repo=repo,
4626-
stream="code",
4627-
**info,
4710+
repo_list=repo_list,
4711+
project_identity_claims=submitted_code_project_identities,
4712+
manifest=manifest,
4713+
manifest_lock=manifest_lock,
4714+
progress=progress,
46284715
)
4629-
return False
46304716

46314717
def mark_no_git_repo(repo: str) -> None:
46324718
info = {

tests/test_streaming_conveyor_progress.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,90 @@ def test_code_project_identity_claims_deduplicate_aliases() -> None:
4242
) == "open-watcom"
4343

4444

45+
def test_libclang_preflight_fails_before_staging_with_child_error(tmp_path) -> None:
46+
import streaming_conveyor
47+
48+
fake_python = tmp_path / "python"
49+
fake_python.write_text(
50+
"#!/bin/sh\necho 'clang bindings unavailable' >&2\nexit 17\n",
51+
encoding="utf-8",
52+
)
53+
fake_python.chmod(0o755)
54+
55+
with pytest.raises(
56+
SystemExit,
57+
match=(
58+
r"libclang preflight failed: .*exit=17: "
59+
r"clang bindings unavailable"
60+
),
61+
):
62+
streaming_conveyor.verify_libclang_preflight(fake_python)
63+
64+
65+
def test_unmapped_code_repo_is_quarantined_without_blocking_next_submission(
66+
tmp_path,
67+
) -> None:
68+
import threading
69+
70+
import streaming_conveyor
71+
72+
repo_list = tmp_path / "repo_list.json"
73+
repo_list.write_text(
74+
json.dumps(
75+
{
76+
"repos": [
77+
{
78+
"name": "public-repo",
79+
"owner_repo": "tests/public-repo",
80+
}
81+
]
82+
}
83+
),
84+
encoding="utf-8",
85+
)
86+
manifest = streaming_conveyor.Manifest(tmp_path / "_done.json")
87+
progress_path = tmp_path / "progress.jsonl"
88+
progress = streaming_conveyor.ProgressWriter(progress_path)
89+
claims: dict[str, str] = {}
90+
lock = threading.Lock()
91+
92+
assert not streaming_conveyor.claim_code_repo_for_submission(
93+
repo="unmapped archive",
94+
repo_list=repo_list,
95+
project_identity_claims=claims,
96+
manifest=manifest,
97+
manifest_lock=lock,
98+
progress=progress,
99+
)
100+
assert manifest.failed["unmapped archive::code"]["stage"] == "project_identity"
101+
assert claims == {}
102+
103+
assert streaming_conveyor.claim_code_repo_for_submission(
104+
repo="public-repo",
105+
repo_list=repo_list,
106+
project_identity_claims=claims,
107+
manifest=manifest,
108+
manifest_lock=lock,
109+
progress=progress,
110+
)
111+
assert claims == {"tests/public-repo": "public-repo"}
112+
113+
events = [
114+
json.loads(line)
115+
for line in progress_path.read_text(encoding="utf-8").splitlines()
116+
]
117+
assert len(events) == 1
118+
assert events[0]["event"] == "unit_failed"
119+
assert events[0]["repo"] == "unmapped archive"
120+
assert events[0]["unit"] == "unmapped archive::code"
121+
assert events[0]["stream"] == "code"
122+
assert events[0]["stage"] == "project_identity"
123+
assert events[0]["detail"] == (
124+
f"{repo_list}: no canonical project identity for bare repo "
125+
"'unmapped archive'"
126+
)
127+
128+
45129
def test_progress_writer_appends_jsonl(tmp_path):
46130
import streaming_conveyor
47131

0 commit comments

Comments
 (0)