feat(MAT-174): Fast pending check loop — Run Now triggers within 15s

Separate 15s poll for manual triggers (lastStatus=pending) from the
5-minute full job sync. Run Now button now fires within seconds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Gick
2026-03-16 14:36:38 +02:00
parent 3928e85279
commit c9b88a155b

View File

@@ -10,7 +10,8 @@ from .executor import execute_job
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
SYNC_INTERVAL = 300 # 5 minutes SYNC_INTERVAL = 300 # 5 minutes — full job reconciliation
PENDING_CHECK_INTERVAL = 15 # 15 seconds — fast check for manual triggers
class CronScheduler: class CronScheduler:
@@ -26,10 +27,18 @@ class CronScheduler:
self._running = False self._running = False
async def start(self): async def start(self):
"""Start the scheduler background loop.""" """Start the scheduler background loops."""
self._running = True self._running = True
logger.info("Cron scheduler starting") logger.info("Cron scheduler starting")
await asyncio.sleep(15) # wait for bot to stabilize await asyncio.sleep(15) # wait for bot to stabilize
# Run full sync + fast pending check in parallel
await asyncio.gather(
self._full_sync_loop(),
self._pending_check_loop(),
)
async def _full_sync_loop(self):
"""Full job reconciliation every 5 minutes."""
while self._running: while self._running:
try: try:
await self._sync_jobs() await self._sync_jobs()
@@ -37,6 +46,31 @@ class CronScheduler:
logger.warning("Cron job sync failed", exc_info=True) logger.warning("Cron job sync failed", exc_info=True)
await asyncio.sleep(SYNC_INTERVAL) await asyncio.sleep(SYNC_INTERVAL)
async def _pending_check_loop(self):
"""Fast poll for manual triggers every 15 seconds."""
while self._running:
try:
await self._check_pending()
except Exception:
logger.debug("Pending check failed", exc_info=True)
await asyncio.sleep(PENDING_CHECK_INTERVAL)
async def _check_pending(self):
"""Quick check for jobs with lastStatus='pending' and run them."""
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(
f"{self.portal_url}/api/cron/jobs/active",
headers={"x-api-key": self.api_key},
)
if resp.status_code != 200:
return
data = resp.json()
for job in data.get("jobs", []):
if job.get("lastStatus") == "pending":
logger.info("Pending trigger: %s", job["name"])
asyncio.create_task(self._run_once(job))
async def stop(self): async def stop(self):
self._running = False self._running = False
for task in self._tasks.values(): for task in self._tasks.values():
@@ -80,12 +114,6 @@ class CronScheduler:
logger.info("Scheduled cron job: %s (%s @ %s %s)", logger.info("Scheduled cron job: %s (%s @ %s %s)",
job["name"], job["schedule"], job.get("scheduleAt", ""), job["timezone"]) job["name"], job["schedule"], job.get("scheduleAt", ""), job["timezone"])
# Check for manual triggers (lastStatus == "pending")
for job_id, job in remote_jobs.items():
if job.get("lastStatus") == "pending":
logger.info("Manual trigger detected for %s", job["name"])
asyncio.create_task(self._run_once(job))
async def _job_loop(self, job: dict): async def _job_loop(self, job: dict):
"""Run a job on its schedule forever.""" """Run a job on its schedule forever."""
try: try: