relayers
This commit is contained in:
1 parent
21964fa986
commit
797f379648
68 files changed
+23871
-1271
No files matched your search
@@ -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'
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
Reference in new issue
Block a user