|
17 | 17 |
|
18 | 18 | # Global thread pool for executing make commands asynchronously. |
19 | 19 | # Registers cleanup via atexit to ensure graceful shutdown on server exit. |
| 20 | +# |
| 21 | +# DESIGN DECISION: Blocking shutdown with wait=True |
| 22 | +# ================================================== |
| 23 | +# Using shutdown(wait=True) blocks server exit until all running commands complete. |
| 24 | +# In worst case (5 concurrent commands at 120s timeout), shutdown could block ~10 minutes. |
| 25 | +# |
| 26 | +# This is an intentional trade-off: |
| 27 | +# - Pro: Commands complete gracefully, no orphaned processes |
| 28 | +# - Con: Server shutdown/restart waits for long-running builds |
| 29 | +# |
| 30 | +# Alternatives considered but not implemented: |
| 31 | +# - wait=False: Commands get killed immediately (data loss risk) |
| 32 | +# - Shorter timeout: Requires additional logic, complexity |
| 33 | +# - cancel_futures=True: Requires Python 3.9+, still blocks for in-progress commands |
| 34 | +# |
| 35 | +# For most deployments, commands complete in seconds and this is not an issue. |
| 36 | +# If needed in the future, add a configurable shutdown timeout. |
20 | 37 | _executor = ThreadPoolExecutor(max_workers=10) |
21 | 38 | atexit.register(lambda: _executor.shutdown(wait=True)) |
22 | 39 |
|
@@ -155,12 +172,6 @@ def _read_stream(stream, max_bytes: int, result: list) -> None: |
155 | 172 | while stream.read(65536): |
156 | 173 | truncated = True |
157 | 174 |
|
158 | | - # Also truncated if we hit the limit exactly and stream wasn't exhausted |
159 | | - if total_bytes >= max_bytes and not truncated: |
160 | | - # Check if there was more data by attempting one more read |
161 | | - if stream.read(1): |
162 | | - truncated = True |
163 | | - |
164 | 175 | result.append((b"".join(chunks), truncated)) |
165 | 176 |
|
166 | 177 |
|
@@ -195,6 +206,21 @@ def _run_with_output_limit( |
195 | 206 | stdout_data: list[bytes] = [] |
196 | 207 | stderr_data: list[bytes] = [] |
197 | 208 |
|
| 209 | + # Reader threads marked as daemon to prevent blocking server shutdown. |
| 210 | + # |
| 211 | + # DESIGN DECISION: daemon=True handles partial startup failures |
| 212 | + # ============================================================== |
| 213 | + # If stdout_thread.start() succeeds but stderr_thread.start() fails |
| 214 | + # (e.g., OS runs out of thread resources), the exception propagates and |
| 215 | + # stdout_thread may be left running. However, daemon=True ensures the |
| 216 | + # thread won't prevent process exit. |
| 217 | + # |
| 218 | + # This is acceptable because: |
| 219 | + # - Thread resource exhaustion is extremely rare in normal operation |
| 220 | + # - The finally block terminates the subprocess, closing its pipes |
| 221 | + # - Daemon threads automatically terminate on process exit |
| 222 | + # - The alternative (try/except around each start) adds complexity |
| 223 | + # for a failure mode that requires OS-level resource exhaustion |
198 | 224 | stdout_thread = threading.Thread( |
199 | 225 | target=_read_stream, |
200 | 226 | args=(process.stdout, MAX_OUTPUT_BYTES, stdout_data), |
|
0 commit comments