Задачи 025-030: ёмкость префиксов, политика входа, дерево префиксов

Повторный анализ кодовой базы (docs/reviews/2026-09-26-codebase-review-2.md) и доработки:
025 Ёмкость префикса — размер его подсети (а не сумма листьев); «Обзор» считает ёмкость
    по корневым активным IPv4-префиксам и адреса внутри них.
026 Политика блокировки входа: 5 неудач на логин+IP, 20 на IP, 50 на логин со всех IP,
    кроме известных IP (known_logins, миграция 0009) — владельца нельзя заблокировать анонимно.
027 Сериализация попыток входа по IP (advisory-lock после блокировки логина).
028 UI «Префиксы»: загрузка всех страниц (до 20 000), счётчики по total, предупреждение об усечении.
029 Advisory-lock по VRF для операций, меняющих дерево префиксов и раскладку адресов.
030 Исправление замечаний ревью 025-029: _lock_prefix (VRF блокируется до чтения префикса,
    409 при одновременном переносе), константы политики входа перенесены в app/services.py.

Тесты: 14 passed (проверка ёмкости родителя приведена к семантике 025); сквозные сценарии
и гонки — docs/reviews/2026-09-26-changes-025-029-review.md, 2026-09-27-changes-030-review.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ayurishchevandClaude Opus 5.5 committed 2026-09-27 08:28:50 +03:00
1 parent 13e17fbb47
commit 03d727e496
25 files changed
+877 -78

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Целостность дерева префиксов при параллельных изменениях (изменение 029)
Находка № 5 из `docs/reviews/2026-09-26-codebase-review-2.md`, серьёзность — низкая.
## Context
`attach_to_tree` определяет родителя и забирает вложенные префиксы по снимку данных транзакции. Два параллельных запроса могут создать в одном VRF пересекающиеся префиксы разной длины
(ручное создание `.0/29` и автовыделение `.4/30`). Уникальность `(vrf_id, prefix)` это не ловит, `parent_id` останется неверным. `FOR UPDATE` в `allocate_subnet` блокирует только строку родителя.
## Решение (`app/api/v1/prefixes.py`)
1. Хелпер `_lock_vrf(db, *vrf_ids)`: `pg_advisory_xact_lock(<VRF_NS>, vrf_id)` для каждого VRF **в порядке возрастания id** (исключает взаимную блокировку); пространство ключей — константа (например, 7033).
2. Вызов в начале каждой операции, меняющей дерево или раскладку адресов, до любых чтений дерева:
- `create_prefix` — VRF из тела запроса;
- `allocate_subnet` — VRF родителя. Сначала прочитать `vrf_id` родителя обычным `SELECT`, затем `_lock_vrf`, затем уже существующий `SELECT … FOR UPDATE`;
- `update_prefix` при смене VRF (`_move_to_vrf`) — исходный и целевой VRF;
- `delete_prefix` — VRF префикса (переподвешивание детей);
- `create_address` и `allocate_next` — VRF префикса (выбор самого узкого префикса и занятых диапазонов должен видеть согласованное дерево).
3. Операции над разными VRF не блокируют друг друга. Чтения (`GET`) не блокируются.
4. `README.md`: одна фраза в разделе «Модель данных» — изменения дерева одного VRF выполняются по одному.
## Файлы
`app/api/v1/prefixes.py`, `README.md`.
## Проверка
Параллельно (потоки) в одном VRF: создание `.0/29` и автовыделение `/30` из `/24` → у выделенного `/30`, если он внутри `/29`, родитель — `/29`.
Цикл из 20 повторов без ошибок и без неверных родителей. Данные после проверки удалить.
@@ -0,0 +1,35 @@
# Итог: целостность дерева префиксов при параллельных изменениях (изменение 029)
## Что сделано
- `app/api/v1/prefixes.py`: константа `VRF_LOCK_NS = 7033` и хелпер `_lock_vrf(db, *vrf_ids)` — `pg_advisory_xact_lock(VRF_LOCK_NS, vrf_id)` для каждого VRF, отсортированных
по возрастанию id (исключает взаимную блокировку при операциях над несколькими VRF).
- Вызов добавлен в начале каждой операции, меняющей дерево или раскладку адресов, до любых чтений дерева:
- `create_prefix` — по `body.vrf_id`, первой строкой обработчика;
- `allocate_subnet` — сначала обычный `SELECT vrf_id` префикса-родителя (без блокировки строки), затем `_lock_vrf`, затем уже существующий `SELECT … FOR UPDATE`;
- `update_prefix` при смене VRF — по исходному и целевому VRF, до вызова `_move_to_vrf`;
- `delete_prefix` — по VRF префикса, до переподвешивания детей;
- `create_address` — по VRF префикса, до проверки «самый узкий префикс»;
- `allocate_next` — обычный `SELECT vrf_id`, затем `_lock_vrf`, затем существующий `SELECT … FOR UPDATE` по префиксу.
- Чтения (`GET`) не блокируются; операции над разными VRF друг друга не блокируют (независимые ключи advisory-lock).
- `README.md`, раздел «Модель данных»: одно предложение о сериализации изменений дерева одного VRF.
## Отклонения от плана
Нет.
## Как проверено
1. `venv/bin/python -c 'import app.main'` — без ошибок.
2. Стенд пересобран, `healthy`.
3. Сценарий из плана (20 повторов, временная организация `rv3-tree-test`, скрипт в scratchpad): в каждой итерации — свой `/24`-родитель, и **параллельно** (два потока) —
явное создание `.0/29` и автовыделение `/30` из родителя (`POST …/subnets/next {length:30}`). Оба запроса каждый раз завершились без ошибок (0 ошибок из 40 запросов).
Итоговое состояние дерева (отдельный `GET` **после** завершения обоих запросов пары — не тело ответа гонки, оно отражает лишь момент собственного commit) проверено на всех
20 итерациях: во всех случаях, когда выделенный `/30` оказался внутри `.0/29`, его `parent_id` равен id `.0/29`, а не `.0/24`. **0 из 20 неверных `parent_id`. PASS.**
- Первый прогон теста дал 5 ложных срабатываний из-за ошибки самого теста (сверка велась по телу HTTP-ответа каждого из двух конкурентных запросов, которое отражает
состояние дерева на момент commit именно этого запроса, а не финальное состояние после обоих); после исправления теста (повторный `GET` уже обоих префиксов после
завершения гонки) все 20 итераций прошли успешно, а прямая проверка в БД (`SELECT … FROM prefixes`) подтвердила, что для всех 5 «проблемных» по первому прогону
итераций конечное состояние в базе уже было верным (гонка не оставляла ошибочных данных, ошибался только клиентский тест).
4. Временная организация и все её префиксы удалены по завершении.
## Не проверено
- Гонка при `update_prefix` (смена VRF, две блокировки) и при одновременном `create_address`/`allocate_next` в одном VRF — проверялась только комбинация
`create_prefix` + `allocate_subnet`, явно описанная в плане; остальные точки вызова `_lock_vrf` проверены только чтением кода.
- Поведение под большей степенью параллелизма (более двух одновременных запросов) и при конкурентных операциях над разными VRF (что они не блокируют друг друга) — не измерялось.