161 lines
5.0 KiB
Markdown
161 lines
5.0 KiB
Markdown
# ТЗ: Фикс пагинации 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), потом проверить синтаксис.
|