"""Polling-based message monitoring loop for Synology Chat. Continuously polls the DSM Chat API for new messages while the channel is connected, with circuit breaker integration for fault isolation, optional security filtering, and per-account inflight limiting to prevent resource exhaustion across accounts. """ from __future__ import annotations import asyncio from collections import defaultdict from collections.abc import Awaitable, Callable from typing import Any from yuxi.channels.adapters.synologychat.client import DSMClient from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError from yuxi.channels.models import ChannelMessage, ChannelStatus from yuxi.utils.logging_config import logger async def polling_loop( client: DSMClient, config: dict[str, Any], normalize_fn: Callable[[dict], ChannelMessage], handle_message_fn: Callable[[ChannelMessage], Awaitable[None]], get_status_fn: Callable[[], ChannelStatus], circuit_breaker: CircuitBreaker, security_filter: Callable[[ChannelMessage], bool] | None = None, max_inflight: int = 10, account_id: str = "", ) -> None: poll_interval = config.get("polling_interval_seconds", 3) max_backoff = config.get("polling_max_backoff_seconds", 60) last_cursor: str = "" consecutive_failures = 0 inflight_count: dict[str, int] = defaultdict(int) inflight_lock = asyncio.Lock() async def _acquire_inflight() -> None: while True: async with inflight_lock: if inflight_count[account_id] < max_inflight: inflight_count[account_id] += 1 return await asyncio.sleep(0.1) async def _release_inflight() -> None: async with inflight_lock: inflight_count[account_id] = max(0, inflight_count[account_id] - 1) async def _handle_with_limit(msg: ChannelMessage) -> None: await _acquire_inflight() try: await handle_message_fn(msg) finally: await _release_inflight() while get_status_fn() == ChannelStatus.CONNECTED: try: async def _poll(): return await client.poll_messages(last_cursor) result = await circuit_breaker.call(_poll) if result.get("success") and result.get("data"): consecutive_failures = 0 data = result["data"] events = data.get("events", []) for event in events: try: message = normalize_fn(event) if security_filter and not security_filter(message): continue await _handle_with_limit(message) except Exception as e: logger.error(f"Error handling poll event: {e}") last_cursor = data.get("next_cursor", last_cursor) except CircuitBreakerOpenError: backoff = min(poll_interval * (2**consecutive_failures), max_backoff) consecutive_failures += 1 logger.warning(f"[SynologyChat] Circuit breaker open, backing off {backoff}s") await asyncio.sleep(backoff) continue except asyncio.CancelledError: break except Exception as e: logger.error(f"Polling error: {e}") await circuit_breaker.record_failure() consecutive_failures += 1 backoff = min(poll_interval * (2**consecutive_failures), max_backoff) await asyncio.sleep(backoff) continue await asyncio.sleep(poll_interval)