Files
remnawave-bedolaga-telegram…/app/services/remnawave_sync_service.py

283 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, time
from typing import Any, Callable, Dict, List, Optional, Tuple
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.database import AsyncSessionLocal
from app.database.crud.server_squad import sync_with_remnawave
from app.services.remnawave_service import (
RemnaWaveConfigurationError,
RemnaWaveService,
)
from app.utils.cache import cache
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class RemnaWaveAutoSyncStatus:
enabled: bool
times: List[time]
next_run: Optional[datetime]
last_run_started_at: Optional[datetime]
last_run_finished_at: Optional[datetime]
last_run_success: Optional[bool]
last_run_reason: Optional[str]
last_run_error: Optional[str]
last_user_stats: Optional[Dict[str, Any]]
last_server_stats: Optional[Dict[str, Any]]
is_running: bool
class RemnaWaveAutoSyncService:
def __init__(
self,
service_factory: Callable[[], RemnaWaveService] = RemnaWaveService,
) -> None:
self._scheduler_task: Optional[asyncio.Task] = None
self._scheduler_lock = asyncio.Lock()
self._sync_lock = asyncio.Lock()
self._service_factory = service_factory
self._service = self._service_factory()
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._initialized = False
self._pending_refresh = False
self._pending_run_immediately = False
self._next_run: Optional[datetime] = None
self._last_run_started_at: Optional[datetime] = None
self._last_run_finished_at: Optional[datetime] = None
self._last_run_success: Optional[bool] = None
self._last_run_reason: Optional[str] = None
self._last_run_error: Optional[str] = None
self._last_user_stats: Optional[Dict[str, Any]] = None
self._last_server_stats: Optional[Dict[str, Any]] = None
async def initialize(self) -> None:
self._loop = asyncio.get_running_loop()
self._initialized = True
run_immediately = self._pending_run_immediately
if self._pending_refresh:
self._pending_refresh = False
self._pending_run_immediately = False
await self.refresh_schedule(run_immediately=run_immediately)
else:
await self.refresh_schedule()
def refresh_configuration(self) -> None:
"""Rebuild the RemnaWave service instance using current settings."""
self._refresh_service()
async def refresh_schedule(self, *, run_immediately: bool = False) -> None:
async with self._scheduler_lock:
if self._scheduler_task and not self._scheduler_task.done():
self._scheduler_task.cancel()
try:
await self._scheduler_task
except asyncio.CancelledError:
pass
finally:
self._scheduler_task = None
if not settings.REMNAWAVE_AUTO_SYNC_ENABLED:
self._next_run = None
return
times = settings.get_remnawave_auto_sync_times()
if not times:
logger.warning(
"⚠️ Автосинхронизация включена, но расписание пустое. Укажите время запуска."
)
self._next_run = None
return
self._scheduler_task = asyncio.create_task(self._run_scheduler(times))
if run_immediately:
asyncio.create_task(self.run_sync_now(reason="immediate"))
def schedule_refresh(self, *, run_immediately: bool = False) -> None:
if not self._initialized:
self._pending_refresh = True
if run_immediately:
self._pending_run_immediately = True
return
loop = self._loop or asyncio.get_running_loop()
loop.create_task(self.refresh_schedule(run_immediately=run_immediately))
async def stop(self) -> None:
async with self._scheduler_lock:
if self._scheduler_task and not self._scheduler_task.done():
self._scheduler_task.cancel()
try:
await self._scheduler_task
except asyncio.CancelledError:
pass
self._scheduler_task = None
self._next_run = None
async def run_sync_now(self, *, reason: str = "manual") -> Dict[str, Any]:
if self._sync_lock.locked():
return {"started": False, "reason": "already_running"}
async with self._sync_lock:
self._last_run_started_at = datetime.utcnow()
self._last_run_finished_at = None
self._last_run_reason = reason
self._last_run_error = None
self._last_run_success = None
try:
user_stats, server_stats = await self._perform_sync()
except RemnaWaveConfigurationError as error:
message = str(error)
self._last_run_error = message
self._last_run_success = False
self._last_user_stats = None
self._last_server_stats = None
self._last_run_finished_at = datetime.utcnow()
logger.error("❌ Автосинхронизация RemnaWave: %s", message)
return {
"started": True,
"success": False,
"error": message,
"user_stats": None,
"server_stats": None,
}
except Exception as error:
message = str(error)
self._last_run_error = message
self._last_run_success = False
self._last_user_stats = None
self._last_server_stats = None
self._last_run_finished_at = datetime.utcnow()
logger.exception("❌ Ошибка автосинхронизации RemnaWave: %s", error)
return {
"started": True,
"success": False,
"error": message,
"user_stats": None,
"server_stats": None,
}
self._last_run_success = True
self._last_run_error = None
self._last_user_stats = user_stats
self._last_server_stats = server_stats
self._last_run_finished_at = datetime.utcnow()
return {
"started": True,
"success": True,
"error": None,
"user_stats": user_stats,
"server_stats": server_stats,
}
def get_status(self) -> RemnaWaveAutoSyncStatus:
times = settings.get_remnawave_auto_sync_times()
enabled = settings.REMNAWAVE_AUTO_SYNC_ENABLED and bool(times)
return RemnaWaveAutoSyncStatus(
enabled=enabled,
times=times,
next_run=self._next_run,
last_run_started_at=self._last_run_started_at,
last_run_finished_at=self._last_run_finished_at,
last_run_success=self._last_run_success,
last_run_reason=self._last_run_reason,
last_run_error=self._last_run_error,
last_user_stats=self._last_user_stats,
last_server_stats=self._last_server_stats,
is_running=self._sync_lock.locked(),
)
async def _run_scheduler(self, times: List[time]) -> None:
try:
while True:
next_run = self._calculate_next_run(times)
self._next_run = next_run
delay = (next_run - datetime.utcnow()).total_seconds()
if delay > 0:
await asyncio.sleep(delay)
await self.run_sync_now(reason="auto")
except asyncio.CancelledError:
raise
finally:
self._next_run = None
def _refresh_service(self) -> RemnaWaveService:
self._service = self._service_factory()
return self._service
async def _perform_sync(self) -> Tuple[Dict[str, Any], Dict[str, Any]]:
service = self._refresh_service()
if not service.is_configured:
raise RemnaWaveConfigurationError(
service.configuration_error or "RemnaWave API не настроен"
)
async with AsyncSessionLocal() as session:
user_stats = await service.sync_users_from_panel(session, "all")
server_stats = await self._sync_servers(session, service)
return user_stats, server_stats
async def _sync_servers(
self,
session: AsyncSession,
service: RemnaWaveService,
) -> Dict[str, Any]:
squads = await service.get_all_squads()
if not squads:
logger.warning("⚠️ Не удалось получить сквады из RemnaWave для автосинхронизации")
return {"created": 0, "updated": 0, "removed": 0, "total": 0}
created, updated, removed = await sync_with_remnawave(session, squads)
try:
await cache.delete_pattern("available_countries*")
except Exception as error:
logger.warning("⚠️ Не удалось очистить кеш стран после автосинхронизации: %s", error)
return {
"created": created,
"updated": updated,
"removed": removed,
"total": len(squads),
}
@staticmethod
def _calculate_next_run(times: List[time]) -> datetime:
now = datetime.utcnow()
today = now.date()
for scheduled in sorted(times):
candidate = datetime.combine(today, scheduled)
if candidate > now:
return candidate
first_time = sorted(times)[0]
next_day = today + timedelta(days=1)
return datetime.combine(next_day, first_time)
def _create_service() -> RemnaWaveAutoSyncService:
service = RemnaWaveAutoSyncService()
return service
remnawave_sync_service = _create_service()