---
type: tech-risks
work_item_id: ET-008
title: "Технические риски — ET-008: GPS-треки с публичных платформ"
version: 1
status: approved
created_at: 2026-06-01
authors:
  - "agent:architect"
---

# Технические риски — ET-008

Технические риски этапа разработки и эксплуатации. Бизнес-риски — в BRD §6 (пересечение есть, здесь акцент на технические митигации). Шкала: вероятность (Н/С/В) × влияние (Н/С/В).

## R-1 — Парсер источника ломается при изменении HTML

- **Описание:** ADR-010/011 источники (`enduro_russia`, `ttrails`) скрейпят HTML-страницы. Платформа может в любой момент изменить разметку (новый шаблон, JS-rendering) → парсер перестаёт извлекать треки.
- **Вероятность / Влияние:** В / С.
- **Митигация:**
  - Каждый source в отдельном модуле (`src/api/gps_tracks/sources/<name>.py`); падение одного не валит других (ADR-007 §I-A).
  - Pipeline пишет `status=error` в `pipeline_runs`; оператор видит через `/api/gps-tracks/health`.
  - Параметризированные тесты с фикстурами HTML-снапшота — при первом упавшем прогоне разработчик обновляет фикстуру и парсер за 1 итерацию.
  - При двух неудачных прогонах подряд — алерт (`07-infra-requirements.md` §10.1). На MVP — ручная проверка.
  - Конфиг `gps_sources.yaml::enabled: false` — мгновенное отключение источника без deploy.

## R-2 — Ложные коллизии дедупа

- **Описание:** ADR-006 алгоритм `bbox+length+date bucket` детерминированно мерджит треки с похожими параметрами. На треках без `created_at` (от источников без даты) — гарантированный merge всех таких треков в одном bbox/length. На дата-датасете — возможны коллизии для популярных маршрутов (двое разных гонщиков проехали тот же 30-км круг в один день).
- **Вероятность / Влияние:** С / С.
- **Митигация:**
  - BRD §5 фиксирует допустимую метрику «< 5% дублей»; QA-скрипт `scripts/dedup_audit.py` проверяет на выборке 100 треков (`04-test-plan.yaml`).
  - При провале метрики — план отступления ADR-006 §8 (сузить length-bucket, добавить activity в ключ).
  - Если меняется формула dedup_key — полный rebuild БД (`rm + python -m scripts.gps_collect`); регенерация ≤ 6 часов.
  - Документация в `08-data-requirements.md` §3.2 для оператора.

## R-3 — Pipeline повреждает БД

- **Описание:** Бaг в Python-коде upsert (ADR-006 §6) при ON CONFLICT может оставить БД в несогласованном состоянии (битый JSON в `sources_json`, частично записанная transaction). SQLite + WAL обычно atomic per-statement, но composite upsert может рассогласоваться.
- **Вероятность / Влияние:** Н / В.
- **Митигация:**
  - Все upsert операции — внутри SQLite `BEGIN IMMEDIATE / COMMIT` (atomic transaction).
  - Ежедневный backup `data/gps_tracks.sqlite` (`07-infra-requirements.md` §4.4).
  - При повреждении: `cp backups/gps_tracks-<date>.sqlite data/gps_tracks.sqlite` + cache-clear API. RTO ≈ 1–2 минуты.
  - Полный rebuild: `rm gps_tracks.sqlite && docker compose --profile batch run --rm gps-collector` — ≤ 6 часов.
  - Изоляция в отдельной БД (ADR-005 D-A) гарантирует, что повреждение не затронет `centralfederal.sqlite` (OSM-данные).

## R-4 — Размер БД превышает 2 ГБ

- **Описание:** REQ-NF-03 предел `data/gps_tracks.sqlite` — 2 ГБ. На MVP-объёме (5000 треков ≈ 105 МБ) запас 20×. Но при расширении на РФ или при отсутствии работающего GC размер может вырасти линейно.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - Health-эндпоинт отдаёт `db_size_mb` — оператор видит.
  - Месячный GC `--gc` удаляет треки старше 5 лет (`07-infra-requirements.md` §7.1).
  - При устойчивом росте > 2 ГБ — миграция на PostGIS (отдельный work item; контракт API стабилен, см. ADR-005 §«Технический долг»).
  - Алерт `db_size_mb > 2000` — пока ручная проверка (post-MVP — автоматический).

