[Bugfix][Frontend] Stop SSL certificate watchers when HTTP serving exits (#55699)

Signed-off-by: git-jxj <65210887+git-jxj@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
This commit is contained in:
jxjandOpenAI Codex authored and GitHub committed 2026-10-26 16:48:13 -04:00
1 parent a2ecc8ac4d
commit 01b05cd69d
2 files changed
+75 -8

No files matched your search

@@ -4,9 +4,12 @@ import asyncio
import tempfile
from pathlib import Path
from ssl import SSLContext
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from vllm.entrypoints.launchers import launcher
from vllm.entrypoints.launchers.utils.ssl import SSLCertRefresher
@@ -100,3 +103,66 @@ async def test_ssl_refresher():
await asyncio.sleep(1)
assert ssl_context.load_cert_chain_count == cert_chain_count
assert ssl_context.load_ca_count == ca_count
@pytest.mark.asyncio
@pytest.mark.parametrize("failure_stage", [None, "serve", "signal", "shutdown_task"])
async def test_serve_http_stops_ssl_refresher_when_server_exits(
monkeypatch, failure_stage
):
config = SimpleNamespace(
ssl=object(),
ssl_keyfile="key.pem",
ssl_certfile="cert.pem",
ssl_ca_certs="ca.pem",
load=MagicMock(),
)
server_error = RuntimeError("server failed") if failure_stage == "serve" else None
server = SimpleNamespace(serve=AsyncMock(side_effect=server_error))
ssl_cert_refresher = MagicMock()
loop = asyncio.get_running_loop()
def add_signal_handler(*_args):
if failure_stage == "signal":
raise RuntimeError("signal failed")
create_task = loop.create_task
def create_task_or_fail(coro, **kwargs):
if (
failure_stage == "shutdown_task"
and coro.cr_code.co_name == "handle_shutdown"
):
coro.close()
raise RuntimeError("shutdown_task failed")
return create_task(coro, **kwargs)
monkeypatch.setattr(loop, "add_signal_handler", add_signal_handler)
monkeypatch.setattr(loop, "create_task", create_task_or_fail)
monkeypatch.setattr(launcher.uvicorn, "Config", lambda *_args, **_kwargs: config)
monkeypatch.setattr(launcher, "NoSignalServer", lambda _config: server)
monkeypatch.setattr(
launcher,
"SSLCertRefresher",
lambda **_kwargs: ssl_cert_refresher,
)
monkeypatch.setattr(launcher, "watchdog_loop", AsyncMock())
app = SimpleNamespace(routes=[], state=SimpleNamespace(engine_client=object()))
shutdown = None
try:
if failure_stage is None:
shutdown = await launcher.serve_http(
app, sock=None, enable_ssl_refresh=True, port=8000
)
else:
error = "server" if failure_stage == "serve" else failure_stage
with pytest.raises(RuntimeError, match=f"{error} failed"):
await launcher.serve_http(
app, sock=None, enable_ssl_refresh=True, port=8000
)
ssl_cert_refresher.stop.assert_called_once_with()
finally:
if shutdown is not None:
await shutdown
await asyncio.sleep(0)
+9 -8
View File
@@ -259,9 +259,6 @@ async def serve_http(
async def dummy_shutdown() -> None:
pass
loop.add_signal_handler(signal.SIGINT, signal_handler)
loop.add_signal_handler(signal.SIGTERM, signal_handler)
async def handle_shutdown() -> None:
await shutdown_event.wait()
@@ -285,12 +282,13 @@ async def serve_http(
server_task.cancel()
if watchdog_task is not None:
watchdog_task.cancel()
if ssl_cert_refresher:
ssl_cert_refresher.stop()
shutdown_task = loop.create_task(handle_shutdown())
shutdown_task = None
try:
loop.add_signal_handler(signal.SIGINT, signal_handler)
loop.add_signal_handler(signal.SIGTERM, signal_handler)
shutdown_task = loop.create_task(handle_shutdown())
await server_task
return dummy_shutdown()
except asyncio.CancelledError:
@@ -306,9 +304,12 @@ async def serve_http(
logger.info_once("[shutdown] API server: shutting down FastAPI HTTP server")
return server.shutdown()
finally:
shutdown_task.cancel()
if shutdown_task is not None:
shutdown_task.cancel()
if watchdog_task is not None:
watchdog_task.cancel()
if ssl_cert_refresher:
ssl_cert_refresher.stop()
async def watchdog_loop(server: uvicorn.Server, engine: EngineClient):