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:
kshitij 2026-07-09 14:49:12 +05:30 committed by GitHub
commit cb79518d4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 2 deletions

View file

@ -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:

View 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}"
)