---
type: tech-risks
work_item_id: ET-015
title: "Технические риски — ET-015: Healthcheck enduro-trails-app через python urllib"
version: 1
status: approved
created_at: 2026-06-05
authors:
  - "agent:architect"
---

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

Технические риски замены `curl` на python `urllib.request`-one-liner
в healthcheck сервиса `app`. Бизнес-риски — в BRD §3 (ложные алёрты,
эрозия доверия, невозможность SLO). Шкала: вероятность (Н/С/В) ×
влияние (Н/С/В).

## R-T-1 — Python one-liner крэшится из-за квотинга / парсинга YAML

- **Описание:** YAML-парсер Compose может неожиданно интерпретировать
  кавычки внутри python-строки. Например, `'http://...'` внутри
  `"... urlopen('http://...', timeout=3) ..."` — двойные кавычки
  снаружи, одинарные внутри. Если строка где-то будет некорректно
  «склеена», команда попадёт в Docker в покорёженном виде и `python -c`
  свалится с SyntaxError.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - **Архитектурное решение (ADR-020 §«Решение»):** YAML-массив
    `["CMD", "python", "-c", "..."]`, **не** `CMD-SHELL`. Docker
    передаёт аргументы напрямую через `exec()`, без `/bin/sh -c`,
    без двойного парсинга. Кавычки внутри 4-го элемента — обычные
    символы строки, YAML их не трогает.
  - **Acceptance гейт:** ST-03 — `python -c 'import yaml,sys;
    print(yaml.safe_load(...)["services"]["app"]["healthcheck"]["test"])'`
    проверяет, что массив парсится корректно и 4-й элемент содержит
    `urllib.request` и `sys.exit`.
  - **Integration гейт:** IT-01 — `docker compose up -d app` и
    переход в `healthy` за ≤ 120 с подтверждает, что строка дошла до
    Docker в правильном виде.

## R-T-2 — Стоимость fork+exec python каждые 30 секунд

- **Описание:** Каждый цикл healthcheck поднимает отдельный процесс
  `python` (~80–150 мс старт интерпретатора). На фоне нагруженного
  uvicorn это может создавать заметный CPU-spike.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** интервал 30 с → накладные расходы
    ~150 мс / 30 с = **0.5% от одного ядра** в пике, в среднем —
    ~0.005% CPU. На mva154 (BRD: idle ~1–2% CPU `app`) это
    пренебрежимо.
  - **Оценка:** даже при росте интервала до `interval: 10s` (что не
    планируется) overhead остался бы < 2% от одного ядра в пике.
  - **Мониторинг:** наблюдение `docker stats enduro-trails-app-1` в
    течение суток после деплоя (см. `07-infra-requirements.md` §7.1).

## R-T-3 — Внутренний `timeout=3` короче, чем фактическое время ответа `/api/health`

- **Описание:** Сейчас `/api/health` отвечает за ~7 мс (BRD §1). Но
  при высокой нагрузке uvicorn (например, медленный SELECT в другом
  запросе блокирует event loop) `/api/health` может отвечать за
  > 3 с, healthcheck свалится в `unhealthy`.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - **Архитектурное решение:** `/api/health` — лёгкий sync handler
    (`async def health()` отдаёт сразу JSON, без IO в БД). FastAPI/uvicorn
    обслуживает его очень быстро. ~7 мс — стабильно.
  - **Запас:** внутренний 3 с — это **430× медленнее** текущего
    среднего ответа. Чтобы попасть в timeout, нужно 430-кратное
    замедление endpoint'а — это уже не «загруженность», а инцидент.
  - **Контракт:** если healthcheck начинает фейлиться из-за
    timeout — это **корректный сигнал**, что приложение деградировало.
    То, что нужно от healthcheck'а.
  - **TD (ADR-020 TD-1):** если `/api/health` станет «дорогим»
    (расширят проверками OSRM/тайлов), нужно будет либо увеличить
    timeout, либо разнести «liveness» и «readiness» — отдельный work-item.

## R-T-4 — `urllib.request.urlopen` бросает разные exception'ы на разные ошибки → разный exit code

