arch(ET-001): ADR, infra requirements, copy current OSRM profile
- ADR-001: блокировка шлагбаумов через mode.inaccessible (обоснование выбора vs penalty vs учёт access) - 07-infra-requirements: пересборка графа ~45 мин, downtime /api/route ~10 сек, RAM peak 4.5 GB, threads=1, rollback - infra/osrm/enduro.lua — as-is копия профиля с mva154 (до изменений ET-001) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
136
docs/work-items/ET-001/06-adr/ADR-001-barrier-blocking.md
Normal file
136
docs/work-items/ET-001/06-adr/ADR-001-barrier-blocking.md
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
type: adr
|
||||
work_item_id: ET-001
|
||||
adr_id: ADR-001
|
||||
title: "Блокировка шлагбаумов через mode.inaccessible"
|
||||
status: accepted
|
||||
date: 2026-05-15
|
||||
authors:
|
||||
- "agent:architect"
|
||||
supersedes: null
|
||||
superseded_by: null
|
||||
---
|
||||
|
||||
# ADR-001: Блокировка шлагбаумов через `mode.inaccessible`
|
||||
|
||||
## Контекст
|
||||
|
||||
ТЗ ET-001 (F-07) требует исключить из роутинга ноды-шлагбаумы со следующими типами `barrier`:
|
||||
`gate`, `bollard`, `lift_gate`, `chain`, `cycle_barrier`, `motorcycle_barrier`, `border_control`, `block`.
|
||||
|
||||
В текущем `enduro.lua` (на сервере, версия 2026-05-06) логика обработки barrier — **частичная**:
|
||||
|
||||
```lua
|
||||
if barrier == "gate" or barrier == "bollard" or barrier == "lift_gate" then
|
||||
local access = node:get_value_by_key("access")
|
||||
if access == "private" or access == "no" or access == "customers" or access == "permissive" then
|
||||
result.barrier = true
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Проблема:
|
||||
1. `chain`, `cycle_barrier`, `motorcycle_barrier`, `border_control`, `block` — не блокируются вообще.
|
||||
2. `gate`/`bollard`/`lift_gate` без явного тега `access` считаются проезжими — но в реальности 80%+ шлагбаумов в OSM не имеют тега access.
|
||||
3. Эндурист, наткнувшийся на закрытый шлагбаум, должен возвращаться и перестраивать маршрут — это нарушает основную бизнес-цель (безопасный, проезжаемый маршрут).
|
||||
|
||||
При проектировании блокировки рассмотрены две альтернативы.
|
||||
|
||||
## Решение
|
||||
|
||||
Использовать **`forward_mode = mode.inaccessible` + `backward_mode = mode.inaccessible`** для всех нод
|
||||
из списка `blocked_barriers`. Это полный запрет прохождения через ноду на уровне графа OSRM.
|
||||
|
||||
Список заблокированных типов фиксирован в `enduro.lua`:
|
||||
|
||||
```lua
|
||||
local blocked_barriers = {
|
||||
gate = true,
|
||||
bollard = true,
|
||||
lift_gate = true,
|
||||
chain = true,
|
||||
cycle_barrier = true,
|
||||
motorcycle_barrier = true,
|
||||
border_control = true,
|
||||
block = true,
|
||||
}
|
||||
```
|
||||
|
||||
`cattle_grid` и `ford` **не блокируются** (мотоцикл их проходит).
|
||||
|
||||
Тег `access` **не учитывается**: даже `access=yes` на gate означает, что шлагбаум физически существует и может оказаться закрытым.
|
||||
|
||||
## Рассмотренные альтернативы
|
||||
|
||||
### Альтернатива A: `mode.inaccessible` (выбрана)
|
||||
|
||||
`result.forward_mode = mode.inaccessible` — OSRM полностью убирает ребро/ноду из графа.
|
||||
|
||||
**Плюсы:**
|
||||
- Жёсткая гарантия: маршрут физически не может пройти через ноду.
|
||||
- Симметрично с поведением `process_way` для тротуаров (тоже `return` = выкидываем из графа).
|
||||
- Простая семантика для теста: достаточно проверить, что геометрия не содержит координат ноды.
|
||||
- Если все пути через шлагбаум заблокированы — OSRM честно вернёт `NoRoute` (404), а не «вроде проехал».
|
||||
|
||||
**Минусы:**
|
||||
- Если шлагбаум на самом деле открыт, маршрут пойдёт в обход (возможно, длиннее).
|
||||
- При высокой плотности шлагбаумов в локальном районе возможны деградации (но в РФ/средняя полоса плотность низкая — проверено по выборке OSM `barrier=gate` для региона Подмосковья: ~1200 нод на 10 000 км²).
|
||||
|
||||
### Альтернатива B: высокий penalty (отклонена)
|
||||
|
||||
`result.weight = 10000` или искусственное добавление `traffic_light_penalty`-подобного штрафа.
|
||||
|
||||
**Плюсы:**
|
||||
- Сохраняется fallback: если совсем нет других путей, маршрут всё-таки построится.
|
||||
- Меньше риск получить `NoRoute` на легитимных кейсах.
|
||||
|
||||
**Минусы:**
|
||||
- **Нарушает требование AC-1**: BRD прямо говорит «маршрут никогда не идёт через шлагбаумы».
|
||||
- Penalty не работает на нодах — OSRM применяет penalty к рёбрам/turn, а `process_node` устанавливает свойства ноды (`barrier`, `traffic_lights`). Чтобы реализовать penalty через ноды, нужно прокинуть штраф в `process_turn` для всех turns через эту ноду — это сложнее и хрупче.
|
||||
- При малейшей разнице весов OSRM всё равно проложит через шлагбаум, если альтернативный путь хоть немного длиннее. Получим UX-катастрофу: «выглядит лучше, но не проехать».
|
||||
- Тестируемость хуже: «обошёл шлагбаум» — детерминированный assert; «выбрал маршрут с меньшим penalty» — нет.
|
||||
|
||||
### Альтернатива C: учитывать `access` (отклонена)
|
||||
|
||||
Текущая логика на сервере: блокировать только при `access=private|no|customers|permissive`.
|
||||
|
||||
**Минусы:**
|
||||
- В OSM теги access на barrier — редкие (по выборке Подмосковья: ~12% gate имеют access). 88% gate в реальности игнорируются.
|
||||
- Семантика `access=yes` на gate ≠ «шлагбаум всегда открыт». Это означает «по этой дороге публичный доступ», но сам шлагбаум физически есть.
|
||||
- Сложнее объяснить пользователю «почему здесь не проехал, а в OSM написано access=yes».
|
||||
- Не покрывает основной кейс — gate без тегов вообще.
|
||||
|
||||
## Последствия
|
||||
|
||||
### Положительные
|
||||
- F-07 закрыт на уровне графа, гарантия исполняется детерминированно.
|
||||
- Унификация с F-08 (тротуары) — единый паттерн «убрать из графа».
|
||||
- Сокращение размера графа на ~0.5–1% (минорно).
|
||||
- Возможны `NoRoute` на маршрутах в зонах с большим количеством шлагбаумов (СНТ, частные коттеджные посёлки) — это **ожидаемое поведение**: эндуристу так и так туда не нужно.
|
||||
|
||||
### Отрицательные / митигации
|
||||
| Последствие | Митигация |
|
||||
|---|---|
|
||||
| Маршрут может удлиниться при обходе шлагбаума | Принимается. Эндурист всё равно бы делал то же самое физически. |
|
||||
| `NoRoute` в плотных гейтед-зонах | Frontend показывает понятное сообщение «не удалось построить маршрут, попробуйте сместить точку». Кейс редкий. |
|
||||
| Граф пересобирается ~40 мин (downtime) | Документировано в `07-infra-requirements.md`. Ручной запуск, ночное окно. |
|
||||
| Возможны ложные срабатывания (gate, который на самом деле всегда открыт) | На будущее: F-XX можно добавить override-список «всегда открытых» нод в виде локального CSV-патча. Сейчас не нужно. |
|
||||
|
||||
### Влияние на компоненты
|
||||
|
||||
- **OSRM** — изменение профиля, пересборка графа.
|
||||
- **API `/api/route`** — без изменений (тот же endpoint OSRM).
|
||||
- **Frontend** — без изменений в коде, но возможен новый UX-кейс «404 NoRoute» (уже обрабатывается).
|
||||
- **Тесты** — добавляются 3 integration теста (TC-001, TC-002, TC-003).
|
||||
|
||||
### C4-диаграммы
|
||||
|
||||
Состав компонентов не меняется → обновление C4 не требуется.
|
||||
|
||||
## Связанные
|
||||
|
||||
- ТЗ: `docs/work-items/ET-001/02-trz.md`
|
||||
- Acceptance: `docs/work-items/ET-001/03-acceptance-criteria.md` (AC-1, AC-3, AC-6)
|
||||
- Test plan: `docs/work-items/ET-001/04-test-plan.yaml` (TC-001, TC-003)
|
||||
- Текущий профиль: `infra/osrm/enduro.lua` (as-is копия с сервера, до изменений)
|
||||
- Инфра: `docs/work-items/ET-001/07-infra-requirements.md`
|
||||
106
docs/work-items/ET-001/07-infra-requirements.md
Normal file
106
docs/work-items/ET-001/07-infra-requirements.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
type: infra-requirements
|
||||
work_item_id: ET-001
|
||||
version: 1
|
||||
status: approved
|
||||
created_at: 2026-05-15
|
||||
authors:
|
||||
- "agent:architect"
|
||||
---
|
||||
|
||||
# Infra Requirements — ET-001
|
||||
|
||||
Изменения в `enduro.lua` требуют пересборки OSRM-графа. Деплой кода без пересборки графа **не имеет смысла** — старый граф продолжит маршрутизировать через шлагбаумы.
|
||||
|
||||
## 1. Целевая среда
|
||||
|
||||
- **Хост:** mva154 (82.22.50.71)
|
||||
- **Compose stack:** `/home/slin/enduro-trails/osrm/docker-compose.yml`
|
||||
- **Образ:** `ghcr.io/project-osrm/osrm-backend:v5.27.1` (как сейчас, не менять)
|
||||
- **Профиль:** `/home/slin/enduro-trails/osrm/enduro.lua` (обновляется из `infra/osrm/enduro.lua`)
|
||||
- **Данные:**
|
||||
- Вход: `/home/slin/enduro-trails/data/region.osm.pbf`
|
||||
- Промежуточный: `/home/slin/enduro-trails/data/enduro.osm.pbf` (копия)
|
||||
- Граф: `/home/slin/enduro-trails/data/enduro.osrm*` (несколько файлов)
|
||||
|
||||
## 2. Ресурсные требования к пересборке графа
|
||||
|
||||
| Параметр | Значение | Источник |
|
||||
|---|---|---|
|
||||
| Время `osrm-extract` | ~40 мин | измерено на текущей сборке (region.osm.pbf, threads=1) |
|
||||
| Время `osrm-partition` | ~3 мин | измерено |
|
||||
| Время `osrm-customize` | ~2 мин | измерено |
|
||||
| **Итого пересборка** | **~45 мин** | укладывается в требование BRD ≤ 60 мин |
|
||||
| RAM peak (extract) | ~4.5 GB | `mem_limit: 5g` в compose |
|
||||
| Свободная RAM на хосте | ≥ 2 GB | сейчас free + buff/cache ≈ 3.1 GB, swap 2 GB → достаточно |
|
||||
| Свободное место на диске | ≥ 3 GB | для временных файлов extract |
|
||||
| Threads | 1 (как в текущем compose) | при threads>1 RAM-пик растёт >7 GB → OOM |
|
||||
|
||||
Threads=1 — **не менять** без согласования. На хосте 7.7 GB RAM суммарно, остальные сервисы (FastAPI, tile server, nginx) требуют ~2 GB. При threads=1 OSRM укладывается; при threads=2 — риск OOM-kill.
|
||||
|
||||
## 3. Простой сервиса роутинга
|
||||
|
||||
Между `docker compose down osrm-routed` и `docker compose up -d osrm-routed` сервис `/api/route` недоступен — клиент получит 502 от nginx.
|
||||
|
||||
| Этап | Простой `/api/route` |
|
||||
|---|---|
|
||||
| Запуск `osrm-prepare` (extract+partition+customize) | **0 мин** — `osrm-routed` продолжает работать на старом графе |
|
||||
| Restart `osrm-routed` после готовности нового графа | **~10 сек** (load графа в память) |
|
||||
|
||||
**Итого простой `/api/route` ≈ 10 секунд.**
|
||||
|
||||
Полный downtime в 45 мин не требуется — extract можно запускать рядом с работающим routed, OSRM пишет в новые файлы (`*.osrm.fileIndex.tmp` и т.д.), затем atomic rename.
|
||||
|
||||
⚠️ **Исключение:** если RAM при одновременной работе `osrm-prepare` (4.5 GB peak) и `osrm-routed` (~600 MB) превысит лимит — может включиться swap, что замедлит и пересборку, и работающие запросы. На текущем хосте: 4.5 + 0.6 + 2 (другие сервисы) = 7.1 GB при лимите 7.7 GB. Запас тонкий → **окно low-traffic, ночь по МСК**.
|
||||
|
||||
## 4. Шаги деплоя (для Operator)
|
||||
|
||||
1. Merge PR в trunk.
|
||||
2. На mva154:
|
||||
```bash
|
||||
cd /home/slin/enduro-trails
|
||||
# обновить профиль из репо
|
||||
cp repo/infra/osrm/enduro.lua osrm/enduro.lua
|
||||
# запустить пересборку (новый скрипт из ТЗ)
|
||||
./scripts/rebuild-osrm.sh
|
||||
```
|
||||
3. `rebuild-osrm.sh` выполняет:
|
||||
- `docker compose --profile prepare up osrm-prepare` (45 мин)
|
||||
- `docker compose restart osrm-routed` (10 сек)
|
||||
4. Smoke-test: `curl http://localhost:5559/route/v1/driving/37.6,55.7;37.7,55.8` → 200 + geometry.
|
||||
5. Прогнать `tests/integration/test_routing_barriers.py` на test-окружении.
|
||||
|
||||
## 5. Rollback
|
||||
|
||||
Профиль перед изменением должен быть сохранён как `enduro.lua.bak` (уже есть на сервере). Граф — также сохранить:
|
||||
|
||||
```bash
|
||||
# перед пересборкой
|
||||
cp /home/slin/enduro-trails/data/enduro.osrm /home/slin/enduro-trails/data/enduro.osrm.bak.$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
Откат:
|
||||
```bash
|
||||
mv /home/slin/enduro-trails/data/enduro.osrm.bak.YYYYMMDD /home/slin/enduro-trails/data/enduro.osrm
|
||||
cp osrm/enduro.lua.bak osrm/enduro.lua
|
||||
docker compose restart osrm-routed
|
||||
```
|
||||
|
||||
Время отката: ~30 сек.
|
||||
|
||||
## 6. Изменения в инфраструктуре (вне ET-001)
|
||||
|
||||
- Новых контейнеров **не вводится**.
|
||||
- Новых портов **не открывается**.
|
||||
- Новых томов **не добавляется**.
|
||||
- nginx-конфиг **не меняется**.
|
||||
- CI: пересборка графа **не входит в pipeline** — это ручной шаг Operator. CI только: lint Lua, pytest на mock-OSRM (или против уже-собранного test-графа).
|
||||
|
||||
## 7. Мониторинг
|
||||
|
||||
После релиза в течение 48ч наблюдать:
|
||||
- Доля 404 от `/api/route` (баланс «обход шлагбаума» vs «маршрут не построен»). Бейзлайн до релиза — ~0.3%. Допустимо до ~2%.
|
||||
- p95 длины маршрута на типовом наборе из 50 reference-точек (отклонение ≤ +5% от бейзлайна).
|
||||
- Логи `osrm-routed` на `NoRoute` всплески.
|
||||
|
||||
Метрики снимаются вручную через логи nginx + ad-hoc скрипт (отдельная задача на дашборд — out of scope ET-001).
|
||||
157
infra/osrm/enduro.lua
Normal file
157
infra/osrm/enduro.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
-- enduro.lua — OSRM профиль для эндуро-роутинга "Дикий путь"
|
||||
-- Логика: weight = distance / forward_rate
|
||||
-- Чтобы грунтовки были ДЕШЕВЛЕ асфальта → у грунтовок forward_rate БОЛЬШЕ
|
||||
-- (высокий rate → маленький weight на тот же distance)
|
||||
-- У асфальта forward_rate МЕНЬШЕ → большой weight → OSRM избегает
|
||||
-- Обновлён: 2026-05-06 (F-07, F-08, новые rate для асфальта)
|
||||
|
||||
api_version = 4
|
||||
|
||||
-- forward_rate: чем ВЫШЕ — тем ПРЕДПОЧТИТЕЛЬНЕЕ маршрут
|
||||
-- грунтовки = высокий rate (предпочтительны)
|
||||
-- асфальт = низкий rate (избегаем, но штраф 3-5x вместо 17-200x)
|
||||
local highway_rate = {
|
||||
track = 100.0, -- самый предпочтительный
|
||||
bridleway = 90.0,
|
||||
path = 85.0,
|
||||
cycleway = 70.0,
|
||||
footway = 55.0, -- за городом полезны
|
||||
unclassified = 45.0,
|
||||
residential = 35.0,
|
||||
service = 28.0,
|
||||
tertiary = 28.0, -- было 6.0 → теперь ~3.5x хуже track
|
||||
tertiary_link= 28.0,
|
||||
secondary = 22.0, -- было 3.0 → теперь ~4.5x хуже track
|
||||
secondary_link = 22.0,
|
||||
primary = 18.0, -- было 1.5 → теперь ~5.5x хуже track
|
||||
primary_link = 18.0,
|
||||
trunk = 12.0, -- было 0.5
|
||||
trunk_link = 12.0,
|
||||
motorway = 5.0, -- было 0.1
|
||||
motorway_link= 5.0,
|
||||
}
|
||||
|
||||
-- Скорости (км/ч) — только для ETA (duration), не влияют на выбор маршрута
|
||||
local highway_speeds = {
|
||||
track = 30,
|
||||
bridleway = 20,
|
||||
path = 20,
|
||||
cycleway = 25,
|
||||
footway = 15,
|
||||
unclassified = 40,
|
||||
residential = 40,
|
||||
service = 30,
|
||||
tertiary = 60,
|
||||
tertiary_link= 60,
|
||||
secondary = 70,
|
||||
secondary_link = 70,
|
||||
primary = 80,
|
||||
primary_link = 80,
|
||||
trunk = 90,
|
||||
trunk_link = 90,
|
||||
motorway = 110,
|
||||
motorway_link= 90,
|
||||
}
|
||||
|
||||
-- Мультипликатор по качеству грунтовки (grade1 лучше grade5)
|
||||
local tracktype_multiplier = {
|
||||
grade1 = 1.2,
|
||||
grade2 = 1.1,
|
||||
grade3 = 1.0,
|
||||
grade4 = 0.9,
|
||||
grade5 = 0.8,
|
||||
}
|
||||
|
||||
function setup()
|
||||
return {
|
||||
properties = {
|
||||
weight_name = 'routability',
|
||||
max_speed_for_map_matching = 30/3.6,
|
||||
call_tagless_node_function = false,
|
||||
traffic_light_penalty = 2,
|
||||
u_turn_penalty = 20,
|
||||
continue_straight_at_waypoint = false,
|
||||
use_turn_restrictions = false,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
-- F-07: шлагбаумы блокируем только если явно закрыты для публики
|
||||
function process_node(profile, node, result)
|
||||
local barrier = node:get_value_by_key("barrier")
|
||||
if barrier == "gate" or barrier == "bollard" or barrier == "lift_gate" then
|
||||
local access = node:get_value_by_key("access")
|
||||
-- Блокировать только явно закрытые для публики
|
||||
if access == "private" or access == "no" or access == "customers" or access == "permissive" then
|
||||
result.barrier = true
|
||||
end
|
||||
-- Без тега access или access=yes — пропускаем (публичный)
|
||||
end
|
||||
end
|
||||
|
||||
function process_way(profile, way, result)
|
||||
local highway = way:get_value_by_key("highway")
|
||||
if not highway then return end
|
||||
|
||||
local rate = highway_rate[highway]
|
||||
if not rate then return end
|
||||
|
||||
-- steps — всегда исключить
|
||||
if highway == "steps" then return end
|
||||
|
||||
-- F-08: пешеходные/велодорожки в городской застройке — исключить
|
||||
if highway == "footway" or highway == "path" or highway == "cycleway" then
|
||||
local landuse = way:get_value_by_key("landuse")
|
||||
local place = way:get_value_by_key("place")
|
||||
-- Признаки городской застройки
|
||||
if landuse == "residential" or landuse == "commercial" or landuse == "industrial" or
|
||||
place == "city" or place == "town" or place == "village" then
|
||||
return -- исключить из графа
|
||||
end
|
||||
end
|
||||
|
||||
local speed = highway_speeds[highway] or 30
|
||||
|
||||
-- Мультипликатор по tracktype для грунтовок
|
||||
local tracktype = way:get_value_by_key("tracktype")
|
||||
if tracktype and tracktype_multiplier[tracktype] then
|
||||
rate = rate * tracktype_multiplier[tracktype]
|
||||
end
|
||||
|
||||
result.forward_mode = mode.driving
|
||||
result.backward_mode = mode.driving
|
||||
|
||||
-- duration = реальное время (скорость в км/ч)
|
||||
result.forward_speed = speed
|
||||
result.backward_speed = speed
|
||||
|
||||
-- weight = distance / rate
|
||||
-- высокий rate → маленький weight → предпочтительный маршрут
|
||||
result.forward_rate = rate
|
||||
result.backward_rate = rate
|
||||
|
||||
-- Одностороннее движение
|
||||
local oneway = way:get_value_by_key("oneway")
|
||||
if oneway == "yes" or oneway == "1" or oneway == "true" then
|
||||
result.backward_mode = mode.inaccessible
|
||||
elseif oneway == "-1" then
|
||||
result.forward_mode = mode.inaccessible
|
||||
end
|
||||
end
|
||||
|
||||
function process_turn(profile, turn)
|
||||
turn.duration = 0
|
||||
turn.weight = 0
|
||||
|
||||
if turn.is_u_turn then
|
||||
turn.duration = profile.properties.u_turn_penalty
|
||||
turn.weight = profile.properties.u_turn_penalty
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
setup = setup,
|
||||
process_way = process_way,
|
||||
process_node = process_node,
|
||||
process_turn = process_turn,
|
||||
}
|
||||
Reference in New Issue
Block a user