Skip to content

Commit afbf4cf

Browse files
committed
feat(v7-h06): job-pause/resume primitive
Closes V7-H06 (cppmega-mlx-nh1i) backend: in-process pause/resume registry mirroring V5-G09 abort_token pattern but with a pause→resume cycle. cppmega_v4/runtime/job_control.py: - pause(token) / resume(token) / is_paused(token) — idempotent. - wait_while_paused(token, poll_s, max_wait_s) — bounded blocking. - reset() — test helper. Tests (tests/v4/test_job_control.py): 6/6 — pause sets, resume clears, pause idempotent, null/empty token never paused, wait_while_paused returns when resume fires from another thread, max_wait bound respected, distinct tokens independent. stage_train wait_while_paused integration + UI Pause/Resume buttons next to Cancel are V7-H06-UI follow-ups.
1 parent ede7f79 commit afbf4cf

2 files changed

Lines changed: 125 additions & 0 deletions

File tree

cppmega_v4/runtime/job_control.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""V7-H06: in-process job-pause/resume registry.
2+
3+
Mirrors V5-G09 abort_token pattern (cppmega_v4.runner.stages.request_abort)
4+
but with a pause→resume cycle instead of a one-shot abort.
5+
6+
The train loop polls is_paused(token) between steps; while paused it
7+
spins on a 50ms sleep without advancing optimizer state. resume(token)
8+
clears the flag. pause(token) sets it.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import time
14+
from threading import Lock
15+
16+
_PAUSED_JOBS: set[str] = set()
17+
_LOCK = Lock()
18+
19+
20+
def pause(token: str) -> None:
21+
"""Mark a job as paused. Idempotent."""
22+
if not token:
23+
return
24+
with _LOCK:
25+
_PAUSED_JOBS.add(str(token))
26+
27+
28+
def resume(token: str) -> None:
29+
"""Clear the pause flag. Idempotent."""
30+
with _LOCK:
31+
_PAUSED_JOBS.discard(str(token))
32+
33+
34+
def is_paused(token: str | None) -> bool:
35+
if not token:
36+
return False
37+
with _LOCK:
38+
return str(token) in _PAUSED_JOBS
39+
40+
41+
def wait_while_paused(token: str | None,
42+
*, poll_s: float = 0.05,
43+
max_wait_s: float = 600.0) -> None:
44+
"""Block while the job is paused, polling every poll_s. Bounded by
45+
max_wait_s to avoid deadlock if resume() never lands."""
46+
if not token:
47+
return
48+
deadline = time.time() + max_wait_s
49+
while is_paused(token) and time.time() < deadline:
50+
time.sleep(poll_s)
51+
52+
53+
def reset() -> None:
54+
"""Test helper — clear the entire paused-jobs set."""
55+
with _LOCK:
56+
_PAUSED_JOBS.clear()
57+
58+
59+
__all__ = ["pause", "resume", "is_paused",
60+
"wait_while_paused", "reset"]

tests/v4/test_job_control.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""V7-H06: job pause/resume primitive tests."""
2+
3+
from __future__ import annotations
4+
5+
import threading
6+
import time
7+
8+
import pytest
9+
10+
from cppmega_v4.runtime import job_control as jc
11+
12+
13+
@pytest.fixture(autouse=True)
14+
def _reset():
15+
jc.reset()
16+
yield
17+
jc.reset()
18+
19+
20+
def test_v7_h06_pause_sets_flag_resume_clears():
21+
assert jc.is_paused("t1") is False
22+
jc.pause("t1")
23+
assert jc.is_paused("t1") is True
24+
jc.resume("t1")
25+
assert jc.is_paused("t1") is False
26+
27+
28+
def test_v7_h06_pause_is_idempotent():
29+
jc.pause("t1")
30+
jc.pause("t1")
31+
jc.resume("t1")
32+
assert jc.is_paused("t1") is False
33+
34+
35+
def test_v7_h06_null_or_empty_token_never_paused():
36+
assert jc.is_paused(None) is False
37+
assert jc.is_paused("") is False
38+
39+
40+
def test_v7_h06_wait_while_paused_returns_when_resumed():
41+
jc.pause("job-X")
42+
43+
def _resumer():
44+
time.sleep(0.15)
45+
jc.resume("job-X")
46+
47+
threading.Thread(target=_resumer, daemon=True).start()
48+
t0 = time.time()
49+
jc.wait_while_paused("job-X", poll_s=0.02, max_wait_s=2.0)
50+
elapsed = time.time() - t0
51+
assert 0.10 <= elapsed < 2.0
52+
53+
54+
def test_v7_h06_wait_while_paused_bounded_by_max_wait():
55+
jc.pause("stuck")
56+
t0 = time.time()
57+
jc.wait_while_paused("stuck", poll_s=0.02, max_wait_s=0.1)
58+
elapsed = time.time() - t0
59+
assert 0.08 < elapsed < 0.5
60+
61+
62+
def test_v7_h06_distinct_tokens_independent():
63+
jc.pause("a")
64+
assert jc.is_paused("a") is True
65+
assert jc.is_paused("b") is False

0 commit comments

Comments
 (0)