You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

101 lines
3.3 KiB
Python

"""
APScheduler-based batch scheduler for the Overlay pipeline.
Integrated into FastAPI's lifespan via start_scheduler() / stop_scheduler().
"""
import logging
from datetime import datetime, timezone
logger = logging.getLogger(__name__)
_scheduler = None
def _get_scheduler():
"""Lazily create the APScheduler instance."""
global _scheduler
if _scheduler is None:
try:
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
_scheduler = AsyncIOScheduler(timezone="UTC")
except ImportError:
logger.warning("apscheduler not installed; overlay batch scheduling disabled")
return None
return _scheduler
async def _run_collect_job() -> None:
"""Scheduled job: collect RSS + wiki + youtube + trends."""
from app.core.database import AsyncSessionLocal
from app.services.overlay.overlay_pipeline import OverlayPipeline
pipeline = OverlayPipeline()
async with AsyncSessionLocal() as db:
try:
counts = await pipeline.collect_all(db)
logger.info(f"[Scheduler] collect_all completed: {counts}")
except Exception as e:
logger.error(f"[Scheduler] collect_all failed: {e}")
async def _run_build_job() -> None:
"""Scheduled job: build features for all TOP_50 symbols."""
from app.core.database import AsyncSessionLocal
from app.services.overlay.overlay_pipeline import OverlayPipeline
pipeline = OverlayPipeline()
async with AsyncSessionLocal() as db:
try:
built = await pipeline.build_features_batch(db)
logger.info(f"[Scheduler] feature_build completed: {built} symbols")
except Exception as e:
logger.error(f"[Scheduler] feature_build failed: {e}")
def start_scheduler() -> None:
"""Start the APScheduler with overlay jobs. Call from FastAPI lifespan startup."""
from app.core.config import settings
if not getattr(settings, "OVERLAY_ENABLED", True):
logger.info("Overlay disabled; scheduler not started")
return
sched = _get_scheduler()
if sched is None:
return
try:
from apscheduler.triggers.cron import CronTrigger
# Collect: 23:30 UTC daily on weekdays (≈ 18:30 ET)
sched.add_job(
_run_collect_job,
trigger=CronTrigger(day_of_week="mon-fri", hour=23, minute=30, timezone="UTC"),
id="overlay_collect",
replace_existing=True,
misfire_grace_time=600,
)
# Feature build: 01:30 UTC (≈ 20:30 ET)
sched.add_job(
_run_build_job,
trigger=CronTrigger(day_of_week="mon-fri", hour=1, minute=30, timezone="UTC"),
id="overlay_feature_build",
replace_existing=True,
misfire_grace_time=600,
)
sched.start()
logger.info("Overlay scheduler started (collect @ 23:30 UTC, build @ 01:30 UTC, weekdays)")
except Exception as e:
logger.error(f"Overlay scheduler start failed: {e}")
def stop_scheduler() -> None:
"""Stop the scheduler. Call from FastAPI lifespan shutdown."""
sched = _get_scheduler()
if sched and sched.running:
sched.shutdown(wait=False)
logger.info("Overlay scheduler stopped")