Files
enduro-trails/docs/work-items/ET-015/10-tech-risks.md
claude-bot 4f80c250cf
All checks were successful
CI / lint (push) Successful in 4s
CI / test (push) Successful in 10s
CI / build (push) Successful in 3s
architect(ET): auto-commit from architect run_id=102
2026-06-05 15:27:58 +00:00

27 KiB
Raw Blame History

type, work_item_id, title, version, status, created_at, authors
type work_item_id title version status created_at authors
tech-risks ET-015 Технические риски — ET-015: Healthcheck enduro-trails-app через python urllib 1 approved 2026-06-05
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 (~80150 мс старт интерпретатора). На фоне нагруженного uvicorn это может создавать заметный CPU-spike.
  • Вероятность / Влияние: Н / Н.
  • Митигация:
    • Архитектурное решение: интервал 30 с → накладные расходы ~150 мс / 30 с = 0.5% от одного ядра в пике, в среднем — ~0.005% CPU. На mva154 (BRD: idle ~12% 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» документа (наследие)