Compare commits
3 Commits
feat/deplo
...
fix/cron-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4373ef2698 | ||
| a547552ff7 | |||
|
|
d6bd3bc10a |
@@ -628,8 +628,47 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
|
||||
_cron_timeout = float(os.getenv("HERMES_CRON_TIMEOUT", 600))
|
||||
_cron_inactivity_limit = _cron_timeout if _cron_timeout > 0 else None
|
||||
_POLL_INTERVAL = 5.0
|
||||
_cron_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||
_cron_future = _cron_pool.submit(agent.run_conversation, prompt)
|
||||
|
||||
# Guard against interpreter shutdown: ThreadPoolExecutor.submit()
|
||||
# raises RuntimeError("cannot schedule new futures after interpreter
|
||||
# shutdown") when Python is finalizing (e.g. gateway restart races).
|
||||
# Fall back to synchronous execution so the job at least attempts.
|
||||
_cron_pool = None
|
||||
try:
|
||||
_cron_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||
_cron_future = _cron_pool.submit(agent.run_conversation, prompt)
|
||||
except RuntimeError:
|
||||
logger.warning(
|
||||
"Job '%s': ThreadPoolExecutor unavailable (interpreter shutdown?) "
|
||||
"— falling back to synchronous execution",
|
||||
job_name,
|
||||
)
|
||||
if _cron_pool is not None:
|
||||
try:
|
||||
_cron_pool.shutdown(wait=False)
|
||||
except Exception:
|
||||
pass
|
||||
_cron_pool = None
|
||||
result = agent.run_conversation(prompt)
|
||||
final_response = result.get("final_response", "") or ""
|
||||
logged_response = final_response if final_response else "(No response generated)"
|
||||
output = f"""# Cron Job: {job_name}
|
||||
|
||||
**Job ID:** {job_id}
|
||||
**Run Time:** {_hermes_now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
**Schedule:** {job.get('schedule_display', 'N/A')}
|
||||
|
||||
## Prompt
|
||||
|
||||
{prompt}
|
||||
|
||||
## Response
|
||||
|
||||
{logged_response}
|
||||
"""
|
||||
logger.info("Job '%s' completed (sync fallback)", job_name)
|
||||
return True, output, final_response, None
|
||||
|
||||
_inactivity_timeout = False
|
||||
try:
|
||||
if _cron_inactivity_limit is None:
|
||||
@@ -656,10 +695,12 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
|
||||
_inactivity_timeout = True
|
||||
break
|
||||
except Exception:
|
||||
_cron_pool.shutdown(wait=False, cancel_futures=True)
|
||||
if _cron_pool is not None:
|
||||
_cron_pool.shutdown(wait=False, cancel_futures=True)
|
||||
raise
|
||||
finally:
|
||||
_cron_pool.shutdown(wait=False)
|
||||
if _cron_pool is not None:
|
||||
_cron_pool.shutdown(wait=False)
|
||||
|
||||
if _inactivity_timeout:
|
||||
# Build diagnostic summary from the agent's activity tracker.
|
||||
@@ -798,16 +839,36 @@ def tick(verbose: bool = True, adapters=None, loop=None) -> int:
|
||||
if verbose:
|
||||
logger.info("%s - %s job(s) due", _hermes_now().strftime('%H:%M:%S'), len(due_jobs))
|
||||
|
||||
executed = 0
|
||||
for job in due_jobs:
|
||||
try:
|
||||
# For recurring jobs (cron/interval), advance next_run_at to the
|
||||
# next future occurrence BEFORE execution. This way, if the
|
||||
# process crashes mid-run, the job won't re-fire on restart.
|
||||
# One-shot jobs are left alone so they can retry on restart.
|
||||
advance_next_run(job["id"])
|
||||
# Parallel job execution with priority sorting
|
||||
PRIORITY_ORDER = {"critical": 0, "high": 1, "normal": 2, "low": 3}
|
||||
due_jobs_sorted = sorted(due_jobs, key=lambda j: PRIORITY_ORDER.get(j.get("priority", "normal"), 2))
|
||||
MAX_PARALLEL = int(os.environ.get("HERMES_CRON_MAX_PARALLEL", "10"))
|
||||
|
||||
executed = 0
|
||||
_job_results = []
|
||||
|
||||
def _process_single_job(job):
|
||||
job_name = job.get("name", job["id"])
|
||||
if sys.is_finalizing():
|
||||
return None
|
||||
try:
|
||||
advance_next_run(job["id"])
|
||||
success, output, final_response, error = run_job(job)
|
||||
return (job, success, output, final_response, error)
|
||||
except Exception as e:
|
||||
logger.error("Job '%s': parallel error: %s", job_name, e)
|
||||
return (job, False, "", None, str(e))
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_PARALLEL) as executor:
|
||||
futures = {executor.submit(_process_single_job, job): job for job in due_jobs_sorted}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
result = future.result()
|
||||
if result is None:
|
||||
continue
|
||||
_job_results.append(result)
|
||||
|
||||
for job, success, output, final_response, error in _job_results:
|
||||
executed += 1
|
||||
|
||||
output_file = save_job_output(job["id"], output)
|
||||
if verbose:
|
||||
|
||||
Reference in New Issue
Block a user