first commit
This commit is contained in:
461
app/repository/replication_metadata_repo.py
Normal file
461
app/repository/replication_metadata_repo.py
Normal file
@@ -0,0 +1,461 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user