From 74609f926c353129c73bfc3704f0deed152733fa Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:27:35 +0530 Subject: [PATCH] fix(dashboard): run residual cron profile I/O off the event loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two async handlers still called the cron profile-walk helpers directly on the FastAPI event loop after the 49fa04a23/346e5673d threadpool migration: - POST /api/cron/fire called _find_cron_job_profile() inline — it walks every profile and lists its jobs (file I/O per profile), stalling the loop before the 202 is returned. - POST /api/cron/blueprints/instantiate called _call_cron_for_profile() inline for create_job. Route both through the existing _run_cron_dashboard_io threadpool wrapper like every other cron dashboard endpoint. Credit: @riceharvest (#50948) originally identified the sync-I/O-in-async- handlers bug class for the desktop boot endpoints; 49fa04a23, 346e5673d, 7d0ddbb2f, d5eee133e and 24d5bda1e have since fixed most of that PR's scope via the managed threadpool + PID cache + alias-map surfaces. This covers the two cron handlers those merges missed. --- hermes_cli/web_server.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index fa270747528..2ba21a8ed95 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -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: