from __future__ import annotations import asyncio import logging import os from typing import Callable, Awaitable, List, Optional from app.core.crypto import get_ed25519_manager from app.core.models.stats.metrics_models import NodeStats from app.core.stats.metrics_collector import MetricsCollector from app.core.stats.stats_aggregator import StatsAggregator from app.core.stats.gossip_manager import GossipManager logger = logging.getLogger(__name__) class StatsDaemon: """ Фоновый сервис статистики: - периодически собирает локальные метрики - сохраняет в агрегатор - периодически рассылает gossip статистику пирам """ def __init__( self, collector: Optional[MetricsCollector] = None, aggregator: Optional[StatsAggregator] = None, gossip: Optional[GossipManager] = None, collect_interval_sec: int = 10, gossip_interval_sec: int = 30, peers_provider: Optional[Callable[[], Awaitable[List[str]]]] = None, ) -> None: self.collector = collector or MetricsCollector() self.aggregator = aggregator or StatsAggregator() self.gossip = gossip or GossipManager() self.collect_interval_sec = max(1, collect_interval_sec) self.gossip_interval_sec = max(5, gossip_interval_sec) self.peers_provider = peers_provider self._collect_task: Optional[asyncio.Task] = None self._gossip_task: Optional[asyncio.Task] = None self._stopping = asyncio.Event() async def start(self) -> None: logger.info("StatsDaemon starting") self._stopping.clear() self._collect_task = asyncio.create_task(self.periodic_collection(), name="stats_collect_loop") self._gossip_task = asyncio.create_task(self.periodic_gossip(), name="stats_gossip_loop") logger.info("StatsDaemon started") async def stop(self) -> None: logger.info("StatsDaemon stopping") self._stopping.set() tasks = [t for t in [self._collect_task, self._gossip_task] if t] for t in tasks: t.cancel() for t in tasks: try: await t except asyncio.CancelledError: pass except Exception as e: logger.warning("StatsDaemon task stop error: %s", e) logger.info("StatsDaemon stopped") async def periodic_collection(self) -> None: """ Периодический сбор локальных метрик и сохранение в агрегатор. """ crypto = get_ed25519_manager() node_id = crypto.node_id public_key = crypto.public_key_hex while not self._stopping.is_set(): try: system, app = await self.collector.get_current_stats() # можно дополнить доступным контентом из локального индекса, пока None node_stats = NodeStats( node_id=node_id, public_key=public_key, system=system, app=app, known_content_items=None, available_content_items=None, ) await self.aggregator.add_local_snapshot(node_stats) except Exception as e: logger.exception("periodic_collection error: %s", e) try: await asyncio.wait_for(self._stopping.wait(), timeout=self.collect_interval_sec) except asyncio.TimeoutError: continue async def periodic_gossip(self) -> None: """ Периодическая рассылка статистики пирам. """ while not self._stopping.is_set(): try: # peers peers: List[str] = [] if self.peers_provider: try: peers = await self.peers_provider() await self.aggregator.set_known_peers(peers) except Exception as e: logger.warning("peers_provider error: %s", e) latest = await self.aggregator.get_latest_local() if latest and peers: # подписать актуальный слепок signed_stats = await self.aggregator.build_local_signed_stats() await self.gossip.broadcast_stats(peers, signed_stats) except Exception as e: logger.exception("periodic_gossip error: %s", e) try: await asyncio.wait_for(self._stopping.wait(), timeout=self.gossip_interval_sec) except asyncio.TimeoutError: continue