from __future__ import annotations import asyncio from datetime import datetime, timedelta, timezone from typing import Optional from app.clients.mci_server_client import MciServerClient from app.models.event import EventStatus, OutboundEvent from app.repositories.event_store import EventStore class EventPublisher: def __init__( self, store: EventStore, client: MciServerClient, max_retry: int = 5, base_delay_seconds: int = 5, interval_seconds: float = 5.0, batch_size: int = 100, ): self.store = store self.client = client self.max_retry = min(max_retry, 10) self.base_delay_seconds = base_delay_seconds self.interval_seconds = interval_seconds self.batch_size = batch_size self._task: Optional[asyncio.Task] = None self._stop_event = asyncio.Event() def start(self) -> None: self._task = asyncio.create_task(self._loop()) async def stop(self) -> None: self._stop_event.set() if self._task: try: await asyncio.wait_for(self._task, timeout=5.0) except asyncio.TimeoutError: self._task.cancel() async def _loop(self) -> None: cleanup_counter = 0 cleanup_interval = max(1, int(24 * 3600 / self.interval_seconds)) while not self._stop_event.is_set(): try: await self._tick() except Exception: pass cleanup_counter += 1 if cleanup_counter >= cleanup_interval: try: await self.cleanup_old_events(10) except Exception: pass cleanup_counter = 0 try: await asyncio.wait_for(self._stop_event.wait(), timeout=self.interval_seconds) except asyncio.TimeoutError: pass async def _tick(self) -> None: events = self.store.fetch_pending(limit=self.batch_size) for event in events: try: await self.client.post_event(event) self.store.mark_delivered(event.event_id) except Exception as e: next_attempt = event.attempt_count + 1 delay = self.base_delay_seconds * (2 ** (next_attempt - 1)) next_retry_at = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(seconds=delay) status = EventStatus.DEAD_LETTER if next_attempt >= self.max_retry else EventStatus.RETRYING self.store.mark_retry(event.event_id, status, next_attempt, next_retry_at, str(e)[:500]) async def cleanup_old_events(self, retention_days: int) -> int: return self.store.cleanup(retention_days)