## R-5 — IP mva154 банится источником

- **Описание:** Скрейпер с фиксированного IP может попасть в чёрный список платформы (особенно при ошибках rate-limit). Pipeline начинает возвращать 429/403 на все запросы → source не пополняется.
- **Вероятность / Влияние:** С / С.
- **Митигация:**
  - Rate-limit в `gps_sources.yaml` per-source (1 сек для OSM, 5 сек для скрейп-источников).
  - Корректный User-Agent с контактом — платформа может связаться, прежде чем банить.
  - Backoff на 429 (`TRZ §6.3`) — exponential до 3 попыток.
  - `pipeline_runs.errors_json` фиксирует HTTP-коды → оператор видит.
  - При бане — приостановить source (`enabled: false`), связаться с платформой, при необходимости отключить полностью.
  - **Прокси через сторонний IP** — не закладывается (нарушает дух прозрачности).

## R-6 — Pipeline жрёт ресурсы и деградирует API во время прогона

- **Описание:** На время прогона `gps-collector` контейнер активен, скачивает GPX, парсит, пишет в БД. Если ресурсы не ограничены — `httpx` + `shapely` могут уйти в GC-storm; SQLite write lock конкурирует с API readers.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - Docker `cpus: "1.0"`, `mem_limit: 512m` (`07-infra-requirements.md` §9.2). Pipeline не вытесняет API даже на одно-CPU-сервере.
  - WAL-mode позволяет API читать БД во время записи pipeline'а (ADR-005 W-A).
  - Cron в 03:00 UTC = 06:00 MSK — низкий traffic.
  - Async-генератор `parser.collect()` — pipeline pulls треки по одному, не накапливает в памяти больше одного (ADR-007 §4).

## R-7 — Дублирование tile-утилит между `main.py` и `gps_tracks/mvt.py`

- **Описание:** ADR-005 §8 принимает дублирование `tile_to_bbox` / `wkb_to_coords` / `simplify_coords` (≈ 100 строк) ради избежания риска регрессии существующего слоя `trails`. Любая правка формулы упрощения требует синхронной правки в двух местах.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - Комментарий в обоих файлах `# ET-008/ADR-005-§8: дубль из main.py; при добавлении третьего MVT-источника — вынести в src/api/tiles_util.py`.
  - Code review-чеклист: при правке `simplify_coords` в одном файле — проверить второй.
  - При появлении третьего MVT-источника — обязательный рефакторинг (отдельный work item).

## R-8 — GeoJSON-эндпоинт превышает SLA на плотных bbox

- **Описание:** REQ-NF-02 предел 300 мс p95 на bbox с ≤ 500 треков. На реальной географии возможны bbox в плотных регионах (например, Подмосковье на z=12) где `total_in_bbox > 2000`. SQL даже с R-tree может проигрывать при ORDER BY + post-filter source.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - Cutoff `limit=500` обрезает результат на уровне SQL.
  - Cutoff zoom 12 — на z=11 уходим в MVT-кэш, нагрузки на GeoJSON-endpoint нет.
  - R-tree обеспечивает O(log n) bbox-prefetch.
  - Дополнительный индекс по `length_m DESC` для ORDER BY (длинные треки приоритетнее) — фиксируется в коде; SQLite сделает sort быстро на 500 строках.
  - Если SLA не выполняется — server-side кэширование GeoJSON-ответов по `(bbox_quantized, activity, source)` (post-MVP).

## R-9 — Лицензионный ADR не enforced

- **Описание:** ADR-007 §6 требует, чтобы pipeline отказывался загружать source-parser без `accepted`-ADR. Если разработчик обходит проверку (например, забывает добавить `license_adr:` поле в `gps_sources.yaml`) — pipeline пойдёт скрейпить без юридического подтверждения. BRD §4 явно требует «зелёного света».
- **Вероятность / Влияние:** Н / В.
- **Митигация:**
  - Pydantic-валидация `gps_sources.yaml` — поле `license_adr` обязательное, отсутствие → exception при старте pipeline.
  - Дополнительная проверка в runtime: `license_adr` должен указывать на существующий файл; YAML frontmatter `status: accepted`. Иначе source skip с `status: skipped_license`.
  - Code review-чеклист в `12-review.md`: при добавлении source в `gps_sources.yaml` обязательна ссылка на accepted-ADR.
  - QA-кейс: `tests/api/test_gps_tracks_licensing_guard.py` — поднимает pipeline с `proposed`-ADR, проверяет что source пропускается.

