122 lines
4.8 KiB
Python
122 lines
4.8 KiB
Python
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 |