151 lines
4.8 KiB
Python
151 lines
4.8 KiB
Python
|
|
"""
|
||
|
|
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()
|