Skip to content

Commit d42a4fd

Browse files
authored
bpo-37531: Enhance regrtest multiprocess timeout (GH-15345) (GH-15871)
* Write a message when killing a worker process * Put a timeout on the second popen.communicate() call (after killing the process) * Put a timeout on popen.wait() call * Catch popen.kill() and popen.wait() exceptions (cherry picked from commit de2d9ee)
1 parent 58ef7d3 commit d42a4fd

File tree

2 files changed

+66
-18
lines changed

2 files changed

+66
-18
lines changed

Lib/test/libregrtest/runtest_mp.py

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,38 @@ def __repr__(self):
126126
info.append(f'pid={popen.pid}')
127127
return '<%s>' % ' '.join(info)
128128

129+
def _kill(self):
130+
dt = time.monotonic() - self.start_time
131+
132+
popen = self._popen
133+
pid = popen.pid
134+
print("Kill worker process %s running for %.1f sec" % (pid, dt),
135+
file=sys.stderr, flush=True)
136+
137+
try:
138+
popen.kill()
139+
return True
140+
except OSError as exc:
141+
print("WARNING: Failed to kill worker process %s: %r" % (pid, exc),
142+
file=sys.stderr, flush=True)
143+
return False
144+
145+
def _close_wait(self):
146+
popen = self._popen
147+
148+
# stdout and stderr must be closed to ensure that communicate()
149+
# does not hang
150+
popen.stdout.close()
151+
popen.stderr.close()
152+
153+
try:
154+
popen.wait(JOIN_TIMEOUT)
155+
except (subprocess.TimeoutExpired, OSError) as exc:
156+
print("WARNING: Failed to wait for worker process %s "
157+
"completion (timeout=%.1f sec): %r"
158+
% (popen.pid, JOIN_TIMEOUT, exc),
159+
file=sys.stderr, flush=True)
160+
129161
def kill(self):
130162
"""
131163
Kill the current process (if any).
@@ -135,30 +167,45 @@ def kill(self):
135167
"""
136168
self._killed = True
137169

138-
popen = self._popen
139-
if popen is None:
170+
if self._popen is None:
140171
return
141-
popen.kill()
142-
# stdout and stderr must be closed to ensure that communicate()
143-
# does not hang
144-
popen.stdout.close()
145-
popen.stderr.close()
146-
popen.wait()
172+
173+
if not self._kill():
174+
return
175+
176+
self._close_wait()
147177

148178
def mp_result_error(self, test_name, error_type, stdout='', stderr='',
149179
err_msg=None):
150180
test_time = time.monotonic() - self.start_time
151181
result = TestResult(test_name, error_type, test_time, None)
152182
return MultiprocessResult(result, stdout, stderr, err_msg)
153183

184+
def _timedout(self, test_name):
185+
self._kill()
186+
187+
stdout = sterr = ''
188+
popen = self._popen
189+
try:
190+
stdout, stderr = popen.communicate(timeout=JOIN_TIMEOUT)
191+
except (subprocess.TimeoutExpired, OSError) as exc:
192+
print("WARNING: Failed to read worker process %s output "
193+
"(timeout=%.1f sec): %r"
194+
% (popen.pid, exc, timeout),
195+
file=sys.stderr, flush=True)
196+
197+
self._close_wait()
198+
199+
return self.mp_result_error(test_name, TIMEOUT, stdout, stderr)
200+
154201
def _runtest(self, test_name):
155202
try:
156203
self.start_time = time.monotonic()
157204
self.current_test_name = test_name
158205

159206
self._popen = run_test_in_subprocess(test_name, self.ns)
160207
popen = self._popen
161-
with popen:
208+
try:
162209
try:
163210
if self._killed:
164211
# If kill() has been called before self._popen is set,
@@ -175,12 +222,7 @@ def _runtest(self, test_name):
175222
# on reading closed stdout/stderr
176223
raise ExitThread
177224

178-
popen.kill()
179-
stdout, stderr = popen.communicate()
180-
self.kill()
181-
182-
return self.mp_result_error(test_name, TIMEOUT,
183-
stdout, stderr)
225+
return self._timedout(test_name)
184226
except OSError:
185227
if self._killed:
186228
# kill() has been called: communicate() fails
@@ -190,8 +232,10 @@ def _runtest(self, test_name):
190232
except:
191233
self.kill()
192234
raise
235+
finally:
236+
self._close_wait()
193237

194-
retcode = popen.wait()
238+
retcode = popen.returncode
195239
finally:
196240
self.current_test_name = None
197241
self._popen = None
@@ -286,10 +330,11 @@ def wait_workers(self):
286330
if not worker.is_alive():
287331
break
288332
dt = time.monotonic() - start_time
289-
print("Wait for regrtest worker %r for %.1f sec" % (worker, dt))
333+
print("Wait for regrtest worker %r for %.1f sec" % (worker, dt),
334+
flush=True)
290335
if dt > JOIN_TIMEOUT:
291336
print("Warning -- failed to join a regrtest worker %s"
292-
% worker)
337+
% worker, flush=True)
293338
break
294339

295340
def _get_result(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Enhance regrtest multiprocess timeout: write a message when killing a worker
2+
process, catch popen.kill() and popen.wait() exceptions, put a timeout on the
3+
second call to popen.communicate().

0 commit comments

Comments
 (0)