- **Описание:** При connection refused — `URLError`; при HTTP 4xx/5xx
  — `HTTPError`; при timeout — `socket.timeout` (или `TimeoutError` в
  3.10+). Все они приведут к ненулевому exit code, но конкретное
  значение зависит от Python. Если в будущем кто-то напишет логику
  «если exit code 1 — это connection refused, если 2 — это
  timeout», она не сработает.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** Docker трактует «exit code 0» как
    healthy, «всё остальное» как unhealthy. Семантика binary,
    различать конкретные коды не нужно (TRZ §3.1).
  - **Документация:** ADR-020 §«Решение» явно фиксирует «status != 200
    → exit 1; любой raise → ненулевой код».
  - **Будущее:** если когда-нибудь захочется различать «приложение
    отвалилось» vs «приложение тормозит», нужно переходить на
    `scripts/healthcheck.py` (TD-3 в ADR-020) с явным `try/except` и
    `sys.exit(2)` / `sys.exit(3)`. YAGNI.

## R-T-5 — `start_period: 20s` слишком короткий или слишком длинный

- **Описание:** Если uvicorn будет грузиться > 20 с (например, при
  холодном кэше БД или большой инициализации), первые проверки
  выпадут как `unhealthy` уже до окончания start_period.
  Противоположно — если задать слишком большой start_period (например,
  120 с), новый деплой будет долго «висеть в starting», что в
  CI/CD затянет проверку.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение (ADR-020 §«Решение»):** uvicorn в этом
    проекте поднимается за < 2 с (вне холодного docker pull). 20 с —
    10× запас, комфортный для редких холодных стартов после
    `docker compose up`.
  - **Контракт start_period в Docker:** в течение start_period
    проваленный healthcheck **не** увеличивает `FailingStreak`. Если
    проверка пройдёт хотя бы раз в start_period, контейнер сразу
    переходит в `healthy`. То есть слишком длинный start_period
    «безопасен» (просто отложит признание `unhealthy` при реальном
    отказе), а слишком короткий — приведёт к ложному `unhealthy`
    при первом запуске.
  - **Acceptance гейт:** AC-01 (≤ 120 с до healthy) включает
    start_period в окно проверки.

## R-T-6 — `localhost:5556` внутри контейнера резолвится не туда

- **Описание:** В некоторых конфигурациях `localhost` может
  резолвиться в IPv6 `::1`, а uvicorn слушает только IPv4 `0.0.0.0`
  (см. Dockerfile CMD). Тогда healthcheck-запрос пойдёт на v6 и не
  достучится.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - **Архитектурное решение:** в `python:3.12-slim` дефолтный
    `getaddrinfo` для `localhost` возвращает обе семьи, `urllib`
    пробует их по порядку. На IPv4-host (mva154) `127.0.0.1` доступен
    первым.
  - **Fallback при провале:** если на каком-то Docker Engine начнут
    наблюдаться проблемы, переписать URL на явный
    `http://127.0.0.1:5556/api/health` (правка ~10 символов).
  - **Acceptance гейт:** IT-01..IT-04 на dev-машине + E2E-01 на
    mva154 проверяют реальное поведение.

## R-T-7 — Эндпоинт `/api/health` в будущем переедет/переименуется

- **Описание:** Сейчас `/api/health` живёт в `src/api/main.py:1224`.
  Если кто-то рефакторит API под APIRouter и сменит путь (например,
  на `/api/v1/health` или `/healthz`), healthcheck сломается.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - **AC-08:** диф `src/api/main.py` против main по `/api/health`
    проверяется — изменения контракта блокируют merge ET-015.
  - **Долгосрочная:** ADR-020 фиксирует, что путь зашит в YAML.
    При будущей миграции на APIRouter (если случится) разработчик
    увидит ADR-индекс, найдёт упоминание `/api/health` и обновит
    healthcheck-команду одной правкой YAML.
  - **CHANGELOG/ADR трейл:** будущая правка пути сама по себе должна
    породить ADR (это change cross-cutting).

## R-T-8 — Python `urllib` SSL/TLS-проверка прокинется на loopback

- **Описание:** Мы делаем `http://...` (не HTTPS), но если кто-то в
  будущем перенесёт healthcheck на `https://localhost`, потребуется
  валидный сертификат или `ssl._create_unverified_context()`.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** на loopback HTTPS не нужен. TLS
    терминирует nginx на хосте, не внутри контейнера.
  - Если когда-нибудь uvicorn получит TLS прямо в контейнере (вряд
    ли — текущий деплой не предполагает), нужно будет либо обходить
    проверку, либо ставить самоподписанный CA в образ. Это уже
    серьёзная архитектурная смена → новый ADR.

## R-T-9 — `python` alias исчезнет в будущих базовых образах

- **Описание:** `python:3.12-slim` сейчас имеет
  `/usr/local/bin/python` и `/usr/local/bin/python3` (оба ведут на
  `python3.12`). Если апстрим решит оставить только `python3`,
  healthcheck-команда сломается.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** Python Docker official images
    поддерживают alias `python` в slim/full тегах как минимум до 3.13.
    Никаких признаков deprecation.
  - **Тривиальная правка:** при необходимости — заменить `python` на
    `python3` в YAML (1 символ).
  - **Тест:** UT-01..UT-03 запускают `python -c` на dev-машине; если
    alias сломан локально, заметим до деплоя.

## R-T-10 — Поломка YAML-формата при ручной правке (длинная строка)

- **Описание:** YAML-строка ~135 символов читается тяжело. Будущий
  редактор может случайно разорвать её newline'ом без `\`, получится
  невалидный YAML.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - **Архитектурное решение (TRZ R-5):** строка лежит внутри
    YAML-массива, длина не ограничена. При желании можно перейти
    на block-scalar (`>-` или `|`) — позволит разнести по строкам.
    Сейчас оставлено в одну строку для read-as-one-blob.
  - **Acceptance гейт:** ST-03 — YAML-парсер на CI поймает поломку.
  - **`make lint`:** валидирует compose YAML.

## R-T-11 — Гонка между `docker compose up -d app` и healthcheck'ом во время деплоя

- **Описание:** В момент пересоздания контейнера старый
  `enduro-trails-app-1` останавливается, новый запускается. Если
  пайплайн деплоя сразу же опрашивает `State.Health.Status`, он
  может прочитать `starting` или даже краткий `unhealthy` и принять
  это за провал.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - **Архитектурное решение (`07-infra-requirements.md` §6.2):**
    процедура деплоя описывает waiting-loop с тайм-аутом 120 с
    (AC-01). Никакой immediate-fail policy.
  - **`start_period: 20s`** буферизирует холодный старт: первые ~20 с
    проваленные проверки не учитываются в `FailingStreak`.

## R-T-12 — Healthcheck помечает контейнер healthy, но БД недоступна

- **Описание:** `/api/health` сейчас возвращает 200 даже если
  `db_exists == false`. Healthcheck скажет `healthy`, хотя приложение
  не сможет отдать `/api/trails`. Ложно-положительный сигнал.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - **Out of scope ET-015** (BRD §7): углубление содержимого
    `/api/health` — отдельный work-item. ADR-020 это явно фиксирует
    как TD-1.
  - **Текущее поведение:** `/api/health` отдаёт `db_exists` в JSON, но
    HTTP-статус — 200. Healthcheck смотрит только на статус.
  - **Не делает хуже:** до ET-015 healthcheck был **всегда** ложным
    (`unhealthy` при работающем приложении). После — healthcheck
    станет **частично достоверным** (фиксирует «uvicorn слушает порт
    и роутер жив»). Это **улучшение**, не «новая дыра».
  - **Будущее:** при появлении мониторинга на базе
    `State.Health.Status` — можно ввести более глубокий
    `/api/health` (с проверкой БД/OSRM), и поведение healthcheck'а
    «бесплатно» углубится. ADR-020 это не блокирует.

## R-T-13 — Ложное срабатывание AC-05 (переход в unhealthy при остановке uvicorn)

- **Описание:** AC-05 / IT-03 требуют, чтобы при остановке uvicorn
  внутри контейнера healthcheck перешёл в `unhealthy` за ≤ 120 с.
  Способ «kill -STOP 1» из ТЗ останавливает init-процесс, но при
  этом останавливается и сам healthcheck-процесс (он же child от 1).
  Возможны странные эффекты на Docker'е.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Альтернативная методика теста (test-plan IT-03):** допустимо
    делать `docker stop` контейнера и проверять, что Docker сам
    помечает `unhealthy` (или просто `exited`).
  - **Реальный сценарий отказа:** uvicorn вернёт 500 на `/api/health`
    при сбое внутреннего state, либо вообще не примет соединение
    (process aborted). В обоих случаях python `urlopen` поднимет
    исключение → ненулевой exit code → Docker фиксирует `unhealthy`.
  - **Это покрывается AC-05 семантически**, не buchstäblich на kill.

## R-T-14 — `make lint` падает на длинной строке в YAML

- **Описание:** Если в проекте настроен `yamllint` с правилом
  `line-length: max 120`, наша 135-символьная строка не пройдёт.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** `yamllint`-конфиг можно либо не
    настраивать на `line-length` для значений multi-line массивов,
    либо переписать строку через block-scalar `>-`.
  - **Проверка:** `make lint` — часть DoD. Если падает — на этапе
    реализации решает реализатор (например, отключает rule для
    конкретной строки `# yamllint disable-line rule:line-length`).
  - **Не блокирует ADR.**

## R-T-15 — Кто-то в будущем добавит `restart: unless-stopped` + `restart_policy.condition: unhealthy`

- **Описание:** Сейчас compose не указывает `restart_policy`. Если
  кто-то добавит «контейнер автоматически перезапускается при
  unhealthy», ET-015 (правильный healthcheck) внезапно станет
  частью retry-логики. Любой реальный кратковременный сбой будет
  крутить контейнер в цикле перезапусков.
- **Вероятность / Влияние:** Н / С.
- **Митигация:**
  - **Архитектурное решение:** ET-015 такую политику **не** вводит
    (out of scope). `restart_policy` сейчас отсутствует — Docker
    использует дефолт «no restart on unhealthy».
  - **Будущее:** при появлении `restart_policy.condition: unhealthy`
    нужно проверить, что start_period достаточен для всех валидных
    стартов, и что `interval × retries` не складывается в шторм
    перезапусков. Это будет тема отдельного ADR.

## R-T-16 — Образ не получает curl, и кто-то будет через `docker exec` пытаться отлаживать API curl-ом

- **Описание:** Оператор зайдёт в контейнер на mva154 для отладки и
  обнаружит, что `curl` нет. Привычка проверять `curl localhost:5556`
  не сработает.
- **Вероятность / Влияние:** С / Н.
- **Митигация:**
  - **Архитектурное решение:** в slim-образе curl никогда не было.
    Это не регрессия ET-015 — это уже было до фикса.
  - **Альтернатива для оператора:** тот же `python -c "import
    urllib.request; print(urllib.request.urlopen('http://localhost:5556/api/health').read())"`
    из контейнера, либо `curl` с хоста против externalного URL.
  - **Будущее (если боль):** добавить debug-образ с `curl` (отдельный
    Dockerfile + tag) — out of scope ET-015.

## R-T-17 — В будущем Compose v3 будет deprecated, перейдём на Compose Spec — структура healthcheck изменится

- **Описание:** Docker Compose Spec (v2024+) уже унифицировал
  формат healthcheck. Если проект мигрирует на новый формат, нужно
  будет переписать `test:` поле.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** Compose Spec **сохраняет** обратную
    совместимость с массивной формой `test: ["CMD", ...]`. Никакой
    обязательной миграции healthcheck-секции при апгрейде Compose не
    предвидится.
  - **Если миграция понадобится:** перенесённая секция останется
    идентичной по смыслу.

## R-T-18 — Прочие пути дёргают `/api/health` для своих целей и трафик растёт

- **Описание:** Если в будущем кто-то добавит внешний мониторинг
  (Uptime Robot, Prometheus blackbox), который тоже бьёт по
  `/api/health`, плюс наш healthcheck — нагрузка пойдёт **дважды**.
  Сейчас 2 req/min — пренебрежимо, но при росте уровней может стать
  заметным.
