triggers and execution queue; subagents
This commit is contained in:
187
backend/src/trigger/scheduler.py
Normal file
187
backend/src/trigger/scheduler.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
APScheduler integration for cron-style triggers.
|
||||
|
||||
Provides scheduling of periodic triggers (e.g., sync exchange state hourly,
|
||||
recompute indicators every 5 minutes, daily portfolio reports).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger as APSCronTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from .queue import TriggerQueue
|
||||
from .types import Priority, Trigger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerScheduler:
|
||||
"""
|
||||
Scheduler for periodic trigger execution.
|
||||
|
||||
Wraps APScheduler to enqueue triggers at scheduled times.
|
||||
"""
|
||||
|
||||
def __init__(self, trigger_queue: TriggerQueue):
|
||||
"""
|
||||
Initialize scheduler.
|
||||
|
||||
Args:
|
||||
trigger_queue: TriggerQueue to enqueue triggers into
|
||||
"""
|
||||
self.trigger_queue = trigger_queue
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
self._job_counter = 0
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the scheduler"""
|
||||
self.scheduler.start()
|
||||
logger.info("TriggerScheduler started")
|
||||
|
||||
def shutdown(self, wait: bool = True) -> None:
|
||||
"""
|
||||
Shut down the scheduler.
|
||||
|
||||
Args:
|
||||
wait: If True, wait for running jobs to complete
|
||||
"""
|
||||
self.scheduler.shutdown(wait=wait)
|
||||
logger.info("TriggerScheduler shut down")
|
||||
|
||||
def schedule_interval(
|
||||
self,
|
||||
trigger: Trigger,
|
||||
seconds: Optional[int] = None,
|
||||
minutes: Optional[int] = None,
|
||||
hours: Optional[int] = None,
|
||||
priority: Optional[Priority] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Schedule a trigger to run at regular intervals.
|
||||
|
||||
Args:
|
||||
trigger: Trigger to execute
|
||||
seconds: Interval in seconds
|
||||
minutes: Interval in minutes
|
||||
hours: Interval in hours
|
||||
priority: Priority override for execution
|
||||
|
||||
Returns:
|
||||
Job ID (can be used to remove job later)
|
||||
|
||||
Example:
|
||||
# Run every 5 minutes
|
||||
scheduler.schedule_interval(
|
||||
IndicatorUpdateTrigger("rsi_14"),
|
||||
minutes=5
|
||||
)
|
||||
"""
|
||||
job_id = f"interval_{self._job_counter}"
|
||||
self._job_counter += 1
|
||||
|
||||
async def job_func():
|
||||
await self.trigger_queue.enqueue(trigger, priority)
|
||||
|
||||
self.scheduler.add_job(
|
||||
job_func,
|
||||
trigger=IntervalTrigger(seconds=seconds, minutes=minutes, hours=hours),
|
||||
id=job_id,
|
||||
name=f"Interval: {trigger.name}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Scheduled interval job: {job_id}, trigger={trigger.name}, "
|
||||
f"interval=(s={seconds}, m={minutes}, h={hours})"
|
||||
)
|
||||
|
||||
return job_id
|
||||
|
||||
def schedule_cron(
|
||||
self,
|
||||
trigger: Trigger,
|
||||
minute: Optional[str] = None,
|
||||
hour: Optional[str] = None,
|
||||
day: Optional[str] = None,
|
||||
month: Optional[str] = None,
|
||||
day_of_week: Optional[str] = None,
|
||||
priority: Optional[Priority] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Schedule a trigger to run on a cron schedule.
|
||||
|
||||
Args:
|
||||
trigger: Trigger to execute
|
||||
minute: Minute expression (0-59, *, */5, etc.)
|
||||
hour: Hour expression (0-23, *, etc.)
|
||||
day: Day of month expression (1-31, *, etc.)
|
||||
month: Month expression (1-12, *, etc.)
|
||||
day_of_week: Day of week expression (0-6, mon-sun, *, etc.)
|
||||
priority: Priority override for execution
|
||||
|
||||
Returns:
|
||||
Job ID (can be used to remove job later)
|
||||
|
||||
Example:
|
||||
# Run at 9:00 AM every weekday
|
||||
scheduler.schedule_cron(
|
||||
SyncExchangeStateTrigger(),
|
||||
hour="9",
|
||||
minute="0",
|
||||
day_of_week="mon-fri"
|
||||
)
|
||||
"""
|
||||
job_id = f"cron_{self._job_counter}"
|
||||
self._job_counter += 1
|
||||
|
||||
async def job_func():
|
||||
await self.trigger_queue.enqueue(trigger, priority)
|
||||
|
||||
self.scheduler.add_job(
|
||||
job_func,
|
||||
trigger=APSCronTrigger(
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day=day,
|
||||
month=month,
|
||||
day_of_week=day_of_week,
|
||||
),
|
||||
id=job_id,
|
||||
name=f"Cron: {trigger.name}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Scheduled cron job: {job_id}, trigger={trigger.name}, "
|
||||
f"schedule=(m={minute}, h={hour}, d={day}, dow={day_of_week})"
|
||||
)
|
||||
|
||||
return job_id
|
||||
|
||||
def remove_job(self, job_id: str) -> bool:
|
||||
"""
|
||||
Remove a scheduled job.
|
||||
|
||||
Args:
|
||||
job_id: Job ID returned from schedule_* methods
|
||||
|
||||
Returns:
|
||||
True if job was removed, False if not found
|
||||
"""
|
||||
try:
|
||||
self.scheduler.remove_job(job_id)
|
||||
logger.info(f"Removed scheduled job: {job_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not remove job {job_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_jobs(self) -> list:
|
||||
"""Get list of all scheduled jobs"""
|
||||
return self.scheduler.get_jobs()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
job_count = len(self.scheduler.get_jobs())
|
||||
running = self.scheduler.running
|
||||
return f"TriggerScheduler(running={running}, jobs={job_count})"
|
||||
Reference in New Issue
Block a user