461 lines
19 KiB
Python
461 lines
19 KiB
Python
# app/services/replication_metadata_repo.py
|
||
|
||
from typing import Optional, List
|
||
from datetime import datetime
|
||
from sqlalchemy.orm import Session
|
||
from app.core.database import db_connector
|
||
from app.models.replication import ReplicationMetadata, ReplicationLog, ReplicationSchedule
|
||
from app.core.logging import migration_logger
|
||
|
||
|
||
class ReplicationMetadataRepo:
|
||
"""Репозиторий для работы с метаданными репликации"""
|
||
|
||
def __init__(self):
|
||
self.engine = db_connector.dst_engine
|
||
self.SessionLocal = db_connector.dst_session
|
||
|
||
def init_metadata_table(self):
|
||
"""Создает таблицу метаданных, если её нет"""
|
||
from app.models.replication import Base
|
||
Base.metadata.create_all(self.engine)
|
||
migration_logger.info("Таблица replication_metadata создана/проверена")
|
||
|
||
def get_table_metadata(self, table_name: str) -> Optional[ReplicationMetadata]:
|
||
"""Получает метаданные для таблицы"""
|
||
session = self.SessionLocal()
|
||
try:
|
||
metadata = session.query(ReplicationMetadata).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
# Если нет, создаем
|
||
if not metadata:
|
||
metadata = ReplicationMetadata.create_if_not_exists(session, table_name)
|
||
|
||
return metadata
|
||
finally:
|
||
session.close()
|
||
|
||
def get_last_sync_time(self, table_name: str) -> Optional[datetime]:
|
||
"""Получает время последней синхронизации таблицы"""
|
||
metadata = self.get_table_metadata(table_name)
|
||
return metadata.last_sync_time if metadata else None
|
||
|
||
def get_last_id(self, table_name: str) -> Optional[int]:
|
||
"""Получает последний обработанный ID для таблицы"""
|
||
metadata = self.get_table_metadata(table_name)
|
||
return metadata.last_id if metadata else None
|
||
|
||
def update_sync_time(self, table_name: str) -> bool:
|
||
"""Обновляет время синхронизации таблицы"""
|
||
session = self.SessionLocal()
|
||
try:
|
||
metadata = session.query(ReplicationMetadata).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if not metadata:
|
||
metadata = ReplicationMetadata(
|
||
table_name=table_name,
|
||
last_sync_time=datetime.now(),
|
||
last_id=0
|
||
)
|
||
session.add(metadata)
|
||
else:
|
||
metadata.last_sync_time = datetime.now()
|
||
metadata.updated_at = datetime.now()
|
||
|
||
session.commit()
|
||
migration_logger.debug(f"Updated sync time for {table_name} to {metadata.last_sync_time}")
|
||
return True
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Error updating sync time for {table_name}: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
def update_last_id(self, table_name: str, last_id: int) -> bool:
|
||
"""Обновляет последний обработанный ID"""
|
||
session = self.SessionLocal()
|
||
try:
|
||
metadata = session.query(ReplicationMetadata).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if not metadata:
|
||
metadata = ReplicationMetadata(
|
||
table_name=table_name,
|
||
last_sync_time=datetime.now(),
|
||
last_id=last_id
|
||
)
|
||
session.add(metadata)
|
||
else:
|
||
metadata.last_id = last_id
|
||
metadata.updated_at = datetime.now()
|
||
|
||
session.commit()
|
||
migration_logger.debug(f"Updated last_id for {table_name} to {last_id}")
|
||
return True
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Error updating last_id for {table_name}: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
def update_table_stats(self, table_name: str, added_rows: int) -> bool:
|
||
"""Обновляет статистику таблицы"""
|
||
session = self.SessionLocal()
|
||
try:
|
||
metadata = session.query(ReplicationMetadata).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if not metadata:
|
||
metadata = ReplicationMetadata(
|
||
table_name=table_name,
|
||
last_sync_time=datetime.now(),
|
||
total_rows=added_rows
|
||
)
|
||
session.add(metadata)
|
||
else:
|
||
metadata.total_rows += added_rows
|
||
metadata.updated_at = datetime.now()
|
||
|
||
session.commit()
|
||
return True
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Error updating stats for {table_name}: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
def log_operation(self, table_name: str, operation: str, records_count: int,
|
||
status: str = "SUCCESS", error_message: Optional[str] = None):
|
||
"""Логирует операцию репликации"""
|
||
session = self.SessionLocal()
|
||
try:
|
||
log = ReplicationLog(
|
||
table_name=table_name,
|
||
operation=operation,
|
||
records_count=records_count,
|
||
status=status,
|
||
error_message=error_message
|
||
)
|
||
session.add(log)
|
||
session.commit()
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Error logging operation: {e}")
|
||
finally:
|
||
session.close()
|
||
|
||
def get_all_stats(self) -> dict:
|
||
"""Получает статистику по всем таблицам"""
|
||
session = self.SessionLocal()
|
||
try:
|
||
metadata_list = session.query(ReplicationMetadata).all()
|
||
|
||
# Обрабатываем None значения в total_rows
|
||
total_rows = sum(m.total_rows for m in metadata_list if m.total_rows is not None)
|
||
active_tables = sum(1 for m in metadata_list if m.is_active)
|
||
|
||
return {
|
||
'total_rows': total_rows,
|
||
'tables_count': len(metadata_list),
|
||
'active_tables': active_tables,
|
||
'tables': [
|
||
{
|
||
'name': m.table_name,
|
||
'last_sync': m.last_sync_time,
|
||
'last_id': m.last_id,
|
||
'rows': m.total_rows,
|
||
'active': m.is_active
|
||
}
|
||
for m in metadata_list
|
||
]
|
||
}
|
||
finally:
|
||
session.close()
|
||
|
||
def get_session(self) -> Session:
|
||
return self.SessionLocal()
|
||
|
||
# ========== Методы для расписаний ==========
|
||
|
||
def init_default_schedules(self, table_names: List[str]):
|
||
"""Инициализирует расписания по умолчанию для списка таблиц"""
|
||
session = self.get_session()
|
||
try:
|
||
for table_name in table_names:
|
||
# Проверяем, есть ли уже расписание
|
||
schedule = session.query(ReplicationSchedule).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if not schedule:
|
||
# Создаем расписание по умолчанию (каждый день в 00:00)
|
||
schedule = ReplicationSchedule(
|
||
table_name=table_name,
|
||
schedule_time=datetime.strptime("00:00", "%H:%M").time(),
|
||
days=[], # Пустой список = все дни
|
||
full_reload=False,
|
||
enabled=True
|
||
)
|
||
session.add(schedule)
|
||
migration_logger.debug(f"Создано расписание по умолчанию для {table_name}")
|
||
|
||
session.commit()
|
||
migration_logger.info(f"Инициализированы расписания по умолчанию для {len(table_names)} таблиц")
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Ошибка инициализации расписаний: {e}")
|
||
finally:
|
||
session.close()
|
||
|
||
def add_schedule(self, table_name: str, schedule_time: str, days: Optional[List[str]] = None,
|
||
full_reload: bool = False, enabled: bool = True,
|
||
name: Optional[str] = None, description: Optional[str] = None) -> Optional[ReplicationSchedule]:
|
||
"""Добавить НОВОЕ расписание для таблицы"""
|
||
session = self.get_session()
|
||
try:
|
||
# Проверяем, существует ли метаданные
|
||
metadata = session.query(ReplicationMetadata).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if not metadata:
|
||
metadata = self._create_metadata(session, table_name)
|
||
|
||
# Парсим время
|
||
time_obj = datetime.strptime(schedule_time, "%H:%M").time()
|
||
|
||
# Создаем новое расписание
|
||
schedule = ReplicationSchedule(
|
||
table_name=table_name,
|
||
schedule_time=time_obj,
|
||
days=days if days else [],
|
||
full_reload=full_reload,
|
||
enabled=enabled,
|
||
name=name or f"{schedule_time} - {'полная' if full_reload else 'инкремент'}",
|
||
description=description
|
||
)
|
||
session.add(schedule)
|
||
session.commit()
|
||
|
||
migration_logger.info(f"Добавлено новое расписание для {table_name} в {schedule_time}")
|
||
return schedule.to_dict()
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Ошибка добавления расписания для {table_name}: {e}")
|
||
return None
|
||
finally:
|
||
session.close()
|
||
|
||
def delete_schedule(self, schedule_id: int) -> bool:
|
||
"""Удалить расписание по ID"""
|
||
session = self.get_session()
|
||
try:
|
||
schedule = session.query(ReplicationSchedule).filter_by(
|
||
id=schedule_id
|
||
).first()
|
||
|
||
if schedule:
|
||
session.delete(schedule)
|
||
session.commit()
|
||
migration_logger.info(f"Удалено расписание ID={schedule_id} для {schedule.table_name}")
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Ошибка удаления расписания: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
def update_schedule(self, schedule_id: int, **kwargs) -> bool:
|
||
"""Обновить существующее расписание по ID"""
|
||
session = self.get_session()
|
||
try:
|
||
schedule = session.query(ReplicationSchedule).filter_by(
|
||
id=schedule_id
|
||
).first()
|
||
|
||
if schedule:
|
||
for key, value in kwargs.items():
|
||
if hasattr(schedule, key) and value is not None:
|
||
if key == 'schedule_time' and isinstance(value, str):
|
||
value = datetime.strptime(value, "%H:%M").time()
|
||
setattr(schedule, key, value)
|
||
|
||
schedule.updated_at = datetime.now()
|
||
session.commit()
|
||
migration_logger.info(f"Обновлено расписание ID={schedule_id}")
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Ошибка обновления расписания: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
def get_schedule(self, table_name: str) -> List[ReplicationSchedule]:
|
||
"""Получает расписание для таблицы"""
|
||
session = self.get_session()
|
||
try:
|
||
schedule = session.query(ReplicationSchedule).filter_by(
|
||
table_name=table_name
|
||
).all()
|
||
return schedule
|
||
finally:
|
||
session.close()
|
||
|
||
def get_all_schedules(self) -> List[ReplicationSchedule]:
|
||
"""Получает все расписания"""
|
||
session = self.get_session()
|
||
try:
|
||
return session.query(ReplicationSchedule).all()
|
||
finally:
|
||
session.close()
|
||
|
||
def get_due_schedules(self, current_time: Optional[datetime] = None) -> List[ReplicationSchedule]:
|
||
"""Получает расписания, которые должны запуститься сейчас"""
|
||
if current_time is None:
|
||
current_time = datetime.now()
|
||
|
||
current_time_obj = current_time.time()
|
||
current_weekday = current_time.weekday()
|
||
|
||
session = self.get_session()
|
||
try:
|
||
schedules = session.query(ReplicationSchedule).filter_by(
|
||
enabled=True
|
||
).all()
|
||
|
||
due = []
|
||
for schedule in schedules:
|
||
# Проверяем время и день
|
||
time_diff = abs(
|
||
(schedule.schedule_time.hour * 60 + schedule.schedule_time.minute) -
|
||
(current_time_obj.hour * 60 + current_time_obj.minute)
|
||
)
|
||
|
||
if time_diff <= 1 and current_weekday in schedule.days_list:
|
||
due.append(schedule)
|
||
|
||
return due
|
||
finally:
|
||
session.close()
|
||
|
||
def update_schedule_last_run(self, table_name: str) -> bool:
|
||
"""Обновляет время последнего запуска расписания"""
|
||
session = self.get_session()
|
||
try:
|
||
schedule = session.query(ReplicationSchedule).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if schedule:
|
||
schedule.last_run = datetime.now()
|
||
schedule.updated_at = datetime.now()
|
||
session.commit()
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Ошибка обновления last_run для {table_name}: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
def disable_schedule(self, table_name: str) -> bool:
|
||
"""Отключает расписание"""
|
||
session = self.get_session()
|
||
try:
|
||
schedule = session.query(ReplicationSchedule).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if schedule:
|
||
schedule.enabled = False
|
||
schedule.updated_at = datetime.now()
|
||
session.commit()
|
||
migration_logger.info(f"Отключено расписание для {table_name}")
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Ошибка отключения расписания для {table_name}: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
def enable_schedule(self, table_name: str) -> bool:
|
||
"""Включает расписание"""
|
||
session = self.get_session()
|
||
try:
|
||
schedule = session.query(ReplicationSchedule).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if schedule:
|
||
schedule.enabled = True
|
||
schedule.updated_at = datetime.now()
|
||
session.commit()
|
||
migration_logger.info(f"Включено расписание для {table_name}")
|
||
return True
|
||
return False
|
||
except Exception as e:
|
||
session.rollback()
|
||
migration_logger.error(f"Ошибка включения расписания для {table_name}: {e}")
|
||
return False
|
||
finally:
|
||
session.close()
|
||
|
||
# ========== Методы для статистики ==========
|
||
|
||
def sync_table_stats(self, table_name: str, id_column: str):
|
||
"""
|
||
Синхронизирует статистику таблицы с реальными данными в PostgreSQL
|
||
"""
|
||
from app.services.data_reader import data_reader
|
||
|
||
stats = data_reader.get_table_stats(table_name, id_column)
|
||
|
||
session = self.get_session()
|
||
try:
|
||
metadata = session.query(ReplicationMetadata).filter_by(
|
||
table_name=table_name
|
||
).first()
|
||
|
||
if metadata:
|
||
metadata.total_rows = stats['total_rows']
|
||
metadata.last_id = stats['max_id']
|
||
metadata.updated_at = datetime.now()
|
||
session.commit()
|
||
|
||
migration_logger.info(f"Синхронизирована статистика {table_name}: {stats['total_rows']} строк, max_id={stats['max_id']}")
|
||
else:
|
||
migration_logger.warning(f"Метаданные для {table_name} не найдены")
|
||
finally:
|
||
session.close()
|
||
|
||
def sync_all_tables_stats(self, table_names: List[str], id_columns: dict):
|
||
"""
|
||
Синхронизирует статистику для всех таблиц
|
||
id_columns: словарь {table_name: id_column_name}
|
||
"""
|
||
migration_logger.info("Синхронизация статистики всех таблиц...")
|
||
|
||
for table_name in table_names:
|
||
id_column = id_columns.get(table_name, f"{table_name.split('_')[-1]}ID")
|
||
self.sync_table_stats(table_name, id_column)
|
||
|
||
migration_logger.info("Синхронизация статистики завершена")
|
||
|
||
# Глобальный экземпляр
|
||
replication_metadata_repo = ReplicationMetadataRepo() |