fix(dashboard): keep session DB reads off event loop

This commit is contained in:
0-CYBERDYNE-SYSTEMS-0 2026-06-27 20:22:24 -07:00
parent 88b3d8638e
commit 414c12a40d
2 changed files with 63 additions and 14 deletions

View file

@ -1910,6 +1910,22 @@ async def fs_default_cwd():
return {"cwd": cwd, "branch": _fs_git_branch(cwd)}
def _count_active_sessions() -> int:
from hermes_state import SessionDB
db = SessionDB()
try:
sessions = db.list_sessions_rich(limit=50)
now = time.time()
return sum(
1 for s in sessions
if s.get("ended_at") is None
and (now - s.get("last_active", s.get("started_at", 0))) < 300
)
finally:
db.close()
@app.get("/api/status")
async def get_status(profile: Optional[str] = None):
status_scope = None
@ -2008,18 +2024,7 @@ async def get_status(profile: Optional[str] = None):
active_sessions = 0
try:
from hermes_state import SessionDB
db = SessionDB()
try:
sessions = db.list_sessions_rich(limit=50)
now = time.time()
active_sessions = sum(
1 for s in sessions
if s.get("ended_at") is None
and (now - s.get("last_active", s.get("started_at", 0))) < 300
)
finally:
db.close()
active_sessions = await asyncio.to_thread(_count_active_sessions)
except Exception:
pass
@ -3137,7 +3142,7 @@ async def get_action_status(name: str, lines: int = 200):
@app.get("/api/sessions")
async def get_sessions(
def get_sessions(
limit: int = 20,
offset: int = 0,
min_messages: int = 0,
@ -3226,7 +3231,7 @@ async def get_sessions(
@app.get("/api/profiles/sessions")
async def get_profiles_sessions(
def get_profiles_sessions(
limit: int = 20,
offset: int = 0,
min_messages: int = 0,

View file

@ -29,6 +29,7 @@ from unittest.mock import patch
import pytest
import hermes_cli.web_server as web_server_mod
import hermes_state
SLOW_SECONDS = 3 # represents the Defender worst-case (scaled down for CI speed)
@ -186,3 +187,46 @@ def test_concurrent_status_probes_all_respond():
f"{len(failed)}/{PROBES} probes failed (codes: {responses}). "
f"This would cause WinError 10054 and orphan accumulation on desktop."
)
def test_status_session_count_does_not_block_event_loop(monkeypatch):
import httpx
class SlowSessionDB:
def list_sessions_rich(self, limit: int = 50):
time.sleep(SLOW_SECONDS)
return []
def close(self):
pass
monkeypatch.setattr(hermes_state, "SessionDB", SlowSessionDB)
results: dict[str, float | int] = {}
async def _run():
transport = httpx.ASGITransport(app=web_server_mod.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
scenario_started = time.perf_counter()
async with asyncio.TaskGroup() as tg:
async def _status():
t = time.perf_counter()
r = await client.get("/api/status", timeout=SLOW_SECONDS + 5)
results["status_ms"] = (time.perf_counter() - t) * 1000
results["status_code"] = r.status_code
async def _version():
await asyncio.sleep(0.1)
t = time.perf_counter()
r = await client.get("/api/version", timeout=5)
results["version_ms"] = (time.perf_counter() - t) * 1000
results["version_elapsed_ms"] = (time.perf_counter() - scenario_started) * 1000
results["version_code"] = r.status_code
tg.create_task(_status())
tg.create_task(_version())
asyncio.run(_run())
assert results.get("version_code") in {200, 401}
assert results.get("status_code") == 200
assert results["version_elapsed_ms"] < SLOW_SECONDS * 1000