""" =============================================================================== FILE: services/snapshots/retention.py ROLE: Политика ротации и очистки промежуточных почасовых срезов в SQLite. =============================================================================== """ import logging from datetime import datetime, timedelta from typing import List, Tuple from core.connection import get_connection logger = logging.getLogger("SNAPSHOT_RETENTION") def cleanup_old_intermediate_snapshots(days_to_keep_all: int = 2) -> int: """ Очищает промежуточные дневные срезы старше days_to_keep_all дней. Правило сохранения для архивных дней: - Сохраняется срез Y (23:59:59). - Сохраняется полуденный срез (ближайший к 13:00). - Все остальные промежуточные снапшоты удаляются из scud_logs. Возвращает количество удаленных записей. """ cutoff_date = datetime.now() - timedelta(days=days_to_keep_all) cutoff_str = cutoff_date.strftime("%Y-%m-%d") deleted_rows_count = 0 with get_connection() as conn: cursor = conn.cursor() # 1. Получаем список всех уникальных дат старше cutoff_date cursor.execute(""" SELECT DISTINCT log_date FROM scud_logs WHERE snapshot_time IS NOT NULL """) all_dates = [row[0] for row in cursor.fetchall()] for raw_date in all_dates: try: # Преобразуем формат ДД.ММ.ГГГГ в объект даты d_clean = raw_date.replace('_', '.') dt_obj = datetime.strptime(d_clean, "%d.%m.%Y") if dt_obj >= cutoff_date: continue # Свежие дни не трогаем — там хранятся все срезы # 2. Для архивной даты ищем все снапшоты cursor.execute(""" SELECT DISTINCT snapshot_id, snapshot_time FROM scud_logs WHERE log_date = ? """, (raw_date,)) snapshots = cursor.fetchall() if len(snapshots) <= 2: continue # Если и так 1-2 среза, ничего чистить не нужно keep_ids = set() # Ищем финальный срез Y / 23:59:59 / 22:00:00 for s_id, s_time in snapshots: s_id_str = str(s_id) s_time_str = str(s_time) if s_id_str.startswith('Y') or '23:59:59' in s_time_str or '22:00:00' in s_time_str: keep_ids.add(s_id) break # Ищем полуденный срез (ближайший к 13:00) noon_target = datetime.strptime(f"{d_clean} 13:00:00", "%d.%m.%Y %H:%M:%S") best_noon_id = None min_noon_diff = float('inf') for s_id, s_time in snapshots: if s_id in keep_ids: continue try: raw_t = str(s_time).strip() if '.' in raw_t.split()[0]: dt_s = datetime.strptime(raw_t, "%d.%m.%Y %H:%M:%S") else: dt_s = datetime.strptime(raw_t, "%Y-%m-%d %H:%M:%S") diff = abs((dt_s - noon_target).total_seconds()) if diff < min_noon_diff: min_noon_diff = diff best_noon_id = s_id except Exception: continue if best_noon_id: keep_ids.add(best_noon_id) # 3. Все снапшоты, не попавшие в keep_ids, удаляем to_delete_ids = [s_id for s_id, _ in snapshots if s_id not in keep_ids] for del_id in to_delete_ids: cursor.execute("DELETE FROM scud_logs WHERE snapshot_id = ?", (del_id,)) deleted_rows_count += cursor.rowcount logger.info(f"Удален промежуточный архивный срез {del_id} за {raw_date}") except Exception as e: logger.warning(f"Ошибка при обработке ротации за дату {raw_date}: {e}") conn.commit() if deleted_rows_count > 0: logger.info(f"Ротация завершена: удалено {deleted_rows_count} строк промежуточных срезов.") return deleted_rows_count