Skip to content

Commit cb01ac5

Browse files
committed
Treat CPU progress as code index activity
1 parent 02bcbe2 commit cb01ac5

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

scripts/streaming_reindex.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,65 @@ def _subprocess_env() -> dict:
236236
return env
237237

238238

239+
def _parse_ps_time_seconds(value: str) -> float | None:
240+
"""Parse ps TIME values like MM:SS.cc, HH:MM:SS.cc, or DD-HH:MM:SS.cc."""
241+
raw = value.strip()
242+
if not raw:
243+
return None
244+
days = 0
245+
if "-" in raw:
246+
day_raw, raw = raw.split("-", 1)
247+
try:
248+
days = int(day_raw)
249+
except ValueError:
250+
return None
251+
parts = raw.split(":")
252+
try:
253+
if len(parts) == 2:
254+
hours = 0
255+
minutes = int(parts[0])
256+
seconds = float(parts[1])
257+
elif len(parts) == 3:
258+
hours = int(parts[0])
259+
minutes = int(parts[1])
260+
seconds = float(parts[2])
261+
else:
262+
return None
263+
except ValueError:
264+
return None
265+
return float(days * 86400 + hours * 3600 + minutes * 60) + seconds
266+
267+
268+
def _process_group_cpu_seconds(pgid: int) -> float | None:
269+
"""Return cumulative CPU seconds for a process group, when ps supports it."""
270+
try:
271+
output = subprocess.check_output(
272+
["ps", "-axo", "pgid=,time="],
273+
text=True,
274+
stderr=subprocess.DEVNULL,
275+
)
276+
except (OSError, subprocess.SubprocessError):
277+
return None
278+
total = 0.0
279+
seen = False
280+
for line in output.splitlines():
281+
fields = line.split()
282+
if len(fields) < 2:
283+
continue
284+
try:
285+
row_pgid = int(fields[0])
286+
except ValueError:
287+
continue
288+
if row_pgid != pgid:
289+
continue
290+
seconds = _parse_ps_time_seconds(fields[1])
291+
if seconds is None:
292+
continue
293+
total += seconds
294+
seen = True
295+
return total if seen else None
296+
297+
239298
def run_checked(
240299
repo: str,
241300
stage: str,
@@ -289,6 +348,7 @@ def terminate_process(reason: str) -> None:
289348
deadline = time.monotonic() + timeout if timeout and timeout > 0 else None
290349
last_activity = time.monotonic()
291350
last_signature: tuple[int, int] | None = None
351+
last_cpu_seconds: float | None = None
292352
while proc.poll() is None:
293353
now = time.monotonic()
294354
if deadline is not None and now > deadline:
@@ -304,12 +364,18 @@ def terminate_process(reason: str) -> None:
304364
if signature != last_signature:
305365
last_signature = signature
306366
last_activity = now
367+
cpu_seconds = _process_group_cpu_seconds(proc.pid)
368+
if cpu_seconds is not None and cpu_seconds != last_cpu_seconds:
369+
last_cpu_seconds = cpu_seconds
370+
last_activity = now
307371
if now - last_activity > stall_timeout:
308-
terminate_process(f"no log progress for {stall_timeout}s")
372+
terminate_process(
373+
f"no log/CPU progress for {stall_timeout}s"
374+
)
309375
raise RepoFailure(
310376
repo,
311377
stage,
312-
f"stalled after {stall_timeout}s without log progress",
378+
f"stalled after {stall_timeout}s without log or CPU progress",
313379
)
314380
time.sleep(1.0)
315381
stdout_data, _ = proc.communicate(timeout=1)

tests/test_streaming_reindex_run_checked.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,48 @@ def test_run_checked_stall_watchdog_fail_loud(tmp_path) -> None:
4949
except streaming_reindex.RepoFailure as exc:
5050
assert exc.repo == "repo"
5151
assert exc.stage == "index_project"
52-
assert "stalled after 1s without log progress" in exc.detail
52+
assert "stalled after 1s without log or CPU progress" in exc.detail
5353
else: # pragma: no cover
5454
raise AssertionError("expected run_checked to fail loud on stall")
5555

5656
assert "started" in log_path.read_text(encoding="utf-8")
5757

5858

59+
def test_run_checked_stall_watchdog_allows_cpu_progress(tmp_path) -> None:
60+
import streaming_reindex
61+
62+
log_path = tmp_path / "cpu-progress.log"
63+
streaming_reindex.run_checked(
64+
"repo",
65+
"index_project",
66+
[
67+
sys.executable,
68+
"-c",
69+
(
70+
"import time\n"
71+
"print('started', flush=True)\n"
72+
"deadline = time.time() + 2.0\n"
73+
"x = 0\n"
74+
"while time.time() < deadline:\n"
75+
" x += 1\n"
76+
),
77+
],
78+
log_path=log_path,
79+
timeout=10,
80+
stall_timeout=1,
81+
)
82+
83+
assert "started" in log_path.read_text(encoding="utf-8")
84+
85+
86+
def test_parse_ps_time_seconds_variants() -> None:
87+
import streaming_reindex
88+
89+
assert streaming_reindex._parse_ps_time_seconds("01:02.50") == 62.5
90+
assert streaming_reindex._parse_ps_time_seconds("03:01:02.50") == 10862.5
91+
assert streaming_reindex._parse_ps_time_seconds("2-03:01:02.50") == 183662.5
92+
93+
5994
def test_stream_repo_subtrees_source_cache_only(tmp_path) -> None:
6095
import streaming_reindex
6196

0 commit comments

Comments
 (0)