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:
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user