mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
Merge pull request #61385 from kshitijk4poor/fix/dashboard-residual-cron-event-loop-io
fix(dashboard): run residual cron profile I/O off the event loop (#50948 follow-through)
This commit is contained in:
commit
cb79518d4f
2 changed files with 96 additions and 2 deletions
|
|
@ -10481,7 +10481,10 @@ async def cron_fire_webhook(request: Request):
|
|||
if not job_id:
|
||||
return JSONResponse({"error": "missing job_id"}, status_code=400)
|
||||
|
||||
profile = _find_cron_job_profile(job_id)
|
||||
# _find_cron_job_profile walks every profile and lists its jobs (file
|
||||
# I/O per profile) — run it off the event loop like the other cron
|
||||
# dashboard endpoints.
|
||||
profile = await _run_cron_dashboard_io(_find_cron_job_profile, job_id)
|
||||
if not profile:
|
||||
# Job is gone (cancelled / completed) — nothing to fire. 200 so NAS
|
||||
# does not retry a fire that is intentionally absent.
|
||||
|
|
@ -10556,7 +10559,11 @@ async def instantiate_blueprint(body: AutomationBlueprintInstantiate, profile: s
|
|||
# Blueprint-created jobs deliver to the dashboard's configured target by
|
||||
# default; the form's deliver slot overrides via spec["deliver"].
|
||||
spec.pop("origin", None)
|
||||
return _call_cron_for_profile(profile, "create_job", **spec)
|
||||
# create_job does per-profile file I/O — keep it off the event loop
|
||||
# like the sibling cron endpoints (partial avoids **spec keys ever
|
||||
# colliding with the wrapper's own parameters).
|
||||
_create = functools.partial(_call_cron_for_profile, profile, "create_job", **spec)
|
||||
return await _run_cron_dashboard_io(_create)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
|
|||
87
tests/hermes_cli/test_cron_dashboard_off_loop.py
Normal file
87
tests/hermes_cli/test_cron_dashboard_off_loop.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Regression tests: cron dashboard handlers must not run profile I/O on the event loop.
|
||||
|
||||
Guards the residual sites missed by the 49fa04a23/346e5673d threadpool
|
||||
migration: POST /api/cron/fire (_find_cron_job_profile) and
|
||||
POST /api/cron/blueprints/instantiate (_call_cron_for_profile create_job).
|
||||
Each stub asserts it is running OFF the event loop thread by checking that
|
||||
no running asyncio loop is present in its thread.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from hermes_cli import web_server
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def loop_probe():
|
||||
"""Collect (tag, on_loop) proof from stubbed profile-I/O helpers."""
|
||||
seen = []
|
||||
|
||||
def probe(tag):
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
seen.append((tag, True))
|
||||
except RuntimeError:
|
||||
seen.append((tag, False))
|
||||
|
||||
return seen, probe
|
||||
|
||||
|
||||
def test_cron_fire_profile_lookup_off_loop(monkeypatch, loop_probe):
|
||||
seen, probe = loop_probe
|
||||
|
||||
def fake_find(job_id):
|
||||
probe("find")
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(web_server, "_find_cron_job_profile", fake_find)
|
||||
|
||||
import plugins.cron_providers.chronos.verify as chv
|
||||
monkeypatch.setattr(chv, "get_fire_verifier", lambda: (lambda **kw: {"sub": "t"}))
|
||||
|
||||
client = TestClient(web_server.app)
|
||||
resp = client.post(
|
||||
"/api/cron/fire",
|
||||
json={"job_id": "missing-job"},
|
||||
headers={"Authorization": "Bearer x"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "gone"
|
||||
assert ("find", False) in seen, (
|
||||
f"_find_cron_job_profile must run off the event loop; proof: {seen}"
|
||||
)
|
||||
|
||||
|
||||
def test_blueprint_instantiate_create_job_off_loop(monkeypatch, loop_probe):
|
||||
seen, probe = loop_probe
|
||||
|
||||
def fake_call(profile, fn, *args, **kwargs):
|
||||
probe("call")
|
||||
return {"id": "bp-job-1", "kwargs_seen": sorted(kwargs.keys())}
|
||||
|
||||
monkeypatch.setattr(web_server, "_call_cron_for_profile", fake_call)
|
||||
monkeypatch.setattr(web_server, "_has_valid_session_token", lambda req: True)
|
||||
|
||||
import cron.blueprint_catalog as bc
|
||||
monkeypatch.setattr(bc, "get_blueprint", lambda key: object())
|
||||
monkeypatch.setattr(
|
||||
bc,
|
||||
"fill_blueprint",
|
||||
lambda bp, vals: {"name": "t", "schedule": "0 9 * * *", "prompt": "hi"},
|
||||
)
|
||||
|
||||
client = TestClient(web_server.app)
|
||||
resp = client.post(
|
||||
"/api/cron/blueprints/instantiate",
|
||||
json={"blueprint": "morning-brief", "values": {}},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
# **spec kwargs must arrive at create_job intact through the partial.
|
||||
assert body["kwargs_seen"] == ["name", "prompt", "schedule"]
|
||||
assert ("call", False) in seen, (
|
||||
f"_call_cron_for_profile must run off the event loop; proof: {seen}"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue