Skip to content

Commit 7f448b2

Browse files
Vinya567yinggeh
andauthored
test: capture and surface tritonserver output on server-startup failure (#126)
ScopedTritonServer.wait_for_server_ready() previously raised a generic "Server failed to start in time." which hid the actual cause. Redirect the triton subprocess's stdout/stderr to a temp file and include the tail (plus pid, exit code, elapsed time, and timeout) in the RuntimeError raised by wait_for_server_ready. Per review feedback, removed the os.unlink() in kill_server() — CI runner collects *.log as artifacts, so leaving the log preserves it for post-mortem. Pure test-harness diagnostics change; no production code path affected. Co-authored-by: Yingge He <157551214+yinggeh@users.noreply.github.com>
1 parent c23498a commit 7f448b2

1 file changed

Lines changed: 65 additions & 22 deletions

File tree

tests/utils.py

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2024-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
# Copyright 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
#
33
# Redistribution and use in source and binary forms, with or without
44
# modification, are permitted provided that the following conditions
@@ -24,15 +24,17 @@
2424
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2525
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626

27+
import collections
2728
import io
2829
import json
30+
import tempfile
2931
import time
3032
import psutil
3133
import subprocess
3234
import requests
3335
from contextlib import redirect_stdout
3436
from triton_cli.main import run
35-
from subprocess import Popen
37+
from subprocess import Popen, STDOUT
3638

3739

3840
class TritonCommands:
@@ -132,6 +134,7 @@ def __init__(self, repo=None, mode="local", timeout=60, frontend=None):
132134
self.frontend = frontend
133135
self.timeout = timeout
134136
self.proc = None
137+
self._log_path = None
135138

136139
def __enter__(self):
137140
self.start()
@@ -154,10 +157,30 @@ def run_server(self, repo=None, mode="local", frontend=None):
154157
args += ["--mode", mode]
155158
if frontend:
156159
args += ["--frontend", frontend]
157-
# Use Popen to run the server in the background as a separate process.
158-
p = Popen(args)
160+
# Redirect stdout/stderr to a temp file so the server's output can be
161+
# surfaced in error messages if startup fails.
162+
log_handle = tempfile.NamedTemporaryFile(
163+
mode="wb", suffix=".log", prefix="triton_server_", delete=False
164+
)
165+
self._log_path = log_handle.name
166+
try:
167+
p = Popen(args, stdout=log_handle, stderr=STDOUT)
168+
finally:
169+
log_handle.close()
159170
return p
160171

172+
def _format_server_output(self, max_lines: int = 50) -> str:
173+
if not self._log_path:
174+
return "<server output not captured>"
175+
try:
176+
with open(self._log_path, errors="replace") as f:
177+
tail = collections.deque(f, maxlen=max_lines)
178+
except OSError as exc:
179+
return f"<failed to read server log {self._log_path}: {exc}>"
180+
if not tail:
181+
return "<no server output produced>"
182+
return "".join(tail)
183+
161184
def wait_for_server_ready(self, timeout: int = 60):
162185
if not self.proc:
163186
raise RuntimeError("Server process wasn't started")
@@ -166,33 +189,53 @@ def wait_for_server_ready(self, timeout: int = 60):
166189
while True:
167190
try:
168191
if self.check_server_ready():
169-
break
192+
return
170193
except Exception as err:
171-
result = self.proc.poll()
172-
if result is not None and result != 0:
173-
raise RuntimeError("Server exited unexpectedly.") from err
194+
exit_code = self.proc.poll()
195+
elapsed = time.time() - start
196+
197+
if exit_code is not None:
198+
state = (
199+
"exited cleanly with code 0"
200+
if exit_code == 0
201+
else f"exited with code {exit_code}"
202+
)
203+
raise RuntimeError(
204+
f"Triton server {state} after {elapsed:.1f}s without "
205+
f"becoming ready (pid={self.proc.pid}, "
206+
f"timeout={timeout}s).\n"
207+
f"--- last server output ---\n"
208+
f"{self._format_server_output()}\n"
209+
f"--- end server output ---"
210+
) from err
174211

175212
time.sleep(0.5)
176213
if time.time() - start > timeout:
177-
raise RuntimeError("Server failed to start in time.") from err
214+
raise RuntimeError(
215+
f"Triton server did not become ready within "
216+
f"{timeout}s (pid={self.proc.pid}, process still "
217+
f"running).\n"
218+
f"--- last server output ---\n"
219+
f"{self._format_server_output()}\n"
220+
f"--- end server output ---"
221+
) from err
178222

179223
def kill_server(self, timeout: int = 60):
180-
if not self.proc:
224+
if self.proc:
225+
try:
226+
self.proc.terminate()
227+
self.proc.wait(timeout=timeout) # Wait for triton to clean up
228+
except subprocess.TimeoutExpired:
229+
self.proc.kill()
230+
self.proc.wait() # Indefinetely wait until the process is cleaned up.
231+
except psutil.NoSuchProcess as e:
232+
print(e)
233+
except AttributeError as e:
234+
print(e)
235+
else:
181236
# If process wasn't started by this point, just print the error and
182237
# gracefully exit for now.
183238
print("ERROR: Server process wasn't started")
184-
return
185-
186-
try:
187-
self.proc.terminate()
188-
self.proc.wait(timeout=timeout) # Wait for triton to clean up
189-
except subprocess.TimeoutExpired:
190-
self.proc.kill()
191-
self.proc.wait() # Indefinetely wait until the process is cleaned up.
192-
except psutil.NoSuchProcess as e:
193-
print(e)
194-
except AttributeError as e:
195-
print(e)
196239

197240
def check_server_ready(self):
198241
if self.frontend == "openai":

0 commit comments

Comments
 (0)