mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-28 10:49:46 +02:00
[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:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user