- **Вероятность / Влияние:** Н / Н.
- **Митигация:**
  - **Архитектурное решение:** `/api/health` дешёвый (~7 мс,
    in-memory). Линейный рост источников выдержит много порядков.
  - **Если действительно станет проблемой:** ввести rate-limit на
    `/api/health` (отдельный work-item) или разнести
    liveness (internal-only) и readiness (external) — TD-1 ADR-020.

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

| # | Риск | Вер | Влиян | Митигация (тип) |
|---|------|-----|-------|------------------|
| R-T-1 | Поломка квотинга YAML/shell | Н | С | CMD-массив без shell; ST-03 валидирует |
| R-T-2 | CPU/RAM overhead fork+exec python | Н | Н | 30 с интервал → ~0.005% CPU; `docker stats` мониторинг |
| R-T-3 | `/api/health` отвечает > 3 с под нагрузкой | Н | С | Endpoint лёгкий; 3 с = 430× запас; деградация = валидный сигнал |
| R-T-4 | Разные exit code при разных ошибках | Н | Н | Docker — binary семантика; различение не нужно |
| R-T-5 | `start_period: 20s` неподходящий | Н | Н | uvicorn стартует < 2 с; 10× запас |
| R-T-6 | `localhost` резолвится в IPv6 | Н | С | Дефолт IPv4 в `python:3.12-slim`; fallback `127.0.0.1` |
| R-T-7 | `/api/health` сменит путь | Н | С | AC-08 блокирует merge; ADR-020 трейл для будущего |
| R-T-8 | TLS на loopback | Н | Н | HTTP loopback, HTTPS только на nginx |
| R-T-9 | `python` alias исчезнет | Н | Н | Долгосрочно стабилен; правка 1 символ |
| R-T-10 | Поломка YAML при ручной правке | С | Н | ST-03 + `make lint`; block-scalar при необходимости |
| R-T-11 | Гонка при деплое | С | Н | Waiting-loop 120 с + `start_period: 20s` |
| R-T-12 | Healthy при недоступной БД | Н | С | Out of scope (BRD §7); не делает хуже; TD-1 |
| R-T-13 | AC-05 не воспроизводится через `kill -STOP 1` | Н | Н | Альтернатива через `docker stop`; покрывает семантику |
| R-T-14 | `yamllint` падает на длинной строке | Н | Н | Конфигурация yamllint или block-scalar |
| R-T-15 | `restart_policy.condition: unhealthy` в будущем | Н | С | Не вводится в ET-015; новый ADR при добавлении |
| R-T-16 | Оператор привык к `curl` для отладки | С | Н | curl и раньше не было; альтернативы есть |
| R-T-17 | Compose v3 deprecation | Н | Н | Спека сохраняет совместимость массивной формы |
| R-T-18 | Внешний мониторинг + healthcheck = 2× нагрузка | Н | Н | Endpoint дешёвый; rate-limit при росте |

## Связанные документы

- `01-brd.md` §3 R1..R3 (бизнес-риски — ложные алёрты, эрозия доверия,
  SLO), §8 (сценарий «как должно стать»)
- `02-trz.md` §3 (целевое состояние), §4 (альтернативы), §8 (риски
  ТЗ — частично пересекаются с этим документом, но фокусируются на
  имплементации; здесь — архитектурный взгляд)
- `03-acceptance-criteria.md` AC-01..AC-10 (все гейты)
- `04-test-plan.yaml` ST-01..ST-07, UT-01..UT-03, IT-01..IT-04,
  E2E-01..E2E-02
- `06-adr/ADR-020-healthcheck-via-python-urllib.md` §«Решение»,
  §«Последствия», §«Технический долг», §«Альтернативы для будущего»
- `07-infra-requirements.md` §6 (deploy procedure), §7
  (observability)
- `08-data-requirements.md`
- `docs/architecture/README.md` §«Деплой», §«Компоненты»
- `docs/work-items/ET-014/10-tech-risks.md` — образец «UI calibration
  risks» документа (наследие)
- `docs/work-items/ET-013/10-tech-risks.md` — образец «layer
  calibration risks» документа (наследие)
