[eric] perf: start 9router in background instead of blocking the http bind

- service lifespan was awaiting ensure_9router (~7.4s, up to ~18s cold) on the boot path

- dispatch already ensures it lazily; serialize ensure_running so no double-spawn

- cuts warm backend-ready ~9-10s toward ~2-3s
This commit is contained in:
Eric
2026-06-16 21:59:16 -07:00
parent 1facda5c33
commit bca0050dcb
2 changed files with 32 additions and 2 deletions
+15
View File
@@ -57,6 +57,11 @@ NINE_ROUTER_NPM_VERSION = os.environ.get("OPENSWARM_ROUTER_VERSION", "0.3.60")
_process: subprocess.Popen | None = None
# Serializes ensure_running() so a background auto-start and a concurrent
# dispatch-time ensure can't both spawn 9Router (double-bind on :20128). Lazily
# created so module import doesn't require a running event loop.
_start_lock: "asyncio.Lock | None" = None
# Short TTL cache for positive is_running() results. The probe is a sync
# httpx.get that blocks the event loop, and under load (9Router busy
# streaming inference) it can exceed its 2s timeout and return False even
@@ -334,6 +339,16 @@ def _report_start_failure(reason: str, *, detail: str = "", **fields: Any) -> No
async def ensure_running():
"""Start 9Router if not already running. Serialized so concurrent callers
(the background auto-start + a dispatch-time ensure) can't double-spawn."""
global _start_lock
if _start_lock is None:
_start_lock = asyncio.Lock()
async with _start_lock:
await _ensure_running_impl()
async def _ensure_running_impl():
"""Start 9Router if not already running."""
global _process
_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1"
+17 -2
View File
@@ -33,6 +33,7 @@ logger = logging.getLogger(__name__)
_pulse_task: asyncio.Task | None = None
_drain_task: asyncio.Task | None = None
_9r_start_task: asyncio.Task | None = None
_last_9r_cost: float | None = None
_last_9r_prompt_tokens: int | None = None
@@ -121,7 +122,7 @@ async def _drain_loop():
@asynccontextmanager
async def service_lifespan():
global _pulse_task, _drain_task
global _pulse_task, _drain_task, _9r_start_task
try:
from backend.apps.settings.settings import load_settings, _save_settings
@@ -193,7 +194,13 @@ async def service_lifespan():
try:
from backend.apps.nine_router import ensure_running as ensure_9router
await ensure_9router()
# Start 9Router in the BACKGROUND instead of awaiting it here. Awaiting
# it was ~7s (up to ~18s cold) of the startup critical path, blocking the
# HTTP bind and the whole UI behind it. 9Router is only needed when the
# user sends an agent message, and the dispatch path calls ensure_running()
# itself (now serialized, so no double-spawn), so the first message waits
# for readiness lazily. This is the single biggest warm-startup win.
_9r_start_task = asyncio.create_task(ensure_9router())
except Exception as e:
logger.debug(f"9Router auto-start skipped: {e}")
@@ -218,6 +225,14 @@ async def service_lifespan():
pass
_drain_task = None
if _9r_start_task and not _9r_start_task.done():
_9r_start_task.cancel()
try:
await _9r_start_task
except (asyncio.CancelledError, Exception):
pass
_9r_start_task = None
try:
from backend.apps.nine_router import stop as stop_9router
stop_9router()