## R-10 — Cache-clear endpoint доступен извне

- **Описание:** `POST /api/gps-tracks/cache/clear` сбрасывает LRU. Если эндпоинт доступен через `/enduro/` — атакующий может вызывать его в цикле, обнуляя кэш и заставляя сервер постоянно перегенерировать тайлы (DoS).
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - `07-infra-requirements.md` §3.1: nginx-правило `location = /enduro/api/gps-tracks/cache/clear { allow 172.16/12; deny all; }`.
  - Pipeline ↔ API дёргает endpoint напрямую через docker-сеть, минуя nginx → работает.
  - При появлении CSP-заголовка — `connect-src 'self'` блокирует внешние POST'ы из браузера (но это уже есть).

## R-11 — Pipeline зависает (вечная проблема скрейперов)

- **Описание:** Парсер одного источника попадает в бесконечный pagination loop или висит на медленном HTTP. Cron-job не завершается, следующий cron-тик попадает на ту же задачу.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - `httpx.AsyncClient(timeout=30)` — таймаут на каждый запрос.
  - Per-source максимум треков на прогон (`max_tracks_per_run` в `gps_sources.yaml`, default 5000) — стопорит pagination loop.
  - Cron-окно (3 дня между прогонами) > потенциального hang-окна; overlapping runs — два docker container'а, ресурсы изолированы; следующий cron не блокируется первым.
  - Опционально: `timeout 21600 docker compose ...` в cron — kill после 6 часов (REQ-NF-02). На MVP — не обязательно, но рекомендовано.

## R-12 — Несогласованность UI/style при `setStyle()`

- **Описание:** При переключении тёмной темы / спутника `map.setStyle()` сбрасывает все runtime-добавленные source/layer. `rebuildMapOverlays()` пересоздаёт; если порядок вызовов нарушен — слой публичных треков может оказаться поверх маршрута или ниже спутника.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - `restorePublicTracksState()` вызывается в `rebuildMapOverlays()` после `restoreTrailsState()`, до `restorePoiState()` и маршрута/GPX (TRZ REQ-F-19).
  - AC-12 «Переживание setStyle()» проверяет: чекбокс работает после смены темы.
  - Идемпотентные `if (!map.getSource(id)) map.addSource(...)` — паттерн из ADR-004 R-6.

## R-13 — Конфликт с ET-006 (личные GPX)

- **Описание:** ET-006 хранит личные GPX треки в `window.gpxTracks` и отображает как `gpx-layer-*`. Если ET-008 случайно использует тот же layer-id или event-handler — взаимная коллизия.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - Префикс `gps-tracks-*` для всех новых id (source, layer, halo) — конфликт исключён.
  - `window.gpsTracksLayer` ≠ `window.gpxTracks` (TRZ §4.4).
  - Z-order: `gps-tracks-layer-*` < `gpx-layer-*` (личные приоритетнее, как уточняется в TRZ §7.1).
  - AC-10 «Совместимость с ET-006» проверяет совместное отображение.

## R-14 — Конфликт с ET-007 (спутник + halo)

- **Описание:** ET-007 уже реализовал паттерн halo для trails на спутнике через `applyTrailHaloVisibility()` (ADR-004 §9). ET-008 добавляет два новых halo (`gps-tracks-halo-mvt-satellite`, `gps-tracks-halo-geo-satellite`) и расширяет `applyBaseLayer()`.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - Новые halo-слои добавляются в оба `style.json` / `style-dark.json` с `visibility: none` — по тому же паттерну ET-007.
  - `applyBaseLayer()` (ET-007) расширяется одним блоком (см. TRZ §7.2):
    ```js
    const gpsHaloOn = (currentBase === 'satellite' && layerState.publicTracks);
    setLayoutProperty('gps-tracks-halo-mvt-satellite', 'visibility', gpsHaloOn && activeMode === 'mvt' ? 'visible' : 'none');
    setLayoutProperty('gps-tracks-halo-geo-satellite', 'visibility', gpsHaloOn && activeMode === 'geo' ? 'visible' : 'none');
    ```
  - AC-11 «Halo на спутнике» проверяет.

