Skip to content

Commit f38bbd4

Browse files
author
Will Lin
committed
[bugfix]: cover signals during worker startup
1 parent 2a09e9b commit f38bbd4

2 files changed

Lines changed: 67 additions & 13 deletions

File tree

fastvideo/tests/worker/test_signal_terminated_worker_is_reported.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
memory drops below six percent, so this is the common failure there and it is
1313
completely silent.
1414
15-
The test replaces worker construction with a lightweight signal injection, so
16-
it exercises the real handler and outer exception path without initializing a
17-
distributed process group or model.
15+
The tests replace startup and worker construction with lightweight signal
16+
injection, so they exercise the real handler and outer exception path without
17+
initializing a distributed process group or model.
1818
"""
1919
from __future__ import annotations
2020

@@ -30,6 +30,10 @@ class _ReadyPipe:
3030

3131
def __init__(self) -> None:
3232
self.closed = False
33+
self.messages = []
34+
35+
def send(self, message) -> None:
36+
self.messages.append(message)
3337

3438
def close(self) -> None:
3539
self.closed = True
@@ -48,22 +52,40 @@ def parent(self):
4852
(signal.SIGINT, "user interrupted", "out-of-memory daemon"),
4953
],
5054
)
55+
@pytest.mark.parametrize("injection_point", ["startup", "worker_construction", "worker_loop"])
5156
def test_worker_main_reports_received_signal(monkeypatch: pytest.MonkeyPatch, signum: int, expected_reason: str,
52-
unexpected_reason: str) -> None:
57+
unexpected_reason: str, injection_point: str) -> None:
5358
installed_handlers = {}
5459
logged_messages = []
60+
workers = []
5561
ready_pipe = _ReadyPipe()
5662

5763
def install_handler(installed_signum, handler):
5864
installed_handlers[installed_signum] = handler
5965

66+
def inject_signal() -> None:
67+
installed_handlers[signum](signum, None)
68+
6069
class SignalledWorker:
6170

71+
READY_STR = "READY"
72+
6273
def __init__(self, *args, **kwargs) -> None:
63-
installed_handlers[signum](signum, None)
74+
self.shutdown_called = False
75+
workers.append(self)
76+
if injection_point == "worker_construction":
77+
inject_signal()
78+
79+
def worker_busy_loop(self) -> None:
80+
assert injection_point == "worker_loop"
81+
inject_signal()
82+
83+
def shutdown(self) -> None:
84+
self.shutdown_called = True
6485

6586
monkeypatch.setattr(multiproc_executor.signal, "signal", install_handler)
66-
monkeypatch.setattr(multiproc_executor, "kill_itself_when_parent_died", lambda: None)
87+
monkeypatch.setattr(multiproc_executor, "kill_itself_when_parent_died",
88+
inject_signal if injection_point == "startup" else lambda: None)
6789
monkeypatch.setattr(multiproc_executor.faulthandler, "enable", lambda: None)
6890
monkeypatch.setattr(multiproc_executor.psutil, "Process", _ParentlessProcess)
6991
monkeypatch.setattr(multiproc_executor, "WorkerMultiprocProc", SignalledWorker)
@@ -80,3 +102,31 @@ def __init__(self, *args, **kwargs) -> None:
80102
assert f"Worker 7 received {signal.Signals(signum).name} ({signum})" in logged_messages[0]
81103
assert expected_reason in logged_messages[0]
82104
assert unexpected_reason not in logged_messages[0]
105+
if injection_point == "worker_loop":
106+
assert ready_pipe.messages == [{"status": "READY"}]
107+
assert workers[0].shutdown_called
108+
109+
110+
def test_worker_main_preserves_unrelated_system_exit(monkeypatch: pytest.MonkeyPatch) -> None:
111+
logged_messages = []
112+
ready_pipe = _ReadyPipe()
113+
114+
class ExitingWorker:
115+
116+
def __init__(self, *args, **kwargs) -> None:
117+
raise SystemExit(23)
118+
119+
monkeypatch.setattr(multiproc_executor.signal, "signal", lambda *args: None)
120+
monkeypatch.setattr(multiproc_executor, "kill_itself_when_parent_died", lambda: None)
121+
monkeypatch.setattr(multiproc_executor.faulthandler, "enable", lambda: None)
122+
monkeypatch.setattr(multiproc_executor.psutil, "Process", _ParentlessProcess)
123+
monkeypatch.setattr(multiproc_executor, "WorkerMultiprocProc", ExitingWorker)
124+
monkeypatch.setattr(multiproc_executor.logger, "exception",
125+
lambda message, *args: logged_messages.append(message % args))
126+
127+
with pytest.raises(SystemExit) as exc_info:
128+
WorkerMultiprocProc.worker_main(ready_pipe=ready_pipe, rank=7)
129+
130+
assert exc_info.value.code == 23
131+
assert ready_pipe.closed
132+
assert logged_messages == []

fastvideo/worker/multiproc_executor.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -551,18 +551,22 @@ def signal_handler(signum, frame):
551551
shutdown_requested = True
552552
raise _WorkerSignalExit(signum)
553553

554-
# Either SIGTERM or SIGINT will terminate the worker
555-
signal.signal(signal.SIGTERM, signal_handler)
556-
signal.signal(signal.SIGINT, signal_handler)
557-
kill_itself_when_parent_died()
558-
faulthandler.enable()
559-
parent_process = psutil.Process().parent()
560-
561554
worker = None
562555
ready_pipe = kwargs.pop("ready_pipe")
563556
rank = kwargs.get("rank")
557+
parent_process = None
564558

565559
try:
560+
# Keep all setup after handler installation inside this guarded
561+
# region. The parent may terminate peer workers as soon as one
562+
# worker fails, including while another peer is still starting.
563+
# Either SIGTERM or SIGINT will terminate the worker.
564+
signal.signal(signal.SIGTERM, signal_handler)
565+
signal.signal(signal.SIGINT, signal_handler)
566+
kill_itself_when_parent_died()
567+
faulthandler.enable()
568+
parent_process = psutil.Process().parent()
569+
566570
worker = WorkerMultiprocProc(*args, **kwargs)
567571

568572
# Send READY once we know everything is loaded

0 commit comments

Comments
 (0)