ADR 0009: шлюзы чтения и конечные leases фиксированных долей
Статус: accepted. Дата: 2026-09-22.
Контекст
Standalone governor ограничивает один процесс. Несколько полных локальных bucket умножили бы бюджет. Нужны независимые read gateways с предсказуемым общим пределом и остановкой при потере координации. Двухсерверные replication/fencing испытания отложены владельцем.
Решение
- Оставляем один writer и один completion worker. То же приложение API получает роль reader с запретом методов кроме GET/HEAD и без консоли. Все используют primary PostgreSQL и один согласованный storage root; асинхронные blob-копии запрещены в этом профиле.
- В infrastructure добавляем PostgresDownloadLease и проверяемое локальное окно. Миграция 5 хранит единственную policy и аренды slots. Slot 0 создаёт policy; остальные обязаны совпасть. Доля каждого участника — floor общего rate / число slots, включая per-principal rate. Локальный governor остаётся исполнителем и не знает SQL.
- Аренда в БД 10 секунд, локальная валидность 8 секунд от начала запроса, heartbeat через 2 секунды. UUID и generation проверяются при продлении. Проверки времени учитывают сетевую задержку, поздний ответ не оживляет expired owner. Shutdown оставляет reservation до DB expiry. Ошибка требует restart.
- Новый mode advisory barrier исключает смешение standalone/shared, прежний writer lock сохраняется. Readers удерживают maintenance barrier и сверяют storage identity, не создавая её. Сохранённая policy исключает случайный обход бюджета после выключения shared-процессов.
- HTTP readiness дополняется role/sharedDownloads. Reader возвращает 405 с Allow. Бюджет касается payload; права/публикация/идемпотентность остаются в существующих слоях.
Session advisory locks освобождаются при завершении DB-сессии, а не гарантированно в момент потери сети клиентом; поэтому сами по себе они не ограничивают продолжительность раздачи из локального файла. См. PostgreSQL advisory locks. Конечное локальное окно дополняет, но не заменяет эти барьеры.
Альтернативы и последствия
Центральное разрешение каждого сетевого кванта добавило бы SQL-зависимость и нагрузку на горячем byte path. Динамическое перераспределение долей потребовало бы отдельного поколения policy и координации отзыва; это следующий возможный этап. Сейчас простаивающая доля недоступна другим процессам, зато сумма выделений проверяется без межпроцессного запроса на каждый chunk.
Гарантия требует стабильных часов с запасом менее 2 секунд за lease; скачки времени/продвижение DB replica не испытаны. Уже разрешённые buffered bytes могут выйти позже. Это не абсолютный лимит физического интерфейса и не fencing прежнего storage writer. Отдельный load balancer, HA PostgreSQL, подтверждённые blob replicas и policy подтверждения записи остаются вне реализации. Read-only HTTP профиль не является минимальной PostgreSQL-role.
Включение профиля требует остановки старых бинарников и миграции; rolling coexistence при переходе запрещён. Policy меняется только offline. Эксплуатация и точные ограничения: READ_GATEWAYS. Проверки включают отдельные reader-процессы, обрыв byte stream, задержку PostgreSQL через TCP proxy, expiry, replacement и запрет позднего возобновления.
Дополнение (2026-10-03): соединение аренды, перезапуск slot и резерв worker
Проверка нескольких процессов на одном сервере показала три слабых места; решения:
- Своё соединение аренды. Продление брало соединение из общего пула. Насыщенный пул (передачи, каталог) мог задержать продление дольше локального окна в 8 секунд, и тогда шлюз под нагрузкой выходил из строя. Теперь аренда держит одно соединение от запуска до остановки и закрывает его, а не возвращает в пул.
- Перезапуск того же slot больше не падает сразу с
busy, пока резервация остановленного процесса не истекла. Старт ждёт неподвижную резервацию: прежний владелец перестал раздавать раньше, его локальное окно 8 с, а резервация в БД 10 с. Продлеваемая резервация означает работающий шлюз с тем же slot и через 2–3 секунды отклоняется какbusy. Ожидание ограничено 15 секундами. Ограждение generation не меняется. - Второй completion worker не завершается с ошибкой в цикле перезапусков, а ждёт в резерве: сообщает
worker.standbyодин раз и пробует каждые 5 секунд. Блокировка worker по-прежнему одна на базу: это не HA и не параллельная обработка. - Локальное окно агента резервных копий проверяет и календарные часы, как аренды шлюзов, потому что монотонные часы стоят, пока хост спит.
Проверки: tests/integration/gateways.test.mjs (reader раздаёт при остановленном writer, перезапущенный writer возвращает slot, замена зависшего шлюза стартует только после истечения резервации), tests/integration/worker-standby.test.mjs (настоящие процессы worker), tests/backup-agent-lease.test.mjs.