[Frontend] Disable uvicorn signal handlers instead of racing them (#50916)

Signed-off-by: Nick Hill <nickhill123@gmail.com>

Nick Hill committed Aug 7, 2026 at 12:40 UTC ac70ce96e0b9f69dd834bd1b0cd2d2b4c4a9db46
3 files changed +18 -15
tests/entrypoints/openai/test_dp_supervisor.py
+1 -1
@@ -312,7 +312,7 @@ async def test_shutdown_if_supervisor_server_error_on_startup(
312 await self._shutdown_event.wait()
313
314 monkeypatch.setattr(dp_sup.asyncio, "get_running_loop", lambda: FakeLoop())
315 - monkeypatch.setattr(dp_sup.uvicorn, "Server", FakeServer)
315 + monkeypatch.setattr(dp_sup, "NoSignalServer", FakeServer)
316 monkeypatch.setattr(DPSupervisor, "_shutdown_children", fake_shutdown_children)
317 monkeypatch.setattr(DPSupervisor, "_start_children", fake_start_children)
318 monkeypatch.setattr(DPSupervisor, "_monitor_children", fake_monitor_children)
vllm/entrypoints/launcher.py
+15 -13
@@ -2,8 +2,10 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import asyncio
5 +import contextlib
6 import signal
7 import socket
8 +from collections.abc import Generator
9 from functools import partial
10 from typing import Any
11
@@ -23,6 +25,18 @@ from vllm.utils.network_utils import find_process_using_port
25 logger = init_logger(__name__)
26
27
28 +class NoSignalServer(uvicorn.Server):
29 + """Uvicorn server that never installs its own SIGINT/SIGTERM handlers.
30 +
31 + Callers register their own handlers on the event loop for graceful
32 + shutdown; uvicorn's would race with and override them (see #49668).
33 + """
34 +
35 + @contextlib.contextmanager
36 + def capture_signals(self) -> Generator[None, None, None]:
37 + yield
38 +
39 +
40 async def serve_http(
41 app: FastAPI,
42 sock: socket.socket | None,
@@ -73,7 +87,7 @@ async def serve_http(
87 config.h11_max_incomplete_event_size = h11_max_incomplete_event_size
88 config.h11_max_header_count = h11_max_header_count
89 config.load()
76 - server = uvicorn.Server(config)
90 + server = NoSignalServer(config)
91 app.state.server = server
92
93 loop = asyncio.get_running_loop()
@@ -92,18 +106,6 @@ async def serve_http(
106 )
107 )
108
95 - # make sure uvicorn server signal handler has been registered.
96 - # Otherwise the app signal handler below will be overwritten in server.serve.
97 - # server.started is set to True after server complete server.startup.
98 - # We need to constraint the time sequence.
99 - logger.info("API server: waiting for HTTP server to start")
100 - while not server.started and not server_task.done():
101 - await asyncio.sleep(0.1)
102 - if server_task.done():
103 - # Propagate startup failure (e.g. port bind error) instead of hanging.
104 - await server_task
105 - logger.info("API server: HTTP server started")
106 -
109 shutdown_event = asyncio.Event()
110
111 def signal_handler() -> None:
vllm/entrypoints/openai/dp_supervisor.py
+2 -1
@@ -23,6 +23,7 @@ import uvloop
23 from fastapi import FastAPI, Response
24
25 import vllm.envs as envs
26 +from vllm.entrypoints.launcher import NoSignalServer
27 from vllm.logger import init_logger
28 from vllm.utils.system_utils import (
29 decorate_logs,
@@ -335,7 +336,7 @@ class DPSupervisor:
336 ssl_cert_reqs=self.args.ssl_cert_reqs,
337 ssl_ciphers=self.args.ssl_ciphers,
338 )
338 - supervisor_server = uvicorn.Server(config)
339 + supervisor_server = NoSignalServer(config)
340 supervisor_server_task = asyncio.create_task(
341 supervisor_server.serve(),
342 name="dp-supervisor",