""" Event Scheduler for automatic event launching in marathons. """ import asyncio import random from datetime import datetime, timedelta from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models import Marathon, MarathonStatus, Event, EventType from app.services.events import EventService # Configuration CHECK_INTERVAL_SECONDS = 300 # Check every 5 minutes EVENT_PROBABILITY = 0.1 # 10% chance per check to start an event MIN_EVENT_GAP_HOURS = 4 # Minimum hours between events # Events that can be auto-triggered (excluding common_enemy which needs a challenge_id) AUTO_EVENT_TYPES = [ EventType.GOLDEN_HOUR, EventType.DOUBLE_RISK, EventType.JACKPOT, EventType.REMATCH, ] class EventScheduler: """Background scheduler for automatic event management.""" def __init__(self): self._running = False self._task: asyncio.Task | None = None async def start(self, session_factory) -> None: """Start the scheduler background task.""" if self._running: return self._running = True self._task = asyncio.create_task(self._run_loop(session_factory)) print("[EventScheduler] Started") async def stop(self) -> None: """Stop the scheduler.""" self._running = False if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass print("[EventScheduler] Stopped") async def _run_loop(self, session_factory) -> None: """Main scheduler loop.""" while self._running: try: async with session_factory() as db: await self._process_events(db) except Exception as e: print(f"[EventScheduler] Error in loop: {e}") await asyncio.sleep(CHECK_INTERVAL_SECONDS) async def _process_events(self, db: AsyncSession) -> None: """Process events - cleanup expired and potentially start new ones.""" # 1. Cleanup expired events await self._cleanup_expired_events(db) # 2. Maybe start new events for eligible marathons await self._maybe_start_events(db) async def _cleanup_expired_events(self, db: AsyncSession) -> None: """End any events that have expired.""" now = datetime.utcnow() result = await db.execute( select(Event).where( Event.is_active == True, Event.end_time < now, ) ) expired_events = result.scalars().all() for event in expired_events: event.is_active = False print(f"[EventScheduler] Ended expired event {event.id} ({event.type})") if expired_events: await db.commit() async def _maybe_start_events(self, db: AsyncSession) -> None: """Potentially start new events for eligible marathons.""" # Get active marathons with auto_events enabled result = await db.execute( select(Marathon).where( Marathon.status == MarathonStatus.ACTIVE.value, Marathon.auto_events_enabled == True, ) ) marathons = result.scalars().all() event_service = EventService() for marathon in marathons: # Skip if random chance doesn't hit if random.random() > EVENT_PROBABILITY: continue # Check if there's already an active event active_event = await event_service.get_active_event(db, marathon.id) if active_event: continue # Check if enough time has passed since last event result = await db.execute( select(Event) .where(Event.marathon_id == marathon.id) .order_by(Event.end_time.desc()) .limit(1) ) last_event = result.scalar_one_or_none() if last_event: time_since_last = datetime.utcnow() - last_event.end_time if time_since_last < timedelta(hours=MIN_EVENT_GAP_HOURS): continue # Start a random event event_type = random.choice(AUTO_EVENT_TYPES) try: event = await event_service.start_event( db=db, marathon_id=marathon.id, event_type=event_type.value, created_by_id=None, # null = auto-started ) print( f"[EventScheduler] Auto-started {event_type.value} for marathon {marathon.id}" ) except Exception as e: print( f"[EventScheduler] Failed to start event for marathon {marathon.id}: {e}" ) # Global scheduler instance event_scheduler = EventScheduler()