# ТЗ: Фикс пагинации fr24_worker — commit per page + дедупликация **Дата:** 2026-04-22 **Статус:** READY FOR DEV **Приоритет:** Критический (из-за бага слиты 44K кредитов за ночь) --- ## Суть проблемы `fetch_flight_summaries()` накапливает **все** страницы в памяти, потом `run()` вставляет всё разом. При ошибке на любой странице — ничего не коммитится, все данные теряются. Дополнительно: `both:SVO,both:DME,both:VKO,both:ZIA` возвращает дубли — рейс SVO→DME попадает в выборку и как рейс SVO, и как рейс DME. Итого: ~3 500 уникальных рейсов → 14 660 записей (×4 дублирование). --- ## Изменения в `fetch_flight_summaries()` → убрать, заменить на генератор Файл: `ingest/tracks_fr24/fr24_worker.py` ### Было ```python def fetch_flight_summaries(target_date: date) -> List[Dict]: all_items = [] offset = 0 while True: data = _get(...) items = data.get("data", ...) all_items.extend(items) if len(items) < PAGE: break offset += PAGE return all_items ``` ### Стало — генератор страниц ```python def iter_flight_summary_pages(target_date: date): """Yield one page (list of flights) at a time. Stops on 402/empty.""" PAGE = 20 airports_param = _build_airports_param() offset = 0 seen_fr24_ids = set() # дедупликация между страницами while True: try: data = _get("/api/flight-summary/full", params={ "flight_datetime_from": f"{target_date}T00:00:00", "flight_datetime_to": f"{target_date}T23:59:59", "airports": airports_param, "limit": PAGE, "offset": offset, }) except Exception as e: log.error("fetch page offset=%d failed: %s", offset, e) break items = data.get("data", data) if isinstance(data, dict) else data if not items or not isinstance(items, list): break # Дедупликация по fr24_id unique = [x for x in items if x.get("fr24_id") not in seen_fr24_ids] seen_fr24_ids.update(x["fr24_id"] for x in items if x.get("fr24_id")) yield unique if len(items) < PAGE: break offset += PAGE ``` --- ## Изменения в `run()` — commit после каждой страницы ### Было ```python summaries = fetch_flight_summaries(target_date) stats["flights_found"] = len(summaries) for item in summaries: ...upsert... conn.commit() ``` ### Стало ```python for page in iter_flight_summary_pages(target_date): stats["flights_found"] += len(page) for item in page: fr24_id = item.get("fr24_id") if not fr24_id: continue try: actual_id = upsert_flight_actual(conn, item, target_date) if actual_id: stats["flights_upserted"] += 1 ...FETCH_TRACKS логика... except Exception as e: conn.rollback() stats["errors"] += 1 log.error("FR24: error processing %s: %s", fr24_id, e) # Коммит после каждой страницы try: conn.commit() log.debug("Committed page, total so far: %d", stats["flights_upserted"]) except Exception as e: conn.rollback() log.error("Commit failed: %s", e) stats["errors"] += 1 ``` --- ## Дополнительно — лимит страниц Добавить защитный лимит: ```python MAX_PAGES = int(os.getenv("FR24_MAX_PAGES", "200")) # 200 × 20 = 4000 рейсов max ``` В генераторе добавить счётчик: ```python page_num = 0 while True: ... page_num += 1 if page_num >= MAX_PAGES: log.warning("Reached MAX_PAGES=%d, stopping pagination", MAX_PAGES) break ``` В `config.py` добавить: ```python MAX_PAGES: int = int(os.getenv("FR24_MAX_PAGES", "200")) ``` --- ## Проверка После деплоя запустить: ``` POST http://fr24-vm:8001/run?date=2026-04-19 ``` (дата когда кредиты были) — проверить что `flight_actual` заполняется постепенно по страницам. В БД сразу должны появляться строки, не ждать конца всей загрузки. --- ## Файлы для изменения - `ingest/tracks_fr24/fr24_worker.py` — главные изменения - `ingest/tracks_fr24/config.py` — добавить `MAX_PAGES` **Деплой:** только `docker cp` (не rebuild), потом проверить синтаксис.