Skip to content

Commit

Permalink
fix: save data on SIGTERM #1307
Browse files Browse the repository at this point in the history
This covers multiprocessing.Process.terminate(), and maybe other cases also.
  • Loading branch information
nedbat committed Jan 23, 2022
1 parent 53b99ff commit 94c24b6
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 3 deletions.
17 changes: 15 additions & 2 deletions coverage/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os
import os.path
import platform
import signal
import sys
import time
import warnings
Expand Down Expand Up @@ -228,6 +229,7 @@ def __init__(
self._exclude_re = None
self._debug = None
self._file_mapper = None
self._old_sigterm = None

# State machine variables:
# Have we initialized everything?
Expand Down Expand Up @@ -526,6 +528,11 @@ def _init_for_start(self):
self._should_write_debug = True

atexit.register(self._atexit)
if not env.WINDOWS:
# The Python docs seem to imply that SIGTERM works uniformly even
# on Windows, but that's not my experience, and this agrees:
# https://stackoverflow.com/questions/35772001/x/35792192#35792192
self._old_sigterm = signal.signal(signal.SIGTERM, self._on_sigterm)

def _init_data(self, suffix):
"""Create a data file if we don't have one yet."""
Expand Down Expand Up @@ -583,15 +590,21 @@ def stop(self):
self._collector.stop()
self._started = False

def _atexit(self):
def _atexit(self, event="atexit"):
"""Clean up on process shutdown."""
if self._debug.should("process"):
self._debug.write(f"atexit: pid: {os.getpid()}, instance: {self!r}")
self._debug.write(f"{event}: pid: {os.getpid()}, instance: {self!r}")
if self._started:
self.stop()
if self._auto_save:
self.save()

def _on_sigterm(self, signum_unused, frame_unused):
"""A handler for signal.SIGTERM."""
self._atexit("sigterm")
signal.signal(signal.SIGTERM, self._old_sigterm)
os.kill(os.getpid(), signal.SIGTERM)

def erase(self):
"""Erase previously collected coverage data.
Expand Down
2 changes: 1 addition & 1 deletion coverage/multiproc.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def _bootstrap(self, *args, **kwargs):
"""Wrapper around _bootstrap to start coverage."""
try:
from coverage import Coverage # avoid circular import
cov = Coverage(data_suffix=True)
cov = Coverage(data_suffix=True, auto_data=True)
cov._warn_preimported_source = False
cov.start()
debug = cov._debug
Expand Down
79 changes: 79 additions & 0 deletions tests/test_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,3 +693,82 @@ def random_load(): # pragma: nested
finally:
os.chdir(old_dir)
should_run[0] = False


@pytest.mark.skipif(env.WINDOWS, reason="SIGTERM doesn't work the same on Windows")
class SigtermTest(CoverageTest):
"""Tests of our handling of SIGTERM."""

def test_sigterm_saves_data(self):
# A terminated process should save its coverage data.
self.make_file("clobbered.py", """\
import multiprocessing
import time
def subproc(x):
if x.value == 3:
print("THREE", flush=True) # line 6, missed
else:
print("NOT THREE", flush=True)
x.value = 0
time.sleep(60)
if __name__ == "__main__":
print("START", flush=True)
x = multiprocessing.Value("L", 1)
proc = multiprocessing.Process(target=subproc, args=(x,))
proc.start()
while x.value != 0:
time.sleep(.05)
proc.terminate()
print("END", flush=True)
""")
self.make_file(".coveragerc", """\
[run]
parallel = True
concurrency = multiprocessing
""")
out = self.run_command("coverage run clobbered.py")
# Under the Python tracer on Linux, we get the "Trace function changed"
# message. Does that matter?
if "Trace function changed" in out:
lines = out.splitlines(True)
assert len(lines) == 4
out = "".join(lines[:3])
assert out == "START\nNOT THREE\nEND\n"
self.run_command("coverage combine")
out = self.run_command("coverage report -m")
assert self.squeezed_lines(out)[2] == "clobbered.py 17 1 94% 6"

def test_sigterm_still_runs(self):
# A terminated process still runs its own SIGTERM handler.
self.make_file("handler.py", """\
import multiprocessing
import signal
import time
def subproc(x):
print("START", flush=True)
def on_sigterm(signum, frame):
print("SIGTERM", flush=True)
signal.signal(signal.SIGTERM, on_sigterm)
x.value = 0
time.sleep(.1)
print("END", flush=True)
if __name__ == "__main__":
x = multiprocessing.Value("L", 1)
proc = multiprocessing.Process(target=subproc, args=(x,))
proc.start()
while x.value != 0:
time.sleep(.02)
proc.terminate()
""")
self.make_file(".coveragerc", """\
[run]
parallel = True
concurrency = multiprocessing
""")
out = self.run_command("coverage run handler.py")
assert out == "START\nSIGTERM\nEND\n"

0 comments on commit 94c24b6

Please sign in to comment.