Skip to content

Commit 70fd743

Browse files
gpsheadmiss-islington
authored andcommitted
gh-141473: Speed up subprocess test_communicate_timeout_large_input long tail (GH-149003)
gh-141473: Speed up test_communicate_timeout_large_input Replace the slow reader's 30s sleep with a parent-driven wake over a loopback socket so post-timeout communicate() doesn't block waiting for the child to wake on its own. Worst-case runtime: ~30s -> <1s. (cherry picked from commit e1384cf) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent 8da3d39 commit 70fd743

1 file changed

Lines changed: 48 additions & 8 deletions

File tree

Lib/test/test_subprocess.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import sysconfig
2222
import select
2323
import shutil
24+
import socket
2425
import threading
2526
import gc
2627
import textwrap
@@ -1043,19 +1044,49 @@ def test_communicate_timeout_large_input(self):
10431044
# On Windows, stdin writing must also honor the timeout rather than
10441045
# blocking indefinitely when the pipe buffer fills.
10451046

1046-
# Input larger than typical pipe buffer (4-64KB on Windows)
1047-
input_data = b"x" * (128 * 1024)
1047+
input_data = b"x" * (128 * 1024) # > typical pipe buffer
1048+
1049+
# Cross-platform wake mechanism: the slow reader connects to a
1050+
# loopback TCP socket and blocks in select() on it (capped at 9s
1051+
# as a safety net we don't expect to hit). After phase 1 raises
1052+
# TimeoutExpired, the parent sends a byte to release the child so
1053+
# it drains stdin. A socket (rather than a raw pipe) is required
1054+
# because Windows select() only supports sockets, not arbitrary
1055+
# file descriptors.
1056+
server = socket.create_server(('127.0.0.1', 0), backlog=1)
1057+
server.settimeout(10) # bound the accept() if the child fails to start
1058+
port = server.getsockname()[1]
1059+
# The child sends one byte (low byte of its PID) first so the parent
1060+
# can detect the rare case of an unrelated process on the same host
1061+
# connecting to our ephemeral port before our child does. A single
1062+
# byte gives 1/256 collision odds, which is plenty for flake-prevention.
1063+
slow_reader = (
1064+
"import os, socket, sys, select; "
1065+
f"s = socket.create_connection(('127.0.0.1', {port}), timeout=9); "
1066+
"s.sendall(bytes([os.getpid() & 0xff])); "
1067+
"select.select([s], [], [], 9); "
1068+
"sys.stdout.buffer.write(sys.stdin.buffer.read())"
1069+
)
10481070

10491071
p = subprocess.Popen(
1050-
[sys.executable, "-c",
1051-
"import sys, time; "
1052-
"time.sleep(30); " # Don't read stdin for a long time
1053-
"sys.stdout.buffer.write(sys.stdin.buffer.read())"],
1072+
[sys.executable, "-c", slow_reader],
10541073
stdin=subprocess.PIPE,
10551074
stdout=subprocess.PIPE,
10561075
stderr=subprocess.PIPE)
10571076

1077+
conn = None
10581078
try:
1079+
conn, _ = server.accept()
1080+
server.close()
1081+
server = None
1082+
1083+
conn.settimeout(5)
1084+
peer_byte = conn.recv(1)
1085+
conn.settimeout(None)
1086+
self.assertEqual(peer_byte, bytes([p.pid & 0xff]),
1087+
f"loopback handshake byte {peer_byte!r} != "
1088+
f"low byte of child PID {p.pid} ({p.pid & 0xff:#x})")
1089+
10591090
timeout = 0.2
10601091
start = time.monotonic()
10611092
try:
@@ -1064,19 +1095,24 @@ def test_communicate_timeout_large_input(self):
10641095
elapsed = time.monotonic() - start
10651096
self.fail(
10661097
f"TimeoutExpired not raised. communicate() completed in "
1067-
f"{elapsed:.2f}s, but subprocess sleeps for 30s. "
1098+
f"{elapsed:.2f}s, but slow reader stalls for up to 9s. "
10681099
"Stdin writing blocked without enforcing timeout.")
10691100
except subprocess.TimeoutExpired:
10701101
elapsed = time.monotonic() - start
10711102

10721103
# Timeout should occur close to the specified timeout value,
10731104
# not after waiting for the subprocess to finish sleeping.
10741105
# Allow generous margin for slow CI, but must be well under
1075-
# the subprocess sleep time.
1106+
# the slow-reader's stall cap.
10761107
self.assertLess(elapsed, 5.0,
10771108
f"TimeoutExpired raised after {elapsed:.2f}s; expected ~{timeout}s. "
10781109
"Stdin writing blocked without checking timeout.")
10791110

1111+
# Release the slow reader so it stops blocking and drains stdin.
1112+
conn.sendall(b'go')
1113+
conn.close()
1114+
conn = None
1115+
10801116
# After timeout, continue communication. The remaining input
10811117
# should be sent and we should receive all data back.
10821118
stdout, stderr = p.communicate()
@@ -1086,6 +1122,10 @@ def test_communicate_timeout_large_input(self):
10861122
f"Expected {len(input_data)} bytes output but got {len(stdout)}")
10871123
self.assertEqual(stdout, input_data)
10881124
finally:
1125+
if conn is not None:
1126+
conn.close()
1127+
if server is not None:
1128+
server.close()
10891129
p.kill()
10901130
p.wait()
10911131

0 commit comments

Comments
 (0)