## R-15 — Pipeline не находит зависимости (defusedxml, pyyaml)

- **Описание:** При смене образа без полного rebuild — `gps-collector` стартует с старым `requirements.txt` → ImportError.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - Deploy-runbook (§7) явно требует `docker compose build` перед запуском нового pipeline.
  - CI-job собирает образ при каждом push → новые зависимости видны на CI, а не в production.

## R-16 — Атрибуция теряется при включении/выключении источников

- **Описание:** BRD-метрика «атрибуция каждого активного источника видна». При динамическом изменении набора enabled-источников (например, оператор временно выключил `ttrails` в `gps_sources.yaml`) клиент может продолжать показывать атрибуцию, потому что в БД уже есть треки с `sources_json` содержащим `ttrails`.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - Атрибуция формируется на клиенте из `/api/gps-tracks/health.tracks_by_source` (только source с tracks_count > 0). Если в БД остались `ttrails` записи — атрибуция корректно отображает.
  - Если source удалён + треки удалены — `tracks_by_source` его не содержит → атрибуция корректно скрывается.
  - AC-15 проверяет.

## Сводная таблица

| ID | Риск | Вер. | Влияние | Класс | Статус |
|---|---|---|---|---|---|
| R-1 | Парсер ломается при смене HTML | В | С | Высокий | принят + per-source изоляция + алерт |
| R-2 | Ложные коллизии dedup | С | С | Средний | принят + метрика BRD + план отступления |
| R-3 | Pipeline повреждает БД | Н | В | Средний | atomic tx + ежедневный backup + rebuild за 6 ч |
| R-4 | Размер БД > 2 ГБ | Н | С | Низкий | GC + health + миграция на PostGIS |
| R-5 | IP mva154 банится | С | С | Средний | rate-limit + UA + backoff + отключение источника |
| R-6 | Pipeline деградирует API | Н | С | Низкий | cgroup limits + WAL + ночное окно |
| R-7 | Дублирование tile-утилит | С | Н | Низкий | принят + комментарии в коде + review-чеклист |
| R-8 | GeoJSON SLA на плотных bbox | С | Н | Низкий | limit + zoom-cutoff + R-tree |
| R-9 | Licensing-ADR не enforced | Н | В | Высокий | runtime-guard + Pydantic-валидация + тест |
| R-10 | Cache-clear доступен извне | Н | С | Низкий | nginx allow/deny |
| R-11 | Pipeline зависает | Н | С | Низкий | httpx timeout + max_tracks_per_run + (опц.) timeout cron |
| R-12 | UI несогласован после setStyle | Н | С | Низкий | паттерн ADR-004 + AC-12 |
| R-13 | Конфликт с ET-006 (GPX) | Н | С | Низкий | префикс + параллельные модели + AC-10 |
| R-14 | Конфликт с ET-007 (halo) | Н | С | Низкий | новые halo по тому же паттерну + AC-11 |
| R-15 | Зависимости pipeline | Н | Н | Низкий | CI-build + runbook |
| R-16 | Атрибуция теряется | Н | Н | Низкий | health-derived rendering |

**Высокие классы:**
- R-1 — операционный, ожидаемый для скрейп-источников; митигация — per-source изоляция и быстрое отключение через конфиг.
- R-9 — критический для legal compliance; митигация многослойная (Pydantic + runtime check + тест).

**Блокирующих рисков нет.** R-1 и R-9 требуют внимания разработки и code review, но не блокируют merge.

## Эскалация

- **arch:major-change** — выставлен на ADR-005 (новая БД) и ADR-007 (новый сервис + cron). Требует архитектурного approve перед merge.
- **back-to:analysis** — не требуется. ТЗ полное, BRD ясный, ADR-010 и ADR-011 явно блокирующие до закрытия licensing review (это операционный pre-requisite, не дефект анализа).
