#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ИНКРЕМЕНТАЛЬНЫЙ sync_rejections. Как было: каждые 5 минут `DELETE FROM rejected_signals`, затем вставка всех 37831 строк заново. Это ~22 млн операций записи в сутки ради таблицы, которая не растёт. Отсюда счётчик 13.9 млн и лишняя нагрузка на вакуум и WAL. Как станет: берём из журнала только записи новее, чем max(ts) в таблице. Полное обновление сохраняем — но раз в сутки (в 3 часа), чтобы не потерять свойство самопочинки, ради которого полный перезалив и делался. Логика не меняется ничем иным: те же поля, тот же INSERT. python3 sync_patch.py <файл> сухой прогон python3 sync_patch.py <файл> --apply применить (с бэкапом) """ import ast, re, shutil, sys, time PATH = sys.argv[1] if len(sys.argv) > 1 else "/opt/sovereign/grafana/sync_to_db.py" APPLY = "--apply" in sys.argv MARK = "# --- инкрементальный режим (arena) ---" OLD_DELETE = 'cur.execute("DELETE FROM rejected_signals")' NEW_BLOCK = '''{mark} # Полный перезалив — только раз в сутки, в 3 часа. В остальное время # берём лишь записи новее последней известной. Полный DELETE каждые # 5 минут давал ~22 млн операций записи в сутки на неизменной таблице. import datetime as _dt _full = _dt.datetime.utcnow().hour == 3 _cutoff = None if _full: cur.execute("DELETE FROM rejected_signals") else: cur.execute("SELECT max(ts) FROM rejected_signals") _row = cur.fetchone() _cutoff = _row[0] if _row else None def _too_old(_r): """True — запись уже в таблице, вставлять не нужно.""" if _cutoff is None: return False _t = _r.get("ts") if not _t: return False try: _p = _dt.datetime.fromisoformat(str(_t).replace("Z", "+00:00")) except ValueError: return False _c = _cutoff if _p.tzinfo and not _c.tzinfo: _c = _c.replace(tzinfo=_p.tzinfo) elif _c.tzinfo and not _p.tzinfo: _p = _p.replace(tzinfo=_c.tzinfo) return _p <= _c {mark}'''.format(mark=MARK) FILTER_ANCHOR = " for r in iter_json_records(f):" FILTER_NEW = (" for r in iter_json_records(f):\n" " if _too_old(r):\n" " continue") def main(): src = open(PATH, encoding="utf-8").read() print(f"файл: {PATH}") if MARK in src: print("✅ патч уже стоит") return problems = [] if OLD_DELETE not in src: problems.append("не найдена строка полного удаления") if FILTER_ANCHOR not in src: problems.append("не найден цикл чтения записей") if problems: print("❌ " + "; ".join(problems) + " — НЕ применяю, структура другая") return out = src.replace(OLD_DELETE, NEW_BLOCK.strip(), 1) out = out.replace(FILTER_ANCHOR, FILTER_NEW, 1) try: ast.parse(out) print("✅ результат синтаксически корректен") except SyntaxError as e: print(f"❌ патч ломает синтаксис (строка {e.lineno}): {e.msg} — НЕ применяю") return print("\nчто меняется:") print(" 1. DELETE всей таблицы → только в 3 часа; иначе берём max(ts)") print(" 2. в цикле чтения добавлен пропуск записей не новее max(ts)") print(f" прирост файла: {len(out)-len(src)} байт") if not APPLY: print("\nсухой прогон. Для применения добавьте --apply") return bak = f"{PATH}.bak.{int(time.time())}" shutil.copy2(PATH, bak) open(PATH, "w", encoding="utf-8").write(out) print(f"\n✅ применено. Бэкап: {bak}") if __name__ == "__main__": main()