Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
44 changes: 44 additions & 0 deletions tests/hermes_cli/test_web_server_boot_handshake.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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)

Expand Down Expand Up @@ -186,3 +187,46 @@ async def _run():
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