Skip to content

Commit c76ea18

Browse files
committed
Use the "spawn" start method unconditionally
Rather than trying to support both "fork" and "spawn", force "spawn" to be used no matter what. Split the monitor process's entry point out into a separate module to avoid having a global multiprocessing `Queue` object get created when it is reimported in the spawned process, as otherwise Python 3.8 and 3.9 report that some semaphores are leaked by the child process.
1 parent 82a11a9 commit c76ea18

3 files changed

Lines changed: 60 additions & 58 deletions

File tree

src/pytest_pystack/_monitor.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import shlex
2+
import subprocess
3+
import sys
4+
from queue import Empty
5+
6+
from ._config import PystackConfig
7+
8+
9+
def monitor(config: PystackConfig, pid, queue, debug_detected):
10+
pystack_cmd = [
11+
config.pystack_path,
12+
"remote",
13+
]
14+
if config.pystack_args:
15+
pystack_cmd += shlex.split(config.pystack_args)
16+
handled_test_cases = set()
17+
while True:
18+
testcase = queue.get()
19+
if testcase is None:
20+
break
21+
22+
if testcase in handled_test_cases:
23+
continue
24+
handled_test_cases.add(testcase)
25+
26+
try:
27+
new_testcase = queue.get(timeout=config.threshold)
28+
if new_testcase != testcase:
29+
print(
30+
f"new test {new_testcase} should not start before previous {testcase} test finished",
31+
file=sys.__stderr__,
32+
)
33+
raise Exception(
34+
"new test should not start before previous test finished"
35+
)
36+
except Empty:
37+
output = ""
38+
output += f"\n\n**** PYSTACK -- {testcase} ***\n"
39+
output += f"Timed out waiting for process {pid} to finish {testcase}:"
40+
proc = subprocess.run(
41+
[*pystack_cmd, str(pid)],
42+
stdout=subprocess.PIPE,
43+
text=True,
44+
)
45+
output += proc.stdout
46+
output += "**** PYSTACK ***\n"
47+
is_debug = debug_detected.is_set()
48+
debug_detected.clear()
49+
if config.print_stderr and not is_debug:
50+
print(output, file=sys.__stderr__)
51+
if config.output_file:
52+
with open(config.output_file, "a") as f:
53+
print(output, file=f)
Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import os
2-
import shlex
3-
import subprocess
4-
import sys
5-
from queue import Empty
62

7-
from ._config import PystackConfig
83
from ._debug_detect import debug_detected
4+
from ._monitor import monitor
95
from ._multiprocessing_context import MP_CTX
106

117
_queue = MP_CTX.Queue()
@@ -15,7 +11,7 @@
1511
def start(config):
1612
global _process
1713
_process = MP_CTX.Process(
18-
target=_run_monitor,
14+
target=monitor,
1915
args=(
2016
config,
2117
os.getpid(),
@@ -34,50 +30,3 @@ def stop():
3430
_process.join(timeout=5)
3531
if _process.is_alive():
3632
_process.kill()
37-
38-
39-
def _run_monitor(config: PystackConfig, pid, queue, debug_detected):
40-
pystack_cmd = [
41-
config.pystack_path,
42-
"remote",
43-
]
44-
if config.pystack_args:
45-
pystack_cmd += shlex.split(config.pystack_args)
46-
handled_test_cases = set()
47-
while True:
48-
testcase = queue.get()
49-
if testcase is None:
50-
break
51-
52-
if testcase in handled_test_cases:
53-
continue
54-
handled_test_cases.add(testcase)
55-
56-
try:
57-
new_testcase = queue.get(timeout=config.threshold)
58-
if new_testcase != testcase:
59-
print(
60-
f"new test {new_testcase} should not start before previous {testcase} test finished",
61-
file=sys.__stderr__,
62-
)
63-
raise Exception(
64-
"new test should not start before previous test finished"
65-
)
66-
except Empty:
67-
output = ""
68-
output += f"\n\n**** PYSTACK -- {testcase} ***\n"
69-
output += f"Timed out waiting for process {pid} to finish {testcase}:"
70-
proc = subprocess.run(
71-
[*pystack_cmd, str(pid)],
72-
stdout=subprocess.PIPE,
73-
text=True,
74-
)
75-
output += proc.stdout
76-
output += "**** PYSTACK ***\n"
77-
is_debug = debug_detected.is_set()
78-
debug_detected.clear()
79-
if config.print_stderr and not is_debug:
80-
print(output, file=sys.__stderr__)
81-
if config.output_file:
82-
with open(config.output_file, "a") as f:
83-
print(output, file=f)
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import multiprocessing
22

3-
# Ensure the "forkserver" spawn method is never used, because we have
4-
# known incompatibilities with it, while both "fork" and "spawn" work.
5-
MP_CTX = multiprocessing.get_context(
6-
"fork" if multiprocessing.get_start_method() == "fork" else "spawn"
7-
)
3+
# We have known incompatibilities with the "forkserver" start method.
4+
# The "fork" method is expected to work, but can potentially lead to
5+
# strange failure modes because of things being inherited by our monitor
6+
# process that shouldn't have been. Use "spawn" unconditionally instead.
7+
MP_CTX = multiprocessing.get_context("spawn")

0 commit comments

Comments
 (0)