166 lines
6.7 KiB
Python
166 lines
6.7 KiB
Python
# app/services/scheduler.py
|
|
|
|
import asyncio
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
import time as time_module
|
|
|
|
from app.core.logging import migration_logger
|
|
from app.core.config import settings
|
|
from app.services.migrator import migrator
|
|
from app.repository.replication_metadata_repo import replication_metadata_repo
|
|
from app.utils.email_sender import email_sender
|
|
|
|
|
|
class MigrationScheduler:
|
|
"""Планировщик миграций на базе PostgreSQL"""
|
|
|
|
def __init__(self):
|
|
self.running = False
|
|
self.repo = replication_metadata_repo
|
|
|
|
# Инициализируем расписания по умолчанию
|
|
self._init_default_schedules()
|
|
|
|
def _init_default_schedules(self):
|
|
"""Инициализация расписаний по умолчанию"""
|
|
self.repo.init_default_schedules(settings.TABLES_TO_COPY)
|
|
migration_logger.info("Расписания по умолчанию инициализированы")
|
|
|
|
def set_schedule(self, table_name: str, schedule_time: str = "00:00",
|
|
days: Optional[List[str]] = None, full_reload: bool = False,
|
|
enabled: bool = True):
|
|
"""Установить расписание для таблицы"""
|
|
if table_name not in settings.TABLES_TO_COPY:
|
|
raise ValueError(f"Таблица {table_name} не найдена в списке для миграции")
|
|
|
|
# Валидация формата времени
|
|
try:
|
|
datetime.strptime(schedule_time, "%H:%M")
|
|
except ValueError:
|
|
raise ValueError(f"Неверный формат времени: {schedule_time}. Используйте HH:MM")
|
|
|
|
schedule = self.repo.set_schedule(
|
|
table_name=table_name,
|
|
schedule_time=schedule_time,
|
|
days=days,
|
|
full_reload=full_reload,
|
|
enabled=enabled
|
|
)
|
|
|
|
if schedule:
|
|
days_str = ', '.join(schedule.days_display) if days else 'все дни'
|
|
migration_logger.info(
|
|
f"Установлено расписание для {table_name}: "
|
|
f"{schedule_time} [{days_str}] (full_reload={full_reload})"
|
|
)
|
|
|
|
def get_schedule(self, table_name: str):
|
|
"""Получить расписание для таблицы"""
|
|
return self.repo.get_schedule(table_name)
|
|
|
|
def get_all_schedules(self):
|
|
"""Получить все расписания"""
|
|
return self.repo.get_all_schedules()
|
|
|
|
def disable_schedule(self, table_name: str):
|
|
"""Отключить расписание"""
|
|
self.repo.disable_schedule(table_name)
|
|
|
|
def enable_schedule(self, table_name: str):
|
|
"""Включить расписание"""
|
|
self.repo.enable_schedule(table_name)
|
|
|
|
def delete_schedule(self, table_name: str):
|
|
"""Удалить расписание"""
|
|
self.repo.delete_schedule(table_name)
|
|
|
|
def get_due_tables(self, current_time: Optional[datetime] = None) -> List:
|
|
"""Получить таблицы для запуска сейчас"""
|
|
due = self.repo.get_due_schedules(current_time)
|
|
return due
|
|
|
|
async def run_due_migrations(self):
|
|
"""Запустить миграции по расписанию"""
|
|
due_schedules = self.get_due_tables()
|
|
|
|
if not due_schedules:
|
|
return
|
|
|
|
migration_logger.info(f"Найдено {len(due_schedules)} таблиц для миграции по расписанию")
|
|
|
|
for schedule in due_schedules:
|
|
try:
|
|
days_str = ', '.join(schedule.days_display)
|
|
migration_logger.info(
|
|
f"Запуск миграции по расписанию для {schedule.table_name} "
|
|
f"в {schedule.schedule_time.strftime('%H:%M')} [{days_str}]"
|
|
)
|
|
|
|
# Запускаем миграцию
|
|
result = await asyncio.to_thread(
|
|
migrator.run_migration,
|
|
tables=[schedule.table_name],
|
|
full_reload=schedule.full_reload,
|
|
send_email=True
|
|
)
|
|
|
|
# Обновляем время последнего запуска
|
|
self.repo.update_schedule_last_run(schedule.table_name)
|
|
|
|
# Логируем успешный запуск
|
|
self.repo.log_operation(
|
|
table_name=schedule.table_name,
|
|
operation='SCHEDULED',
|
|
records_count=0,
|
|
status='SUCCESS'
|
|
)
|
|
|
|
migration_logger.info(f"Миграция по расписанию для {schedule.table_name} завершена")
|
|
|
|
except Exception as e:
|
|
error_msg = f"Ошибка при миграции по расписанию для {schedule.table_name}: {e}"
|
|
migration_logger.error(error_msg)
|
|
|
|
# Логируем ошибку
|
|
self.repo.log_operation(
|
|
table_name=schedule.table_name,
|
|
operation='SCHEDULED',
|
|
records_count=0,
|
|
status='ERROR',
|
|
error_message=str(e)[:500]
|
|
)
|
|
|
|
email_sender.send_error_notification(
|
|
error_message=error_msg,
|
|
table_name=schedule.table_name
|
|
)
|
|
|
|
def start_scheduler(self):
|
|
"""Запустить планировщик"""
|
|
self.running = True
|
|
migration_logger.info("Планировщик миграций запущен")
|
|
|
|
while self.running:
|
|
try:
|
|
# Проверяем, есть ли таблицы для миграции
|
|
asyncio.run(self.run_due_migrations())
|
|
|
|
# Ждем 60 секунд до следующей проверки
|
|
time_module.sleep(60)
|
|
|
|
except KeyboardInterrupt:
|
|
self.stop_scheduler()
|
|
break
|
|
except Exception as e:
|
|
migration_logger.error(f"Ошибка в планировщике: {e}")
|
|
time_module.sleep(60)
|
|
|
|
def stop_scheduler(self):
|
|
"""Остановить планировщик"""
|
|
self.running = False
|
|
migration_logger.info("Планировщик миграций остановлен")
|
|
|
|
|
|
# Глобальный экземпляр
|
|
scheduler = MigrationScheduler() |