Skip to content

Commit 5eff5b8

Browse files
committed
Added a thread to clear the stdout pipe
1 parent 9386aed commit 5eff5b8

1 file changed

Lines changed: 24 additions & 7 deletions

File tree

render_machine/render_utils.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
TIMEOUT_ERROR_EXIT_CODE = 124
2020
POLL_INTERVAL_SECONDS = 0.2
2121
SIGTERM_GRACE_PERIOD_SECONDS = 0.2
22-
STDOUT_READ_TIMEOUT_SECONDS = 5
22+
STDOUT_READ_TIMEOUT_SECONDS = 2
2323
F_SETPIPE_SIZE = 1031 # Linux-only constant
2424
PIPE_SIZE_KB = 1024 # 1MB
2525

@@ -120,11 +120,27 @@ def execute_script( # noqa: C901
120120
# Set the pipe size to 1MB to avoid buffer overflows
121121
fcntl.fcntl(proc.stdout.fileno(), F_SETPIPE_SIZE, PIPE_SIZE_KB * 1024) # 1MB
122122

123+
# Drain stdout in a background thread to prevent pipe buffer deadlock.
124+
# macOS has a 64KB pipe buffer; without continuous draining, scripts that produce
125+
# more output than that block on write and never exit, causing spurious timeouts.
126+
output_chunks: list[str] = []
127+
128+
def _drain_stdout() -> None:
129+
try:
130+
for chunk in iter(lambda: proc.stdout.read(8192), ""):
131+
output_chunks.append(chunk)
132+
except (OSError, ValueError):
133+
pass
134+
135+
reader = threading.Thread(target=_drain_stdout, daemon=True)
136+
reader.start()
137+
123138
try:
124139
while proc.poll() is None:
125140
if time.time() - start_time >= script_timeout:
126141
_kill_process(proc)
127-
partial_stdout = proc.stdout.read()
142+
reader.join(timeout=2)
143+
partial_stdout = "".join(output_chunks)
128144
exc = subprocess.TimeoutExpired(cmd, script_timeout)
129145
exc.stdout = partial_stdout
130146
raise exc
@@ -136,12 +152,13 @@ def execute_script( # noqa: C901
136152
else:
137153
time.sleep(POLL_INTERVAL_SECONDS)
138154

139-
# Use communicate() with a timeout because child processes may hold the pipe open after the main process exits.
140-
try:
141-
stdout, _ = proc.communicate(timeout=STDOUT_READ_TIMEOUT_SECONDS)
142-
except subprocess.TimeoutExpired:
155+
# Wait for the reader to finish draining remaining output.
156+
# Close stdout if child processes keep the pipe open beyond the grace period.
157+
reader.join(timeout=STDOUT_READ_TIMEOUT_SECONDS)
158+
if reader.is_alive():
143159
proc.stdout.close()
144-
stdout = ""
160+
reader.join(timeout=1)
161+
stdout = "".join(output_chunks)
145162
elapsed_time = time.time() - start_time
146163

147164
sanitized_script_output = _sanitize_script_output(stdout)

0 commit comments

Comments
 (0)