first commit

This commit is contained in:
brusnitsyn
2026-03-08 20:21:15 +09:00
commit c201d36ae6
24 changed files with 3770 additions and 0 deletions

View 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()