Skip to content

Commit cbd8f58

Browse files
improve multi-server shutdown
1 parent aab27c6 commit cbd8f58

3 files changed

Lines changed: 30 additions & 3 deletions

File tree

src/agentevals/api/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ async def event_generator():
6565
try:
6666
while True:
6767
event = await queue.get()
68+
if event is None:
69+
break
6870
yield f"data: {json.dumps(event)}\n\n"
6971
except asyncio.CancelledError:
7072
pass
@@ -125,4 +127,4 @@ async def on_startup():
125127
@app.on_event("shutdown")
126128
async def on_shutdown():
127129
if _trace_manager:
128-
await _trace_manager.stop_cleanup_task()
130+
await _trace_manager.shutdown()

src/agentevals/cli.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,27 @@ def list_metrics() -> None:
155155
click.echo()
156156

157157

158+
def _link_server_shutdown(*servers) -> None:
159+
"""Link multiple uvicorn servers so a single SIGINT shuts down all of them.
160+
161+
Uvicorn installs per-server signal handlers; the last server's handler
162+
overwrites earlier ones. This replaces handle_exit on every server with
163+
a shared callback that sets should_exit / force_exit on all of them.
164+
"""
165+
import signal as _signal
166+
167+
def _shared_exit(sig, frame):
168+
force = all(s.should_exit for s in servers)
169+
for s in servers:
170+
if force and sig == _signal.SIGINT:
171+
s.force_exit = True
172+
else:
173+
s.should_exit = True
174+
175+
for s in servers:
176+
s.handle_exit = _shared_exit
177+
178+
158179
@main.command("serve")
159180
@click.option(
160181
"--dev",
@@ -258,6 +279,7 @@ async def _run_dev_servers():
258279
)
259280
main_server = uvicorn.Server(main_config)
260281
otlp_server = uvicorn.Server(otlp_config)
282+
_link_server_shutdown(main_server, otlp_server)
261283
await asyncio.gather(main_server.serve(), otlp_server.serve())
262284

263285
asyncio.run(_run_dev_servers())
@@ -283,6 +305,7 @@ async def _run_ui_servers():
283305
)
284306
main_server = uvicorn.Server(main_config)
285307
otlp_server = uvicorn.Server(otlp_config)
308+
_link_server_shutdown(main_server, otlp_server)
286309
await asyncio.gather(main_server.serve(), otlp_server.serve())
287310

288311
asyncio.run(_run_ui_servers())

src/agentevals/streaming/ws_server.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ def start_cleanup_task(self) -> None:
6161
self._cleanup_task = asyncio.create_task(self._cleanup_old_sessions_loop())
6262
logger.info("Started session cleanup task (TTL: %s, max: %d)", self.session_ttl, self.max_sessions)
6363

64-
async def stop_cleanup_task(self) -> None:
65-
"""Stop the background cleanup task."""
64+
async def shutdown(self) -> None:
65+
"""Gracefully shut down: close SSE clients and cancel background tasks."""
66+
for queue in self.sse_queues:
67+
queue.put_nowait(None)
6668
if self._cleanup_task:
6769
self._cleanup_task.cancel()
6870
try:

0 commit comments

Comments
 (0)