mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
🐛 fix(dashboard): use managed cron threadpool
(cherry picked from commit 97fabc289c)
This commit is contained in:
parent
346e5673de
commit
49fa04a235
2 changed files with 40 additions and 9 deletions
|
|
@ -21,6 +21,7 @@ from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import inspect
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -93,6 +94,7 @@ try:
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# First try lazy-installing the dashboard extras. Only the user actually
|
# First try lazy-installing the dashboard extras. Only the user actually
|
||||||
# running `hermes dashboard` needs fastapi+uvicorn; lazy install keeps
|
# running `hermes dashboard` needs fastapi+uvicorn; lazy install keeps
|
||||||
|
|
@ -108,6 +110,7 @@ except ImportError:
|
||||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from starlette.concurrency import run_in_threadpool
|
||||||
except Exception:
|
except Exception:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
"Web UI requires fastapi and uvicorn.\n"
|
"Web UI requires fastapi and uvicorn.\n"
|
||||||
|
|
@ -10004,7 +10007,12 @@ def _list_cron_jobs_sync(profile: str = "all"):
|
||||||
|
|
||||||
async def _run_cron_dashboard_io(func, *args, **kwargs):
|
async def _run_cron_dashboard_io(func, *args, **kwargs):
|
||||||
"""Run cron dashboard profile/job I/O outside the FastAPI event loop."""
|
"""Run cron dashboard profile/job I/O outside the FastAPI event loop."""
|
||||||
return await asyncio.to_thread(func, *args, **kwargs)
|
if inspect.iscoroutinefunction(func):
|
||||||
|
raise TypeError("_run_cron_dashboard_io only accepts sync callables")
|
||||||
|
result = await run_in_threadpool(func, *args, **kwargs)
|
||||||
|
if inspect.isawaitable(result):
|
||||||
|
raise TypeError("_run_cron_dashboard_io sync callable returned an awaitable")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/cron/jobs")
|
@app.get("/api/cron/jobs")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""Regression tests for dashboard cron job profile routing."""
|
"""Regression tests for dashboard cron job profile routing."""
|
||||||
|
|
||||||
|
from queue import Empty, SimpleQueue
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -24,6 +25,15 @@ def isolated_profiles(tmp_path, monkeypatch):
|
||||||
return {"default": default_home, "worker_alpha": worker_home}
|
return {"default": default_home, "worker_alpha": worker_home}
|
||||||
|
|
||||||
|
|
||||||
|
def _drain_queue(q):
|
||||||
|
values = []
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
values.append(q.get_nowait())
|
||||||
|
except Empty:
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
def test_call_cron_for_profile_routes_storage_and_restores_globals(isolated_profiles):
|
def test_call_cron_for_profile_routes_storage_and_restores_globals(isolated_profiles):
|
||||||
from cron import jobs as cron_jobs
|
from cron import jobs as cron_jobs
|
||||||
from hermes_cli import web_server
|
from hermes_cli import web_server
|
||||||
|
|
@ -174,17 +184,17 @@ async def test_cron_profile_scan_runs_off_event_loop(isolated_profiles, monkeypa
|
||||||
)
|
)
|
||||||
|
|
||||||
event_loop_thread = threading.get_ident()
|
event_loop_thread = threading.get_ident()
|
||||||
profile_scan_threads = []
|
profile_scan_threads = SimpleQueue()
|
||||||
worker_threads = []
|
worker_threads = SimpleQueue()
|
||||||
original_profile_dicts = web_server._cron_profile_dicts
|
original_profile_dicts = web_server._cron_profile_dicts
|
||||||
original_find = web_server._find_cron_job_profile
|
original_find = web_server._find_cron_job_profile
|
||||||
|
|
||||||
def tracking_profile_dicts():
|
def tracking_profile_dicts():
|
||||||
profile_scan_threads.append(threading.get_ident())
|
profile_scan_threads.put(threading.get_ident())
|
||||||
return original_profile_dicts()
|
return original_profile_dicts()
|
||||||
|
|
||||||
def tracking_find(job_id):
|
def tracking_find(job_id):
|
||||||
worker_threads.append(threading.get_ident())
|
worker_threads.put(threading.get_ident())
|
||||||
return original_find(job_id)
|
return original_find(job_id)
|
||||||
|
|
||||||
monkeypatch.setattr(web_server, "_cron_profile_dicts", tracking_profile_dicts)
|
monkeypatch.setattr(web_server, "_cron_profile_dicts", tracking_profile_dicts)
|
||||||
|
|
@ -195,10 +205,23 @@ async def test_cron_profile_scan_runs_off_event_loop(isolated_profiles, monkeypa
|
||||||
|
|
||||||
assert any(job["id"] == worker_job["id"] for job in jobs)
|
assert any(job["id"] == worker_job["id"] for job in jobs)
|
||||||
assert paused["profile"] == "worker_alpha"
|
assert paused["profile"] == "worker_alpha"
|
||||||
assert profile_scan_threads
|
profile_scan_thread_ids = _drain_queue(profile_scan_threads)
|
||||||
assert worker_threads
|
worker_thread_ids = _drain_queue(worker_threads)
|
||||||
assert all(thread_id != event_loop_thread for thread_id in profile_scan_threads)
|
assert profile_scan_thread_ids
|
||||||
assert all(thread_id != event_loop_thread for thread_id in worker_threads)
|
assert worker_thread_ids
|
||||||
|
assert all(thread_id != event_loop_thread for thread_id in profile_scan_thread_ids)
|
||||||
|
assert all(thread_id != event_loop_thread for thread_id in worker_thread_ids)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cron_dashboard_io_rejects_async_callables():
|
||||||
|
from hermes_cli import web_server
|
||||||
|
|
||||||
|
async def async_callable():
|
||||||
|
return "nope"
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="only accepts sync callables"):
|
||||||
|
await web_server._run_cron_dashboard_io(async_callable)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue