This commit is contained in:
user committed 2025-07-02 19:25:20 +03:00
1 parent 21964fa986
commit 797f379648
68 files changed
+23871 -1271

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
"""MY Network - Distributed Content Replication System."""
from .node_service import MyNetworkNodeService
from .sync_manager import ContentSyncManager
from .peer_manager import PeerManager
from .bootstrap_manager import BootstrapManager
__all__ = [
'MyNetworkNodeService',
'ContentSyncManager',
'PeerManager',
'BootstrapManager'
]
+312
View File
@@ -0,0 +1,312 @@
"""Bootstrap Manager - управление bootstrap нодами и начальной конфигурацией."""
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
from datetime import datetime
logger = logging.getLogger(__name__)
class BootstrapManager:
"""Менеджер для работы с bootstrap конфигурацией."""
def __init__(self, bootstrap_path: str = "bootstrap.json"):
self.bootstrap_path = Path(bootstrap_path)
self.config = {}
self.nodes_history_path = Path("nodes_history.json")
self.nodes_history = {"successful_connections": [], "last_updated": None}
logger.info(f"Bootstrap Manager initialized with path: {self.bootstrap_path}")
async def load_bootstrap_config(self) -> Dict[str, Any]:
"""Загрузка bootstrap конфигурации."""
try:
if not self.bootstrap_path.exists():
logger.error(f"Bootstrap config not found: {self.bootstrap_path}")
raise FileNotFoundError(f"Bootstrap config not found: {self.bootstrap_path}")
with open(self.bootstrap_path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
logger.info(f"Bootstrap config loaded: {len(self.config.get('bootstrap_nodes', []))} nodes")
# Загрузить историю нод
await self._load_nodes_history()
return self.config
except Exception as e:
logger.error(f"Error loading bootstrap config: {e}")
raise
async def _load_nodes_history(self) -> None:
"""Загрузка истории подключенных нод."""
try:
if self.nodes_history_path.exists():
with open(self.nodes_history_path, 'r', encoding='utf-8') as f:
self.nodes_history = json.load(f)
logger.info(f"Loaded nodes history: {len(self.nodes_history.get('successful_connections', []))} nodes")
else:
logger.info("No nodes history found, starting fresh")
except Exception as e:
logger.error(f"Error loading nodes history: {e}")
self.nodes_history = {"successful_connections": [], "last_updated": None}
async def save_nodes_history(self) -> None:
"""Сохранение истории нод."""
try:
self.nodes_history["last_updated"] = datetime.utcnow().isoformat()
with open(self.nodes_history_path, 'w', encoding='utf-8') as f:
json.dump(self.nodes_history, f, indent=2, ensure_ascii=False)
logger.debug("Nodes history saved")
except Exception as e:
logger.error(f"Error saving nodes history: {e}")
def get_bootstrap_nodes(self) -> List[Dict[str, Any]]:
"""Получить список bootstrap нод."""
return self.config.get('bootstrap_nodes', [])
def get_network_settings(self) -> Dict[str, Any]:
"""Получить настройки сети."""
return self.config.get('network_settings', {})
def get_sync_settings(self) -> Dict[str, Any]:
"""Получить настройки синхронизации."""
return self.config.get('sync_settings', {})
def get_content_settings(self) -> Dict[str, Any]:
"""Получить настройки контента."""
return self.config.get('content_settings', {})
def get_security_settings(self) -> Dict[str, Any]:
"""Получить настройки безопасности."""
return self.config.get('security_settings', {})
def get_api_settings(self) -> Dict[str, Any]:
"""Получить настройки API."""
return self.config.get('api_settings', {})
def get_monitoring_settings(self) -> Dict[str, Any]:
"""Получить настройки мониторинга."""
return self.config.get('monitoring_settings', {})
def get_storage_settings(self) -> Dict[str, Any]:
"""Получить настройки хранилища."""
return self.config.get('storage_settings', {})
def get_consensus_settings(self) -> Dict[str, Any]:
"""Получить настройки консенсуса."""
return self.config.get('consensus', {})
def get_feature_flags(self) -> Dict[str, Any]:
"""Получить флаги функций."""
return self.config.get('feature_flags', {})
def is_feature_enabled(self, feature_name: str) -> bool:
"""Проверить, включена ли функция."""
return self.get_feature_flags().get(feature_name, False)
def get_regional_settings(self, region: str = None) -> Dict[str, Any]:
"""Получить региональные настройки."""
regional_settings = self.config.get('regional_settings', {})
if region and region in regional_settings:
return regional_settings[region]
return regional_settings
def get_emergency_settings(self) -> Dict[str, Any]:
"""Получить настройки экстренных ситуаций."""
return self.config.get('emergency_settings', {})
def is_emergency_mode(self) -> bool:
"""Проверить, включен ли режим экстренной ситуации."""
return self.get_emergency_settings().get('emergency_mode', False)
def get_nodes_from_history(self) -> List[Dict[str, Any]]:
"""Получить ноды из истории успешных подключений."""
return self.nodes_history.get('successful_connections', [])
def add_successful_connection(self, node_info: Dict[str, Any]) -> None:
"""Добавить информацию об успешном подключении."""
try:
# Обновить существующую запись или добавить новую
existing_node = None
for i, node in enumerate(self.nodes_history['successful_connections']):
if node['node_id'] == node_info['node_id']:
existing_node = i
break
connection_info = {
"node_id": node_info['node_id'],
"address": node_info['address'],
"last_seen": datetime.utcnow().isoformat(),
"connection_count": node_info.get('connection_count', 1),
"performance_score": node_info.get('performance_score', 1.0),
"features": node_info.get('features', []),
"region": node_info.get('region', 'unknown'),
"metadata": node_info.get('metadata', {})
}
if existing_node is not None:
# Обновить существующую запись
old_info = self.nodes_history['successful_connections'][existing_node]
connection_info['connection_count'] = old_info.get('connection_count', 0) + 1
connection_info['first_seen'] = old_info.get('first_seen', connection_info['last_seen'])
self.nodes_history['successful_connections'][existing_node] = connection_info
else:
# Добавить новую запись
connection_info['first_seen'] = connection_info['last_seen']
self.nodes_history['successful_connections'].append(connection_info)
# Ограничить историю (максимум 100 нод)
if len(self.nodes_history['successful_connections']) > 100:
# Сортировать по последнему подключению и оставить 100 самых свежих
self.nodes_history['successful_connections'].sort(
key=lambda x: x['last_seen'],
reverse=True
)
self.nodes_history['successful_connections'] = \
self.nodes_history['successful_connections'][:100]
logger.debug(f"Added successful connection to history: {node_info['node_id']}")
except Exception as e:
logger.error(f"Error adding successful connection: {e}")
def remove_failed_connection(self, node_id: str) -> None:
"""Удалить ноду из истории при неудачном подключении."""
try:
self.nodes_history['successful_connections'] = [
node for node in self.nodes_history['successful_connections']
if node['node_id'] != node_id
]
logger.debug(f"Removed failed connection from history: {node_id}")
except Exception as e:
logger.error(f"Error removing failed connection: {e}")
def get_preferred_nodes(self, max_nodes: int = 10) -> List[Dict[str, Any]]:
"""Получить предпочтительные ноды для подключения."""
try:
# Комбинировать bootstrap ноды и ноды из истории
all_nodes = []
# Добавить bootstrap ноды (высокий приоритет)
for node in self.get_bootstrap_nodes():
all_nodes.append({
"node_id": node['id'],
"address": node['address'],
"priority": 100, # Высокий приоритет для bootstrap
"features": node.get('features', []),
"region": node.get('region', 'unknown'),
"source": "bootstrap"
})
# Добавить ноды из истории
for node in self.get_nodes_from_history():
# Пропустить, если уже есть в bootstrap
if any(n['node_id'] == node['node_id'] for n in all_nodes):
continue
# Рассчитать приоритет на основе performance_score и connection_count
priority = min(90, node.get('performance_score', 0.5) * 50 +
min(40, node.get('connection_count', 1) * 2))
all_nodes.append({
"node_id": node['node_id'],
"address": node['address'],
"priority": priority,
"features": node.get('features', []),
"region": node.get('region', 'unknown'),
"source": "history"
})
# Сортировать по приоритету и взять топ
all_nodes.sort(key=lambda x: x['priority'], reverse=True)
return all_nodes[:max_nodes]
except Exception as e:
logger.error(f"Error getting preferred nodes: {e}")
return []
def validate_config(self) -> bool:
"""Валидация конфигурации bootstrap."""
try:
required_fields = ['version', 'network_id', 'bootstrap_nodes']
for field in required_fields:
if field not in self.config:
logger.error(f"Missing required field: {field}")
return False
# Проверить bootstrap ноды
bootstrap_nodes = self.config.get('bootstrap_nodes', [])
if not bootstrap_nodes:
logger.error("No bootstrap nodes configured")
return False
for node in bootstrap_nodes:
required_node_fields = ['id', 'address']
for field in required_node_fields:
if field not in node:
logger.error(f"Bootstrap node missing field: {field}")
return False
logger.info("Bootstrap configuration validated successfully")
return True
except Exception as e:
logger.error(f"Error validating config: {e}")
return False
def get_config_checksum(self) -> str:
"""Получить чек-сумму конфигурации."""
return self.config.get('checksum', '')
def verify_config_signature(self) -> bool:
"""Проверить подпись конфигурации."""
# Заглушка для проверки подписи
# В реальной реализации здесь была бы криптографическая проверка
signature = self.config.get('signature', '')
return bool(signature)
async def update_bootstrap_config(self, new_config: Dict[str, Any]) -> bool:
"""Обновление bootstrap конфигурации."""
try:
# Сохранить резервную копию
backup_path = self.bootstrap_path.with_suffix('.backup')
if self.bootstrap_path.exists():
self.bootstrap_path.rename(backup_path)
# Сохранить новую конфигурацию
with open(self.bootstrap_path, 'w', encoding='utf-8') as f:
json.dump(new_config, f, indent=2, ensure_ascii=False)
# Перезагрузить конфигурацию
await self.load_bootstrap_config()
logger.info("Bootstrap configuration updated successfully")
return True
except Exception as e:
logger.error(f"Error updating bootstrap config: {e}")
# Восстановить из резервной копии
try:
if backup_path.exists():
backup_path.rename(self.bootstrap_path)
except:
pass
return False
+386
View File
@@ -0,0 +1,386 @@
"""MY Network Node Service - основной сервис ноды."""
import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Set, Any
from pathlib import Path
from app.core.database_compatible import get_async_session
from app.core.models.content_compatible import Content
from app.core.cache import cache
from .bootstrap_manager import BootstrapManager
from .peer_manager import PeerManager
from .sync_manager import ContentSyncManager
logger = logging.getLogger(__name__)
class MyNetworkNodeService:
"""Основной сервис ноды MY Network."""
def __init__(self, node_id: str = None, storage_path: str = "./storage/my-network"):
self.node_id = node_id or self._generate_node_id()
self.storage_path = Path(storage_path)
self.storage_path.mkdir(parents=True, exist_ok=True)
# Инициализация менеджеров
self.bootstrap_manager = BootstrapManager()
self.peer_manager = PeerManager(self.node_id)
self.sync_manager = ContentSyncManager(self.node_id)
# Состояние ноды
self.is_running = False
self.start_time = None
self.last_sync_time = None
self.node_metrics = {
"requests_30min": 0,
"total_requests": 0,
"content_synced": 0,
"active_peers": 0,
"storage_used_mb": 0
}
# История запросов для балансировки нагрузки
self.request_history = []
logger.info(f"MY Network Node Service initialized with ID: {self.node_id}")
def _generate_node_id(self) -> str:
"""Генерация уникального ID ноды."""
import uuid
return f"node-{uuid.uuid4().hex[:8]}"
async def start(self) -> None:
"""Запуск ноды MY Network."""
try:
logger.info(f"Starting MY Network Node: {self.node_id}")
# Загрузка bootstrap конфигурации
await self.bootstrap_manager.load_bootstrap_config()
# Инициализация peer manager
await self.peer_manager.initialize()
# Подключение к bootstrap нодам
await self._connect_to_bootstrap_nodes()
# Обнаружение других нод в сети
await self._discover_network_nodes()
# Запуск синхронизации контента
await self.sync_manager.start_sync_process()
# Запуск фоновых задач
asyncio.create_task(self._background_tasks())
self.is_running = True
self.start_time = datetime.utcnow()
logger.info(f"MY Network Node {self.node_id} started successfully")
except Exception as e:
logger.error(f"Failed to start MY Network Node: {e}")
raise
async def stop(self) -> None:
"""Остановка ноды MY Network."""
try:
logger.info(f"Stopping MY Network Node: {self.node_id}")
self.is_running = False
# Остановка синхронизации
await self.sync_manager.stop_sync_process()
# Отключение от пиров
await self.peer_manager.disconnect_all()
logger.info(f"MY Network Node {self.node_id} stopped")
except Exception as e:
logger.error(f"Error stopping MY Network Node: {e}")
async def _connect_to_bootstrap_nodes(self) -> None:
"""Подключение к bootstrap нодам."""
bootstrap_nodes = self.bootstrap_manager.get_bootstrap_nodes()
for node in bootstrap_nodes:
try:
# Не подключаться к самому себе
if node["id"] == self.node_id:
continue
success = await self.peer_manager.connect_to_peer(
node["id"],
node["address"]
)
if success:
logger.info(f"Connected to bootstrap node: {node['id']}")
else:
logger.warning(f"Failed to connect to bootstrap node: {node['id']}")
except Exception as e:
logger.error(f"Error connecting to bootstrap node {node['id']}: {e}")
async def _discover_network_nodes(self) -> None:
"""Обнаружение других нод в сети."""
try:
# Запрос списка нод у подключенных пиров
connected_peers = self.peer_manager.get_connected_peers()
for peer_id in connected_peers:
try:
nodes_list = await self.peer_manager.request_nodes_list(peer_id)
for node_info in nodes_list:
# Пропустить себя
if node_info["id"] == self.node_id:
continue
# Попытаться подключиться к новой ноде
if not self.peer_manager.is_connected(node_info["id"]):
await self.peer_manager.connect_to_peer(
node_info["id"],
node_info["address"]
)
except Exception as e:
logger.error(f"Error discovering nodes from peer {peer_id}: {e}")
except Exception as e:
logger.error(f"Error in network discovery: {e}")
async def _background_tasks(self) -> None:
"""Фоновые задачи ноды."""
while self.is_running:
try:
# Обновление метрик
await self._update_metrics()
# Очистка истории запросов (оставляем только за последние 30 минут)
await self._cleanup_request_history()
# Проверка состояния пиров
await self.peer_manager.check_peers_health()
# Периодическая синхронизация
if self._should_sync():
await self.sync_manager.sync_with_network()
self.last_sync_time = datetime.utcnow()
# Обновление кэша статистики
await self._update_cache_stats()
await asyncio.sleep(30) # Проверка каждые 30 секунд
except Exception as e:
logger.error(f"Error in background tasks: {e}")
await asyncio.sleep(60) # Увеличиваем интервал при ошибке
async def _update_metrics(self) -> None:
"""Обновление метрик ноды."""
try:
# Подсчет запросов за последние 30 минут
cutoff_time = datetime.utcnow() - timedelta(minutes=30)
recent_requests = [
req for req in self.request_history
if req["timestamp"] > cutoff_time
]
self.node_metrics.update({
"requests_30min": len(recent_requests),
"active_peers": len(self.peer_manager.get_connected_peers()),
"storage_used_mb": await self._calculate_storage_usage(),
"uptime_hours": self._get_uptime_hours()
})
# Сохранение в кэш для быстрого доступа
await cache.set(
f"my_network:node:{self.node_id}:metrics",
self.node_metrics,
ttl=60
)
except Exception as e:
logger.error(f"Error updating metrics: {e}")
async def _cleanup_request_history(self) -> None:
"""Очистка истории запросов."""
cutoff_time = datetime.utcnow() - timedelta(minutes=30)
self.request_history = [
req for req in self.request_history
if req["timestamp"] > cutoff_time
]
def _should_sync(self) -> bool:
"""Проверка, нужно ли запускать синхронизацию."""
if not self.last_sync_time:
return True
# Синхронизация каждые 5 минут
return datetime.utcnow() - self.last_sync_time > timedelta(minutes=5)
async def _calculate_storage_usage(self) -> int:
"""Подсчет использования хранилища в МБ."""
try:
total_size = 0
if self.storage_path.exists():
for file_path in self.storage_path.rglob("*"):
if file_path.is_file():
total_size += file_path.stat().st_size
return total_size // (1024 * 1024) # Конвертация в МБ
except Exception as e:
logger.error(f"Error calculating storage usage: {e}")
return 0
def _get_uptime_hours(self) -> float:
"""Получение времени работы ноды в часах."""
if not self.start_time:
return 0.0
uptime = datetime.utcnow() - self.start_time
return uptime.total_seconds() / 3600
async def _update_cache_stats(self) -> None:
"""Обновление статистики в кэше."""
try:
stats = {
"node_id": self.node_id,
"is_running": self.is_running,
"start_time": self.start_time.isoformat() if self.start_time else None,
"last_sync_time": self.last_sync_time.isoformat() if self.last_sync_time else None,
"metrics": self.node_metrics,
"connected_peers": list(self.peer_manager.get_connected_peers()),
"sync_status": await self.sync_manager.get_sync_status()
}
await cache.set(
f"my_network:node:{self.node_id}:status",
stats,
ttl=30
)
except Exception as e:
logger.error(f"Error updating cache stats: {e}")
def record_request(self, request_info: Dict[str, Any]) -> None:
"""Записать информацию о запросе для метрик."""
self.request_history.append({
"timestamp": datetime.utcnow(),
"endpoint": request_info.get("endpoint", "unknown"),
"method": request_info.get("method", "GET"),
"client_ip": request_info.get("client_ip", "unknown")
})
self.node_metrics["total_requests"] += 1
def get_load_info(self) -> Dict[str, Any]:
"""Получить информацию о нагрузке ноды для балансировки."""
return {
"node_id": self.node_id,
"requests_30min": self.node_metrics["requests_30min"],
"load_percentage": min(100, (self.node_metrics["requests_30min"] / 1000) * 100),
"active_peers": self.node_metrics["active_peers"],
"storage_used_mb": self.node_metrics["storage_used_mb"],
"uptime_hours": self._get_uptime_hours(),
"is_healthy": self.is_running and self.node_metrics["active_peers"] > 0
}
async def replicate_content(self, content_hash: str, target_nodes: List[str] = None) -> Dict[str, Any]:
"""Реплицировать контент на другие ноды."""
try:
logger.info(f"Starting replication of content: {content_hash}")
# Найти контент в локальной БД
async with get_async_session() as session:
content = await session.get(Content, {"hash": content_hash})
if not content:
raise ValueError(f"Content not found: {content_hash}")
# Определить целевые ноды
if not target_nodes:
target_nodes = self.peer_manager.select_replication_nodes()
# Запустить репликацию через sync manager
result = await self.sync_manager.replicate_content_to_nodes(
content_hash,
target_nodes
)
logger.info(f"Content replication completed: {content_hash}")
return result
except Exception as e:
logger.error(f"Error replicating content {content_hash}: {e}")
raise
async def get_network_status(self) -> Dict[str, Any]:
"""Получить статус всей сети MY Network."""
try:
connected_peers = self.peer_manager.get_connected_peers()
sync_status = await self.sync_manager.get_sync_status()
# Получить статус от всех подключенных пиров
peer_statuses = {}
for peer_id in connected_peers:
try:
peer_status = await self.peer_manager.request_peer_status(peer_id)
peer_statuses[peer_id] = peer_status
except Exception as e:
peer_statuses[peer_id] = {"error": str(e)}
return {
"local_node": {
"id": self.node_id,
"status": "running" if self.is_running else "stopped",
"metrics": self.node_metrics,
"uptime_hours": self._get_uptime_hours()
},
"network": {
"connected_peers": len(connected_peers),
"total_discovered_nodes": len(peer_statuses) + 1,
"sync_status": sync_status,
"last_sync": self.last_sync_time.isoformat() if self.last_sync_time else None
},
"peers": peer_statuses
}
except Exception as e:
logger.error(f"Error getting network status: {e}")
return {"error": str(e)}
async def get_content_sync_status(self, content_hash: str) -> Dict[str, Any]:
"""Получить статус синхронизации конкретного контента."""
return await self.sync_manager.get_content_sync_status(content_hash)
# Глобальный экземпляр сервиса ноды
_node_service: Optional[MyNetworkNodeService] = None
def get_node_service() -> MyNetworkNodeService:
"""Получить глобальный экземпляр сервиса ноды."""
global _node_service
if _node_service is None:
_node_service = MyNetworkNodeService()
return _node_service
async def initialize_my_network() -> None:
"""Инициализация MY Network."""
node_service = get_node_service()
await node_service.start()
async def shutdown_my_network() -> None:
"""Остановка MY Network."""
global _node_service
if _node_service:
await _node_service.stop()
_node_service = None
+477
View File
@@ -0,0 +1,477 @@
"""Peer Manager - управление подключениями к другим нодам."""
import asyncio
import aiohttp
import logging
from datetime import datetime, timedelta
from typing import Dict, List, Set, Optional, Any
from urllib.parse import urlparse
logger = logging.getLogger(__name__)
class PeerConnection:
"""Представление подключения к пиру."""
def __init__(self, peer_id: str, address: str):
self.peer_id = peer_id
self.address = address
self.connected_at = datetime.utcnow()
self.last_ping = None
self.last_pong = None
self.is_healthy = True
self.ping_failures = 0
self.request_count = 0
self.features = []
self.metadata = {}
@property
def uptime(self) -> timedelta:
"""Время подключения."""
return datetime.utcnow() - self.connected_at
@property
def ping_latency(self) -> Optional[float]:
"""Задержка пинга в миллисекундах."""
if self.last_ping and self.last_pong:
return (self.last_pong - self.last_ping).total_seconds() * 1000
return None
def mark_ping_sent(self):
"""Отметить отправку пинга."""
self.last_ping = datetime.utcnow()
def mark_pong_received(self):
"""Отметить получение понга."""
self.last_pong = datetime.utcnow()
self.ping_failures = 0
self.is_healthy = True
def mark_ping_failed(self):
"""Отметить неудачный пинг."""
self.ping_failures += 1
if self.ping_failures >= 3:
self.is_healthy = False
class PeerManager:
"""Менеджер для управления подключениями к пирам."""
def __init__(self, node_id: str):
self.node_id = node_id
self.connections: Dict[str, PeerConnection] = {}
self.blacklisted_peers: Set[str] = set()
self.connection_semaphore = asyncio.Semaphore(25) # Макс 25 исходящих подключений
self.session: Optional[aiohttp.ClientSession] = None
logger.info(f"Peer Manager initialized for node: {node_id}")
async def initialize(self) -> None:
"""Инициализация менеджера пиров."""
try:
# Создать HTTP сессию для запросов
timeout = aiohttp.ClientTimeout(total=30, connect=10)
self.session = aiohttp.ClientSession(
timeout=timeout,
headers={'User-Agent': f'MY-Network-Node/{self.node_id}'}
)
logger.info("Peer Manager initialized successfully")
except Exception as e:
logger.error(f"Error initializing Peer Manager: {e}")
raise
async def cleanup(self) -> None:
"""Очистка ресурсов."""
if self.session:
await self.session.close()
self.session = None
self.connections.clear()
logger.info("Peer Manager cleaned up")
async def connect_to_peer(self, peer_id: str, address: str) -> bool:
"""Подключение к пиру."""
try:
# Проверить, что не подключаемся к себе
if peer_id == self.node_id:
logger.debug(f"Skipping connection to self: {peer_id}")
return False
# Проверить черный список
if peer_id in self.blacklisted_peers:
logger.debug(f"Peer {peer_id} is blacklisted")
return False
# Проверить, уже подключены ли
if peer_id in self.connections:
connection = self.connections[peer_id]
if connection.is_healthy:
logger.debug(f"Already connected to peer: {peer_id}")
return True
else:
# Удалить нездоровое подключение
del self.connections[peer_id]
async with self.connection_semaphore:
logger.info(f"Connecting to peer: {peer_id} at {address}")
# Попытка подключения через handshake
success = await self._perform_handshake(peer_id, address)
if success:
# Создать подключение
connection = PeerConnection(peer_id, address)
self.connections[peer_id] = connection
logger.info(f"Successfully connected to peer: {peer_id}")
return True
else:
logger.warning(f"Failed to connect to peer: {peer_id}")
return False
except Exception as e:
logger.error(f"Error connecting to peer {peer_id}: {e}")
return False
async def _perform_handshake(self, peer_id: str, address: str) -> bool:
"""Выполнить handshake с пиром."""
try:
if not self.session:
return False
# Парсить адрес
parsed_url = self._parse_peer_address(address)
if not parsed_url:
return False
handshake_url = f"{parsed_url}/api/my/handshake"
handshake_data = {
"node_id": self.node_id,
"protocol_version": "1.0.0",
"features": [
"content_sync",
"consensus",
"monitoring"
],
"timestamp": datetime.utcnow().isoformat()
}
async with self.session.post(handshake_url, json=handshake_data) as response:
if response.status == 200:
response_data = await response.json()
# Проверить ответ
if (response_data.get("node_id") == peer_id and
response_data.get("status") == "accepted"):
# Сохранить информацию о пире
if peer_id in self.connections:
self.connections[peer_id].features = response_data.get("features", [])
self.connections[peer_id].metadata = response_data.get("metadata", {})
return True
logger.warning(f"Handshake failed with peer {peer_id}: HTTP {response.status}")
return False
except Exception as e:
logger.error(f"Error in handshake with peer {peer_id}: {e}")
return False
def _parse_peer_address(self, address: str) -> Optional[str]:
"""Парсинг адреса пира."""
try:
# Поддержка форматов:
# my://host:port
# http://host:port
# https://host:port
# host:port
if address.startswith("my://"):
# Конвертировать MY протокол в HTTP
address = address.replace("my://", "http://")
elif not address.startswith(("http://", "https://")):
# Добавить HTTP префикс
address = f"http://{address}"
parsed = urlparse(address)
if parsed.hostname:
return f"{parsed.scheme}://{parsed.netloc}"
return None
except Exception as e:
logger.error(f"Error parsing peer address {address}: {e}")
return None
async def disconnect_from_peer(self, peer_id: str) -> None:
"""Отключение от пира."""
try:
if peer_id in self.connections:
connection = self.connections[peer_id]
# Попытаться отправить уведомление об отключении
try:
await self._send_disconnect_notification(peer_id)
except:
pass # Игнорировать ошибки при отключении
# Удалить подключение
del self.connections[peer_id]
logger.info(f"Disconnected from peer: {peer_id}")
except Exception as e:
logger.error(f"Error disconnecting from peer {peer_id}: {e}")
async def _send_disconnect_notification(self, peer_id: str) -> None:
"""Отправить уведомление об отключении."""
try:
if peer_id not in self.connections or not self.session:
return
connection = self.connections[peer_id]
parsed_url = self._parse_peer_address(connection.address)
if parsed_url:
disconnect_url = f"{parsed_url}/api/my/disconnect"
disconnect_data = {
"node_id": self.node_id,
"reason": "graceful_shutdown",
"timestamp": datetime.utcnow().isoformat()
}
async with self.session.post(disconnect_url, json=disconnect_data) as response:
if response.status == 200:
logger.debug(f"Disconnect notification sent to {peer_id}")
except Exception as e:
logger.debug(f"Error sending disconnect notification to {peer_id}: {e}")
async def disconnect_all(self) -> None:
"""Отключение от всех пиров."""
disconnect_tasks = []
for peer_id in list(self.connections.keys()):
disconnect_tasks.append(self.disconnect_from_peer(peer_id))
if disconnect_tasks:
await asyncio.gather(*disconnect_tasks, return_exceptions=True)
logger.info("Disconnected from all peers")
async def check_peers_health(self) -> None:
"""Проверка здоровья всех подключений."""
ping_tasks = []
for peer_id in list(self.connections.keys()):
ping_tasks.append(self._ping_peer(peer_id))
if ping_tasks:
await asyncio.gather(*ping_tasks, return_exceptions=True)
# Удалить нездоровые подключения
unhealthy_peers = [
peer_id for peer_id, conn in self.connections.items()
if not conn.is_healthy
]
for peer_id in unhealthy_peers:
logger.warning(f"Removing unhealthy peer: {peer_id}")
await self.disconnect_from_peer(peer_id)
async def _ping_peer(self, peer_id: str) -> None:
"""Пинг пира."""
try:
if peer_id not in self.connections or not self.session:
return
connection = self.connections[peer_id]
parsed_url = self._parse_peer_address(connection.address)
if not parsed_url:
connection.mark_ping_failed()
return
ping_url = f"{parsed_url}/api/my/ping"
connection.mark_ping_sent()
async with self.session.get(ping_url) as response:
if response.status == 200:
connection.mark_pong_received()
logger.debug(f"Ping successful to {peer_id}, latency: {connection.ping_latency:.1f}ms")
else:
connection.mark_ping_failed()
logger.debug(f"Ping failed to {peer_id}: HTTP {response.status}")
except Exception as e:
if peer_id in self.connections:
self.connections[peer_id].mark_ping_failed()
logger.debug(f"Ping error to {peer_id}: {e}")
def get_connected_peers(self) -> Set[str]:
"""Получить множество подключенных пиров."""
return {
peer_id for peer_id, conn in self.connections.items()
if conn.is_healthy
}
def is_connected(self, peer_id: str) -> bool:
"""Проверить, подключены ли к пиру."""
return (peer_id in self.connections and
self.connections[peer_id].is_healthy)
def get_peer_info(self, peer_id: str) -> Optional[Dict[str, Any]]:
"""Получить информацию о пире."""
if peer_id not in self.connections:
return None
connection = self.connections[peer_id]
return {
"peer_id": peer_id,
"address": connection.address,
"connected_at": connection.connected_at.isoformat(),
"uptime_seconds": connection.uptime.total_seconds(),
"is_healthy": connection.is_healthy,
"ping_latency_ms": connection.ping_latency,
"ping_failures": connection.ping_failures,
"request_count": connection.request_count,
"features": connection.features,
"metadata": connection.metadata
}
def get_all_peers_info(self) -> Dict[str, Dict[str, Any]]:
"""Получить информацию обо всех пирах."""
return {
peer_id: self.get_peer_info(peer_id)
for peer_id in self.connections.keys()
}
def select_replication_nodes(self, count: int = 3) -> List[str]:
"""Выбрать ноды для репликации контента."""
healthy_peers = [
peer_id for peer_id, conn in self.connections.items()
if conn.is_healthy
]
if len(healthy_peers) <= count:
return healthy_peers
# Выбрать ноды с лучшими характеристиками
peer_scores = []
for peer_id in healthy_peers:
connection = self.connections[peer_id]
# Рассчитать оценку на основе различных факторов
latency_score = 1.0
if connection.ping_latency:
latency_score = max(0.1, 1.0 - (connection.ping_latency / 1000))
uptime_score = min(1.0, connection.uptime.total_seconds() / 3600) # Время работы в часах
failure_score = max(0.1, 1.0 - (connection.ping_failures / 10))
total_score = (latency_score * 0.4 + uptime_score * 0.3 + failure_score * 0.3)
peer_scores.append((peer_id, total_score))
# Сортировать по оценке и взять топ
peer_scores.sort(key=lambda x: x[1], reverse=True)
return [peer_id for peer_id, _ in peer_scores[:count]]
async def request_nodes_list(self, peer_id: str) -> List[Dict[str, Any]]:
"""Запросить список нод у пира."""
try:
if peer_id not in self.connections or not self.session:
return []
connection = self.connections[peer_id]
parsed_url = self._parse_peer_address(connection.address)
if not parsed_url:
return []
nodes_url = f"{parsed_url}/api/my/nodes"
async with self.session.get(nodes_url) as response:
if response.status == 200:
data = await response.json()
return data.get("nodes", [])
else:
logger.warning(f"Failed to get nodes list from {peer_id}: HTTP {response.status}")
return []
except Exception as e:
logger.error(f"Error requesting nodes list from {peer_id}: {e}")
return []
async def request_peer_status(self, peer_id: str) -> Dict[str, Any]:
"""Запросить статус пира."""
try:
if peer_id not in self.connections or not self.session:
return {"error": "Not connected"}
connection = self.connections[peer_id]
parsed_url = self._parse_peer_address(connection.address)
if not parsed_url:
return {"error": "Invalid address"}
status_url = f"{parsed_url}/api/my/status"
async with self.session.get(status_url) as response:
if response.status == 200:
return await response.json()
else:
return {"error": f"HTTP {response.status}"}
except Exception as e:
logger.error(f"Error requesting peer status from {peer_id}: {e}")
return {"error": str(e)}
def add_to_blacklist(self, peer_id: str, duration_hours: int = 24) -> None:
"""Добавить пира в черный список."""
self.blacklisted_peers.add(peer_id)
# Запланировать удаление из черного списка
async def remove_from_blacklist():
await asyncio.sleep(duration_hours * 3600)
self.blacklisted_peers.discard(peer_id)
logger.info(f"Removed {peer_id} from blacklist")
asyncio.create_task(remove_from_blacklist())
logger.info(f"Added {peer_id} to blacklist for {duration_hours} hours")
def get_connection_stats(self) -> Dict[str, Any]:
"""Получить статистику подключений."""
healthy_connections = sum(1 for conn in self.connections.values() if conn.is_healthy)
return {
"total_connections": len(self.connections),
"healthy_connections": healthy_connections,
"blacklisted_peers": len(self.blacklisted_peers),
"average_latency_ms": self._calculate_average_latency(),
"connection_details": [
{
"peer_id": peer_id,
"uptime_hours": conn.uptime.total_seconds() / 3600,
"ping_latency_ms": conn.ping_latency,
"is_healthy": conn.is_healthy
}
for peer_id, conn in self.connections.items()
]
}
def _calculate_average_latency(self) -> Optional[float]:
"""Рассчитать среднюю задержку."""
latencies = [
conn.ping_latency for conn in self.connections.values()
if conn.ping_latency is not None and conn.is_healthy
]
if latencies:
return sum(latencies) / len(latencies)
return None
+698
View File
@@ -0,0 +1,698 @@
"""Content Sync Manager - синхронизация контента между нодами."""
import asyncio
import aiohttp
import hashlib
import logging
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Any, Set
from sqlalchemy import select, and_
from app.core.database_compatible import get_async_session
from app.core.models.content_compatible import Content, ContentMetadata
from app.core.cache import cache
logger = logging.getLogger(__name__)
class ContentSyncStatus:
"""Статус синхронизации контента."""
def __init__(self, content_hash: str):
self.content_hash = content_hash
self.sync_started = datetime.utcnow()
self.sync_completed = None
self.nodes_synced = set()
self.nodes_failed = set()
self.total_nodes = 0
self.bytes_synced = 0
self.status = "syncing" # syncing, completed, failed, partial
self.error_message = None
@property
def is_completed(self) -> bool:
return self.status in ["completed", "partial"]
@property
def success_rate(self) -> float:
if self.total_nodes == 0:
return 0.0
return len(self.nodes_synced) / self.total_nodes
def to_dict(self) -> Dict[str, Any]:
return {
"content_hash": self.content_hash,
"status": self.status,
"sync_started": self.sync_started.isoformat(),
"sync_completed": self.sync_completed.isoformat() if self.sync_completed else None,
"nodes_synced": list(self.nodes_synced),
"nodes_failed": list(self.nodes_failed),
"total_nodes": self.total_nodes,
"bytes_synced": self.bytes_synced,
"success_rate": self.success_rate,
"error_message": self.error_message
}
class ContentSyncManager:
"""Менеджер синхронизации контента между нодами MY Network."""
def __init__(self, node_id: str):
self.node_id = node_id
self.sync_queue: asyncio.Queue = asyncio.Queue()
self.active_syncs: Dict[str, ContentSyncStatus] = {}
self.sync_history: List[ContentSyncStatus] = []
self.is_running = False
self.sync_workers: List[asyncio.Task] = []
self.session: Optional[aiohttp.ClientSession] = None
# Настройки синхронизации
self.max_concurrent_syncs = 5
self.chunk_size = 1024 * 1024 # 1MB chunks
self.sync_timeout = 300 # 5 minutes per content
self.retry_attempts = 3
logger.info(f"Content Sync Manager initialized for node: {node_id}")
async def start_sync_process(self) -> None:
"""Запуск процесса синхронизации."""
try:
# Создать HTTP сессию
timeout = aiohttp.ClientTimeout(total=self.sync_timeout)
self.session = aiohttp.ClientSession(timeout=timeout)
# Запустить worker'ы для синхронизации
self.is_running = True
for i in range(self.max_concurrent_syncs):
worker = asyncio.create_task(self._sync_worker(f"worker-{i}"))
self.sync_workers.append(worker)
logger.info(f"Started {len(self.sync_workers)} sync workers")
except Exception as e:
logger.error(f"Error starting sync process: {e}")
raise
async def stop_sync_process(self) -> None:
"""Остановка процесса синхронизации."""
try:
self.is_running = False
# Остановить worker'ы
for worker in self.sync_workers:
worker.cancel()
if self.sync_workers:
await asyncio.gather(*self.sync_workers, return_exceptions=True)
# Закрыть HTTP сессию
if self.session:
await self.session.close()
self.session = None
self.sync_workers.clear()
logger.info("Sync process stopped")
except Exception as e:
logger.error(f"Error stopping sync process: {e}")
async def _sync_worker(self, worker_name: str) -> None:
"""Worker для обработки очереди синхронизации."""
logger.info(f"Sync worker {worker_name} started")
while self.is_running:
try:
# Получить задачу из очереди
sync_task = await asyncio.wait_for(
self.sync_queue.get(),
timeout=1.0
)
# Обработать задачу синхронизации
await self._process_sync_task(sync_task)
except asyncio.TimeoutError:
continue # Продолжить ожидание
except Exception as e:
logger.error(f"Error in sync worker {worker_name}: {e}")
await asyncio.sleep(5) # Пауза при ошибке
logger.info(f"Sync worker {worker_name} stopped")
async def _process_sync_task(self, sync_task: Dict[str, Any]) -> None:
"""Обработка задачи синхронизации."""
try:
task_type = sync_task.get("type")
content_hash = sync_task.get("content_hash")
target_nodes = sync_task.get("target_nodes", [])
if task_type == "replicate":
await self._replicate_content(content_hash, target_nodes)
elif task_type == "download":
source_node = sync_task.get("source_node")
await self._download_content(content_hash, source_node)
elif task_type == "verify":
await self._verify_content_integrity(content_hash)
else:
logger.warning(f"Unknown sync task type: {task_type}")
except Exception as e:
logger.error(f"Error processing sync task: {e}")
async def replicate_content_to_nodes(self, content_hash: str, target_nodes: List[str]) -> Dict[str, Any]:
"""Реплицировать контент на указанные ноды."""
try:
# Создать статус синхронизации
sync_status = ContentSyncStatus(content_hash)
sync_status.total_nodes = len(target_nodes)
self.active_syncs[content_hash] = sync_status
# Добавить задачу в очередь
sync_task = {
"type": "replicate",
"content_hash": content_hash,
"target_nodes": target_nodes
}
await self.sync_queue.put(sync_task)
logger.info(f"Queued replication of {content_hash} to {len(target_nodes)} nodes")
return {
"status": "queued",
"content_hash": content_hash,
"target_nodes": target_nodes,
"sync_id": content_hash
}
except Exception as e:
logger.error(f"Error queuing content replication: {e}")
raise
async def _replicate_content(self, content_hash: str, target_nodes: List[str]) -> None:
"""Реплицировать контент на целевые ноды."""
try:
if content_hash not in self.active_syncs:
logger.warning(f"No sync status found for content: {content_hash}")
return
sync_status = self.active_syncs[content_hash]
# Получить контент из локальной БД
content_info = await self._get_local_content_info(content_hash)
if not content_info:
sync_status.status = "failed"
sync_status.error_message = "Content not found locally"
return
# Реплицировать на каждую ноду
replication_tasks = []
for node_id in target_nodes:
task = self._replicate_to_single_node(content_hash, node_id, content_info)
replication_tasks.append(task)
# Ждать завершения всех репликаций
results = await asyncio.gather(*replication_tasks, return_exceptions=True)
# Обработать результаты
for i, result in enumerate(results):
node_id = target_nodes[i]
if isinstance(result, Exception):
sync_status.nodes_failed.add(node_id)
logger.error(f"Replication to {node_id} failed: {result}")
elif result:
sync_status.nodes_synced.add(node_id)
sync_status.bytes_synced += content_info.get("file_size", 0)
logger.info(f"Successfully replicated to {node_id}")
else:
sync_status.nodes_failed.add(node_id)
# Завершить синхронизацию
self._complete_sync(sync_status)
except Exception as e:
if content_hash in self.active_syncs:
self.active_syncs[content_hash].status = "failed"
self.active_syncs[content_hash].error_message = str(e)
logger.error(f"Error replicating content {content_hash}: {e}")
async def _replicate_to_single_node(self, content_hash: str, node_id: str, content_info: Dict[str, Any]) -> bool:
"""Реплицировать контент на одну ноду."""
try:
if not self.session:
return False
# Получить адрес ноды (через peer manager)
from .node_service import get_node_service
node_service = get_node_service()
peer_info = node_service.peer_manager.get_peer_info(node_id)
if not peer_info:
logger.warning(f"No peer info for node: {node_id}")
return False
# Парсить адрес
peer_address = node_service.peer_manager._parse_peer_address(peer_info["address"])
if not peer_address:
return False
# Проверить, нужна ли репликация
check_url = f"{peer_address}/api/my/content/{content_hash}/exists"
async with self.session.get(check_url) as response:
if response.status == 200:
exists_data = await response.json()
if exists_data.get("exists", False):
logger.debug(f"Content {content_hash} already exists on {node_id}")
return True
# Начать репликацию
replicate_url = f"{peer_address}/api/my/content/replicate"
# Подготовить данные для репликации
replication_data = {
"content_hash": content_hash,
"metadata": content_info,
"source_node": self.node_id
}
async with self.session.post(replicate_url, json=replication_data) as response:
if response.status == 200:
# Передать сам файл
success = await self._upload_content_to_node(
content_hash,
peer_address,
content_info
)
return success
else:
logger.warning(f"Replication request failed to {node_id}: HTTP {response.status}")
return False
except Exception as e:
logger.error(f"Error replicating to node {node_id}: {e}")
return False
async def _upload_content_to_node(self, content_hash: str, peer_address: str, content_info: Dict[str, Any]) -> bool:
"""Загрузить файл контента на ноду."""
try:
if not self.session:
return False
# Найти файл локально
file_path = Path(content_info.get("file_path", ""))
if not file_path.exists():
logger.error(f"Local file not found: {file_path}")
return False
upload_url = f"{peer_address}/api/my/content/{content_hash}/upload"
# Создать multipart upload
with open(file_path, 'rb') as file:
data = aiohttp.FormData()
data.add_field('file', file, filename=content_info.get("filename", "unknown"))
async with self.session.post(upload_url, data=data) as response:
if response.status == 200:
result = await response.json()
return result.get("success", False)
else:
logger.error(f"File upload failed: HTTP {response.status}")
return False
except Exception as e:
logger.error(f"Error uploading content to node: {e}")
return False
async def _get_local_content_info(self, content_hash: str) -> Optional[Dict[str, Any]]:
"""Получить информацию о локальном контенте."""
try:
async with get_async_session() as session:
# Найти контент по хешу
stmt = select(Content).where(Content.md5_hash == content_hash or Content.sha256_hash == content_hash)
result = await session.execute(stmt)
content = result.scalar_one_or_none()
if not content:
return None
# Получить метаданные
metadata_stmt = select(ContentMetadata).where(ContentMetadata.content_id == content.id)
metadata_result = await session.execute(metadata_stmt)
metadata = metadata_result.scalar_one_or_none()
return {
"id": content.id,
"hash": content_hash,
"filename": content.filename,
"original_filename": content.original_filename,
"file_path": content.file_path,
"file_size": content.file_size,
"file_type": content.file_type,
"mime_type": content.mime_type,
"encrypted": content.encrypted if hasattr(content, 'encrypted') else False,
"metadata": metadata.to_dict() if metadata else {}
}
except Exception as e:
logger.error(f"Error getting local content info: {e}")
return None
async def download_content_from_network(self, content_hash: str, source_nodes: List[str] = None) -> bool:
"""Скачать контент из сети."""
try:
# Добавить задачу загрузки в очередь
for source_node in (source_nodes or []):
sync_task = {
"type": "download",
"content_hash": content_hash,
"source_node": source_node
}
await self.sync_queue.put(sync_task)
logger.info(f"Queued download of {content_hash} from {len(source_nodes or [])} nodes")
return True
except Exception as e:
logger.error(f"Error queuing content download: {e}")
return False
async def _download_content(self, content_hash: str, source_node: str) -> bool:
"""Скачать контент с конкретной ноды."""
try:
if not self.session:
return False
# Получить адрес исходной ноды
from .node_service import get_node_service
node_service = get_node_service()
peer_info = node_service.peer_manager.get_peer_info(source_node)
if not peer_info:
logger.warning(f"No peer info for source node: {source_node}")
return False
peer_address = node_service.peer_manager._parse_peer_address(peer_info["address"])
if not peer_address:
return False
# Получить метаданные контента
metadata_url = f"{peer_address}/api/my/content/{content_hash}/metadata"
async with self.session.get(metadata_url) as response:
if response.status != 200:
logger.error(f"Failed to get content metadata: HTTP {response.status}")
return False
content_metadata = await response.json()
# Скачать файл
download_url = f"{peer_address}/api/my/content/{content_hash}/download"
async with self.session.get(download_url) as response:
if response.status != 200:
logger.error(f"Failed to download content: HTTP {response.status}")
return False
# Сохранить файл локально
local_path = await self._save_downloaded_content(
content_hash,
response,
content_metadata
)
if local_path:
# Сохранить в базу данных
await self._save_content_to_db(content_hash, local_path, content_metadata)
logger.info(f"Successfully downloaded content {content_hash} from {source_node}")
return True
return False
except Exception as e:
logger.error(f"Error downloading content from {source_node}: {e}")
return False
async def _save_downloaded_content(self, content_hash: str, response: aiohttp.ClientResponse, metadata: Dict[str, Any]) -> Optional[Path]:
"""Сохранить скачанный контент."""
try:
# Создать путь для сохранения
storage_path = Path("./storage/my-network/downloaded")
storage_path.mkdir(parents=True, exist_ok=True)
filename = metadata.get("filename", f"{content_hash}")
file_path = storage_path / filename
# Сохранить файл
with open(file_path, 'wb') as f:
async for chunk in response.content.iter_chunked(self.chunk_size):
f.write(chunk)
# Проверить целостность
if await self._verify_file_integrity(file_path, content_hash):
return file_path
else:
file_path.unlink() # Удалить поврежденный файл
return None
except Exception as e:
logger.error(f"Error saving downloaded content: {e}")
return None
async def _verify_file_integrity(self, file_path: Path, expected_hash: str) -> bool:
"""Проверить целостность файла."""
try:
# Вычислить хеш файла
hash_md5 = hashlib.md5()
hash_sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(self.chunk_size), b""):
hash_md5.update(chunk)
hash_sha256.update(chunk)
file_md5 = hash_md5.hexdigest()
file_sha256 = hash_sha256.hexdigest()
# Проверить соответствие
return expected_hash in [file_md5, file_sha256]
except Exception as e:
logger.error(f"Error verifying file integrity: {e}")
return False
async def _save_content_to_db(self, content_hash: str, file_path: Path, metadata: Dict[str, Any]) -> None:
"""Сохранить информацию о контенте в базу данных."""
try:
async with get_async_session() as session:
# Создать запись контента
content = Content(
filename=metadata.get("filename", file_path.name),
original_filename=metadata.get("original_filename", file_path.name),
file_path=str(file_path),
file_size=file_path.stat().st_size,
file_type=metadata.get("file_type", "unknown"),
mime_type=metadata.get("mime_type", "application/octet-stream"),
md5_hash=content_hash if len(content_hash) == 32 else None,
sha256_hash=content_hash if len(content_hash) == 64 else None,
is_active=True,
processing_status="completed"
)
session.add(content)
await session.flush()
# Сохранить метаданные если есть
if metadata.get("metadata"):
content_metadata = ContentMetadata(
content_id=content.id,
**metadata["metadata"]
)
session.add(content_metadata)
await session.commit()
logger.info(f"Saved content {content_hash} to database")
except Exception as e:
logger.error(f"Error saving content to database: {e}")
def _complete_sync(self, sync_status: ContentSyncStatus) -> None:
"""Завершить синхронизацию."""
sync_status.sync_completed = datetime.utcnow()
# Определить итоговый статус
if len(sync_status.nodes_synced) == sync_status.total_nodes:
sync_status.status = "completed"
elif len(sync_status.nodes_synced) > 0:
sync_status.status = "partial"
else:
sync_status.status = "failed"
# Переместить в историю
self.sync_history.append(sync_status)
del self.active_syncs[sync_status.content_hash]
# Ограничить историю
if len(self.sync_history) > 100:
self.sync_history = self.sync_history[-100:]
logger.info(f"Sync completed for {sync_status.content_hash}: {sync_status.status}")
async def sync_with_network(self) -> Dict[str, Any]:
"""Синхронизация с сетью - обнаружение и загрузка нового контента."""
try:
from .node_service import get_node_service
node_service = get_node_service()
connected_peers = node_service.peer_manager.get_connected_peers()
if not connected_peers:
return {"status": "no_peers", "message": "No connected peers for sync"}
# Получить списки контента от всех пиров
network_content = {}
for peer_id in connected_peers:
try:
peer_content = await self._get_peer_content_list(peer_id)
network_content[peer_id] = peer_content
except Exception as e:
logger.error(f"Error getting content list from {peer_id}: {e}")
# Найти новый контент для загрузки
new_content = await self._identify_new_content(network_content)
# Запустить загрузку нового контента
download_tasks = []
for content_hash, source_nodes in new_content.items():
download_tasks.append(
self.download_content_from_network(content_hash, source_nodes)
)
if download_tasks:
results = await asyncio.gather(*download_tasks, return_exceptions=True)
successful_downloads = sum(1 for r in results if r is True)
return {
"status": "sync_completed",
"new_content_found": len(new_content),
"downloads_queued": len(download_tasks),
"immediate_successes": successful_downloads
}
else:
return {
"status": "up_to_date",
"message": "No new content found"
}
except Exception as e:
logger.error(f"Error in network sync: {e}")
return {"status": "error", "message": str(e)}
async def _get_peer_content_list(self, peer_id: str) -> List[Dict[str, Any]]:
"""Получить список контента от пира."""
try:
if not self.session:
return []
from .node_service import get_node_service
node_service = get_node_service()
peer_info = node_service.peer_manager.get_peer_info(peer_id)
if not peer_info:
return []
peer_address = node_service.peer_manager._parse_peer_address(peer_info["address"])
if not peer_address:
return []
content_list_url = f"{peer_address}/api/my/content/list"
async with self.session.get(content_list_url) as response:
if response.status == 200:
data = await response.json()
return data.get("content", [])
else:
logger.warning(f"Failed to get content list from {peer_id}: HTTP {response.status}")
return []
except Exception as e:
logger.error(f"Error getting content list from {peer_id}: {e}")
return []
async def _identify_new_content(self, network_content: Dict[str, List[Dict[str, Any]]]) -> Dict[str, List[str]]:
"""Определить новый контент для загрузки."""
try:
# Получить список локального контента
local_hashes = await self._get_local_content_hashes()
# Найти новый контент
new_content = {}
for peer_id, content_list in network_content.items():
for content_info in content_list:
content_hash = content_info.get("hash")
if not content_hash:
continue
# Проверить, есть ли у нас этот контент
if content_hash not in local_hashes:
if content_hash not in new_content:
new_content[content_hash] = []
new_content[content_hash].append(peer_id)
return new_content
except Exception as e:
logger.error(f"Error identifying new content: {e}")
return {}
async def _get_local_content_hashes(self) -> Set[str]:
"""Получить множество хешей локального контента."""
try:
async with get_async_session() as session:
stmt = select(Content.md5_hash, Content.sha256_hash).where(Content.is_active == True)
result = await session.execute(stmt)
hashes = set()
for row in result:
if row.md5_hash:
hashes.add(row.md5_hash)
if row.sha256_hash:
hashes.add(row.sha256_hash)
return hashes
except Exception as e:
logger.error(f"Error getting local content hashes: {e}")
return set()
async def get_sync_status(self) -> Dict[str, Any]:
"""Получить статус синхронизации."""
return {
"is_running": self.is_running,
"active_syncs": len(self.active_syncs),
"queue_size": self.sync_queue.qsize(),
"workers_count": len(self.sync_workers),
"recent_syncs": [
sync.to_dict() for sync in self.sync_history[-10:]
],
"current_syncs": {
content_hash: sync.to_dict()
for content_hash, sync in self.active_syncs.items()
}
}
async def get_content_sync_status(self, content_hash: str) -> Dict[str, Any]:
"""Получить статус синхронизации конкретного контента."""
# Проверить активные синхронизации
if content_hash in self.active_syncs:
return self.active_syncs[content_hash].to_dict()
# Проверить историю
for sync in reversed(self.sync_history):
if sync.content_hash == content_hash:
return sync.to_dict()
return {
"content_hash": content_hash,
"status": "not_found",
"message": "No sync information found for this content"
}