# Техническая документация СлойРА

> Редакция 0.1 · 27 сентября 2026 года. Описание продукта и проектная спецификация технической реализации.

СлойРА — программный слой управления доступом, кешированием и размещением контента.
Клиентская поставка 2.1.74 использует отдельную конфигурацию Nginx с модулями
secure_link/auth_request и Python-агентом для проверки лицензии и чтения контента.
Lua и нативный модуль Nginx описаны в целевой архитектуре;
нативный бинарный модуль в текущий архив не включён.

Файлы, изображения, документы, видеозаписи и сегменты потокового видео
проходят через общую систему учёта популярности.
Часто востребованные данные получают место в оперативной памяти и на быстрых дисках.
Редко используемые данные остаются на более ёмких носителях.
Сервер доставки передаёт сведения о запросах и промахах кеша в контур размещения Ceph,
чтобы выбор класса хранения учитывал работу всей системы.

## Как читать эту редакцию

Документация содержит два уровня утверждений.

| Уровень | Что означает |
| --- | --- |
| Описание продукта | Функциональность, сообщённая разработчиком: Nginx, Lua, защита с ответом 403, многоуровневый кеш, учёт популярности и связь с Ceph |
| Проектная спецификация | Предложенные технические решения: форматы конфигурации, API, алгоритмы, имена метрик, управляющий агент, процедуры миграции и критерии приёмки |

Для поставки 2.1.74 есть исходные компоненты, проверенный ZIP и результаты
компонентных проверок и установки с настоящим Nginx на macOS и Linux x86_64.
Проверены подписи лицензии, ограниченные онлайн-разрешения, защита cache HIT,
локальные уровни хранения и контракт Ceph-адаптера. Работа с настоящим
Ceph-кластером, нагрузочные показатели и все перечисленные платформы этим
не подтверждаются. Проектные API и расширения в остальных разделах остаются
целевыми контрактами и не означают, что каждый из них уже реализован.

Для установки используйте инструкции внутри персонального ZIP действующей
поставки: `README.md`, `docs/INSTALL.md`, `docs/LICENSING.md`, `docs/OPERATIONS.md`
и `docs/API.md`. Они описывают фактические команды и параметры этого выпуска.
В 2.1.74 heartbeat выполняется раз в 60 секунд, онлайн-разрешение действует
не более 300 секунд; без связи новые запросы прекращаются по истечении
последнего разрешения. Подписанное продление применяется через `license-update`.

Примеры настроек и числовые значения относятся к демонстрационным профилям.
Перед применением их сопоставляют со схемой конкретного выпуска,
ресурсами узла и требованиями к доступности данных.

## Что решает СлойРА

### Контроль доступа перед выдачей контента

Каждый запрос сначала проходит проверку легитимности.
Политика может учитывать подпись ссылки, срок действия, область доступа,
разрешённые методы и ограничения клиента.
Если право доступа не подтверждено, сервер возвращает HTTP 403.

Проверка выполняется и для содержимого, уже находящегося в кеше.
Наличие быстрой локальной копии не даёт клиенту дополнительных прав.
Ограничение частоты запросов и отказ инфраструктуры имеют отдельные причины
и диагностические коды.

### Размещение в RAM и на дисках

Кеш доставки состоит из оперативной памяти и выбранных дисковых уровней.
NVMe и SATA/SAS SSD рассматриваются как разные эксплуатационные профили.
HDD используется для объектов, которым не требуется дорогой быстрый уровень.

Решение учитывает частоту обращений, размер объекта, давность последнего запроса,
стоимость повторного чтения и доступное место.
Однократная загрузка очень большого файла не должна вытеснять всю горячую выборку.
Защита от массового обхода каталога входит в политику допуска в кеш.

### Обратная связь для Ceph

Кеш меняет нагрузку на исходное хранилище.
Большое число обращений может обслуживаться в памяти и быть невидимым для Ceph.
Поэтому проект предусматривает два самостоятельных сигнала:
логический спрос клиентов и фактические чтения из исходного хранилища.

На их основе контроллер строит очередь размещения.
Объекты переводятся между подготовленными классами хранения
с учётом целостности, свободного места, квот и состояния кластера.
Изменение размещения не должно менять публичный адрес файла.

## Два контура хранения

```text
Клиент
  |
  v
Nginx + плагин СлойРА + политика Lua
  |
  +-- проверка доступа --> отказ 403
  |
  +-- RAM --> NVMe / SSD --> HDD
  |             локальный кеш доставки
  |
  +-- исходное хранилище Ceph
          |
          +-- горячий пул / класс SSD
          +-- холодный пул / класс HDD

Учёт запросов и промахов
  --> локальная очередь событий
  --> агрегатор спроса
  --> контроллер размещения
  --> проверяемый перенос объекта
```

Локальный кеш можно восстановить из исходного хранилища.
Он не заменяет механизм долговечности Ceph и не учитывается как резервная копия.
Перемещение исходного объекта между пулами имеет другие требования,
чем вытеснение его временной копии из кеша.

В этой редакции основной сценарий интеграции — выделенные пулы и каталог размещения.
Вариант с RGW описывается отдельно, поскольку S3-адресация, storage classes
и прямой доступ RADOS требуют разных адаптеров.

## Маршруты чтения

| Задача читателя | Разделы |
| --- | --- |
| Оценить применимость | Обзор, архитектура, ёмкость и экономика |
| Подготовить сервер | Платформы и установка, Nginx и Lua, эксплуатация |
| Настроить доступ | Защита доступа, HTTP-доставка, справочник конфигурации |
| Разобраться с кешем | Иерархия кеша, популярность, наблюдаемость |
| Подключить Ceph | Интеграция с Ceph, контракты API, эксплуатация |
| Принять реализацию | Проверки и runbooks, каталог сценариев |
| Написать интеграцию | Nginx и Lua, контракты API, справочник конфигурации |

## Языки и компоненты

| Компонент | Технология | Статус в этой редакции |
| --- | --- | --- |
| HTTP-сервер | Nginx | Основа продукта по описанию разработчика |
| Политики обработки | Lua | Реализация указана разработчиком |
| Нативный модуль | C17, динамический HTTP-модуль | Предложенная техническая реализация |
| Среда Lua | LuaJIT с языковым профилем Lua 5.1 | Целевой вариант интеграции |
| Локальный агент | Rust | Предложенный управляющий компонент |
| Контроллер размещения | Rust, адаптер Ceph | Предложенный управляющий компонент |
| Формат настроек | YAML с версионированием схемы | Проектный контракт |
| Обмен событиями | JSON; пакетная передача через HTTPS | Проектный контракт |
| Диагностика | Метрики Prometheus и структурированные журналы | Проектный контракт |

Выбор C и Rust разделяет обработку HTTP-запросов и фоновые операции.
Рабочий процесс Nginx не должен ожидать сканирования диска,
пакетной отправки телеметрии или переноса объектов Ceph.
Lua используется для контролируемой политики запроса,
а не для запуска произвольных блокирующих операций в обработчике.

## Платформы

Основной целевой профиль — Linux на x86_64.
В матрицу квалификации включены Ubuntu LTS, Debian,
а также семейства Rocky Linux и AlmaLinux.
ARM64 рассматривается как отдельный профиль сборки и испытаний.

Указание дистрибутива в проектной матрице не является сертификатом совместимости.
Для выпуска фиксируются точные версии ОС, ядра, Nginx, LuaJIT,
библиотек и адаптера Ceph.
Динамический модуль проверяется с конкретной ABI-конфигурацией Nginx.

Windows и macOS могут использоваться для работы с документацией и разработки,
но в этой редакции не являются целевыми производственными ОС сервера доставки.

## Экономия и измерения

Разработчик сообщает об экономии дискового пространства до 70%.
В этой редакции значение сохраняется как заявленный результат,
для которого ещё требуется опубликованная методика и воспроизводимый протокол.

Перенос объекта с SSD на HDD сам по себе не уменьшает число логических байтов.
Он уменьшает потребность в быстром классе хранения.
Уменьшение общего физического объёма возможно только при изменении
количества временных копий, правил кеширования или других явно измеряемых факторов.
Сравнение должно сохранять одинаковые требования к долговечности данных.

В разделе ёмкости отдельно рассчитываются логический объём,
физическое размещение с репликацией, локальный кеш,
миграционный резерв и стоимость используемых носителей.

## Границы защиты

Проверка запроса позволяет ограничить автоматизированный доступ
к контенту и снизить несанкционированную загрузку.
Она не даёт абсолютной защиты от копирования уже выданных данных.
Клиент с действительными правами может получать разрешённые ему байты.

СлойРА дополняет TLS, сетевую защиту, правила выдачи ссылок
и безопасность приложения владельца контента.
Идентификатор клиента не следует сводить только к IP-адресу:
NAT, мобильные сети и прокси делают такой признак неоднозначным.

## Установка и выпуск

Продукт поставляется отдельно от Nginx.
Целевой комплект включает модуль, политики Lua,
управляющий агент, схему настроек и документацию выпуска.

Действующая модель поставки предусматривает ZIP-архив,
ключ и инструкцию в кабинете покупателя после подтверждения оплаты.
Команды получения пакетов из публичного репозитория не предполагаются,
пока такой репозиторий не опубликован и не включён в описание выпуска.

Администратор клиента выполняет установку, настройку и обновление.
Лицензия включает обновления и исправления ошибок.
Срок начинается после подтверждения оплаты; продление выполняется вручную.
Точный регламент проверки лицензии и поведения при её недоступности
должен быть согласован отдельно от технических примеров этого справочника.

## Принципы реализации

- Авторизация предшествует выдаче любого байта защищённого объекта.
- Идентичность кеша включает арендатора, объект и версию содержимого.
- Физическое чтение из Ceph не приравнивается к числу клиентских запросов.
- Повторная доставка телеметрии не должна удваивать счётчики спроса.
- Сбой аналитики не должен останавливать уже разрешённую выдачу контента.
- Решение о размещении воспроизводимо по версии политики и входному окну.
- Перенос не удаляет исходную копию до проверки новой и переключения каталога.
- Нехватка места учитывается до начала копирования и во время переноса.
- Откат настроек восстанавливает последнюю проверенную конфигурацию.
- Секреты, подписи ссылок и содержимое файлов не попадают в телеметрию.

## Версионирование документации

Номер этой редакции относится к документу, а не к выпущенной версии программы.
Перед выпуском команда связывает редакцию справочника с артефактами сборки,
схемой конфигурации и результатами приёмки.

При несовместимом изменении контракта повышается его основная версия.
Добавление необязательного поля допускается только с определённым поведением
для старого получателя.
Изменение единиц измерения или смысла существующего поля
не считается совместимым расширением.

Исправления этой редакции должны сохранять различие между описанием продукта,
целевым поведением и результатами, подтверждёнными испытаниями.

---

# Архитектура SLOYRA

> Статус: проектная спецификация v0.1. Имена API и параметры в примерах задают целевой контракт; наличие описания не подтверждает готовность реализации.

## Назначение архитектуры

SLOYRA проектируется как отдельно устанавливаемое расширение Nginx для защищённой доставки файлов и управления локальным кешем.
Отправная точка проекта — проверка легитимности клиента, отказ с 403 и размещение востребованного контента в ОЗУ, на NVMe SSD, SSD или HDD.
Популярность объекта оценивается по разрешённым запросам и фактически обслуженным байтам.
Агрегированные сведения о промахах передаются интеграции с Ceph.
Эта интеграция позволяет согласовать решения локального кеша с размещением объектов в серверном хранилище.
Перемещение данных внутри Ceph выполняет отдельный адаптер с собственными правами и ограничениями.
HTTP-обработчик не выполняет управляющие операции Ceph в процессе ответа клиенту.

Плагин остаётся частью существующего тракта Nginx.
Сетевое принятие соединений, TLS и базовая обработка HTTP принадлежат Nginx.
SLOYRA добавляет политику доступа, выбор представления объекта, кеширование и измерение спроса.
Защита проверяется до обращения к локальному кешу, включая ответы из памяти.
Отдельный контур управления подготавливает неизменяемые снимки конфигурации.
Каждый запрос обслуживается по одному снимку от начала до завершения.

## Что является проектным решением

Этот справочник описывает архитектурный контракт, по которому можно реализовать продукт.
Репозиторий документации сам по себе не является бинарной поставкой расширения.
Численные бюджеты ниже задают исходную конфигурацию стенда.
Они требуют подтверждения нагрузочным испытанием на выбранном оборудовании.
Имена процессов, сокетов, полей и ошибок закрепляют предлагаемый интерфейс.
Изменение контракта требует новой ревизии схемы и совместимого перехода.

## Состав целевой поставки

| Артефакт | Язык | Ответственность |
| --- | --- | --- |
| `ngx_http_sloyra_module.so` | C17 | Связь с фазами Nginx и трактом файлов |
| `sloyra-policy` | Lua в профиле 5.1 | Ограниченная логика допуска |
| `sloyra-agent` | Rust | Снимки конфигурации и фоновые задачи |
| `sloyra-ceph-adapter` | Rust | Передача агрегатов и применение планов |
| `sloyra.yaml` | YAML | Канонический вход конфигурации |
| `policy.bundle` | Версионированный пакет | Политики и тестовые векторы |
| `build-manifest.json` | JSON | Происхождение и совместимость бинарников |

C17 выбран для нативной границы с Nginx.
Rust выбран для управляющих сервисов с независимым жизненным циклом.
LuaJIT используется как целевая среда Lua-политик в соответствующем профиле поставки.
Синтаксический профиль Lua 5.1 не означает взаимозаменяемость всех интерпретаторов Lua.
Выполняемый пользовательский код не получает административный интерфейс кеша.
Прямой FFI-доступ политики к памяти модуля не входит в публичный контракт.

## Компоненты и связи

```text
HTTP client
  -> Nginx listener / TLS
  -> request normalization
  -> SLOYRA access policy
  -> representation selector
  -> RAM index
  -> local cache: RAM / NVMe / SSD / HDD
  -> origin fetch on admitted miss
  -> response filters
  -> bounded telemetry queue

sloyra-agent
  -> validate sloyra.yaml
  -> publish immutable snapshot
  -> manage cache tasks
  -> aggregate admitted demand
  -> spool telemetry

sloyra-ceph-adapter
  -> consume object demand
  -> propose placement plan
  -> validate resource budget
  -> apply through selected Ceph integration
```

Стрелка в схеме означает логическую зависимость, а не обязательный отдельный сетевой вызов.
Внутри worker предпочтительны прямые вызовы и структуры фиксированного размера.
Обращение к агенту не требуется для каждого cache hit.
Сбор статистики использует ограниченную очередь с наблюдаемыми потерями.
Недоступность потребителя телеметрии не превращает разрешённый запрос в 403.

## Границы доверия

| Граница | Вход | Правило |
| --- | --- | --- |
| Клиент → Nginx | HTTP-запрос | Все поля недоверенные |
| Nginx → policy | Нормализованный контекст | Поля доступны только для чтения |
| policy → native | Решение | Проверка enum и длины полей |
| worker → agent | Событие | Проверка схемы и квоты |
| agent → worker | Снимок | Целостность, поколение, совместимость |
| agent → adapter | Агрегаты | Идемпотентный идентификатор пакета |
| adapter → Ceph | План | Ограниченные права и бюджет перемещения |
| operator → agent | Команда | Локальная аутентификация и аудит |

Административный и клиентский интерфейсы используют разные слушатели.
Управляющий сокет не публикуется через публичный HTTP-location.
Внешний запрос не может выбрать имя Lua-файла.
Параметр клиента не может изменить путь к локальному устройству.
Идентификатор арендатора выводится из доверенного маршрута и проверенных полномочий.
Заголовок с похожим именем не становится доверенным автоматически.

## Главные инварианты

1. Неавторизованный клиент не получает тело из любого уровня кеша.
2. Один запрос использует одно поколение политики.
3. Cache key включает область изоляции и версию представления.
4. Незавершённая запись не публикуется как готовый объект.
5. Ошибка локального кеша не меняет права доступа.
6. Отмена клиента освобождает все принадлежащие ему ресурсы.
7. Фоновая миграция не перезаписывает открытый читателем объект.
8. Телеметрия не содержит секрет подписи или полный bearer-токен.
9. Метрика учитывает свою единицу измерения.
10. Перемещение объекта не считается новым пользовательским спросом.
11. Промах по чанку не приравнивается к промаху по всему файлу.
12. Состояние управления может отставать без остановки локального чтения.

## Модель запроса

Контекст запроса создаётся после выбора виртуального сервера.
Он содержит идентификатор запроса, маршрут, поколение конфигурации и ссылку на бюджет ресурсов.
Необходимая клиентская информация копируется в ограниченное представление.
Сырые заголовки не передаются всем компонентам без необходимости.
После решения allow контекст получает непрозрачный дескриптор полномочий.
Дескриптор связывает tenant, объект, методы и срок действия.
Доступ к кешу без такого дескриптора считается ошибкой программирования.

```yaml
# Проектная форма внутреннего контекста; это не sloyra.yaml.
request:
  id: req-example-01
  config_generation: 42
  route_id: media
  tenant_id: tenant-a
  method: GET
  object_id: asset-210
  representation_id: original-v3
  authorization:
    decision: allow
    reason: signed_url_valid
    deadline_unix_s: 1800000300
  cache:
    namespace: tenant-a/media
    read_allowed: true
    write_allowed: true
```

## Машина состояний запроса

```text
accepted
  -> normalized
  -> policy_pending
  -> authorized
  -> representation_selected
  -> cache_lookup
  -> cache_read | origin_wait
  -> headers_committed
  -> body_streaming
  -> completed

policy_pending -> denied
policy_pending -> dependency_failed
any_uncommitted_state -> cancelled
body_streaming -> truncated
```

Состояния denied и dependency_failed различаются в метриках.
После отправки заголовков нельзя заменить уже начатый ответ новым статусом 503.
Если поток оборвался после отправки заголовков, фиксируется ошибка доставки и соединение завершается по правилам протокола.
Повторная проверка доступа требуется для нового HTTP-запроса, даже если соединение уже использовалось.
Длинная передача по умолчанию использует допуск, выданный в начале запроса.
Немедленное прекращение уже идущих передач при отзыве является отдельным проектным режимом.

## Каноническая конфигурация

Все параметры SLOYRA описываются в `sloyra.yaml`.
Nginx-конфигурация описывает транспорт и подключение модулей.
Псевдодирективы SLOYRA в `nginx.conf` не вводятся этим документом.
Механизм чтения YAML и передачи снимка должен быть реализован до использования примеров.
Пять верхних секций остаются стабильными: server, access, cache, telemetry, placement.

```yaml
# Проектный пример. Требует будущего компилятора конфигурации SLOYRA.
server:
  schema_version: 1
  node_id: edge-a01
  deployment_id: delivery-prod
  control_socket: /run/sloyra/control.sock
  snapshot_directory: /var/lib/sloyra/snapshots
  shutdown_grace_ms: 30000
  config:
    reject_unknown_fields: true
    max_snapshot_bytes: 16777216
    keep_generations: 3
access:
  enabled: true
  default_action: deny
  policy_bundle: /etc/sloyra/policy.bundle
  decision_timeout_ms: 20
  clock_skew_s: 5
  on_dependency_error: unavailable
cache:
  namespace: media
  key_schema_version: 1
  max_object_bytes: 17179869184
  write_admission: demand_based
  index:
    max_entries: 2000000
    reserve_percent: 10
telemetry:
  enabled: true
  queue_capacity_events: 65536
  flush_interval_ms: 1000
  object_id_mode: keyed_hash
  on_overflow: count_and_drop
placement:
  enabled: true
  dry_run: true
  planner_interval_s: 60
  max_parallel_moves: 2
```

Параметры памяти задаются в байтах.
Интервалы имеют суффикс единицы в имени.
Проценты проверяются в диапазоне от 0 до 100.
Пустая строка не считается отсутствующим параметром.
Неизвестный ключ отклоняется, чтобы опечатка не меняла поведение молча.
Парсер ограничивает глубину YAML и общий размер входа.
Секреты хранятся отдельно и подключаются по непрозрачной ссылке.

## Атомарная публикация конфигурации

1. Агент получает новую ревизию исходного файла.
2. Парсер строит строго типизированную модель.
3. Валидатор проверяет диапазоны, маршруты и ссылки на секреты.
4. Компилятор строит снимок с номером поколения.
5. Снимок записывается во временный файл в том же каталоге.
6. Проверяются контрольная сумма и возможность повторного чтения.
7. Ссылка на активное поколение заменяется атомарно.
8. Новые запросы получают новое поколение.
9. Старые запросы сохраняют ссылки на прежнее поколение.
10. Сборщик удаляет поколение после освобождения последней ссылки.

Неверный новый файл оставляет прежнее поколение активным.
При отсутствии первого корректного снимка защищённые маршруты не обслуживают контент.
Агент не делает частичного применения половины полей.
Поколение включает и правила доступа, и правила ключа кеша.
Ротация секрета допускает перекрытие поколений на ограниченное время.

## Схема идентификаторов

| Идентификатор | Область уникальности | Назначение |
| --- | --- | --- |
| `deployment_id` | Установка | Отделяет окружения |
| `node_id` | Установка | Обозначает узел |
| `boot_id` | Узел | Отделяет перезапуски |
| `request_id` | Узел и запуск | Связывает журнал запроса |
| `config_generation` | Установка или узел | Выбирает снимок |
| `policy_revision` | Пакет политики | Делает решение воспроизводимым |
| `object_id` | Арендатор | Обозначает логический объект |
| `representation_id` | Объект | Отделяет кодирование и версию |
| `chunk_id` | Представление | Адресует диапазон хранения |
| `move_id` | Планировщик | Обеспечивает идемпотентность |

Идентификатор не содержит имя пользователя, email или полный URL.
Случайный request_id не является токеном доступа.
Номер поколения не должен уходить назад при обычной публикации.
Откат фиксируется как новое поколение со ссылкой на прежнее содержимое.

## Модель владения ресурсами

Память запроса освобождается вместе с его контекстом.
Разделяемые индексы имеют самостоятельный срок жизни.
Каждый открытый файл учитывается бюджетом дескрипторов.
Каждая запись временного объекта учитывается бюджетом незавершённых загрузок.
Каждый ожидающий подписчик request collapse учитывается отдельным лимитом.
Сетевой буфер не может расти пропорционально размеру всего видеофайла.
Ресурс, передаваемый между компонентами, имеет одного явного владельца.
Разделяемое владение реализуется ссылками, а не копированием незащищённых указателей.

## Разделение данных и управления

Плоскость данных отвечает за текущий запрос.
Она обязана иметь конечное время ожидания каждого внешнего действия.
Плоскость управления отвечает за желаемое состояние.
Она может повторять операцию с идемпотентным идентификатором.
Плоскость наблюдения измеряет обе стороны.
Она не имеет полномочий разрешать доступ к контенту.

Потеря управляющего соединения сама по себе не инвалидирует уже загруженную локальную политику.
Срок допустимой автономной работы задаётся для конкретного типа политики.
Короткоживущие подписанные ссылки продолжают проверяться локально при доступном ключе.
Политика, требующая свежего внешнего решения, возвращает 503 при исчерпании разрешённого окна.
Решение 403 используется для установленного отказа в правах.

## Каталог внутренних контрактов

Карточки ниже задают границы компонентов для реализации и ревью.
Каждый контракт требует отдельной проверки ошибки и освобождения ресурсов.

### A-01. normalizer

Вход: Сырые method, authority, path и query.
Выход: CanonicalRequest.
Основное правило: Отклонить неоднозначные кодировки и управляющие символы.
Отказ: 400 до выполнения policy.
Наблюдаемость: `normalization_rejected_total`.
Инвариант: Канонический путь не зависит от порядка несемантических параметров.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-02. route-resolver

Вход: CanonicalRequest и снимок маршрутов.
Выход: RouteBinding.
Основное правило: Выбрать ровно один маршрут и tenant.
Отказ: 404 для отсутствующего маршрута.
Наблюдаемость: `route_resolution_total`.
Инвариант: Маршрут другого tenant нельзя выбрать заголовком клиента.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-03. policy-runtime

Вход: RouteBinding и ограниченные атрибуты.
Выход: AccessDecision.
Основное правило: Вернуть allow, deny или unavailable.
Отказ: 503 при исключении среды.
Наблюдаемость: `policy_duration_seconds`.
Инвариант: Ни один кеш не читается до allow.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-04. credential-verifier

Вход: CanonicalRequest и keyring snapshot.
Выход: VerifiedCredential.
Основное правило: Проверить подпись, audience и срок.
Отказ: 403 для неверного credential.
Наблюдаемость: `credential_verification_total`.
Инвариант: Сравнение подписи не раскрывает совпавший префикс.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-05. representation-resolver

Вход: ObjectBinding и Accept-Encoding.
Выход: RepresentationBinding.
Основное правило: Зафиксировать версию и способ кодирования.
Отказ: 502 при неоднозначных метаданных origin.
Наблюдаемость: `representation_resolution_total`.
Инвариант: Диапазоны относятся к выбранному представлению.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-06. cache-index

Вход: CacheKey и generation fence.
Выход: CacheHandle или miss.
Основное правило: Возвращать только опубликованные записи.
Отказ: miss при незавершённой записи.
Наблюдаемость: `cache_lookup_total`.
Инвариант: Дескриптор удерживает объект от удаления.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-07. ram-reader

Вход: CacheHandle и byte interval.
Выход: ReadStream.
Основное правило: Читать только подтверждённые границы буфера.
Отказ: повтор lookup при вытеснении до захвата ссылки.
Наблюдаемость: `ram_read_bytes_total`.
Инвариант: Счётчик ссылки обновляется атомарно.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-08. disk-reader

Вход: CacheHandle и descriptor budget.
Выход: ReadStream.
Основное правило: Открыть файл по внутреннему идентификатору.
Отказ: origin fallback до отправки заголовков.
Наблюдаемость: `disk_read_failures_total`.
Инвариант: Пользовательский URI не конкатенируется с путём диска.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-09. origin-fetcher

Вход: AuthorizedOriginRequest.
Выход: OriginStream.
Основное правило: Применить connect, headers и body deadlines.
Отказ: 502 или 504 по классу сбоя.
Наблюдаемость: `origin_attempts_total`.
Инвариант: Нельзя переключить origin на произвольный host из query.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-10. fill-coordinator

Вход: RepresentationKey и fill options.
Выход: LeaderLease или Follower.
Основное правило: Объединять только совместимые загрузки.
Отказ: 503 при исчерпании очереди.
Наблюдаемость: `fill_waiters_current`.
Инвариант: Разные приватные области не делят загрузку.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-11. cache-writer

Вход: OriginStream и expected metadata.
Выход: UnpublishedEntry.
Основное правило: Записывать во временный объект с ограничением размера.
Отказ: discard при обрыве origin.
Наблюдаемость: `cache_fill_bytes_total`.
Инвариант: Публикация возможна только после проверки полноты.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-12. cache-publisher

Вход: UnpublishedEntry и validator.
Выход: PublishedEntry.
Основное правило: Выполнить compare-and-publish поколения.
Отказ: discard при более новой версии.
Наблюдаемость: `cache_publish_conflicts_total`.
Инвариант: Старый writer не заменяет новый объект.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-13. evictor

Вход: Кандидаты и ограничения уровня.
Выход: EvictionResult.
Основное правило: Удалять холодные незакреплённые записи.
Отказ: skip при открытом читателе.
Наблюдаемость: `cache_evictions_total`.
Инвариант: Eviction не меняет объект в origin.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-14. migration-worker

Вход: MovePlan и source handle.
Выход: MoveReceipt.
Основное правило: Копировать, проверить, переключить ссылку.
Отказ: сохранить source при сбое destination.
Наблюдаемость: `placement_move_failures_total`.
Инвариант: Последняя целая копия не удаляется преждевременно.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-15. demand-recorder

Вход: Разрешённый завершённый запрос.
Выход: DemandEvent.
Основное правило: Отделить запросы, чанки и байты.
Отказ: count_and_drop при переполнении.
Наблюдаемость: `telemetry_dropped_total`.
Инвариант: Запрещённые запросы не прогревают popularity.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-16. aggregator

Вход: DemandEvent с event_id.
Выход: DemandWindow.
Основное правило: Дедуплицировать в пределах объявленного окна.
Отказ: quarantine при неправильной схеме.
Наблюдаемость: `aggregation_rejected_total`.
Инвариант: Повтор пакета не удваивает demand.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-17. telemetry-spool

Вход: DemandWindow и quota.
Выход: DurableBatch.
Основное правило: Ограничить объём и срок хранения.
Отказ: drop_oldest_with_counter при политике потерь.
Наблюдаемость: `spool_bytes_current`.
Инвариант: Заполнение spool не заполняет весь системный диск.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-18. ceph-adapter

Вход: DurableBatch и mapping revision.
Выход: PlacementSignal.
Основное правило: Разрешать только известные object mapping.
Отказ: hold при неизвестном mapping.
Наблюдаемость: `adapter_unmapped_objects_total`.
Инвариант: HTTP URI не принимается за имя объекта Ceph.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-19. config-compiler

Вход: sloyra.yaml и policy bundle.
Выход: ConfigSnapshot.
Основное правило: Проверить все ссылки до публикации.
Отказ: reject без изменения active generation.
Наблюдаемость: `config_validation_failures_total`.
Инвариант: Частичный снимок не становится активным.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-20. snapshot-loader

Вход: ConfigSnapshot и ABI descriptor.
Выход: SnapshotHandle.
Основное правило: Проверить checksum, size и schema.
Отказ: retain previous при ошибке.
Наблюдаемость: `snapshot_load_failures_total`.
Инвариант: Worker подтверждает именно загруженное поколение.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-21. admin-api

Вход: Аутентифицированная локальная команда.
Выход: OperationReceipt.
Основное правило: Записать audit до изменения состояния.
Отказ: 409 при конфликте ожидаемого поколения.
Наблюдаемость: `admin_operations_total`.
Инвариант: Оператор видит идентификатор принятой операции.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.


### A-22. health-reporter

Вход: Локальные состояния компонентов.
Выход: HealthSnapshot.
Основное правило: Разделить liveness, readiness и degraded.
Отказ: degraded при потере telemetry.
Наблюдаемость: `health_state`.
Инвариант: Публичная проверка не раскрывает секретные пути.
Владелец тайм-аута: вызывающий компонент.
Форма проверки: отдельный успешный сценарий и инъекция указанного отказа.

## Сценарии жизненного цикла

Сценарий считается пройденным, когда наблюдаемое состояние совпало с контрактом.
Имена событий ниже относятся к проектному журналу SLOYRA.
Они не являются именами существующих сообщений Nginx.

### L-01. Первый запуск

Исходное состояние: active_generation отсутствует.
Действие: Агент валидирует конфигурацию и публикует поколение 1.
Ожидаемый результат: ready только после загрузки worker.
Событие журнала: `config.initialized`.
Контрольный крайний случай: Запрос до готовности получает 503.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-01
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-02. Ошибочный YAML

Исходное состояние: active_generation = 7.
Действие: Новая ревизия не проходит типизацию.
Ожидаемый результат: Поколение 7 продолжает обслуживать запросы.
Событие журнала: `config.rejected`.
Контрольный крайний случай: В журнале только путь ключа и код ошибки.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-02
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-03. Неверный policy bundle

Исходное состояние: Пакет отсутствует или checksum не совпал.
Действие: Компилятор отклоняет весь снимок.
Ожидаемый результат: Ни один worker не получает частичный пакет.
Событие журнала: `policy.bundle_rejected`.
Контрольный крайний случай: Права на чтение проверяются отдельно от содержимого.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-03
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-04. Graceful reload

Исходное состояние: Поколение 8 активно.
Действие: Nginx создаёт новые worker с валидным модулем.
Ожидаемый результат: Старые запросы завершаются по своему снимку.
Событие журнала: `worker.draining`.
Контрольный крайний случай: Открытые handle удерживают старый индекс.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-04
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-05. Откат конфигурации

Исходное состояние: Поколение 9 ухудшает доступность.
Действие: Оператор публикует содержимое 8 как поколение 10.
Ожидаемый результат: Новые запросы используют 10.
Событие журнала: `config.rollback_applied`.
Контрольный крайний случай: Аудит содержит target_content_generation = 8.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-05
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-06. Падение агента

Исходное состояние: Снимок уже загружен.
Действие: Worker продолжает локальные разрешённые операции.
Ожидаемый результат: Фоновое планирование приостановлено.
Событие журнала: `control.disconnected`.
Контрольный крайний случай: Срок автономии зависит от класса политики.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-06
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-07. Возврат агента

Исходное состояние: Очередь накопила события.
Действие: Агент сверяет boot_id и sequence.
Ожидаемый результат: Повторы не увеличивают агрегаты.
Событие журнала: `control.resynchronized`.
Контрольный крайний случай: Старый PID не считается идентичностью процесса.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-07
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-08. Переполнение очереди

Исходное состояние: Telemetry queue достигла capacity.
Действие: Новые необязательные события отбрасываются с учётом.
Ожидаемый результат: Доставка контента остаётся доступной.
Событие журнала: `telemetry.overflow`.
Контрольный крайний случай: Отдельно публикуется неполнота статистики.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-08
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-09. Пустой cache index

Исходное состояние: Файлы на диске существуют.
Действие: Фоновый scanner восстанавливает только проверенные записи.
Ожидаемый результат: Неизвестный файл не становится hit сразу.
Событие журнала: `cache.rebuild_started`.
Контрольный крайний случай: Scanner ограничен IOPS.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-09
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-10. Утрата RAM-сегмента

Исходное состояние: Worker потерял локальный handle.
Действие: Повторный lookup идёт по следующему уровню.
Ожидаемый результат: Права клиента остаются прежними.
Событие журнала: `cache.ram_lost`.
Контрольный крайний случай: Счётчик miss указывает фактический уровень.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-10
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-11. Отказ NVMe

Исходное состояние: Чтение возвращает ошибку устройства.
Действие: Уровень временно исключается из выбора.
Ожидаемый результат: Разрешённый запрос пробует origin до headers.
Событие журнала: `cache.tier_degraded`.
Контрольный крайний случай: Новые записи на отказавший уровень запрещены.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-11
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-12. Заполнение HDD

Исходное состояние: Свободное место ниже reserve.
Действие: Admission отключает новые записи.
Ожидаемый результат: Чтение целых объектов продолжается.
Событие журнала: `cache.tier_full`.
Контрольный крайний случай: Временные файлы входят в оценку занятого места.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-12
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-13. Ошибка destination при миграции

Исходное состояние: Source цел и доступен.
Действие: Копирование отменяется без переключения index.
Ожидаемый результат: Source остаётся обслуживаемой копией.
Событие журнала: `placement.copy_failed`.
Контрольный крайний случай: Временный destination очищается после снятия ссылок.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-13
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-14. Сбой после копирования

Исходное состояние: Destination записан, index ещё старый.
Действие: Recovery проверяет move journal.
Ожидаемый результат: Выбирается одна опубликованная версия.
Событие журнала: `placement.recovered`.
Контрольный крайний случай: Удаление source требует подтверждённого commit.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-14
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-15. Обновление origin

Исходное состояние: ETag изменился.
Действие: Новая загрузка получает representation version.
Ожидаемый результат: Старые читатели заканчивают прежнюю версию.
Событие журнала: `object.version_changed`.
Контрольный крайний случай: Чанки разных ETag никогда не объединяются.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-15
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-16. Purge во время fill

Исходное состояние: Writer ещё не опубликовал файл.
Действие: Purge увеличивает generation fence.
Ожидаемый результат: Запоздалый writer отбрасывает результат.
Событие журнала: `cache.fill_fenced`.
Контрольный крайний случай: Purge распространяется на whole-object и chunk index.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-16
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-17. Отзыв ключа

Исходное состояние: Ключ был активным.
Действие: Агент публикует keyring без права проверки revoked key.
Ожидаемый результат: Новые запросы со старой подписью отклоняются.
Событие журнала: `access.key_revoked`.
Контрольный крайний случай: Политика длинных передач документируется отдельно.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-17
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-18. Скачок настенных часов

Исходное состояние: Рассогласование превышает допуск.
Действие: Node уходит в degraded_clock.
Ожидаемый результат: Допуск новых short-lived токенов ограничен.
Событие журнала: `clock.untrusted`.
Контрольный крайний случай: Duration продолжает измеряться монотонными часами.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-18
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-19. Медленный клиент

Исходное состояние: Клиент читает ниже рабочего порога.
Действие: Backpressure ограничивает producer.
Ожидаемый результат: Буферы не растут до размера объекта.
Событие журнала: `delivery.backpressure`.
Контрольный крайний случай: Fill может продолжаться только в отдельном бюджете.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-19
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-20. Отмена лидера fill

Исходное состояние: Главный клиент разорвал соединение.
Действие: Coordinator проверяет ожидающих подписчиков.
Ожидаемый результат: Полезная загрузка может продолжиться для них.
Событие журнала: `fill.leader_detached`.
Контрольный крайний случай: Клиентский lifetime отделён от fill lifetime.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-20
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-21. Отмена последнего подписчика

Исходное состояние: Никто больше не ждёт объект.
Действие: Coordinator применяет политику background_fill.
Ожидаемый результат: Необязательная загрузка прекращается.
Событие журнала: `fill.no_waiters`.
Контрольный крайний случай: Событие origin bytes сохраняет уже потраченные байты.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-21
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-22. Новый tenant

Исходное состояние: Опубликован отдельный namespace.
Действие: Индекс и квоты создаются в новом поколении.
Ожидаемый результат: Старые арендаторы сохраняют свои бюджеты.
Событие журнала: `tenant.activated`.
Контрольный крайний случай: Конфликт имён каталогов отклоняется валидатором.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-22
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-23. Удаление tenant

Исходное состояние: Tenant переведён в draining.
Действие: Новые запросы запрещены, старые завершаются по политике.
Ожидаемый результат: После grace удаляются локальные данные.
Событие журнала: `tenant.removed`.
Контрольный крайний случай: Агрегаты с идентификатором tenant проходят retention.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-23
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-24. Потеря mapping Ceph

Исходное состояние: Объект известен edge, но неизвестен adapter.
Действие: Сигнал помещается в ограниченную pending-очередь.
Ожидаемый результат: Данные Ceph не перемещаются догадкой.
Событие журнала: `adapter.mapping_missing`.
Контрольный крайний случай: URL и object_id не взаимозаменяемы.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-24
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-25. Остановка узла

Исходное состояние: Оператор объявляет drain.
Действие: Listener снимается из внешней балансировки.
Ожидаемый результат: Активные запросы заканчиваются в grace.
Событие журнала: `node.draining`.
Контрольный крайний случай: Новые планы миграции не запускаются.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-25
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-26. Аварийное выключение

Исходное состояние: Процесс завершился без drain.
Действие: Следующий запуск проверяет journal и temporary entries.
Ожидаемый результат: Публикуются только целостные объекты.
Событие журнала: `node.recovered`.
Контрольный крайний случай: Незавершённая запись не считается cache hit.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-26
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-27. Несовместимое обновление схемы

Исходное состояние: Новый agent требует другой snapshot version.
Действие: Handshake обнаруживает несовместимость.
Ожидаемый результат: Рабочий снимок удерживается до согласованного обновления.
Событие журнала: `control.schema_mismatch`.
Контрольный крайний случай: Автоматическое угадывание полей запрещено.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-27
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-28. Смена cache key schema

Исходное состояние: Добавлен новый Vary dimension.
Действие: Namespace получает новую версию ключа.
Ожидаемый результат: Старые записи становятся недоступными новой схеме.
Событие журнала: `cache.key_schema_changed`.
Контрольный крайний случай: Фоновая очистка не задерживает публикацию.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-28
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-29. Повтор управляющей команды

Исходное состояние: Оператор повторил operation_id.
Действие: Agent возвращает прежний receipt.
Ожидаемый результат: Операция не запускается дважды.
Событие журнала: `admin.replayed`.
Контрольный крайний случай: Разный payload с тем же id даёт конфликт.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-29
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-30. Частичная недоступность сети

Исходное состояние: Origin A не отвечает, origin B разрешён.
Действие: Fetcher ограниченно пробует B в том же request budget.
Ожидаемый результат: Результат не пересекает tenant boundary.
Событие журнала: `origin.failover`.
Контрольный крайний случай: Общее время не равно сумме неограниченных retries.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-30
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-31. Нехватка файловых дескрипторов

Исходное состояние: FD budget достиг резерва.
Действие: Admission не открывает новые cache readers.
Ожидаемый результат: Нода сообщает overload без утечки handle.
Событие журнала: `resource.fd_exhausted`.
Контрольный крайний случай: Управляющий канал имеет отдельный резерв.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-31
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

### L-32. Двойной agent

Исходное состояние: Два процесса претендуют на node lease.
Действие: Второй не получает exclusive ownership.
Ожидаемый результат: Только один публикует снимки и планы.
Событие журнала: `control.lease_conflict`.
Контрольный крайний случай: Lease использует boot_id и generation fencing.

```yaml
# Проектная карточка приёмочного сценария.
scenario: architecture-L-32
assertions:
  resource_leak: false
  secret_in_log: false
  active_generation_known: true
```

## Бюджеты и ограничения

Бюджет запроса складывается из CPU, памяти, дескрипторов и времени ожидания.
Бюджет fill дополнительно включает origin connections и временное место.
Бюджет migration включает чтение source, запись destination и резерв свободного места.
Бюджет telemetry включает очередь в памяти, spool и скорость выгрузки.
Планировщик не занимает весь I/O budget пользовательского чтения.
Каждый лимит имеет измеритель текущего использования и счётчик отказов.

| Ресурс | Основной владелец | Поведение при исчерпании |
| --- | --- | --- |
| Request context | Worker | Отказ до отправки тела |
| Policy CPU | Worker | Прервать правило и вернуть unavailable |
| Origin connection | Fetch coordinator | Очередь до deadline |
| Fill buffer | Cache writer | Backpressure или отмена |
| Cache index | Agent и worker | Admission off для новых записей |
| Disk reserve | Tier manager | Прекратить новые fills |
| File descriptor | Worker | Ограничить конкуренцию |
| Telemetry queue | Recorder | Считать потери |
| Spool bytes | Agent | Применить retention |
| Move bandwidth | Planner | Отложить миграцию |

## Семантика здоровья

Liveness отвечает, способен ли процесс исполнять свой цикл.
Readiness отвечает, можно ли направлять новые запросы на узел.
Degraded объясняет частично недоступную возможность.
Отсутствие интеграции Ceph может дать degraded placement при сохранении доставки.
Отсутствие обязательной политики доступа снимает readiness защищённых маршрутов.
Отказ одного cache tier не обязан снимать readiness всего узла.
Проверка здоровья не делает origin-запрос на каждый вызов.
Она использует ограниченный по возрасту результат отдельной проверки.

```yaml
# Проектный ответ локального health API.
health:
  live: true
  ready: true
  state: degraded
  generation: 42
  components:
    access: ready
    cache_ram: ready
    cache_nvme: degraded
    cache_hdd: ready
    telemetry: ready
    placement: paused
  reasons:
    - code: nvme_read_error
      since_unix_s: 1800000000
```

## Требования к пакету выпуска

Пакет должен включать manifest точной сборки.
Manifest указывает версию Nginx и параметры его конфигурации.
Manifest указывает архитектуру CPU и используемую libc.
Manifest указывает версии связанных криптографических библиотек.
Manifest указывает LuaJIT ABI при наличии Lua-профиля.
Manifest указывает snapshot schema и минимальную версию агента.
Manifest содержит контрольные суммы модулей и policy bundle.
Проверка manifest предшествует замене работающих бинарников.

Пример не обещает переносимость одного `.so` между произвольными дистрибутивами.
Совместимость подтверждается конкретной комбинацией пакетов на стенде.
Nginx поддерживает загрузку динамического модуля через `load_module` в основном контексте конфигурации.
Этот механизм является частью Nginx, а имя модуля SLOYRA здесь является проектным.
Источник: [Nginx core: load_module](https://nginx.org/en/docs/ngx_core_module.html#load_module).

## Критерии готовности архитектуры

Доступ до cache hit проверяется автоматизированным сценарием с горячим объектом.
Изоляция tenant проверяется двумя пользователями с одинаковым URL.
Reload проверяется на активной передаче большого файла.
Сбой writer проверяется между записью последнего байта и публикацией.
Purge проверяется одновременно с двумя конкурентными fill.
Потеря agent проверяется без потери уже загруженного снимка.
Отказ telemetry проверяется без появления неограниченной очереди.
Аварийный restart проверяется на незавершённом move journal.
Несовместимый модуль отклоняется до изменения рабочего процесса.
Секреты проверяются в access log, error log, trace и диагностическом архиве.

## Первичные источники

Архитектурные интерфейсы SLOYRA выше являются собственным проектным предложением.
Ссылки ниже подтверждают лишь свойства базовых технологий.

- [Nginx: разработка модулей и модель обработки запросов](https://nginx.org/en/docs/dev/development_guide.html).
- [Nginx: конфигурация сборки](https://nginx.org/en/docs/configure.html).
- [Nginx: загрузка динамических модулей](https://nginx.org/en/docs/ngx_core_module.html#load_module).
- [Nginx: управление процессами и reload](https://nginx.org/en/docs/control.html).
- [ngx_lua: официальный репозиторий и ограничения LuaJIT](https://github.com/openresty/lua-nginx-module).

---

# Интеграция с Nginx и Lua

> Статус: проектная спецификация v0.1. Имена API и параметры в примерах задают целевой контракт; примеры SLOYRA требуют реализации и не являются готовой инструкцией установки.

## Профили интеграции

Предусмотрены два целевых профиля поставки.
Native-профиль подключает `ngx_http_sloyra_module.so` к совместимой сборке Nginx.
Lua-профиль исполняет правила допуска через ngx_lua и использует адаптер доставки.
Для одного маршрута выбирается один владелец проверки доступа.
Одновременное исполнение двух независимых policy chain не допускается без явной композиции.
Native-профиль нужен для управления файловыми дескрипторами, shared index и потоком тела.
Lua-профиль удобен для прототипирования правил и интеграции с существующими приложениями.
Функциональная эквивалентность двух профилей подтверждается общей коллекцией сценариев.

Lua-профиль не получает автоматически все возможности нативного кеша.
Если используется штатный proxy cache Nginx, это фиксируется как отдельный backend кеширования.
Перенос объектов между RAM, NVMe, SSD и HDD требует собственного согласованного механизма.
Обычная смена каталога proxy cache сама по себе таким механизмом не является.
Документация выпуска должна перечислять реализованные возможности каждого профиля.

## Факты базовой платформы

`load_module` загружает динамический модуль Nginx.
Сборочный параметр `--add-dynamic-module` предназначен для добавления внешнего динамического модуля.
`--with-compat` включает механизм совместимости динамических модулей, но не заменяет проверку конкретной сборки.
Источники: [Nginx core](https://nginx.org/en/docs/ngx_core_module.html#load_module), [Nginx configure](https://nginx.org/en/docs/configure.html).

В актуальном ngx_lua средой выполнения является LuaJIT; стандартный PUC-Rio Lua больше не поддерживается начиная с ngx_lua v0.10.16.
Поэтому формулировка «Lua 5.1» в SLOYRA относится к профилю языка, а не к обещанию загрузки любого интерпретатора.
Сетевые операции должны использовать доступные в нужной фазе неблокирующие API ngx_lua.
Источник: [официальная документация ngx_lua](https://github.com/openresty/lua-nginx-module#description).

## Native bootstrap

Канонические параметры сохраняются в `sloyra.yaml`.
Имя YAML-файла передаётся будущему bootstrap-коду через `SLOYRA_CONFIG`.
Переменная содержит абсолютный путь, а не YAML-текст.
Bootstrap валидирует путь и читает только разрешённый файл.
Путь не может изменяться HTTP-запросом.
Новый bootstrap обязан повторять валидацию при создании поколения конфигурации.

```nginx
# Проектная схема подключения будущего модуля SLOYRA.
# load_module и env — реальные директивы Nginx.
# Файл .so и обработка SLOYRA_CONFIG должны существовать в поставке.
load_module /usr/lib/nginx/modules/ngx_http_sloyra_module.so;
env SLOYRA_CONFIG=/etc/sloyra/sloyra.yaml;

worker_processes auto;

 events {
    worker_connections 4096;
}

http {
    include /etc/nginx/mime.types;

    server {
        listen 8080;
        server_name media.example.invalid;

        location /health/transport {
            return 204;
        }

        # Маршрут SLOYRA связывается будущим bootstrap из YAML.
        # Здесь намеренно нет вымышленных директив sloyra_*.
    }
}
```

Порт 8080 в примере предназначен для изолированного стенда.
Продакшен-слушатели, сертификаты и сетевые политики определяются окружением.
Health transport подтверждает работу HTTP-слушателя, а не readiness SLOYRA.
Наличие `load_module` ещё не означает, что защищённый маршрут привязан к политике.
Проверка привязки маршрутов является обязательной частью активации.

```yaml
# Проектный фрагмент sloyra.yaml; числа иллюстрируют профиль стенда.
server:
  schema_version: 1
  integration:
    profile: native
    config_source: environment
    config_environment_name: SLOYRA_CONFIG
    route_binding: compiled_snapshot
  routes:
    - id: media
      server_name: media.example.invalid
      path_prefix: /media/
      tenant_id: tenant-a
      access_policy: signed-media
access:
  enabled: true
  policy_bundle: /etc/sloyra/policy.bundle
cache:
  namespace: tenant-a/media
telemetry:
  enabled: true
placement:
  dry_run: true
```

## Lua bootstrap

Пример ниже показывает точку подключения, а не существующую библиотеку SLOYRA.
`access_by_lua_file` принадлежит ngx_lua и требует соответствующего модуля.
Файл `access.lua` и импорт `sloyra.runtime` в примере являются целевыми артефактами проекта.
Перед развёртыванием проверяется, что access handler действительно выполняется до доставки кешированного ответа.
Любой внутренний redirect обязан сохранять доказательство допуска или запускать проверку заново.

```nginx
# Схема стендового Lua-профиля, требующая будущей библиотеки SLOYRA.
http {
    lua_package_path '/opt/sloyra/lua/?.lua;;';

    server {
        listen 8080;
        server_name media.example.invalid;

        location /media/ {
            access_by_lua_file /opt/sloyra/lua/access.lua;
            proxy_pass http://origin_pool;
        }
    }

    upstream origin_pool {
        server 192.0.2.10:8080;
    }
}
```

```lua
-- Проектный интерфейс sloyra.runtime; готовой библиотеки здесь нет.
local runtime = require("sloyra.runtime")

local ctx, context_error = runtime.request_context()
if not ctx then
    runtime.record_failure("context_unavailable", context_error)
    return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end

local decision, policy_error = runtime.authorize(ctx)
if policy_error then
    runtime.record_failure("policy_unavailable", policy_error)
    return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end

if decision.action == "deny" then
    runtime.record_decision(ctx, decision)
    return ngx.exit(ngx.HTTP_FORBIDDEN)
end

if decision.action == "throttle" then
    runtime.apply_retry_after(decision.retry_after_s)
    return ngx.exit(429)
end

if decision.action ~= "allow" then
    runtime.record_failure("unknown_decision", nil)
    return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end

runtime.bind_authorization(ctx, decision)
```

Код не пишет bearer-токен в error log.
Код не использует `io.open` для проверки секрета на каждом запросе.
Код не выполняет shell-команду для получения подписи.
Код не создаёт новый сетевой клиент без deadline.
Возвращаемая action проверяется по закрытому списку.
Отсутствие результата не трактуется как allow.

## Контракт policy API

Входной объект политики доступен только для чтения.
Строковые поля имеют максимальную длину до передачи в Lua.
Поля времени явно различают Unix timestamp и duration.
Секреты представлены непрозрачными handle.
Проверка HMAC вызывается через криптографический интерфейс, а не самописную арифметику Lua.
Результат политики копируется в структуру native-слоя после проверки типов.
Произвольные объекты Lua не сохраняются как указатели для отложенного C callback.

```lua
-- Иллюстрация чистой проектной политики.
local policy = {}

function policy.evaluate(request, services)
    if request.method ~= "GET" and request.method ~= "HEAD" then
        return {
            action = "deny",
            reason = "method_not_allowed_by_policy",
        }
    end

    local credential = services.verify_signed_url(request)
    if credential.state == "unavailable" then
        return {
            action = "unavailable",
            reason = "credential_verifier_unavailable",
        }
    end

    if credential.state ~= "valid" then
        return {
            action = "deny",
            reason = "credential_invalid",
        }
    end

    return {
        action = "allow",
        reason = "credential_valid",
        scope = credential.scope,
    }
end

return policy
```

## Фазы запроса

| Точка | Действие SLOYRA | Ограничение |
| --- | --- | --- |
| После разбора запроса | Создать context | Не читать тело без необходимости |
| Выбор маршрута | Привязать tenant и policy | Не доверять tenant header клиента |
| Access | Проверить права | Обязательно до cache lookup |
| Content | Выбрать cache/origin | Требовать authorization handle |
| Header filter | Проверить безопасные заголовки | Не запускать новый policy RPC |
| Body filter | Учесть фактические байты | Не копировать весь объект в Lua |
| Log | Записать итоговое событие | Не блокировать на отправке телеметрии |
| Cleanup | Освободить handle | Идемпотентное завершение |

Один и тот же callback не должен завершать запрос дважды.
Callback после отмены запроса проверяет generation и состояние контекста.
Асинхронная операция удерживает необходимые объекты до завершения.
Временные буферы не переживают свой memory pool без отдельного копирования.
Указатель на память request pool нельзя передавать долгоживущему agent queue.

## Проектные структуры границы C

Фрагменты показывают форму ABI, а не компилируемую реализацию.
Фактические заголовочные файлы должны проходить ревью совместимости.
Все длины передаются отдельно от указателей.
Строки не предполагаются NUL-terminated без явного поля.
Размер структуры проверяется до доступа к добавленным полям.

```c
/* Project-only ABI sketch, not a released SDK. */
#include <stdint.h>
#include <stddef.h>

typedef struct {
    const unsigned char *data;
    size_t len;
} sloyra_bytes_t;

typedef enum {
    SLOYRA_ALLOW = 1,
    SLOYRA_DENY = 2,
    SLOYRA_THROTTLE = 3,
    SLOYRA_UNAVAILABLE = 4
} sloyra_action_t;

typedef struct {
    uint32_t abi_version;
    uint32_t struct_size;
    uint64_t config_generation;
    uint64_t monotonic_deadline_ns;
    sloyra_bytes_t tenant_id;
    sloyra_bytes_t route_id;
    sloyra_bytes_t canonical_path;
} sloyra_request_view_t;

typedef struct {
    uint32_t abi_version;
    uint32_t struct_size;
    sloyra_action_t action;
    uint32_t reason_code;
    uint32_t retry_after_s;
    uint64_t authorization_handle;
} sloyra_access_result_t;
```

Структуры межпроцессного обмена сериализуются явно.
Сырой `memcpy` структуры C не задаёт переносимый сетевой формат.
Padding, endian и размер указателя не становятся частью контракта случайно.
Поля reserved и unknown enum проверяются валидатором.
Счётчики байтов используют достаточную разрядность для больших файлов.
Integer overflow переводится в контролируемую ошибку, а не в укороченный буфер.

## Контроль совместимости

Manifest относится к конкретной сборке, а не только к маркетинговой версии.
Проверка проводится до активации listener с защищённым маршрутом.
ABI LuaJIT и ABI Nginx оцениваются раздельно.
Библиотека OpenSSL не подменяется случайной копией из search path.
Зависимости модуля должны разрешаться предсказуемо.
Доступ к каталогу модулей на запись имеет только управляющий процесс установки.

```json
{
  "artifact": "ngx_http_sloyra_module.so",
  "status": "design-example",
  "module_version": "0.1.0-design",
  "target": {
    "os_family": "linux",
    "architecture": "x86_64",
    "libc": "record-exact-build",
    "nginx_version": "record-exact-build",
    "nginx_configure_hash": "sha256:replace-at-build",
    "compiler": "record-exact-build"
  },
  "interfaces": {
    "sloyra_abi": 1,
    "snapshot_schema": 1,
    "policy_api": 1,
    "telemetry_schema": 1
  },
  "dependencies": {
    "luajit": "record-exact-build-if-enabled",
    "openssl": "record-exact-build"
  },
  "features": {
    "native_cache": true,
    "lua_policy": true,
    "ceph_adapter": "separate-process"
  }
}
```

Значения `record-exact-build` являются обязательными заполнителями сборочного конвейера.
Пакет с такими незаполненными значениями не допускается к публикации.
Наличие JSON-примера не подтверждает поддержку конкретной версии Nginx.
Матрица поддержки создаётся из фактически проверенных комбинаций.

## Инженерные требования к реализации

Каждая карточка ниже содержит независимую причину и наблюдаемое условие приёмки.

### N-01. Ограничение времени policy

Причина: Сложная Lua-ветка может задержать обработку других запросов worker.
Требование: Ввести budget и запрещённые операции.
Код диагностики: `policy_budget_exceeded`.
Условие приёмки: Длинная цепочка правил прекращается с unavailable.
Независимая проверка: Независимый простой запрос сохраняет допустимую задержку.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-01
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-02. Блокирующий файловый I/O

Причина: io.open и чтение больших файлов удерживают исполняющий поток.
Требование: Загружать policy и ключи до обработки запросов.
Код диагностики: `policy_blocking_io_detected`.
Условие приёмки: Запрос не читает секрет с диска.
Независимая проверка: Аудит вызовов runtime не содержит файлового чтения.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-02
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-03. Shell-команды

Причина: Запуск внешней команды расширяет поверхность ошибок и не даёт предсказуемой задержки.
Требование: Запретить os.execute и io.popen в policy sandbox.
Код диагностики: `policy_forbidden_api`.
Условие приёмки: Попытка запуска команды отклонена при загрузке.
Независимая проверка: Ни один дочерний процесс не появляется.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-03
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-04. Неблокирующий RPC

Причина: Сетевой запрос может зависнуть при недоступной зависимости.
Требование: Использовать разрешённый async API и deadline.
Код диагностики: `policy_rpc_timeout`.
Условие приёмки: Сбой зависимости возвращает unavailable.
Независимая проверка: Задержка не превышает общий request budget.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-04
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-05. Фаза header filter

Причина: В момент отправки заголовков допуск уже должен быть определён.
Требование: Не разрешать remote authorization из header filter.
Код диагностики: `policy_phase_violation`.
Условие приёмки: Привязка неверного callback отклоняется.
Независимая проверка: Разрешённый ответ имеет проверенный auth handle.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-05
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-06. Фаза body filter

Причина: Фильтр может вызываться много раз для одной передачи.
Требование: Учёт инкрементальный без накопления тела.
Код диагностики: `body_filter_memory_limit`.
Условие приёмки: Большой файл не удерживается целиком.
Независимая проверка: Память зависит от размера окна передачи.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-06
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-07. Фаза log

Причина: Отправка логов может зависнуть при недоступном collector.
Требование: Писать только в bounded queue.
Код диагностики: `log_queue_overflow`.
Условие приёмки: Collector outage увеличивает counter потерь.
Независимая проверка: Завершение HTTP не ждёт collector.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-07
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-08. Разделяемые Lua globals

Причина: Глобальные изменяемые значения могут смешать контексты запросов.
Требование: Хранить request state в scoped context.
Код диагностики: `policy_context_leak`.
Условие приёмки: Два конкурентных tenant не видят данные друг друга.
Независимая проверка: Race harness проверяет переключение coroutine.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-08
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-09. Кеш require

Причина: Загруженная библиотека живёт дольше отдельного запроса.
Требование: Версионировать пакет и заменять поколение целиком.
Код диагностики: `policy_module_generation`.
Условие приёмки: Обновление не меняет исполняющийся запрос.
Независимая проверка: Новый запрос видит ровно новую revision.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-09
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-10. Обработка Lua exception

Причина: Необработанное исключение не является решением deny.
Требование: Защитить границу вызова и нормализовать ошибку.
Код диагностики: `policy_runtime_error`.
Условие приёмки: Ошибка возвращает 503 до заголовков.
Независимая проверка: Stack trace проходит редакцию секретов.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-10
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-11. Ограничение регулярных выражений

Причина: Невыгодный паттерн может расходовать CPU на длинном вводе.
Требование: Ограничить длину входа и разрешённые шаблоны.
Код диагностики: `policy_regex_budget`.
Условие приёмки: Враждебная строка не нарушает CPU budget.
Независимая проверка: Метрика включает rule_id из ограниченного набора.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-11
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-12. Безопасный FFI

Причина: Произвольный FFI обходит ограничения policy API.
Требование: Не предоставлять FFI пользовательским пакетам.
Код диагностики: `policy_ffi_denied`.
Условие приёмки: Импорт запрещённого модуля отклоняется.
Независимая проверка: Только поставляемые trusted bindings имеют native access.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-12
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-13. Memory pool request

Причина: Асинхронный callback может пережить завершённый запрос.
Требование: Удерживать независимый контекст и отмену.
Код диагностики: `request_lifetime_violation`.
Условие приёмки: Отмена не приводит к use-after-free.
Независимая проверка: Sanitizer сценарий покрывает поздний callback.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-13
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-14. Shared index locking

Причина: Глобальный lock при каждом байте создаёт очередь worker.
Требование: Делить индекс по shard и ограничить критическую секцию.
Код диагностики: `cache_lock_wait_seconds`.
Условие приёмки: Чтение тела не удерживает index lock.
Независимая проверка: Нагрузка с горячим объектом не создаёт единую блокировку.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-14
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-15. Атомарность счётчиков

Причина: Несогласованный инкремент теряет события при конкуренции.
Требование: Использовать согласованный атомарный или локальный aggregate.
Код диагностики: `counter_merge_conflict`.
Условие приёмки: Суммы совпадают с контролируемым набором запросов.
Независимая проверка: Байты не переполняются на файле более 4 GiB.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-15
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-16. Дескриптор файла

Причина: Передача handle между потоками требует явного владельца.
Требование: Учитывать refcount и закрывать один раз.
Код диагностики: `fd_owner_error`.
Условие приёмки: Cancel и completion не делают double-close.
Независимая проверка: Повторное использование номера FD не затрагивает другой запрос.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-16
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-17. Ограничение очереди I/O

Причина: Даже неблокирующий механизм может накопить бесконечную очередь.
Требование: Задать max pending и admission policy.
Код диагностики: `io_queue_saturated`.
Условие приёмки: Новый запрос получает предсказуемый отказ.
Независимая проверка: Очередь не превышает объявленный размер.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-17
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-18. Reload и shared memory

Причина: Новые worker могут встретить старый формат сегмента.
Требование: Проверять schema marker и создавать совместимую область.
Код диагностики: `shared_memory_schema_mismatch`.
Условие приёмки: Несовместимые поколения не делят структуру.
Независимая проверка: Старые worker завершают чтение прежнего сегмента.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-18
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-19. Файлы policy bundle

Причина: Writable directory позволяет заменить исполняемый код.
Требование: Проверять владельца и запрет записи worker.
Код диагностики: `policy_bundle_permissions`.
Условие приёмки: Пакет из небезопасного каталога не активируется.
Независимая проверка: Ошибка содержит путь без секретного содержимого.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-19
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-20. Атомарность обновления policy

Причина: Замена одного Lua-файла меняет часть графа imports.
Требование: Доставлять пакет с manifest и checksum.
Код диагностики: `policy_bundle_incomplete`.
Условие приёмки: Отсутствующий dependency отклоняет пакет.
Независимая проверка: Рабочая revision сохраняется без partial reload.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-20
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-21. DNS в policy

Причина: Разрешение произвольного имени создаёт SSRF и задержки.
Требование: Список зависимостей задаётся снимком.
Код диагностики: `policy_dependency_forbidden`.
Условие приёмки: Host из query не используется как RPC endpoint.
Независимая проверка: Redirect RPC не меняет доверенный endpoint.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-21
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-22. Внутренние redirect

Причина: Новый location может обойти исходную access chain.
Требование: Сохранять auth binding только при совместимом route.
Код диагностики: `access_redirect_recheck`.
Условие приёмки: Переход к другому tenant требует нового допуска.
Независимая проверка: Публичный location не открывает приватный объект.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-22
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-23. Subrequest context

Причина: Подзапрос может иметь другой путь и назначение.
Требование: Передавать минимум полномочий с явным типом.
Код диагностики: `subrequest_scope_mismatch`.
Условие приёмки: Auth subrequest не получает content capability.
Независимая проверка: Рекурсивная авторизация ограничена глубиной.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-23
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-24. Системные часы

Причина: Wall clock может сдвинуться при синхронизации.
Требование: Разделить сроки токенов и измерение duration.
Код диагностики: `clock_skew_detected`.
Условие приёмки: Негативная duration невозможна.
Независимая проверка: Подпись проверяется по контролируемому wall time.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-24
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-25. Секрет в диагностике

Причина: Исключение библиотеки может включить исходный URL.
Требование: Применять redaction до форматирования лога.
Код диагностики: `log_redaction_applied`.
Условие приёмки: Токен отсутствует в error log.
Независимая проверка: Диагностический архив использует тот же redactor.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-25
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-26. Нагрузочный профиль

Причина: Средняя задержка скрывает остановки event loop.
Требование: Измерять percentile и event loop lag.
Код диагностики: `worker_event_loop_lag_seconds`.
Условие приёмки: Lua-профиль тестируется с медленной зависимостью.
Независимая проверка: Публикуются нагрузка и версия runtime.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-26
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-27. Смешение интерпретаторов

Причина: Lua bytecode не является универсальным форматом поставки.
Требование: Привязывать bytecode к точному runtime или хранить исходники.
Код диагностики: `policy_bytecode_incompatible`.
Условие приёмки: Несовместимый файл отклонён до активации.
Независимая проверка: Исходники проходят проверку API профиля.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-27
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

### N-28. Разрешение библиотек

Причина: Неограниченный package.path позволяет подменить зависимость.
Требование: Использовать фиксированные каталоги immutable package.
Код диагностики: `policy_import_denied`.
Условие приёмки: Импорт из writable каталога не выполняется.
Независимая проверка: Пакет не читает библиотеки из текущей директории.

```yaml
# Проектное требование для реализации.
requirement_id: nginx-N-28
layer: integration
verification:
  success_path: required
  failure_injection: required
  resource_accounting: required
```

## Практические процедуры интегратора

### Перед сборкой модуля

Получите точную версию целевого Nginx.
Сохраните параметры его конфигурации.
Проверьте архитектуру CPU и системную libc.
Определите используемую криптографическую библиотеку.
Зафиксируйте ngx_lua, LuaJIT и сопутствующие библиотеки при Lua-профиле.
Создайте manifest сборки до упаковки `.so`.
Не используйте случайный модуль из другого окружения по совпадению имени файла.

### Перед активацией

Проверьте синтаксис конфигурации Nginx в изолированном окружении.
Проверьте YAML валидатором проектной схемы SLOYRA.
Убедитесь, что policy bundle доступен для чтения и недоступен worker для записи.
Сопоставьте каждый защищённый маршрут с access policy.
Проверьте, что служебные origin locations недоступны внешнему клиенту.
Проверьте ответ 403 на горячем объекте с неверной подписью.
Проверьте ответ 200 на том же объекте с корректным допуском.
Зафиксируйте module manifest и active generation в журнале развёртывания.

### При росте задержек Lua

Сначала сравните policy latency и общую request latency.
Проверьте долю обращений к внешней зависимости.
Проверьте максимальную длину входных строк.
Проверьте длинные регулярные выражения и циклы по коллекциям.
Проверьте операции файловой системы и случайно подключённые блокирующие библиотеки.
Проверьте GC pressure и создание больших временных таблиц.
Сопоставьте задержки с event loop lag.
Откатывайте пакет политики новым поколением конфигурации.

### При несовместимом модуле

Сохраните работающие бинарники и их manifest.
Не заменяйте файл `.so` под работающим процессом без процедуры обновления.
Проверьте, какой именно бинарник Nginx запускает service manager.
Проверьте search path системных библиотек.
Пересоберите модуль под зафиксированную конфигурацию.
Проверьте запуск на тестовом listener.
Дождитесь readiness нового поколения.
Только после этого переводите трафик.

### При подозрении на обход доступа

Откройте схему маршрутов и внутренних redirect.
Найдите location, реально выдающий тело.
Проверьте наличие auth binding перед content handler.
Повторите запрос с уже прогретым кешем.
Повторите запрос через HEAD и Range.
Повторите запрос с другой authority и tenant.
Проверьте прямой доступ к origin из внешней сети.
Сопоставьте policy_revision, route_id и cache outcome в одном trace.

## Контрольные сценарии поставки

| Сценарий | Обязательный результат |
| --- | --- |
| Модуль отсутствует | Активация маршрута отклонена |
| Модуль не той архитектуры | Ошибка до подачи трафика |
| Unknown snapshot schema | Сохранение рабочего поколения |
| Lua syntax error | Новый пакет не применяется |
| Lua runtime exception | 503 и очищенный context |
| Runtime returns nil | Unavailable, без cache lookup |
| Runtime returns unknown action | Unavailable, диагностический счётчик |
| Secret file unreadable | Readiness защищённого маршрута снята |
| Policy RPC timeout | 503 в пределах request budget |
| Collector timeout | Контент доставляется, потеря телеметрии учитывается |
| Client disconnect | FD и buffers освобождены |
| Reload under load | Нет смешения policy generations |
| Internal redirect | Сфера допуска повторно проверена |
| Cache hit with invalid token | 403 без выдачи тела |
| Slow origin | Event loop остаётся доступным |
| Large response | Lua не удерживает тело целиком |
| Duplicate callback | Нет double finalize |
| Wrong ABI descriptor | Подключение отклонено |
| Keyring revision change | Новый запрос видит согласованный набор ключей |
| Agent unavailable | Действует явно заданная автономная политика |

## Первичные источники

Примеры SLOYRA выше описывают будущие интерфейсы проекта.
Официальные материалы используются для проверки базовой модели Nginx и ngx_lua.

- [Nginx: параметры сборки](https://nginx.org/en/docs/configure.html).
- [Nginx: динамическая загрузка модулей](https://nginx.org/en/docs/ngx_core_module.html#load_module).
- [Nginx: руководство разработчика](https://nginx.org/en/docs/dev/development_guide.html).
- [Nginx: управление процессами](https://nginx.org/en/docs/control.html).
- [ngx_lua: API, фазы и ограничения](https://github.com/openresty/lua-nginx-module).

---

# Проверка доступа и защита от автоматизированного сбора

> Статус: проектная спецификация v0.1. Имена API и параметры в примерах задают целевой контракт; расширения модели угроз требуют отдельной реализации и проверки.

## Цель проверки

SLOYRA проверяет, вправе ли клиент получить конкретное представление объекта.
Нелегитимный запрос получает 403 до обращения к кешированному телу.
Проверка применяется к GET, HEAD и запросам диапазонов.
Уже прогретый объект не даёт исключения из политики.
Повторное соединение не освобождает от проверки нового запроса.
Проверка доступа к файлу отличается от защиты от перегрузки.
Запрет по полномочиям и ограничение скорости учитываются разными причинами.

Защита от массового сбора строится из короткоживущих полномочий и лимитов использования.
Подписанная ссылка подтверждает право предъявителя в заданной области и времени.
Сама по себе она не доказывает, что клиентом является человек.
Авторизованный клиент способен сохранить разрешённый ему контент.
Цель проекта — ограничивать неразрешённое получение и измеримо затруднять злоупотребление легитимными полномочиями.
Параметры защиты выбираются с учётом плееров, мобильных сетей и корпоративных NAT.

## Модель угроз

| Угроза | Защита в целевом контракте |
| --- | --- |
| Ссылка без полномочий | Подпись или доверенная сессия |
| Подмена пути | Подпись канонического ресурса |
| Подмена tenant | Изоляция маршрута и signed tenant |
| Перебор файлов | Отказ до cache lookup и ограничение запроса |
| Повтор украденной ссылки | Срок, scope и при необходимости nonce |
| Выгрузка многих объектов | Квоты субъектов и анализ поведения |
| Обход через Range | Та же access chain |
| Обход через HEAD | Та же access chain |
| Cache poisoning | Проверка ключа, origin и заголовков |
| Межпользовательская утечка | Private cache isolation |
| Подмена client IP | Список доверенных proxy |
| Компрометация подписывающего ключа | Отзыв key id и ротация |
| Утечка через журнал | Редакция credential до записи |
| Отказ auth backend | 503 по установленной политике |

Модель не заменяет безопасность самого Nginx и операционной системы.
Администратор с правом чтения кеш-дисков может читать незашифрованный кеш.
Шифрование локального носителя относится к отдельной модели угроз.
Секрет подписи не должен храниться в том же каталоге, что пользовательские объекты.
Компрометация управляющего процесса рассматривается отдельно от злонамеренного HTTP-клиента.

## Порядок обработки запроса

1. Ограничить размер URI и количество заголовков.
2. Проверить транспортный маршрут и authority.
3. Нормализовать path и разрешённые query-параметры.
4. Определить tenant из доверенной привязки.
5. Извлечь credential с проверкой единственности полей.
6. Проверить формат времени, key id и версии схемы.
7. Найти ключ в локальном неизменяемом keyring.
8. Проверить HMAC и семантику подписанных ограничений.
9. Применить квоты и ограничения поведения.
10. Создать authorization handle.
11. Выполнить cache lookup по разрешённой области.
12. Передать итоговую причину в очищенную телеметрию.

Дорогие операции выполняются после дешёвых ограничений формата.
До криптографической проверки допускается отклонять явно неверный ввод.
Внешний ответ не обязан раскрывать точную причину отказа подписи.
Внутренний reason code сохраняется для эксплуатационной диагностики.

## Signed URL: профиль v1

Целевой алгоритм — HMAC-SHA256 с полным 32-байтовым MAC.
HMAC является конструкцией с секретным ключом; для SLOYRA выбрана SHA-256.
Основа конструкции описана в [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104.html).
Контрольные криптографические векторы доступны в [RFC 4231](https://www.rfc-editor.org/rfc/rfc4231.html).
Схема канонической строки ниже является собственным контрактом SLOYRA.
Она не является универсальным стандартом signed URL.

В базовом профиле разрешаются только GET и HEAD.
GET и HEAD подписываются отдельно, если scope явно не разрешает оба метода.
Плеер получает два разрешения либо ссылку с method scope `GET,HEAD`.
Строка method scope входит в подпись и нормализуется по фиксированному порядку.
Профиль v1 не допускает произвольные методы в этом поле.

| Поле | Смысл | Проверка |
| --- | --- | --- |
| `sv` | Версия схемы | Ровно `1` |
| `kid` | Идентификатор ключа | Из ограниченного набора keyring |
| `st` | Tenant | Совпадает с доверенным маршрутом |
| `sa` | Audience | Совпадает с публичным сервисом |
| `sm` | Допустимые методы | `GET`, `HEAD` или `GET,HEAD` |
| `nbf` | Начало допуска | Unix seconds |
| `exp` | Конец допуска | Unix seconds |
| `sn` | Nonce | Необязательный, но подписанный |
| `sb` | Привязка клиента | Необязательная, но подписанная |
| `sig` | MAC | Base64url без padding |

Числа времени записываются десятичными цифрами без знака и ведущих нулей.
`exp` должен быть больше `nbf`.
Максимальная длительность допуска ограничивается серверной политикой.
Значение из токена не может расширить этот серверный предел.
`kid` является индексом, а не путём к файлу или URL сервиса ключей.
Неизвестный `kid` не вызывает запрос по адресу из credential.

## Канонизация пути

Профиль v1 использует строгое подмножество допустимых URI.
Это проектное ограничение для единообразия подписывающей стороны и edge.
Общие синтаксические правила URI описаны в [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html).

Путь должен начинаться с `/`.
Фрагмент URI не входит в HTTP-запрос и не включается в подпись.
Неверная percent-последовательность отклоняется.
Hex-цифры допустимых percent-последовательностей приводятся к верхнему регистру.
Percent-encoding unreserved-символов раскрывается один раз.
Закодированные `/`, обратная косая черта, NUL и управляющие символы отклоняются.
Сегменты `.` и `..` после раскрытия unreserved-символов отклоняются.
Двойной `/` отклоняется в базовом профиле.
Обратная косая черта отклоняется и в сыром, и в закодированном виде.
Путь не подвергается второй percent-декодировке после проверки подписи.
Регистр букв пути сохраняется.
Не выполняется Unicode-нормализация, меняющая последовательность байтов.
Каталог origin получает тот же смысл пути, который был подписан.

Если legacy origin требует другое поведение, создаётся новая версия canonicalization.
Нельзя незаметно изменить профиль v1 на одном edge-узле.
Подписывающая сторона и verifier используют один набор тестовых векторов.
При расхождении версия не активируется.

## Канонизация query

Credential-поля обрабатываются отдельным парсером и входят в подписываемые поля.
Параметр `sig` исключается из подписываемого материала.
Все остальные поля, влияющие на представление объекта, должны быть подписаны.
Порядок query-параметров не меняет идентичность канонического запроса.
Ключи и значения декодируются один раз, затем кодируются по общей функции.
Символ `+` рассматривается как плюс, а не как пробел.
Пробел кодируется как `%20`.
Имена параметров чувствительны к регистру.
Повтор credential-поля отклоняется, даже если значения совпали.
Повторы content-поля разрешаются только объявленной схемой маршрута.
Для разрешённых повторов сортировка выполняется по имени и затем по значению в байтовом порядке.
Пустое значение отличается от отсутствующего параметра.
Необъявленные content-параметры отклоняются в строгом профиле.
Tracking-параметр можно исключить из подписи только одновременно с исключением из origin-запроса.

### Привязка к версии объекта

При `access.signature.bind_version: true` разрешение обязательно связывается с неизменяемой content version.
Профиль SLOYRA-URL-1 кодирует версию внутри уже подписываемого canonical path либо canonical content query.
В примере этой главы используется объявленный content-параметр `version=v17`.
Маршрут обязан сопоставить его именно immutable version v17 и проверить это сопоставление до cache lookup.
Параметр version нельзя убрать как tracking и нельзя игнорировать при запросе origin.
Вариант с версией в пути должен однозначно извлекать её из объявленного сегмента маршрута.
Если версия отсутствует или запрос разрешился в другую версию, результат — 403.
Добавление отдельного двенадцатого поля потребует новой версии подписываемого формата; v1 не меняется молча.

Маршрут `latest` разрешается только при явно выбранном `bind_version: false`.
Для него политика проверяет право на текущую версию после разрешения latest и до выдачи любого тела.
Cache key всё равно включает фактическую immutable version.
Старая ссылка не даёт автоматически право на новую версию, если policy scope такого права не содержит.
Настройка `access.canonicalization.profile: strict_v1` задаёт правила URI для описанного подписываемого формата SLOYRA-URL-1.

## Канонизация authority и audience

Публичный audience задаётся конфигурацией маршрута.
Он не выводится из произвольного `X-Forwarded-Host`.
Host приводится к ASCII lowercase после проверки корректного разбора.
IDN приводится к заранее согласованному ASCII-представлению при регистрации маршрута.
Необычный порт включается в authority.
Политика удаления стандартного порта фиксируется версией canonicalization.
Терминация TLS на доверенном балансировщике учитывается конфигурацией маршрута.
Клиент не может объявить себя доверенным proxy заголовком.

## Формат подписываемых байтов

Для устранения неоднозначности используется последовательность полей с длиной в байтах.
Каждое поле сериализуется как ASCII-длина, двоеточие и байты значения.
Внешнего разделителя между полями нет.
Порядок полей фиксирован версией схемы.
Длина считается после кодирования в UTF-8.
В базовом URI-профиле канонические path и query представлены ASCII.

```text
Порядок полей v1:
  protocol marker = SLOYRA-URL-1
  key id
  tenant id
  audience
  method scope
  canonical path
  canonical content query
  not-before decimal
  expires decimal
  nonce or empty string
  client binding or empty string

ENCODE(v) = ASCII(byte_length(v)) || ":" || UTF8(v)
PREIMAGE = ENCODE(field_1) || ... || ENCODE(field_11)
MAC = HMAC-SHA256(secret_key, PREIMAGE)
SIGNATURE = BASE64URL_NO_PADDING(MAC)
```

Base64url использует URL-безопасный алфавит из [RFC 4648](https://www.rfc-editor.org/rfc/rfc4648.html#section-5).
Отсутствие padding в этом профиле — явно выбранное правило SLOYRA.
Verifier принимает только каноническую 43-символьную форму полного SHA-256 MAC.
После декодирования проверяется длина 32 байта.
Повторное каноническое кодирование должно дать исходное значение.
Это исключает неканонические варианты последних битов.
Сравнение выполняется библиотечной операцией с постоянным временем для равных длин.

```python
# Проектный эталон только сериализации и MAC.
# Это не SDK и не полный parser URL.
import base64
import hashlib
import hmac


def encode_field(value: str) -> bytes:
    data = value.encode("utf-8")
    return str(len(data)).encode("ascii") + b":" + data


def sign_canonical_fields(secret: bytes, fields: list[str]) -> str:
    if len(fields) != 11:
        raise ValueError("expected 11 canonical fields")
    preimage = b"".join(encode_field(field) for field in fields)
    mac = hmac.new(secret, preimage, hashlib.sha256).digest()
    return base64.urlsafe_b64encode(mac).rstrip(b"=").decode("ascii")


# Исключительно синтетический ключ для теста; не для развёртывания.
example_key = bytes.fromhex("11" * 32)
example_fields = [
    "SLOYRA-URL-1",
    "test-key-01",
    "tenant-a",
    "media.example.invalid",
    "GET,HEAD",
    "/media/movie.mp4",
    "lang=ru&version=v17",
    "1800000000",
    "1800000300",
    "",
    "",
]
example_signature = sign_canonical_fields(example_key, example_fields)
assert len(example_signature) == 43
```

Этот пример не извлекает доверенный tenant и не проверяет сроки.
Подписывать сырые поля HTTP-запроса этой функцией напрямую нельзя.
Сначала применяются все правила canonicalization и проверки схемы.
Секрет не передаётся клиенту вместе с подписанной ссылкой.

## Ротация ключей

Keyring хранит несколько ключей с разными статусами.
`signing` разрешает выпуск новых ссылок и проверку существующих.
`verify_only` разрешает только проверку ранее выпущенных ссылок.
`revoked` запрещает проверку независимо от срока токена.
Для одного audience в обычном режиме действует один основной signing key.
Аварийный отзыв не ждёт окончания максимального срока ссылки.

1. Создать новый ключ в хранилище секретов.
2. Добавить его как verify-capable на все проверяющие узлы.
3. Дождаться подтверждения нового keyring generation.
4. Переключить подписывающую сторону на новый kid.
5. Перевести прежний ключ в verify_only.
6. Дождаться максимального срока старых ссылок и clock skew.
7. Удалить старый ключ из verifier после проверки отсутствия зависимостей.
8. Сохранить аудит kid и периода действия без материала ключа.

```yaml
# Проектный фрагмент sloyra.yaml.
access:
  signature:
    algorithm: hmac_sha256
    keyring: /etc/sloyra/keys.json
    max_ttl: 5m
    clock_skew: 5s
    require_exp: true
    require_kid: true
    bind_method: true
    bind_tenant: true
    bind_version: true
  canonicalization:
    profile: strict_v1
```

Приведённые интервалы — пример короткоживущего профиля.
Слишком короткий срок может нарушить восстановление воспроизведения после сетевого сбоя.
Слишком длинный срок увеличивает окно повторного использования ссылки.
Выбранное значение проверяется на реальном клиентском поведении.

## Replay и привязка клиента

Базовый signed URL допускает повторное использование до истечения срока.
Это нужно плееру для повторных запросов и разных диапазонов.
Одноразовый nonce несовместим с таким поведением без протокола обмена на сессию.
Если требуется single-use, nonce хранится в согласованном реестре с TTL.
При недоступности обязательного реестра используется 503.
Локальный nonce-cache одного узла не гарантирует single-use на всём кластере.

Привязка к IP включается только осознанно.
Смена адреса в мобильной сети может сделать корректную ссылку недействительной.
Привязка к подсети уменьшает ложные отказы, но ослабляет ограничение.
Проксируемый адрес доверяется только от явно указанных upstream proxy.
Fingerprint браузера считается вспомогательным сигналом, а не секретом.
User-Agent и Referer не являются доказательством полномочий.

## Изоляция приватного кеша

Authorization и cache key выполняют разные задачи.
Допуск проверяет право текущего клиента.
Cache key определяет, какие байты можно переиспользовать.
Подписанный query credential можно убрать из cache key только после успешной проверки.
Такое удаление допустимо, если содержимое одинаково для всех разрешённых клиентов данного scope.
Персонализированные ответы получают отдельную область либо вообще не сохраняются.
Scope включает tenant и все атрибуты, влияющие на байты ответа.

`Cache-Control: private` не превращается в shared-cache объект.
Ответы с `no-store` не сохраняются в целевом профиле SLOYRA.
Правила shared caching для авторизованных запросов определяются [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html).
SLOYRA выбирает консервативный режим: сомнение в области ответа отключает сохранение.
Кеш решения allow не используется как кеш содержимого и имеет собственный TTL.
Отзыв полномочий инвалидирует кеш решения, даже если файл остаётся локально.

## HTTP-коды безопасности

| Условие | Статус | Смысл |
| --- | --- | --- |
| Подпись неверна | 403 | Доступ не разрешён |
| Срок ссылки истёк | 403 | Полномочие больше не действует |
| Scope не соответствует объекту | 403 | Объект вне полномочий |
| Легитимный субъект исчерпал квоту | 429 | Временное ограничение интенсивности |
| Обязательный auth backend недоступен | 503 | Решение получить невозможно |
| Внутренняя ошибка policy | 503 | Сбой сервиса проверки |
| Некорректный HTTP/URI | 400 | Запрос не прошёл разбор |
| Неподдерживаемый метод маршрута | 405 | Метод не реализован маршрутом |

403 не используется как универсальный код для неисправности инфраструктуры.
Для 429 можно выдать согласованный `Retry-After`.
Семантика 429 описана в [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585.html#section-4).
Клиенту не возвращается stack trace или имя внутреннего ключевого файла.
Ответы об ошибке доступа не содержат ETag приватного объекта.
Запрещённый запрос не прогревает кеш и не повышает рейтинг популярности файла.

## Каталог проверок canonicalization и допуска

Векторы ниже задают ожидаемое поведение профиля v1.
`allow` означает успешность описанной стадии при выполнении остальных условий.
Он не отменяет проверку остальных ограничений.

### S-01. Простой путь

Вход: `/media/movie.mp4`.
Каноническое действие: `/media/movie.mp4`.
Результат стадии: `allow`.
Внутренняя причина: `path_identity`.
Проверяемое свойство: Регистр и границы сегментов сохранены.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-01
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-02. Uppercase в пути

Вход: `/Media/Movie.mp4`.
Каноническое действие: `/Media/Movie.mp4`.
Результат стадии: `allow`.
Внутренняя причина: `path_case_preserved`.
Проверяемое свойство: Подпись нижнего регистра не подходит.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-02
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-03. Unreserved escape

Вход: `/media/%7Emovie.mp4`.
Каноническое действие: `/media/~movie.mp4`.
Результат стадии: `allow`.
Внутренняя причина: `unreserved_normalized`.
Проверяемое свойство: Подписывающая сторона использует тот же канон.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-03
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-04. Encoded slash

Вход: `/media/a%2Fb.mp4`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `encoded_separator`.
Проверяемое свойство: Origin не получает альтернативную иерархию.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-04
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-05. Encoded backslash

Вход: `/media/a%5Cb.mp4`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `encoded_backslash`.
Проверяемое свойство: Windows-подобный разделитель не трактуется неоднозначно.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-05
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-06. Dot segment

Вход: `/media/../private.mp4`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `dot_segment`.
Проверяемое свойство: Сегмент не нормализуется в другой защищённый маршрут.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-06
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-07. Encoded dot segment

Вход: `/media/%2e%2e/private.mp4`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `dot_segment`.
Проверяемое свойство: Однократное раскрытие предшествует проверке сегментов.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-07
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-08. Двойной слеш

Вход: `/media//movie.mp4`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `duplicate_separator`.
Проверяемое свойство: Signer не принимает вариант с другой маршрутизацией.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-08
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-09. Неверный escape

Вход: `/media/%XZ.mp4`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `invalid_percent_escape`.
Проверяемое свойство: Не выполняется частичная проверка подписи.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-09
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-10. NUL в пути

Вход: `/media/a%00.mp4`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `control_byte`.
Проверяемое свойство: C-строка не может незаметно укоротить ресурс.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-10
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-11. Двойное кодирование

Вход: `/media/%252e%252e/movie.mp4`.
Каноническое действие: `/media/%252e%252e/movie.mp4`.
Результат стадии: `allow`.
Внутренняя причина: `single_decode_only`.
Проверяемое свойство: Origin обязан сохранять этот байтовый смысл без второго decode.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-11
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-12. Плюс в query

Вход: `lang=ru+en`.
Каноническое действие: `lang=ru%2Ben`.
Результат стадии: `allow`.
Внутренняя причина: `query_plus_literal`.
Проверяемое свойство: Плюс не превращается в пробел.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-12
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-13. Пробел в query

Вход: `name=a%20b`.
Каноническое действие: `name=a%20b`.
Результат стадии: `allow`.
Внутренняя причина: `query_space_encoded`.
Проверяемое свойство: Каноническая форма использует percent escape.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-13
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-14. Перестановка query

Вход: `quality=720&lang=ru`.
Каноническое действие: `lang=ru&quality=720`.
Результат стадии: `allow`.
Внутренняя причина: `query_sorted`.
Проверяемое свойство: Одинаковая семантика даёт одинаковую подпись.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-14
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-15. Повтор sig

Вход: `sig=A&sig=A`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `duplicate_credential`.
Проверяемое свойство: Даже одинаковые credential values не объединяются.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-15
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-16. Повтор exp

Вход: `exp=1800000300&exp=1800000400`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `duplicate_credential`.
Проверяемое свойство: Нет выбора первого либо последнего значения.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-16
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-17. Ведущий ноль времени

Вход: `exp=01800000300`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `noncanonical_timestamp`.
Проверяемое свойство: Строковое и численное представление согласованы.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-17
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-18. Отрицательное время

Вход: `exp=-1`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `timestamp_format`.
Проверяемое свойство: Парсер не преобразует отрицательное значение в unsigned.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-18
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-19. Переполнение времени

Вход: `exp=999999999999999999999999`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `timestamp_overflow`.
Проверяемое свойство: Проверка длины предшествует преобразованию.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-19
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-20. Нулевой интервал

Вход: `nbf=1800000000&exp=1800000000`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `invalid_time_window`.
Проверяемое свойство: Пустое окно допуска не расширяется clock skew.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-20
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-21. Слишком далёкий срок

Вход: `exp-nbf превышает server max_lifetime`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `lifetime_exceeded`.
Проверяемое свойство: Корректная HMAC не отменяет серверный предел.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-21
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-22. Истёкшая ссылка

Вход: `now больше exp плюс skew`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `credential_expired`.
Проверяемое свойство: Горячий cache hit не меняет результат.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-22
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-23. Будущая ссылка

Вход: `now меньше nbf минус skew`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `credential_not_yet_valid`.
Проверяемое свойство: Clock skew ограничен server policy.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-23
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-24. Неизвестный kid

Вход: `kid=unknown-key`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `unknown_key`.
Проверяемое свойство: Не выполняется сетевой fetch по строке kid.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-24
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-25. Отозванный ключ

Вход: `kid имеет state revoked`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `revoked_key`.
Проверяемое свойство: Даже правильная подпись не принимается.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-25
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-26. Verify-only ключ

Вход: `kid имеет state verify_only`.
Каноническое действие: `проверить MAC и сроки`.
Результат стадии: `allow`.
Внутренняя причина: `verify_only_key`.
Проверяемое свойство: Новые ссылки этим ключом подписывающий сервис не выпускает.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-26
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-27. Неверный tenant

Вход: `st=tenant-b для маршрута tenant-a`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `tenant_scope_mismatch`.
Проверяемое свойство: Тело tenant-b не читается из общей RAM.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-27
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-28. Неверный audience

Вход: `sa=other.example.invalid`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `audience_mismatch`.
Проверяемое свойство: Ссылка другого сервиса не переносится на этот listener.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-28
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-29. HEAD с GET-only scope

Вход: `sm=GET при запросе HEAD`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `method_scope_mismatch`.
Проверяемое свойство: Метаданные защищаются так же, как тело.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-29
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-30. HEAD с объединённым scope

Вход: `sm=GET,HEAD при запросе HEAD`.
Каноническое действие: `проверить остальные ограничения`.
Результат стадии: `allow`.
Внутренняя причина: `method_scope_valid`.
Проверяемое свойство: Клиент получает метаданные без тела.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-30
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-31. Подмена content query

Вход: `quality=1080 вместо подписанного quality=720`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `signature_mismatch`.
Проверяемое свойство: Разное представление требует другого MAC.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-31
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-32. Padding у sig

Вход: `sig оканчивается знаком =`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `signature_encoding`.
Проверяемое свойство: Принимается только выбранная каноническая форма.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-32
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-33. Укороченный MAC

Вход: `декодированная sig содержит 16 байт`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `signature_length`.
Проверяемое свойство: Не включается неявная поддержка truncation.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-33
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-34. Unknown schema

Вход: `sv=2 при verifier v1`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `unsupported_signature_version`.
Проверяемое свойство: Версия не угадывается по полям.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-34
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-35. Отсутствующая зависимость

Вход: `обязательный nonce store не отвечает`.
Каноническое действие: `решение unavailable`.
Результат стадии: `503`.
Внутренняя причина: `nonce_store_unavailable`.
Проверяемое свойство: Клиент не помечается нарушителем из-за инфраструктуры.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-35
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-36. Квота легитимного субъекта

Вход: `правильная подпись и exhausted quota`.
Каноническое действие: `решение throttle`.
Результат стадии: `429`.
Внутренняя причина: `subject_quota_exceeded`.
Проверяемое свойство: Попытка учитывается отдельно от неверных полномочий.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-36
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-37. Недоверенный forwarded IP

Вход: `X-Forwarded-For от прямого клиента`.
Каноническое действие: `использовать transport peer`.
Результат стадии: `allow`.
Внутренняя причина: `untrusted_forwarded_header`.
Проверяемое свойство: Правило IP binding не принимает произвольный заголовок.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-37
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-38. Необъявленный query

Вход: `debug_origin=internal`.
Каноническое действие: `отклонение`.
Результат стадии: `400`.
Внутренняя причина: `unknown_content_parameter`.
Проверяемое свойство: Неизвестный параметр не уходит на origin.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-38
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-39. Пустой query value

Вход: `lang=`.
Каноническое действие: `lang=`.
Результат стадии: `allow`.
Внутренняя причина: `empty_value_preserved`.
Проверяемое свойство: Пустое значение не исчезает из подписи.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-39
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

### S-40. Неверный nonce replay

Вход: `nonce уже использован в single-use режиме`.
Каноническое действие: `отклонение`.
Результат стадии: `403`.
Внутренняя причина: `nonce_replayed`.
Проверяемое свойство: Replay проверяется в объявленной области согласованности.

```yaml
# Проектный вектор, не реальный credential.
vector_id: access-S-40
profile: sloyra-url-v1
cache_state: irrelevant_to_authorization
origin_fetch_before_allow: forbidden
```

## Наблюдаемость без утечки credential

Журнал хранит request_id, route_id, policy_revision и reason code.
Kid можно журналировать, если он является публичным идентификатором ключа.
Секрет keyring не журналируется никогда.
Полный query с sig не попадает в стандартный access log.
Токены в Cookie и Authorization удаляются до записи диагностического события.
Hash субъекта использует отдельный ключ псевдонимизации.
Этот ключ не совпадает с ключом signed URL.

| Поле | Обработка |
| --- | --- |
| `Authorization` | Полное удаление |
| Session cookie | Полное удаление |
| `sig` | Полное удаление |
| `kid` | Ограниченный публичный идентификатор |
| Client IP | По политике приватности и retention |
| User-Agent | Ограничение длины и очистка управляющих байтов |
| Canonical path | Hash либо разрешённый шаблон маршрута |
| Query | Только явный allowlist безопасных полей |
| Stack trace | Redaction перед сериализацией |
| Policy reason | Enum фиксированного словаря |

Счётчики не используют полный URL как metric label.
Идентификатор пользователя не создаёт отдельную временную серию.
Детальное расследование использует ограниченный trace с отдельным сроком хранения.
Включение debug не отключает redaction.

## Реакция на подозрение в компрометации

Сначала определяют, скомпрометирован credential или signing key.
Одна утёкшая ссылка обычно требует отзыва сессии, nonce или конкретной области.
Утечка signing key требует перевода kid в revoked на verifier.
Новые ссылки выпускаются независимым ключом.
Проверяется скорость доставки нового keyring generation на все узлы.
Узлы, не подтвердившие обязательный отзыв, снимаются из защищённого трафика.
Аудит сохраняет время, оператора, key id и поколение без материала секрета.
Проверка завершения отзыва выполняется новой попыткой с тестовой старой ссылкой.

## Условия приёмки подсистемы

Горячий приватный объект с неверной подписью возвращает 403.
GET, HEAD, Range и условный запрос проходят одну цепочку допуска.
Неверный токен не изменяет счётчик popularity объекта.
Два tenant с одинаковым путём не обмениваются телом или метаданными.
Проверка signature canonicalization одинакова на signer и verifier.
Ротация ключей не создаёт промежутка случайного allow.
Ошибка auth dependency отличается от установленного запрета.
Повторный nonce обнаруживается в заявленной области согласованности.
Логи и диагностические архивы не содержат credential.
Проверка срока не использует монотонные duration как Unix time.
Условный 304 выдаётся только после успешного допуска.
Отсутствие cache entry не раскрывает больше информации запрещённому клиенту.

## Первичные источники

- [RFC 2104: HMAC](https://www.rfc-editor.org/rfc/rfc2104.html).
- [RFC 4231: тестовые векторы HMAC-SHA-256](https://www.rfc-editor.org/rfc/rfc4231.html).
- [RFC 3986: синтаксис URI](https://www.rfc-editor.org/rfc/rfc3986.html).
- [RFC 4648: Base64url](https://www.rfc-editor.org/rfc/rfc4648.html#section-5).
- [RFC 9110: HTTP semantics](https://www.rfc-editor.org/rfc/rfc9110.html).
- [RFC 9111: HTTP caching](https://www.rfc-editor.org/rfc/rfc9111.html).
- [RFC 6585: статус 429](https://www.rfc-editor.org/rfc/rfc6585.html#section-4).

---

# HTTP-доставка файлов, диапазоны и совместное заполнение кеша

> Статус: проектная спецификация v0.1. Имена API и параметры в примерах задают целевой контракт; HTTP-сценарии являются требованиями к будущей реализации SLOYRA.

## Контракт доставки

SLOYRA выдаёт разрешённому клиенту выбранное представление объекта.
Представление определяется маршрутом, версией объекта и параметрами согласования.
Разные кодирования одного содержимого могут иметь разные размеры и валидаторы.
Кеш хранит байты конкретного представления.
Политика доступа проверяется до чтения тела, до 304 и до выдачи метаданных HEAD.
Локальный tier влияет на источник байтов, но не меняет HTTP-смысл ответа.

Nginx завершает поддерживаемый транспорт HTTP.
SLOYRA не обещает HTTP/3 только потому, что подключён как модуль.
Доступность HTTP/2 и HTTP/3 зависит от сборки и настройки базового сервера.
Матрица выпуска должна проверять модуль отдельно на каждом включённом протоколе.
Range, cache key и авторизация имеют одинаковый прикладной смысл для разных транспортов.

## Основная модель

```text
request received
  -> validate HTTP fields
  -> resolve route
  -> authorize current request
  -> select representation
  -> evaluate preconditions
  -> evaluate Range for GET
  -> locate complete body or required chunks
  -> coordinate admitted origin fill
  -> commit response headers
  -> stream bounded buffers
  -> record actual bytes and terminal result
```

До фиксации заголовков можно выбрать другой источник тех же байтов.
После фиксации заголовков нельзя незаметно изменить ETag или Content-Encoding.
Если источник возвращает другую версию, начинается новый выбор представления до отправки ответа.
Клиент не должен получить первые чанки старого файла и последние чанки нового.
Тело проверяется на соответствие заявленной длине там, где длина известна.

## HTTP-семантика и проектные ограничения

Range применяется к GET; ответ на HEAD не содержит тела.
Условные запросы и byte ranges обрабатываются по [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html).
Кешируемость, fresh/stale и обновление сохранённых ответов следуют [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html).
SLOYRA дополнительно ограничивает сложность запроса диапазонов и размер служебных структур.
Примеры ниже фиксируют выбранное поведение профиля доставки v1.
Числа лимитов служат исходными параметрами стенда.

## Представление объекта

| Поле | Назначение |
| --- | --- |
| `object_id` | Логический объект приложения |
| `object_version` | Неизменяемая версия данных |
| `representation_id` | Кодирование и вариант |
| `content_length` | Размер выбранного представления |
| `content_type` | Тип выдаваемого содержимого |
| `content_encoding` | Кодирование байтов |
| `etag` | Валидатор представления |
| `last_modified` | Время изменения, если известно |
| `vary_dimensions` | Нормализованные измерения варианта |
| `cache_scope` | Область безопасного повторного использования |

Если origin не предоставляет стабильную версию, совместное использование частичных записей ограничивается.
Слабый ETag нельзя использовать как доказательство побайтовой идентичности чанков.
Набор чанков привязан к одному сильному валидатору либо другому доверенному идентификатору версии.
Размер объекта хранится вместе с версией.
Изменение размера означает новую запись метаданных и повторную проверку ranges.

## Порядок условных проверок

Профиль реализует стандартный порядок preconditions для поддерживаемых методов.
Сначала проверяются условия, предотвращающие использование неподходящего представления.
Затем применяется условие неизменности для чтения.
Range рассматривается после того, как запрос остался кандидатом на обычный успешный ответ.
Ответ 304 не обходит access policy.
Ответ 412 не содержит тело защищённого объекта.

| Поле запроса | Целевое использование SLOYRA |
| --- | --- |
| `If-Match` | Требование совпадения выбранной версии |
| `If-Unmodified-Since` | Условие по времени при допустимости |
| `If-None-Match` | Проверка возможности ответа 304 |
| `If-Modified-Since` | Проверка даты при отсутствии приоритетного ETag-условия |
| `If-Range` | Выбор между частичным и полным телом |

Список ETag разбирается структурированным парсером.
Запятая внутри строки не обрабатывается простым неограниченным split без учёта грамматики.
Число элементов списка ограничивается.
Слишком большой условный заголовок отклоняется на входе.
Проверки даты не используют локаль процесса.

## Диапазоны байтов

Базовый профиль поддерживает один диапазон на GET.
Формы `start-end`, `start-` и `-suffix_length` приводятся к закрытому интервалу.
Счёт начинается с нуля.
Длина выдачи вычисляется как `end - start + 1` после проверки переполнения.
Правая граница не превышает последний байт выбранного представления.
Нулевой суффикс не описывает ненулевой диапазон.
Для пустого представления байтовых диапазонов нет.

Запрос с несколькими ranges в базовом профиле игнорирует Range и получает полный ответ 200 после остальных проверок.
Это поведение указывается в профиле, чтобы клиенты не рассчитывали на multipart.
Расширенный профиль может поддерживать ограниченный multipart/byteranges.
Его активация требует отдельного тестирования памяти, длины ответа и поведения клиента.
Нельзя преобразовывать любую сложную Range-строку в произвольный 206.

```yaml
# Проектный фрагмент sloyra.yaml для профиля доставки.
server:
  delivery:
    methods:
      - GET
      - HEAD
    ranges:
      enabled: true
      profile: single_range
      on_multiple: ignore_and_send_full
      max_header_bytes: 2048
      max_parsed_ranges: 8
    preconditions:
      enabled: true
      max_etag_values: 32
    response:
      expose_cache_debug: false
      max_header_bytes: 32768
cache:
  partial_objects:
    enabled: true
    require_strong_validator: true
```

## Range и внутренние чанки

HTTP-диапазон не обязан совпадать с внутренним размером чанка.
Запрос 1000–1999 может потребовать чтения одного большого чанка.
В журнале различаются requested bytes, cache read bytes и origin fetch bytes.
Чтение 1 MiB ради ответа в 1 KiB не считается 1 MiB пользовательской доставки.
Популярность участка учитывается по реально запрошенному диапазону.
Фоновая предвыборка не выдаётся за дополнительный пользовательский запрос.

```text
object bytes:   0 -------------------------------------- 1048575
chunk A:        [0 ------------------ 524287]
chunk B:                                  [524288 ---- 1048575]
client range:                         [520000 -- 530000]
needed chunks:  A and B
client bytes:   10001
internal reads: measured independently
origin bytes:   only actually fetched missing parts
```

При отсутствии одного чанка выдача может объединить локальный и origin-источник.
Все части при этом обязаны иметь одинаковую representation version.
До отправки заголовков подтверждается совместимость метаданных.
Если origin сообщает новую версию, старые чанки не используются в новом ответе.
Если подтверждение версии невозможно, выполняется согласованный полный fetch либо controlled failure.

## Request collapse

Несколько разрешённых запросов одного представления могут ждать одну загрузку из origin.
Они остаются отдельными логическими запросами клиентов.
Origin fetch при этом является одной физической операцией.
Cache lookup каждого клиента учитывается отдельно.
Ожидающий клиент не записывается как самостоятельный origin fetch.
Механизм не объединяет разные tenant или несовместимые private scopes.

```text
request A -> allow -> miss -> fill leader
request B -> allow -> miss -> fill follower
request C -> deny  -> 403

leader -> one origin fetch
origin -> verified chunks
verified chunks -> response A
verified chunks -> response B
request C never joins the fill
```

Ключ совместной загрузки включает scope, object version и representation.
При чанковом fill он дополнительно включает согласованную единицу загрузки.
Разные клиентские ranges могут ждать один chunk fill.
Один медленный follower не блокирует лидера и остальных подписчиков безгранично.
Каждый подписчик имеет собственный deadline и лимит буферизации.
Лидерские полномочия проверяются перед началом fill, но не заменяют полномочия подписчиков.

## Состояния fill

```text
absent
  -> elected
  -> fetching_headers
  -> validating_metadata
  -> streaming
  -> verifying_complete
  -> published

any_pending_state -> failed
streaming -> cancelled
published -> invalidated
```

Lease лидера имеет срок и generation fence.
Умерший лидер не удерживает объект в состоянии loading навсегда.
Новый лидер не публикует результат старого lease.
Отмена одного клиента не обязательно отменяет полезную загрузку для других.
После отмены последнего подписчика действует отдельная политика background fill.
Background fill ограничен бюджетом и не повышает число пользовательских запросов.

## Приватные ответы

Локальный shared cache сохраняет только ответы, разрешённые политикой и HTTP-метаданными.
`Set-Cookie` в базовом профиле вызывает bypass сохранения, пока маршрут явно не докажет безопасное поведение.
`Vary: *` не создаёт повторно используемую shared запись.
Каждое поддерживаемое Vary-измерение включается в ключ.
Неизвестное Vary-измерение переводит ответ в bypass вместо угадывания.
Ответ с Authorization требует явно совместимой shared-cache семантики.

Для персонализированного контента можно включить отдельную private scope.
Scope не определяется только именем cookie.
Он должен учитывать действующего субъекта и версию прав, если они влияют на байты.
В базовом профиле безопаснее отключить сохранение таких ответов.
Purge приватной области не удаляет данные других арендаторов.

## Ошибки origin

Connect timeout, ожидание заголовков и stalled body имеют разные счётчики.
Общий request deadline ограничивает сумму повторов.
Повтор разрешён только до необратимой передачи несовместимого тела клиенту.
Повтор на другом origin должен сохранить идентичность объекта и tenant.
Redirect от origin не может вывести fetch к произвольному адресу.
Список допустимых адресов задаётся маршрутом.

После частичной отправки тела нельзя начать новый ответ с другим статусом.
В этом случае фиксируется delivery failure и корректно завершается транспорт.
Незавершённое тело не публикуется как полный cache entry.
Частично полученные чанки можно публиковать только при отдельном доказательстве их полноты и версии.
Клиентский разрыв не делает origin автоматически неисправным.

## Контрольные HTTP-сценарии

В примерах размер объекта равен 1000 байтам, если не указано другое.
Все демонстрационные credentials заменены маркером и не являются рабочими ссылками.
Тела сокращены в комментариях для читаемости.
Предполагается успешная проверка доступа, кроме специально указанных сценариев.

### H-01. Полная выдача

```http
GET /media/a.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Передать ровно 1000 байт.
Класс результата: `whole_object`.
Контроль: Полный файл может прийти из любого здорового tier.

### H-02. HEAD

```http
HEAD /media/a.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Не передавать тело.
Класс результата: `metadata_only`.
Контроль: Нулевые body bytes не считаются просмотром всего файла.

### H-03. Начальный диапазон

```http
GET /media/a.bin HTTP/1.1
Range: bytes=0-99
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-99/1000
Content-Length: 100
```

Действие SLOYRA: Передать первые 100 байт.
Класс результата: `single_range`.
Контроль: Границы интервала включительные.

### H-04. Средний диапазон

```http
GET /media/a.bin HTTP/1.1
Range: bytes=200-399
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 200-399/1000
Content-Length: 200
```

Действие SLOYRA: Передать 200 байт с позиции 200.
Класс результата: `single_range`.
Контроль: Не выдавать лишние границы внутреннего чанка.

### H-05. Открытая правая граница

```http
GET /media/a.bin HTTP/1.1
Range: bytes=900-
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 900-999/1000
Content-Length: 100
```

Действие SLOYRA: Передать хвост объекта.
Класс результата: `open_end`.
Контроль: Размер версии должен быть известен до вычисления ответа.

### H-06. Суффикс

```http
GET /media/a.bin HTTP/1.1
Range: bytes=-50
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 950-999/1000
Content-Length: 50
```

Действие SLOYRA: Передать последние 50 байт.
Класс результата: `suffix`.
Контроль: Суффикс не интерпретируется как отрицательная позиция.

### H-07. Суффикс длиннее объекта

```http
GET /media/a.bin HTTP/1.1
Range: bytes=-2000
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-999/1000
Content-Length: 1000
```

Действие SLOYRA: Передать все доступные байты как удовлетворённый range.
Класс результата: `large_suffix`.
Контроль: Размер ответа не превышает representation length.

### H-08. Правая граница за концом

```http
GET /media/a.bin HTTP/1.1
Range: bytes=900-2000
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 900-999/1000
Content-Length: 100
```

Действие SLOYRA: Ограничить правую границу последним байтом.
Класс результата: `clamped_end`.
Контроль: При вычислении длины не возникает переполнения.

### H-09. Левая граница за концом

```http
GET /media/a.bin HTTP/1.1
Range: bytes=1000-1200
```

```http
HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */1000
Content-Length: 0
```

Действие SLOYRA: Не читать cache body.
Класс результата: `unsatisfied_range`.
Контроль: Метаданные доступны только уже авторизованному клиенту.

### H-10. Range на пустом объекте

```http
GET /media/empty.bin HTTP/1.1
Range: bytes=0-
```

```http
HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */0
Content-Length: 0
```

Действие SLOYRA: Не создавать фиктивный байт нулевой позиции.
Класс результата: `empty_object`.
Контроль: Пустой файл без Range возвращает обычный успешный ответ.

### H-11. Несколько ranges в v1

```http
GET /media/a.bin HTTP/1.1
Range: bytes=0-9,100-109
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Применить on_multiple = ignore_and_send_full.
Класс результата: `multiple_ignored`.
Контроль: Не возвращать один диапазон, будто клиент запросил только его.

### H-12. Неподдерживаемая единица

```http
GET /media/a.bin HTTP/1.1
Range: pages=1-2
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Игнорировать неизвестную range unit.
Класс результата: `unknown_unit`.
Контроль: Origin не получает придуманный перевод pages в bytes.

### H-13. Range в HEAD

```http
HEAD /media/a.bin HTTP/1.1
Range: bytes=0-99
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Игнорировать Range и не передавать тело.
Класс результата: `head_range_ignored`.
Контроль: Content-Length описывает полное представление.

### H-14. Совпадающий If-None-Match

```http
GET /media/a.bin HTTP/1.1
If-None-Match: "v1"
```

```http
HTTP/1.1 304 Not Modified
ETag: "v1"
Cache-Control: max-age=60
```

Действие SLOYRA: Выдать 304 после allow.
Класс результата: `not_modified`.
Контроль: Не открывать body reader ради пустого ответа.

### H-15. Несовпадающий If-None-Match

```http
GET /media/a.bin HTTP/1.1
If-None-Match: "old"
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Выдать актуальное тело.
Класс результата: `modified`.
Контроль: Клиентский старый валидатор не меняет cache key.

### H-16. Неверный If-Match

```http
GET /media/a.bin HTTP/1.1
If-Match: "old"
```

```http
HTTP/1.1 412 Precondition Failed
Content-Length: 0
Cache-Control: no-store
```

Действие SLOYRA: Не выдавать тело другой версии.
Класс результата: `precondition_failed`.
Контроль: Failure не повышает delivered bytes.

### H-17. Совпадающий If-Range

```http
GET /media/a.bin HTTP/1.1
Range: bytes=0-99
If-Range: "v1"
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-99/1000
Content-Length: 100
```

Действие SLOYRA: Выдать диапазон совпавшей версии.
Класс результата: `if_range_match`.
Контроль: Сильный валидатор привязан к выбранным байтам.

### H-18. Несовпадающий If-Range

```http
GET /media/a.bin HTTP/1.1
Range: bytes=0-99
If-Range: "old"
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Игнорировать Range и выдать полное актуальное тело.
Класс результата: `if_range_mismatch`.
Контроль: Не отвечать 412 только из-за If-Range mismatch.

### H-19. Слабый If-Range

```http
GET /media/a.bin HTTP/1.1
Range: bytes=0-99
If-Range: W/"v1"
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Не использовать слабый валидатор для partial resume.
Класс результата: `if_range_weak`.
Контроль: Профиль выбирает полный ответ.

### H-20. Неверный токен при горячем кеше

```http
GET /media/a.bin?sig=INVALID HTTP/1.1
Range: bytes=0-99
```

```http
HTTP/1.1 403 Forbidden
Content-Length: 0
Cache-Control: no-store
```

Действие SLOYRA: Остановиться до cache lookup.
Класс результата: `access_denied`.
Контроль: Наличие горячего файла не видно в публичном ответе.

### H-21. Неверный токен в conditional

```http
GET /media/a.bin?sig=INVALID HTTP/1.1
If-None-Match: "v1"
```

```http
HTTP/1.1 403 Forbidden
Content-Length: 0
Cache-Control: no-store
```

Действие SLOYRA: Не выдавать 304 до допуска.
Класс результата: `access_denied`.
Контроль: ETag приватного файла не раскрывается.

### H-22. Перегрузка разрешённого субъекта

```http
GET /media/a.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Cache-Control: no-store
```

Действие SLOYRA: Применить отдельный rate-limit budget.
Класс результата: `throttled`.
Контроль: Квота не имитирует неверную подпись.

### H-23. Auth dependency недоступна

```http
GET /media/a.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 503 Service Unavailable
Retry-After: 1
Cache-Control: no-store
```

Действие SLOYRA: Не превращать инфраструктурный сбой в allow.
Класс результата: `access_unavailable`.
Контроль: Публичный ответ не раскрывает адрес auth backend.

### H-24. Origin не дал заголовки вовремя

```http
GET /media/cold.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 504 Gateway Timeout
Content-Length: 0
Cache-Control: no-store
```

Действие SLOYRA: Завершить запрос в общем deadline.
Класс результата: `origin_headers_timeout`.
Контроль: Незавершённый fill освобождает lease.

### H-25. Origin вернул повреждённую длину

```http
GET /media/cold.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 502 Bad Gateway
Content-Length: 0
Cache-Control: no-store
```

Действие SLOYRA: При обнаружении до commit отклонить ответ.
Класс результата: `origin_metadata_invalid`.
Контроль: После commit используется delivery failure вместо второго статуса.

### H-26. Origin запрещает сохранение

```http
GET /media/private.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Length: 1000
```

Действие SLOYRA: Передать разрешённые байты без публикации cache entry.
Класс результата: `cache_bypass_no_store`.
Контроль: Telemetry сохраняет факт origin fetch без создания кеша.

### H-27. Персонализированный ответ

```http
GET /media/profile.bin HTTP/1.1
Authorization: REDACTED
```

```http
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 1000
```

Действие SLOYRA: Не сохранять в shared namespace.
Класс результата: `cache_bypass_private`.
Контроль: У другого пользователя нет доступа к полученному телу.

### H-28. Неизвестный Vary

```http
GET /media/a.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 200 OK
Vary: X-Unregistered-Variant
Content-Length: 1000
```

Действие SLOYRA: Передать ответ, отключить повторное shared использование.
Класс результата: `cache_bypass_vary`.
Контроль: Не выдумывать нормализацию неизвестного измерения.

### H-29. Сжатое представление

```http
GET /media/a.txt HTTP/1.1
Accept-Encoding: gzip
```

```http
HTTP/1.1 200 OK
Content-Encoding: gzip
Vary: Accept-Encoding
ETag: "gzip-v1"
```

Действие SLOYRA: Выбрать отдельное gzip representation.
Класс результата: `encoding_variant`.
Контроль: Диапазоны не адресуют несжатый исходник этого ответа.

### H-30. Изменение версии между чанками

```http
GET /media/a.bin HTTP/1.1
Range: bytes=400-599
```

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 400-599/1000
ETag: "v2"
```

Действие SLOYRA: До headers заново собрать диапазон целиком из v2.
Класс результата: `version_restart`.
Контроль: Нельзя соединить v1 cache chunk с v2 origin chunk.

### H-31. Cache read failure до headers

```http
GET /media/a.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Получить те же байты разрешённым origin fallback.
Класс результата: `cache_read_recovered`.
Контроль: Ошибка cache tier отражается отдельно от HTTP success.

### H-32. Warm metadata и cold body

```http
HEAD /media/a.bin HTTP/1.1
Host: media.example.invalid
```

```http
HTTP/1.1 200 OK
Content-Length: 1000
ETag: "v1"
```

Действие SLOYRA: Ответить из валидных метаданных без body fetch.
Класс результата: `metadata_hit`.
Контроль: HEAD не обязан скачивать весь origin файл.

## Бюджеты медленного клиента

Producer учитывает готовность downstream принимать данные.
Размер буфера ограничен независимо от длины файла.
Для каждого follower ведётся собственное состояние отправки.
Отстающий follower может переключиться на опубликованный cache object после завершения fill.
Если это невозможно в его deadline, завершается только его запрос.
Буфер одного клиента не закрепляет без ограничения все чанки большого объекта.

## Метрики доставки

| Измеритель | Единица | Что означает |
| --- | --- | --- |
| Logical requests | Запросы | Отдельные клиентские обращения |
| Access allowed | Решения | Успешно допущенные обращения |
| Cache lookups | Операции | Проверки конкретной единицы кеша |
| Origin fetches | Операции | Физические обращения к origin |
| Requested bytes | Байты | Запрошенный диапазон выбранного представления |
| Delivered bytes | Байты | Фактически отправленное тело клиенту |
| Cache read bytes | Байты | Фактически прочитанные локальные данные |
| Origin bytes | Байты | Фактически полученные данные origin |
| Prefetch bytes | Байты | Фоновые дополнительные чтения |
| Fill wait duration | Секунды | Ожидание общей загрузки |
| Time to first byte | Секунды | Задержка до первых байтов ответа |
| Delivery duration | Секунды | Время передачи текущему клиенту |

Сумма delivered bytes всех followers может превышать origin bytes одного fill.
Это ожидаемый эффект совместного использования.
Запрос с 304 имеет нулевые body bytes.
HEAD имеет нулевые body bytes даже при известном полном Content-Length.
Обрыв клиента учитывает только действительно отправленные байты.
Промах метаданных и промах тела хранятся разными outcome.

## Порядок диагностики неправильного Range

Зафиксируйте method, нормализованный Range и If-Range без credential.
Определите representation_id и strong validator.
Проверьте длину именно выбранного кодирования.
Проверьте порядок preconditions и Range.
Проверьте список внутренних чанков и их версии.
Сравните Content-Range с фактической длиной тела.
Проверьте, не произошло ли преобразование тела после расчёта границ.
Сопоставьте requested bytes и origin bytes без попытки приравнять их.

## Порядок диагностики request collapse

Проверьте совпадение tenant и private scope у ожидающих запросов.
Проверьте representation version в ключе fill.
Найдите leader lease и generation fence.
Проверьте число followers и их deadlines.
Отделите отмену лидера-клиента от отмены общей загрузки.
Проверьте, кто владел origin connection после смены состояния.
Убедитесь, что каждого клиента посчитали один раз как logical request.
Убедитесь, что каждую физическую попытку origin посчитали отдельно.

## Готовность протокольного профиля

Реализация проходит одну матрицу на RAM, NVMe, SSD, HDD и origin path.
Одинаковый запрос получает одинаковые status, validator и тело независимо от tier.
Проверка signed URL повторяется для каждого варианта запроса.
Purge во время fill не возвращает удалённую версию новым клиентам.
Медленный downstream не вызывает неограниченную память.
Обрыв origin не публикует неполный полный объект.
Повторные conditional requests не повышают byte popularity как полные просмотры.
Multipart включается только после отдельной приёмки.
HTTP/2 и HTTP/3 проверяются на фактически выбранной сборке Nginx.

## Первичные источники

- [RFC 9110: HTTP semantics, conditional requests, ranges](https://www.rfc-editor.org/rfc/rfc9110.html).
- [RFC 9111: правила HTTP-кеширования](https://www.rfc-editor.org/rfc/rfc9111.html).
- [Nginx: параметры HTTP/2 и HTTP/3 в сборке](https://nginx.org/en/docs/configure.html).
- [Nginx: proxy module и обработка origin](https://nginx.org/en/docs/http/ngx_http_proxy_module.html).

---

# Иерархия кеша: RAM, NVMe SSD и HDD

> Статус: проектная спецификация v0.1.

## Назначение

SLOYRA размещает популярные файлы и диапазоны в более быстром кеше.
Базовый сценарий включает RAM, NVMe SSD и HDD.
Распределение по популярности подтверждено постановкой задачи.
Алгоритмы, форматы и ограничения ниже являются проектными контрактами.
Их наличие в документе не означает готовность соответствующего кода.
Реализация должна пройти описанные проверки перед выпуском.

Кеш является восстанавливаемой копией origin.
Утрата узла кеша не должна приводить к утрате исходного объекта.
Хранение origin и размещение внутри Ceph описаны отдельно.
Иерархия кеша не заменяет резервное копирование.
Популярность влияет на скорость и стоимость обслуживания.
Популярность не меняет права доступа к содержимому.

## Граница компонентов

Плагин Nginx обслуживает критический путь запроса.
Базовая реализация плагина проектируется на C17.
LuaJIT с семантикой Lua 5.1 используется для политик.
Управляющий агент на Rust принимает решения вне worker loop.
Длительное копирование выполняется фоновыми исполнителями.
Запрос не ждёт завершения общего пересчёта популярности.

| Компонент | Ответственность |
| --- | --- |
| Access gate | Допуск клиента до lookup |
| Key builder | Каноническая идентичность |
| Tier index | Поиск опубликованной копии |
| Fill coordinator | Единственный владелец заполнения |
| RAM store | Ограниченный быстрый буфер |
| Disk store | Атомарная публикация файлов |
| Placement agent | Повышение и вытеснение |
| Event spool | Доставка телеметрии |
| Origin adapter | Чтение версии источника |
| Health monitor | Исключение неисправного tier |

## Основные инварианты

- Проверка доступа предшествует выдаче кешированного тела.
- `tenant_id` входит в идентичность кеша.
- Версия содержимого входит в идентичность кеша.
- Диапазон описывается в байтах после нормализации.
- Представление и кодирование входят в ключ.
- Частично записанный файл не виден читателям.
- Объект без подтверждённой целостности не публикуется.
- RAM не является единственной долговечной копией.
- Удаление локального кеша не удаляет origin.
- Ошибка телеметрии не превращается в разрешение доступа.
- Ошибка политики доступа не трактуется как cache miss.
- Перенос кеша не изменяет ETag origin.
- Обновление конфигурации применяется поколениями.
- Читатель удерживает ссылку на выбранное поколение.
- Любой лимит измеряется до выделения ресурса.
- Переполнение счётчика обрабатывается насыщением.

## Маршрут чтения

1. Принять запрос и назначить локальный request ID.
2. Определить tenant из доверенной конфигурации.
3. Нормализовать разрешённые части URI.
4. Проверить подпись, срок и access policy.
5. Определить требуемую версию объекта.
6. Разобрать и проверить Range.
7. Сформировать объектную идентичность.
8. Зарегистрировать `logical_request`.
9. Выполнить lookup в разрешённых tiers.
10. Зарегистрировать `cache_lookup` для попыток.
11. Зафиксировать ссылку на найденную копию.
12. Передать тело с backpressure клиента.
13. Освободить ссылку читателя.
14. Записать завершённые `served_bytes`.

При полном miss выполняется origin fetch.
Он имеет собственный `fill_id`.
Он не заменяет logical request.
Он может обслужить несколько ожидающих клиентов.
Каждая физическая попытка получает `origin_attempt_id`.

## Уровни хранения

### RAM

В RAM помещается ограниченная доля горячего рабочего набора.
Кандидат оценивается по пользе на занятый байт.
Размер структур управления входит в RAM budget.
Накладные расходы аллокатора входят в RAM budget.
Одновременно выдаваемые буферы входят в RAM budget.
Зарезервированные fill buffers учитываются до загрузки тела.

RAM entry содержит immutable payload.
Читатели получают ссылку и generation.
Вытеснение снимает entry из публичного индекса.
Освобождение памяти ждёт завершения активных читателей.
Большой медленный клиент не удерживает неограниченную память.
Для него допускается переключение источника до начала тела.
После начала тела источник меняется только при сохранении байтовой идентичности.

### NVMe SSD

NVMe SSD хранит популярные данные вне RAM.
Решение учитывает задержку, очередь и стоимость записи.
Один новый объект не должен заполнять весь tier.
Временные файлы размещаются на том же filesystem.
Это позволяет опубликовать файл атомарным rename.
Атомарность имени не заменяет проверку содержимого.
Режим durability локального кеша задаётся явно.

Для восстанавливаемого кеша допустима потеря последних fills при сбое питания.
После рестарта незавершённые entries удаляются или перепроверяются.
Неподтверждённая запись не считается durable origin.
Контроль свободного места использует реальные доступные байты.
Reserved blocks и ограничения проекта filesystem учитываются отдельно.

### HDD

HDD хранит менее горячую часть рабочего набора.
Admission ограничивает случайные короткие обращения.
Последовательный read-ahead может снижать число seeks.
Read-ahead не считается клиентским спросом.
Чтение заранее имеет отдельный traffic class.
Его можно остановить при перегрузке диска.

Перенос NVMe → HDD выполняется при устойчивом охлаждении.
Повторное повышение блокируется cooldown только для фонового переноса.
Критический клиентский miss остаётся обслуживаемым.
HDD tier может отсутствовать в конкретной конфигурации.
В этом случае следующий источник — origin.

## Идентичность объекта

Ключ не равен URL в исходном виде.
Подписанные токены не должны дробить кеш одного объекта.
Удаление параметров URL допускается только по allowlist.
Значимые параметры представления должны сохраняться.
Два tenant не разделяют кеш без отдельного контракта изоляции.

```text
ObjectIdentity = (
  tenant_id,
  namespace,
  canonical_object_id,
  content_version,
  representation_id,
  content_encoding
)
```

```text
ChunkIdentity = (
  ObjectIdentity,
  chunk_layout_version,
  byte_start,
  byte_length
)
```

`content_version` обязана идентифицировать неизменное содержимое.
Слабый ETag сам по себе не даёт такой гарантии.
Если источник не даёт версию, применяется validation policy.
До проверки свежести данные не выдаются как новая версия.
Выбор политики фиксируется для namespace.

| Поле | Проверка |
| --- | --- |
| tenant_id | Непустая доверенная строка |
| namespace | Из конфигурации маршрута |
| canonical_object_id | Канонический формат адаптера |
| content_version | Без переиспользования для иных байтов |
| representation_id | Учитывает преобразования |
| content_encoding | Не смешивает identity и gzip |
| chunk_layout_version | Меняется при новой нарезке |
| byte_start | Неотрицательное целое |
| byte_length | Положительное целое |
| byte_start + byte_length | Проверяется на overflow |

## Хеширование ключа

Дисковый путь строится из криптографического digest идентичности.
Исходные компоненты ключа сохраняются в metadata.
При lookup digest дополняется сравнением identity.
Хеш-коллизия не разрешает выдачу чужого содержимого.
Формат сериализации должен быть однозначным.
Конкатенация строк без длины недопустима.

```text
encode(identity):
  append schema_version
  for field in fixed_field_order:
    append field_type
    append encoded_length
    append encoded_value
```

Формат чисел фиксирует порядок байтов.
UTF-8 строки не нормализуются произвольно.
Unicode normalization допустима только в контракте namespace.
Размер ключа ограничивается до хеширования.
Полный URL с секретами не помещается в метрики.

## Файлы и куски

Полный объект удобен для небольших файлов.
Куски уменьшают стоимость случайного доступа к большим объектам.
Граница куска стабильна в рамках `chunk_layout_version`.
Последний кусок может быть короче остальных.
Объектная длина берётся из доверенного metadata origin.

```text
chunk_index = floor(byte_offset / chunk_size)
chunk_start = chunk_index * chunk_size
chunk_end_exclusive = min(chunk_start + chunk_size, object_size)
chunk_length = chunk_end_exclusive - chunk_start
```

При `chunk_size = 4 MiB` запрос к байту `6 MiB` относится к куску 1.
Начало этого куска равно `4 MiB`.
Конец равен `8 MiB`, если объект достаточно длинный.
В клиентский ответ попадает только запрошенный диапазон.
Дополнительно загруженные байты учитываются как origin amplification.

### Пример нормализации диапазона

Объект имеет размер `10 MiB`.
Запрос `bytes=3145728-6291455` запрашивает `3 MiB`.
Он пересекает куски 0 и 1 размером по `4 MiB`.
При пустом кеше физическая загрузка составляет `8 MiB`.
Логический спрос составляет `3 MiB`.
Коэффициент усиления чтения равен `8 / 3 ≈ 2,667`.
Прогрев обоих кусков не добавляет второй logical request.

### Несколько диапазонов

Поддержка multipart range включается отдельно.
Число диапазонов ограничивается до планирования чтения.
Пересекающиеся диапазоны можно объединить для физического чтения.
Ответ сохраняет HTTP-семантику первоначального запроса.
Единицы подсчёта спроса определяются после нормализации.
Один байт, запрошенный дважды в одном запросе, не удваивает popularity bytes.
Фактически переданные дубликаты входят в served bytes.

### Суффиксные диапазоны

`bytes=-N` требует знания размера объекта.
Отрицательная длина отвергается.
Нулевой суффикс не вызывает origin body fetch.
Если `N` превышает размер, диапазон покрывает весь объект.
Нельзя преобразовать суффикс без metadata в произвольный первый кусок.

## Состояния локального entry

| Состояние | Видно читателю | Значение |
| --- | --- | --- |
| absent | Нет | Данных нет |
| reserved | Нет | Выделен бюджет |
| fetching | Нет | Идёт загрузка |
| validating | Нет | Проверяются байты |
| ready | Да | Entry опубликован |
| stale | Условно | Нужна freshness policy |
| moving | Да | Старый источник ещё доступен |
| retiring | Нет для новых | Ожидает старых читателей |
| quarantined | Нет | Обнаружена ошибка |
| deleted | Нет | Ресурс освобождён |

Переход `fetching → ready` напрямую запрещён.
Переход `quarantined → ready` требует новой полной проверки.
Переход `retiring → ready` допускается только новым поколением индекса.
Потеря owner не делает временный файл готовым.

```text
absent
  -> reserved
  -> fetching
  -> validating
  -> ready
  -> retiring
  -> deleted
```

```text
fetching
  -> failed
  -> reservation_released
  -> absent
```

## Контракт заполнения

Заполнение резервирует место до чтения тела.
При неизвестной длине резерв растёт ограниченными порциями.
Объект больше максимума переводится в pass-through.
Pass-through не обходит access gate.
Прерывание клиента не обязательно прерывает общий fill.
Продолжение определяется наличием waiters и полезностью admission.

При ответе origin проверяются версия и длина.
Ответ `206` должен соответствовать ожидаемому Content-Range.
Ответ `200` на Range обрабатывается явно.
Нельзя записать весь объект под ключ одного куска.
Нельзя записать кусок под ключ полного объекта.
Перенаправление origin повторно проверяется адаптером.

### Публикация на диске

1. Создать уникальный временный файл.
2. Записать заголовок формата и generation.
3. Записать тело в пределах reservation.
4. Проверить длину и digest.
5. Сохранить metadata версии.
6. Закрыть либо синхронизировать файл по durability policy.
7. Выполнить атомарную публикацию имени.
8. Добавить entry в индекс.
9. Освободить fill lease.
10. Разбудить waiters.

Entry в индексе никогда не указывает на `.partial`.
Сбой после rename, но до индекса допускает orphan.
Orphan обнаруживается сканером и проверяется.
Сбой до rename оставляет незавершённый файл.
Такие файлы очищаются по owner epoch и возрасту.

## Объединение одновременных запросов

Collapse применяется к одной ChunkIdentity.
Tenant и версия входят в ключ collapse.
Права доступа проверяются у каждого waiter отдельно.
Владелец fill не передаёт свою авторизацию другим клиентам.

При 100 клиентах к пустому кусочку допустим один origin fetch.
Телеметрия содержит 100 logical requests.
Физический успешный fill содержит один `fill_id`.
Число origin attempts может быть больше одного при retry.
Waiters не создают дополнительные fills до истечения политики ожидания.

### Лимит ожидания

Timeout waiters отделён от timeout origin.
При истечении waiter может получить контролируемый отказ.
Pass-through после timeout требует отдельного origin budget.
Безлимитный обход collapse при timeout запрещён.
Иначе перегруженный origin получил бы лавину повторов.

### Потеря владельца

Lease содержит fencing token.
Новый owner получает больший token.
Старый owner не публикует результат с устаревшим token.
Временный файл старого owner не удаляет файл нового owner.
Переход owner регистрируется отдельной причиной.

## TTL и свежесть

TTL регулирует право повторного использования версии.
Eviction age регулирует полезность хранения.
Это разные величины.
Объект может быть свежим, но экономически невыгодным.
Объект может быть горячим, но требовать revalidation.

Immutable namespace допускает длительное хранение по версии.
Mutable namespace требует validators либо короткий TTL.
Stale выдаётся только в пределах явного разрешения.
Отозванное содержимое не выдаётся через stale fallback.
Ошибки подписи не дают stale bypass.

| Событие | Действие |
| --- | --- |
| TTL истёк | Проверить freshness policy |
| Версия изменилась | Создать новую identity |
| Origin недоступен | Проверить stale-if-error budget |
| Объект отозван | Заблокировать выдачу всех tiers |
| Политика доступа обновлена | Применить новую access revision |
| Копия повреждена | Quarantine и новый fill |

## Eviction

Вытеснение начинается до полного заполнения диска.
High watermark запускает освобождение.
Low watermark завершает обычный цикл.
Emergency watermark запрещает новые fills.
Значения проверяются как `low < high < emergency < 1`.

Кандидаты сравниваются внутри tenant budget.
Pin исключает entry из обычного eviction.
Pin имеет владельца, причину и срок.
Бессрочный pin доступен только отдельной роли.
Сумма pinned bytes не превышает выделенный бюджет.

Вытеснение метаданных не должно забывать занятое место.
Сначала entry снимается с публичного lookup.
Затем заканчиваются активные чтения.
Затем файл удаляется.
После успешного удаления обновляются физические счётчики.

### Гистерезис

Повышение требует score выше `promote_threshold`.
Понижение требует score ниже `demote_threshold`.
Порог понижения меньше порога повышения.
Состояние должно сохраняться minimum dwell time.
Авария устройства может обойти dwell time.
Нехватка места может прервать фоновые promotions.

## Квоты tenant

Квота задаёт hard limit и целевой soft share.
Hard limit применяется до reservation.
Soft share влияет на выбор eviction victims.
Свободная общая ёмкость может временно перераспределяться.
Возврат чужой доли происходит постепенно.

Один tenant не может закрепить весь общий RAM tier.
Origin concurrency также ограничивается по tenant.
Телеметрия quotas не раскрывает имена чужих объектов.
Ошибки quota имеют отдельную причину от disk full.

## Backpressure

Backpressure передаётся от диска к очереди fills.
От очереди fills он передаётся admission.
От admission он передаётся планировщику origin.
Уже начавшаяся выдача ограничивается доступным буфером.
Worker не блокируется на полном канале telemetry.

| Ресурс | Мягкий предел | Жёсткий предел |
| --- | --- | --- |
| RAM | Снизить admission | Pass-through/отказ по policy |
| NVMe bytes | Eviction | Запрет новых fills |
| HDD queue | Снизить фоновые работы | Circuit breaker |
| Origin slots | Ожидание с deadline | Контролируемый отказ |
| Fill waiters | Ограничить очередь | Быстрый отказ |
| Event spool | Coalescing | Quality gap marker |
| File descriptors | Закрыть idle handles | Отказ нового fill |

## Локальный пример конфигурации

Все ключи SLOYRA в примере являются проектными.
Значения предназначены для лабораторного профиля.
Перед production нужны нагрузочные измерения.

```yaml
cache:
  identity_schema: 1
  chunking:
    enabled: true
    size: 4MiB
    layout_version: 1
    max_ranges: 8
  tiers:
    ram:
      enabled: true
      budget: 8GiB
      max_entry: 16MiB
    nvme:
      enabled: true
      path: /var/cache/sloyra/nvme
      budget: 800GiB
      low_watermark: 0.75
      high_watermark: 0.85
      emergency_watermark: 0.93
    hdd:
      enabled: true
      path: /var/cache/sloyra/hdd
      budget: 8TiB
      background_concurrency: 2
  fill:
    lease: 30s
    waiter_deadline: 2s
    max_waiters_per_key: 256
    max_attempts: 2
    publication: verified_atomic
  freshness:
    immutable_versions: true
    stale_on_revocation: false
  eviction:
    policy: benefit_per_byte
    min_residency: 5m
    active_reader_grace: 60s
telemetry:
  logical_request: true
  cache_lookup: true
  origin_fetch: true
  spool_limit: 2GiB
placement:
  local_enabled: true
  dry_run: true
```

## Совместимость с кешем Nginx

SLOYRA не должен одновременно считать один fill дважды.
При использовании штатного proxy cache выбирается единый владелец данных.
Адаптер может наблюдать его результат и добавлять уровни политики.
Альтернативно SLOYRA управляет собственным хранилищем.
Эти режимы явно различаются конфигурацией.

Штатный `proxy_cache_lock` ограничивает заполнение одного cache key.
Его timeout и age имеют собственную семантику.
Адаптер обязан учитывать фактические origin attempts.
Описание директив находится в [документации proxy module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html).

Модуль slice делит ответы на диапазоны.
Ключ кеша в таком режиме должен учитывать диапазон.
Совместимость конкретной сборки проверяется отдельно.
Пример и ограничения доступны в [документации slice module](https://nginx.org/en/docs/http/ngx_http_slice_module.html).

## Проектный формат диагностического entry

Это схема результата диагностики, а не обещание существующего API.
Секретные URL и ключи доступа исключаются.

```json
{
  "schema": 1,
  "tenant_id": "demo-media",
  "object_hash": "sha256:example",
  "version": "v17",
  "chunk": {
    "start": 4194304,
    "length": 4194304,
    "layout": 1
  },
  "tier": "nvme",
  "state": "ready",
  "generation": 82,
  "reader_count": 3,
  "verified": true,
  "pin": null,
  "score_revision": 440,
  "policy_revision": "p12"
}
```

## Матрица принятия решения

| Условие | Lookup | Fill | Выдача |
| --- | --- | --- | --- |
| Клиент не допущен | Не нужен | Нет | 403 |
| RAM ready | RAM | Нет | RAM |
| NVMe ready | RAM, NVMe | Нет | NVMe |
| HDD ready | Все local | По policy | HDD |
| Полный miss | Все local | Да | Origin/fill |
| Только stale | По policy | Revalidate | По freshness |
| Corrupt entry | Quarantine | Да | Новый источник |
| Неверная версия | Miss текущей версии | Да | Только нужная версия |
| Нет budget | Lookup разрешён | Нет | Pass-through по budget |
| Origin недоступен | Lookup разрешён | Ошибка | Stale либо 5xx |
| Отзыв объекта | Заблокирован | Нет | По revocation policy |

## Метрики

`requested_bytes` описывает нормализованный логический запрос.
`served_bytes` описывает реально переданное клиенту тело.
`origin_bytes` описывает реально прочитанные байты источника.
Прочитанные заголовки не входят в эти три body counters.
Traffic accounting может хранить отдельные wire bytes.

Hit ratio по запросам не равен hit ratio по байтам.
RAM hit не означает отсутствие дискового чтения ОС в других компонентах.
HDD hit может быть обслужен из page cache ОС.
Такое попадание остаётся logical HDD tier hit.
Аппаратная телеметрия измеряется отдельным каналом.

```text
request_hit_ratio = cache_served_requests / eligible_requests
byte_hit_ratio = local_served_bytes / served_bytes
read_amplification = origin_bytes / origin_needed_bytes
fill_efficiency = admitted_bytes_reused / admitted_bytes
```

Нулевой знаменатель даёт `null`, а не ноль процентов.
Временные интервалы и tenant scope обязаны совпадать.
Частичный hit отражается числом байтов каждого tier.
Одно число `HIT` не заменяет карту источников ответа.

## Диагностика неполадок

При росте miss сначала проверить изменение рабочего набора.
Затем проверить identity schema и version churn.
После этого проверить квоты и watermark.
Очистка всего кеша не является первым диагностическим шагом.

При росте latency сравнить client wait и origin latency.
Далее проверить fill waiter count.
Затем проверить очереди HDD и NVMe.
Проверить число временных файлов и reserved bytes.
Растущий reservation leak означает ошибку жизненного цикла.

При снижении byte hit проверить размеры объектов.
Много маленьких hits может скрывать misses крупных файлов.
При хорошем hit ratio и высокой origin нагрузке проверить retries.
Также проверить revalidation, prefetch и range amplification.

## Условия выпуска

- Чужой tenant не получает совпавший ключ.
- Смена версии не выдаёт старые байты как новые.
- Crash в любой фазе fill не публикует partial.
- Потеря владельца не даёт двух публикаций generation.
- Eviction не нарушает активное чтение.
- Повреждение файла обнаруживается и изолируется.
- Disk full не вызывает бесконечный retry.
- Backpressure не блокирует event loop.
- Отзыв объекта действует на все tiers.
- Счётчики проходят reconciliation по fixtures ниже.

## Контрольные сценарии кеша

Каждый сценарий задаёт вход и ожидаемое поведение реализации.
Это проектные примеры для будущих интеграционных проверок.
Поле `logical_requests` считает только допущенные клиентские запросы.
Поле `origin_attempts` считает реальные попытки чтения origin.
Поле `new_ready_entries` считает опубликованные новые entries.

### C01. Готовый кусок в RAM

Существующая версия совпадает.

```yaml
case: ram_hit
initial_state: ready_ram
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: ram
  action: none
```

served_bytes увеличивается на переданное тело.

### C02. Готовый кусок на NVMe

RAM lookup завершился miss.

```yaml
case: nvme_hit
initial_state: ready_nvme
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: nvme
  action: none
```

RAM promotion не блокирует ответ.

### C03. Готовый кусок на HDD

Нет готовой копии в верхних tiers.

```yaml
case: hdd_hit
initial_state: ready_hdd
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: hdd
  action: optional_promotion
```

Origin не вызывается ради статистики.

### C04. Первый запрос к куску

Есть reservation и доступный origin slot.

```yaml
case: origin_fill
initial_state: absent
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: origin
  action: fill
```

После проверки entry становится ready.

### C05. Отказ доступа к горячему объекту

Access gate возвращает 403.

```yaml
case: forbidden_hot
initial_state: ready_ram
access: deny
expected:
  logical_requests: 0
  origin_attempts: 0
  new_ready_entries: 0
  response_source: none
  action: none
```

Наличие кеша не меняет решение.

### C06. Сто клиентов к пустому куску

Все запросы имеют одну identity.

```yaml
case: collapse_100
initial_state: absent
access: allow
expected:
  logical_requests: 100
  origin_attempts: 1
  new_ready_entries: 1
  response_source: shared_fill
  action: collapse
```

Спрос равен 100 при одном физическом fill.

### C07. Повтор после неполной загрузки

Первый ответ оборвался до полной длины.

```yaml
case: retry_once
initial_state: absent
access: allow
expected:
  logical_requests: 1
  origin_attempts: 2
  new_ready_entries: 1
  response_source: origin
  action: retry
```

origin_bytes включает обе попытки.

### C08. Новая версия при старом кеше

Версия изменилась с v17 на v18.

```yaml
case: different_version
initial_state: ready_old_version
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: origin
  action: fill_new_identity
```

Старая запись не подменяет новую.

### C09. Одинаковый путь у другого tenant

Пространства tenant изолированы.

```yaml
case: different_tenant
initial_state: ready_other_tenant
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: origin
  action: fill_new_identity
```

Collapse также разделён по tenant.

### C10. Диапазон пересекает две границы

Два куска загружаются в рамках одного запроса.

```yaml
case: range_two_chunks
initial_state: two_chunks_absent
access: allow
expected:
  logical_requests: 1
  origin_attempts: 2
  new_ready_entries: 2
  response_source: origin
  action: parallel_bounded
```

requested_bytes не равны fetched chunk bytes.

### C11. Первый кусок найден, второй отсутствует

Версия обоих кусков совпадает.

```yaml
case: range_partial_hit
initial_state: one_chunk_ready
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: mixed
  action: fill_missing
```

Метрики фиксируют локальные и origin байты.

### C12. Диапазон за концом объекта

Размер объекта известен.

```yaml
case: invalid_range
initial_state: metadata_only
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: none
  action: range_reject
```

Тело origin не загружается.

### C13. Суффикс меньше последнего куска

Размер подтверждён metadata.

```yaml
case: suffix_range
initial_state: last_chunk_ready
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: nvme
  action: slice
```

Выдаются только последние N байтов.

### C14. HEAD к отсутствующему объекту

Body fetch не требуется.

```yaml
case: head_only
initial_state: absent
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: metadata
  action: metadata_fetch
```

HEAD не прогревает большой объект.

### C15. Клиент отключился во время fill

Есть другой waiter либо полезный admission.

```yaml
case: client_cancel
initial_state: fetching
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: origin
  action: continue_if_admitted
```

served_bytes меньше requested_bytes.

### C16. Tenant исчерпал hard quota

Origin budget разрешает чтение.

```yaml
case: quota_exhausted
initial_state: absent
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 0
  response_source: origin
  action: pass_through
```

Кеш не резервирует чужую квоту.

### C17. Свободное место ниже emergency

Доступен pass-through budget.

```yaml
case: disk_full
initial_state: absent
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 0
  response_source: origin
  action: disable_admission
```

Нет бесконечной записи временных файлов.

### C18. Checksum локального entry неверен

Origin возвращает правильную версию.

```yaml
case: corrupt_payload
initial_state: corrupt_nvme
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: origin
  action: quarantine
```

Повреждённый entry снят с lookup.

### C19. TTL истёк, версия неизменна

Validator подтверждает прежние байты.

```yaml
case: stale_revalidate
initial_state: stale_nvme
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: nvme
  action: metadata_revalidate
```

Metadata request учитывается отдельно от body.

### C20. Origin недоступен, stale разрешён

Окно stale не истекло, отзыва нет.

```yaml
case: stale_origin_down
initial_state: stale_nvme
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 0
  response_source: nvme_stale
  action: bounded_stale
```

Ошибка origin отражена отдельным событием.

### C21. Объект отозван после прогрева

Access revision содержит tombstone.

```yaml
case: revoked
initial_state: ready_all_tiers
access: deny
expected:
  logical_requests: 0
  origin_attempts: 0
  new_ready_entries: 0
  response_source: none
  action: revoke
```

Stale fallback запрещён.

### C22. Старый owner завершил после lease

Новый owner получил больший token.

```yaml
case: owner_fenced
initial_state: fetching_old_owner
access: allow
expected:
  logical_requests: 1
  origin_attempts: 2
  new_ready_entries: 1
  response_source: new_owner
  action: fencing
```

Старый файл не публикуется.

### C23. Eviction при активном клиенте

Новый lookup уже исключает entry.

```yaml
case: active_reader_eviction
initial_state: ready_nvme
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: nvme
  action: retire
```

Старый handle живёт до завершения.

### C24. Перезапуск с незавершённым файлом

В индексе нет подтверждённой ready записи.

```yaml
case: restart_partial
initial_state: orphan_partial
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: origin
  action: recover
```

Partial не принимается за полный объект.

### C25. Истёк срок закрепления

Текущий запрос удерживает reader ref.

```yaml
case: pin_expiry
initial_state: ready_hdd_pinned
access: allow
expected:
  logical_requests: 1
  origin_attempts: 0
  new_ready_entries: 0
  response_source: hdd
  action: unpin_then_evaluate
```

Истечение pin не удаляет origin.

### C26. Однократное последовательное сканирование

Политика требует повторного использования.

```yaml
case: cold_scan
initial_state: absent_many
access: allow
expected:
  logical_requests: 20
  origin_attempts: 20
  new_ready_entries: 0
  response_source: origin
  action: admission_reject
```

Рабочий горячий набор не вытесняется.

### C27. Спекулятивный кусок не был запрошен

Фоновая операция имеет отдельный class.

```yaml
case: prefetch_unused
initial_state: prefetched_ready
access: none
expected:
  logical_requests: 0
  origin_attempts: 1
  new_ready_entries: 1
  response_source: none
  action: prefetch
```

Нельзя прибавлять logical_request.

### C28. Один путь с разными кодировками

Запрошено gzip представление.

```yaml
case: identity_encoding
initial_state: ready_identity_only
access: allow
expected:
  logical_requests: 1
  origin_attempts: 1
  new_ready_entries: 1
  response_source: origin
  action: variant_fill
```

Identity body не выдаётся как gzip.

## Источники и граница достоверности

- [Nginx proxy module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) — существующая семантика proxy cache и lock.
- [Nginx slice module](https://nginx.org/en/docs/http/ngx_http_slice_module.html) — существующая модель разделения ответов на диапазоны.
- [Ceph CRUSH maps, Squid](https://docs.ceph.com/en/squid/rados/operations/crush-map/) — разделение локального кеша и placement origin.

Проверка первоисточников выполнена 27 сентября 2026 года.
Примеры SLOYRA, структуры entry и YAML являются проектными.
Числовые лимиты являются стартовыми предположениями для испытаний.
Показатели производительности этим документом не подтверждаются.

---

# Популярность, логический спрос и физическая нагрузка

> Статус: проектная спецификация v0.1.

## Что измеряется

SLOYRA использует популярность для выбора уровня хранения.
Популярность вычисляется для файлов и для отдельных кусков.
Этот функциональный принцип подтверждён постановкой задачи.
Формулы, схемы событий и пороги ниже проектируются в версии v0.1.

Система различает интерес клиента и работу источника.
Попадание в RAM не означает исчезновение спроса.
Промах кеша не означает, что объект обязательно популярен.
Фоновый прогрев создаёт физическую нагрузку без клиентского запроса.
Повтор origin создаёт дополнительную нагрузку без нового спроса.

Для backend Ceph полезны оба ряда измерений.
Логический спрос показывает устойчивую потребность в объекте.
Физические чтения показывают текущую нагрузку после локального кеша.
Решение о размещении использует явно выбранную модель.

## Три класса событий

### logical_request

Событие создаётся для допущенного клиентского запроса.
Оно создаётся независимо от результата cache lookup.
Запрос получает `logical_request_id`.
Повторы доставки события сохраняют этот идентификатор.
Новый реальный HTTP-запрос получает новый идентификатор.

Access denial имеет отдельное security event.
Он не повышает popularity защищённого объекта.
HEAD может увеличивать metadata demand.
HEAD не увеличивает body byte demand.
Range учитывается после нормализации.

### cache_lookup

Событие описывает попытку найти требуемые байты в tier.
Один logical request может иметь несколько lookups.
Несколько кусков дают несколько результатов.
Событие не является самостоятельным голосом популярности.
Повторный lookup после ожидания не удваивает спрос.

Результат различает `hit`, `miss`, `stale` и `error`.
`miss` означает отсутствие пригодной копии.
`error` означает невозможность надёжно проверить tier.
Смешивать их в один miss counter запрещено.

### origin_fetch

Событие описывает физическое чтение origin.
Логическое заполнение определяется `fill_id`.
Попытка определяется `origin_attempt_id`.
Retry сохраняет fill_id и меняет attempt_id.
Фоновый prefetch имеет собственный инициатор.

В событии есть ожидаемые и фактические байты.
Частичная попытка учитывается по фактически прочитанным байтам.
Origin error не обнуляет уже прочитанное тело.
Collapse сохраняет список связей с logical requests в агрегированном виде.
Не требуется сохранять все клиентские IDs в горячем пути.

## Значение байтовых счётчиков

| Счётчик | Что означает |
| --- | --- |
| requested_bytes | Нормализованный размер логически запрошенного тела |
| served_bytes | Реально отправленное клиенту тело |
| origin_bytes | Реально полученное тело origin |
| admitted_bytes | Байты, допущенные в кеш |
| reused_bytes | Байты admission, затем выданные повторно |
| prefetched_bytes | Байты фонового прогрева |
| discarded_fill_bytes | Загрузка, не опубликованная в кеше |
| validation_wire_bytes | Трафик проверки metadata |
| migration_bytes | Данные переноса между классами |

`requested_bytes` не уменьшается при обрыве клиента.
`served_bytes` отражает обрыв.
`origin_bytes` зависит от фактического завершения fill.
Эти ряды нельзя заменять друг другом в формулах.

При неизвестной длине запрос содержит `requested_bytes_known: false`.
Неизвестное число не интерпретируется как подтверждённый ноль.
Wire-контракт допускает `requested_bytes=0` лишь вместе с `requested_bytes_known=false`.
При агрегации такой placeholder исключается.
После получения длины допускается correction event.
Коррекция имеет собственный ID и ссылку на исходное событие.

## Минимальная идентичность

```text
DemandKey = (
  tenant_id,
  namespace,
  object_id,
  content_version,
  representation_id,
  chunk_layout_version,
  normalized_range
)
```

Версия не должна переиспользоваться после изменения байтов.
Разные версии можно объединять только в аналитике каталога.
Перенос старой версии и популярность новой версии не смешиваются.
Стабильный object family ID разрешён как дополнительная агрегация.
Он не заменяет identity для чтения и удаления.

## Объектная и кусочная популярность

Объектный запрос получает один request credit.
Куски делят этот credit пропорционально запрошенному покрытию.
Байтовый спрос каждого куска равен пересечению с запросом.
Так длинный Range не превращается в сотню независимых запросов.
При этом реально популярный первый кусок виден отдельно.

```text
L = размер объединения запрошенных диапазонов
l_j = число запрошенных байтов в куске j
request_credit_j = l_j / L
byte_demand_j = l_j
sum(request_credit_j) = 1
sum(byte_demand_j) = L
```

Для полного файла используется та же формула.
Для HEAD request credit относится к metadata track.
Для нулевого тела body credit равен нулю.
Для ошибочного Range тело не получает credit.
Контроль существования объекта может учитываться отдельно.

### Рассчитанный пример

Клиент запрашивает `6 MiB`.
Первый кусок содержит `4 MiB` покрытия.
Второй содержит `2 MiB` покрытия.
Первый получает `4 / 6 = 0,666667` request credit.
Второй получает `2 / 6 = 0,333333` request credit.
Суммарный объектный request credit остаётся равен 1.
Byte demand равен соответственно 4 и 2 MiB.

## Событийное время

`observed_at` задаёт время на ingress.
`completed_at` задаёт завершение операции.
`ingested_at` задаёт получение агрегатором.
Popularity windows строятся по observed time.
Физическая нагрузка может строиться по завершению попытки.
Выбранная временная ось указана в имени серии.

Узлы синхронизируют часы.
Скачок wall clock не ломает локальные deadlines.
Deadlines используют монотонные часы.
Преобразование в UTC происходит при формировании события.
Слишком далёкое будущее помещается в quarantine.
Слишком старое событие проходит late-event policy.

## Затухание спроса

В v0.1 предлагается экспоненциальное затухание.
Каждый вклад уменьшается вдвое за период half-life.
Вклад не исчезает резко на границе окна.
Расчёт может выполняться лениво при обновлении записи.

```text
D(t) = Σ_i w_i × 2^(-(t - t_i) / H)
```

`D(t)` — decayed demand.
`w_i` — вес события.
`t_i` — время события.
`H` — half-life в тех же единицах времени.
`H` обязан быть положительным.

```text
D_new = D_old × 2^(-Δt / H) + w_new
```

При `Δt = 0` затухания нет.
При `Δt = H` старый вес делится на два.
При `Δt = 2H` старый вес делится на четыре.
При отрицательном Δt событие обрабатывается как позднее.
Нельзя усиливать старое значение отрицательным временем.

### Два масштаба времени

Короткий half-life реагирует на всплеск спроса.
Длинный half-life выделяет устойчивое использование.
Оба значения хранятся одновременно.
Один счётчик недостаточен для отличия премьеры от постоянного каталога.

```text
H_short = 15 min
H_long = 24 h
D_mix = α × D_short + (1 - α) × D_long_normalized
α = 0.7
```

Нормализация длинного ряда обязательна.
Иначе его большая накопленная масса доминирует по конструкции.
Для стационарного потока можно использовать эквивалентную интенсивность.

```text
rate_short = D_short × ln(2) / H_short
rate_long = D_long × ln(2) / H_long
rate_mix = α × rate_short + (1 - α) × rate_long
```

Оба H должны быть в секундах для requests per second.
Размерность `rate_mix` — событий в секунду.
Для байтового ряда это bytes per second.

## Пример затухания

В момент `t=0` пришло 80 единиц спроса.
При H=15 минут к `t=15` остаётся 40.
При `t=30` остаётся 20.
Если в `t=30` приходит ещё 12, новое значение равно 32.
К `t=45` без новых запросов останется 16.

```text
D(30 min) = 80 × 2^(-30/15) + 12
D(30 min) = 80 × 0.25 + 12
D(30 min) = 32
```

Тот же расчёт применяется к byte demand.
Нельзя использовать разные clocks для request и byte series.

## Основной score

Score представляет полезность размещения в конкретном tier.
Он не является абсолютной характеристикой объекта.
Один объект может быть выгоден на SSD и невыгоден в RAM.
Score учитывает прогноз спроса, стоимость miss и занимаемые байты.

```text
benefit_i,tier =
  predicted_reads_i × saved_latency_ms_i,tier
  + β × predicted_bytes_i × saved_origin_cost_per_byte

cost_i,tier =
  resident_bytes_i × capacity_price_per_byte_tier
  + expected_write_bytes_i × write_penalty_tier

score_i,tier = benefit_i,tier / max(cost_i,tier, ε)
```

Коэффициент β приводит разные единицы пользы к одной шкале.
Его значение версионируется вместе с policy.
В первой реализации допускается более простой latency-per-byte score.
Нельзя сравнивать результаты разных formula revisions без нормализации.

### Упрощённый score для admission

```text
S = (r × ΔL × q) / (B_MiB + m_MiB)
```

`r` — decayed request credits на горизонте оценки.
`ΔL` — ожидаемый выигрыш задержки в миллисекундах.
`q` — уверенность в данных от 0 до 1.
`B_MiB` — размер содержимого в MiB.
`m_MiB` — metadata overhead в MiB.
Единица результата — условная latency benefit per MiB.

### Рассчитанное сравнение

Объект A имеет r=120, ΔL=8 ms, q=0,9 и размер 4 MiB.
При metadata overhead 0,01 MiB получаем `864 / 4,01 ≈ 215,46`.
Объект B имеет r=200, ΔL=3 ms, q=0,95 и размер 64 MiB.
Получаем `570 / 64,01 ≈ 8,90`.
Объект A выгоднее по выбранному score, хотя у B больше запросов.

Это иллюстрация формулы, не измерение реального оборудования.
ΔL получают из наблюдений на конкретном deployment.
Для cold-start ΔL берётся из явно обозначенного prior.

## Спрос и давление на backend

Local cache demand строится по logical_request.
Backend pressure строится по origin_fetch.
Ceph placement получает оба сигнала.
Имена метрик отражают выбранный источник.

```text
logical_demand = all_allowed_client_accesses
backend_pressure = physical_origin_attempts_and_bytes
```

Если горячий файл целиком помещается в RAM, origin pressure может быть нулевым.
Это не означает, что файл никому не нужен.
Если RAM достаточно и риск cold miss приемлем, origin может оставаться на HDD.
Если важна быстрая аварийная перезагрузка кешей, контроллер может выбрать SSD.
Различие задаётся recovery-aware policy.

### Recovery reserve

Policy может резервировать SSD для важных данных прогрева.
Резерв учитывает вероятность потери кеша и стоимость восстановления.
Эта вероятность является модельным параметром.
Она не выдаётся за измеренную без данных эксплуатации.

```text
expected_recovery_cost =
  cache_loss_probability × cold_start_cost
```

Резерв не должен вытеснять весь текущий hot set.
Его ёмкость ограничивается отдельной квотой.
Каждое размещение сохраняет причину `recovery_reserve`.

## Miss как диагностический сигнал

Высокий miss может означать новый контент.
Он также может означать случайный scan.
Он может быть следствием слишком маленького cache budget.
Он может возникать из-за неверной canonicalization.
Он может отражать frequent version changes.
Поэтому miss count не используется как единственный popularity score.

### Типы miss

| Причина | Смысл | Следствие |
| --- | --- | --- |
| cold | Версия ещё не заполнялась | Кандидат admission |
| capacity | Entry вытеснен | Проверить working set |
| expired | Требуется revalidation | Проверить TTL |
| version | Запрошена новая версия | Новый identity |
| bypass | Политика не кеширует | Не лечить ёмкостью |
| quota | Tenant ограничен | Проверить квоту |
| corrupt | Копия повреждена | Quarantine |
| unavailable | Tier не читается | Health incident |
| pin_conflict | Нет вытесняемых bytes | Проверить pins |

## Дедупликация доставки

Transport допускает at-least-once delivery.
Уникальный event ID делает повтор идемпотентным.
Агрегатор хранит dedup horizon.
Горизонт больше максимального срока повторной доставки.
Событие за пределами горизонта не применяется молча второй раз.
Его учитывают в quarantine либо correction workflow.

```text
EventIdentity = (
  producer_id,
  producer_epoch,
  monotonic_sequence
)
```

После рестарта producer увеличивает epoch.
Sequence не переиспользуется внутри epoch.
Batch ID не заменяет event ID.
Повтор части batch не должен удвоить уже принятые элементы.

### Дедупликация не равна объединению пользователей

Два разных клиента к одному файлу — два logical requests.
Повтор доставки одного события — один logical request.
Два HTTP retry от клиента — два наблюдённых HTTP-запроса.
Если application retry ID доверенный, можно строить отдельную сессию намерения.
Такое объединение не меняет базовый HTTP demand counter.

## Collapse и retries

Collapse уменьшает число fills.
Он не уменьшает число ожидающих пользователей.
Один retry увеличивает физические attempts.
Он не добавляет новый request credit объекту.

```text
100 logical requests
1 fill
2 origin attempts
first attempt: 1 MiB
second attempt: 4 MiB
origin bytes: 5 MiB
ready bytes: 4 MiB
```

При выдаче всем 100 клиентам полного куска размером 4 MiB:
`requested_bytes = 400 MiB`.
`served_bytes = 400 MiB`, если никто не оборвал чтение.
`origin_bytes = 5 MiB`.
Внутренний transport overhead в эти величины не включён.

## Confidence

Неполная телеметрия уменьшает уверенность.
Она не доказывает отсутствие спроса.
Поэтому потеря событий блокирует агрессивное понижение.
Повышение при надёжном положительном сигнале может оставаться разрешённым.

```text
q = completeness × freshness × identity_quality
```

Все множители ограничены диапазоном [0, 1].
Completeness оценивает долю доставленных событий.
Freshness оценивает задержку доставки.
Identity quality показывает пригодность version/range mapping.
Нулевой quality делает автоматический перенос недопустимым.

### Gap markers

Producer периодически сообщает диапазон sequence.
Агрегатор обнаруживает пропуски.
Невосстановимый пропуск становится quality gap.
Quality gap имеет node, tenant scope и время.
Нельзя распределить потерянный спрос по случайным объектам.
Общий unknown demand учитывается отдельным bucket.

## Sampling

Базовые счётчики спроса собираются без sampling при доступном ресурсе.
Для высоких нагрузок допускается известная вероятность выборки p.
Вес принятого события равен `1 / p`.
Вероятность включается в событие.
Неизвестная sampling rate делает оценку непригодной.

```text
estimated_requests = Σ_sampled (1 / p_i)
```

Sampling повышает дисперсию оценки редких объектов.
Порог admission должен учитывать эту дисперсию.
Тяжёлые hitters можно отслеживать точнее отдельным sketch.
Sketch не заменяет точный location catalog.

## Cardinality

Каждый объект не обязан становиться label временного ряда.
Подробные events хранятся в аналитическом потоке.
Метрики мониторинга агрегируются по tenant, tier и reason.
Top-N каталог ограничен по размеру.
Полный object ID доступен только в авторизованной диагностике.

Memory limit применяется к числу активных identities.
Редкие старые записи агрегируются либо удаляются после decay.
Удаление popularity state не удаляет данные.
После удаления state объект возвращается к cold-start prior.

## Cold start

Новый объект начинает с ограниченного prior.
Prior зависит от namespace, а не от секретного пользовательского профиля.
Премьера может получить временный pin по явной политике.
Pin имеет срок и учитывается в ёмкости.
Нельзя навсегда повышать все новые файлы.

Для нового узла можно загрузить агрегированный heatmap.
Heatmap подписывается и содержит policy revision.
Устаревшая heatmap не отменяет свежие локальные наблюдения.
Перенос history между tenant запрещён по умолчанию.

## Защита от искусственного нагрева

Только допущенные запросы влияют на content popularity.
Access checks уменьшают очевидный нелегитимный трафик.
Допущенный трафик всё равно может быть чрезмерным.
Поэтому применяются tenant fairness и admission caps.

Вклад одного источника может ограничиваться в дополнительной risk model.
Raw demand при этом сохраняется отдельно.
Клиентские IP не помещаются в object score labels.
Решение использует короткоживущие агрегаты, если они нужны.
Причина ограничения доступна для аудита.

## Policy revisions

Каждая формула имеет revision.
Каждый набор коэффициентов имеет revision.
Score snapshot хранит обе версии.
Решение о переносе хранит использованный snapshot.
Это позволяет объяснить причину после изменения конфигурации.

Rolling update не смешивает несовместимые шкалы.
Переход выполняется через shadow scoring.
Старый и новый score рассчитываются параллельно ограниченное время.
Сравниваются решения и стоимость миграции.
После проверки новая revision становится active.

## Проектный event envelope

```json
{
  "schema_version": 1,
  "event_type": "logical_request",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180420
  },
  "observed_at": "2026-09-27T10:00:00Z",
  "tenant_id": "demo-media",
  "object_id_hash": "sha256:example",
  "content_version": "v17",
  "representation_id": "source",
  "range": {
    "start": 0,
    "length": 4194304
  },
  "requested_bytes": 4194304,
  "requested_bytes_known": true,
  "sample_probability": 1.0,
  "policy_revision": "p12"
}
```

Это проектная wire schema SLOYRA.
Она не является форматом встроенного события Ceph.
Adapter переводит её в backend telemetry contract.

## Проектная конфигурация

```yaml
telemetry:
  event_schema: 1
  delivery: at_least_once
  dedup_horizon: 72h
  batch:
    max_events: 4096
    max_bytes: 4MiB
    flush_interval: 1s
  time:
    max_future_skew: 30s
    allowed_lateness: 10m
  popularity:
    short_half_life: 15m
    long_half_life: 24h
    short_weight: 0.7
    body_sampling_probability: 1.0
    max_active_identities: 1000000
placement:
  scoring:
    formula_revision: latency_per_byte_v1
    confidence_min: 0.9
    freeze_demotions_on_gap: true
    shadow_revision: null
```

## Контроль агрегатов

Агрегатор сравнивает producer totals и принятые events.
Разница не должна скрываться округлением.
События коррекции меняют отдельный correction counter.
Ручной reset счётчиков имеет audit event.

```text
accepted_unique + rejected + quarantined
  = received_unique

received_total
  = received_unique + duplicate_deliveries
```

Интервалы сравнения должны учитывать late arrivals.
Для незакрытого окна показывается provisional state.
Для закрытого окна сохраняется final revision.
Поздняя коррекция создаёт новую revision окна.

## Критерии качества модели

Хорошая модель уменьшает latency при заданном бюджете.
Она не обязана максимизировать число promotions.
Стабильная модель снижает churn между tiers.
Решения должны быть объяснимы через ограниченный набор факторов.

| Критерий | Измерение |
| --- | --- |
| Польза | Latency saved per resident byte |
| Точность | Доля promoted bytes с повторным чтением |
| Стабильность | Moved bytes / resident bytes за сутки |
| Свежесть | Возраст score snapshot |
| Полнота | Доля закрытых последовательностей producer |
| Изоляция | Нарушения tenant budget |
| Стоимость | Origin и migration I/O |
| Безопасность | Ошибки identity/version matching |

## Контрольные числовые наборы

Следующие наборы проверяют арифметику затухания.
Каждый содержит старый спрос, интервал и новый вклад.
Формула одна: `old × 2^(-elapsed / half_life) + added`.
Значения округлены до шести знаков только при отображении.
Внутреннее вычисление сохраняет больше точности.

### D01. Затухание за 0 минут при H=15

```yaml
case: decay_01
old_demand: 80
half_life_minutes: 15
elapsed_minutes: 0
added_credit: 0
expected_demand: 80.000000
absolute_tolerance: 0.000001
```

### D02. Затухание за 15 минут при H=15

```yaml
case: decay_02
old_demand: 80
half_life_minutes: 15
elapsed_minutes: 15
added_credit: 0
expected_demand: 40.000000
absolute_tolerance: 0.000001
```

### D03. Затухание за 30 минут при H=15

```yaml
case: decay_03
old_demand: 80
half_life_minutes: 15
elapsed_minutes: 30
added_credit: 12
expected_demand: 32.000000
absolute_tolerance: 0.000001
```

### D04. Затухание за 45 минут при H=15

```yaml
case: decay_04
old_demand: 80
half_life_minutes: 15
elapsed_minutes: 45
added_credit: 0
expected_demand: 10.000000
absolute_tolerance: 0.000001
```

### D05. Затухание за 5 минут при H=10

```yaml
case: decay_05
old_demand: 100
half_life_minutes: 10
elapsed_minutes: 5
added_credit: 3
expected_demand: 73.710678
absolute_tolerance: 0.000001
```

### D06. Затухание за 20 минут при H=10

```yaml
case: decay_06
old_demand: 100
half_life_minutes: 10
elapsed_minutes: 20
added_credit: 3
expected_demand: 28.000000
absolute_tolerance: 0.000001
```

### D07. Затухание за 5 минут при H=5

```yaml
case: decay_07
old_demand: 16
half_life_minutes: 5
elapsed_minutes: 5
added_credit: 1
expected_demand: 9.000000
absolute_tolerance: 0.000001
```

### D08. Затухание за 10 минут при H=5

```yaml
case: decay_08
old_demand: 16
half_life_minutes: 5
elapsed_minutes: 10
added_credit: 1
expected_demand: 5.000000
absolute_tolerance: 0.000001
```

### D09. Затухание за 15 минут при H=5

```yaml
case: decay_09
old_demand: 16
half_life_minutes: 5
elapsed_minutes: 15
added_credit: 1
expected_demand: 3.000000
absolute_tolerance: 0.000001
```

### D10. Затухание за 30 минут при H=60

```yaml
case: decay_10
old_demand: 250
half_life_minutes: 60
elapsed_minutes: 30
added_credit: 20
expected_demand: 196.776695
absolute_tolerance: 0.000001
```

### D11. Затухание за 60 минут при H=60

```yaml
case: decay_11
old_demand: 250
half_life_minutes: 60
elapsed_minutes: 60
added_credit: 20
expected_demand: 145.000000
absolute_tolerance: 0.000001
```

### D12. Затухание за 120 минут при H=60

```yaml
case: decay_12
old_demand: 250
half_life_minutes: 60
elapsed_minutes: 120
added_credit: 20
expected_demand: 82.500000
absolute_tolerance: 0.000001
```

### D13. Затухание за 15 минут при H=15

```yaml
case: decay_13
old_demand: 1
half_life_minutes: 15
elapsed_minutes: 15
added_credit: 1
expected_demand: 1.500000
absolute_tolerance: 0.000001
```

### D14. Затухание за 30 минут при H=15

```yaml
case: decay_14
old_demand: 1
half_life_minutes: 15
elapsed_minutes: 30
added_credit: 1
expected_demand: 1.250000
absolute_tolerance: 0.000001
```

### D15. Затухание за 60 минут при H=15

```yaml
case: decay_15
old_demand: 0
half_life_minutes: 15
elapsed_minutes: 60
added_credit: 12
expected_demand: 12.000000
absolute_tolerance: 0.000001
```

### D16. Затухание за 600 минут при H=60

```yaml
case: decay_16
old_demand: 4096
half_life_minutes: 60
elapsed_minutes: 600
added_credit: 0
expected_demand: 4.000000
absolute_tolerance: 0.000001
```

### D17. Затухание за 90 минут при H=30

```yaml
case: decay_17
old_demand: 64
half_life_minutes: 30
elapsed_minutes: 90
added_credit: 0
expected_demand: 8.000000
absolute_tolerance: 0.000001
```

### D18. Затухание за 120 минут при H=30

```yaml
case: decay_18
old_demand: 64
half_life_minutes: 30
elapsed_minutes: 120
added_credit: 4
expected_demand: 8.000000
absolute_tolerance: 0.000001
```

### D19. Затухание за 720 минут при H=1440

```yaml
case: decay_19
old_demand: 120
half_life_minutes: 1440
elapsed_minutes: 720
added_credit: 10
expected_demand: 94.852814
absolute_tolerance: 0.000001
```

### D20. Затухание за 1440 минут при H=1440

```yaml
case: decay_20
old_demand: 120
half_life_minutes: 1440
elapsed_minutes: 1440
added_credit: 10
expected_demand: 70.000000
absolute_tolerance: 0.000001
```

## Контрольные наборы телеметрии

Числа ниже относятся к телам ответов.
MiB в таблицах означает 1 048 576 байтов.
Нулевой origin body traffic не исключает metadata requests.

### T01. ram_hit

Попадание не теряет логический спрос.

```yaml
logical_requests: 1
requested_mib: 4
served_mib: 4
origin_mib: 0
fill_count: 0
origin_attempt_count: 0
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T02. cold_fill

Один запрос создаёт один fill.

```yaml
logical_requests: 1
requested_mib: 4
served_mib: 4
origin_mib: 4
fill_count: 1
origin_attempt_count: 1
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T03. collapse_10

Десять клиентов делят одну загрузку.

```yaml
logical_requests: 10
requested_mib: 40
served_mib: 40
origin_mib: 4
fill_count: 1
origin_attempt_count: 1
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T04. retry_partial

Один MiB первой попытки входит в нагрузку.

```yaml
logical_requests: 1
requested_mib: 4
served_mib: 4
origin_mib: 5
fill_count: 1
origin_attempt_count: 2
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T05. range_amplification

Два куска покрывают диапазон размером три MiB.

```yaml
logical_requests: 1
requested_mib: 3
served_mib: 3
origin_mib: 8
fill_count: 2
origin_attempt_count: 2
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T06. client_cancel

Отмена уменьшает served, но не requested.

```yaml
logical_requests: 1
requested_mib: 4
served_mib: 1
origin_mib: 4
fill_count: 1
origin_attempt_count: 1
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T07. prefetch

Фоновая загрузка не создаёт logical request.

```yaml
logical_requests: 0
requested_mib: 0
served_mib: 0
origin_mib: 4
fill_count: 1
origin_attempt_count: 1
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T08. two_versions

Разные версии не объединяются collapse.

```yaml
logical_requests: 2
requested_mib: 8
served_mib: 8
origin_mib: 8
fill_count: 2
origin_attempt_count: 2
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T09. two_tenants

Изоляция tenant действует и на заполнение.

```yaml
logical_requests: 2
requested_mib: 8
served_mib: 8
origin_mib: 8
fill_count: 2
origin_attempt_count: 2
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T10. metadata_only

HEAD не добавляет body bytes.

```yaml
logical_requests: 1
requested_mib: 0
served_mib: 0
origin_mib: 0
fill_count: 0
origin_attempt_count: 0
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T11. partial_hit

Половина ответа уже находилась в кеше.

```yaml
logical_requests: 1
requested_mib: 8
served_mib: 8
origin_mib: 4
fill_count: 1
origin_attempt_count: 1
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

### T12. origin_failure

Неуспешная попытка могла прочитать часть тела.

```yaml
logical_requests: 1
requested_mib: 4
served_mib: 0
origin_mib: 1
fill_count: 1
origin_attempt_count: 1
request_credit_source: logical_request
physical_pressure_source: origin_fetch
```

## Источники и область проектирования

- [Nginx proxy module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) — существующие механизмы proxy cache и объединения заполнения.
- [Ceph placement and storage classes](https://docs.ceph.com/en/squid/radosgw/placement/) — интерфейсные границы backend placement.

Проверка источников выполнена 27 сентября 2026 года.
Формулы popularity, события и scoring revisions являются проектом SLOYRA.
Ни Nginx, ни Ceph не обещают автоматически вычислять эту модель.
Числовые fixtures рассчитаны по формулам данного документа.
Они не являются результатами нагрузочного теста готового продукта.

---

# Интеграция с Ceph и перенос между классами хранения

> Статус: проектная спецификация v0.1.

## Цель интеграции

SLOYRA передаёт backend информацию о спросе и промахах кеша.
Backend использует её для размещения файлов и кусков на SSD или HDD.
Такое назначение интеграции подтверждено постановкой задачи.
Контроллер размещения, каталог и протокол переноса ниже проектируются.
Конкретные интерфейсы SLOYRA не являются существующими API Ceph.

Передаются не только промахи.
`logical_request` сохраняет спрос, скрытый локальными cache hits.
`origin_fetch` показывает реальную физическую нагрузку источника.
`cache_lookup` объясняет распределение работы между уровнями.
Backend получает качество и свежесть каждого агрегата.

## Принцип размещения в Ceph

CRUSH выбирает расположение данных согласно правилам пула.
Device class ограничивает набор подходящих OSD.
Правило может выбирать устройства класса SSD либо HDD.
Назначение одного объекта другому классу не происходит от записи popularity label.
Для управляемого выбора SLOYRA использует отдельные pools либо RGW storage classes.
Это следует из [модели CRUSH и pools](https://docs.ceph.com/en/squid/rados/operations/crush-map/).

Применение нового CRUSH rule ко всему pool затрагивает его данные.
Такой приём не используется для ежедневного нагрева одного файла.
Popularity controller не меняет device class физических OSD.
Он выбирает разрешённое место конкретной версии объекта.
Администратор Ceph отвечает за базовую топологию и failure domains.

## Два поддерживаемых проектных адаптера

### RADOS adapter

SLOYRA владеет своим пространством объектов.
Данные размещаются в отдельном hot pool и cold pool.
Location catalog указывает актуальный pool и object locator.
Каждая версия immutable после публикации.
Перенос выполняется копированием, проверкой и переключением каталога.

Adapter использует документированные возможности librados.
Составные операции внутри одного объекта не являются транзакцией между pools.
Каталог обеспечивает собственную атомарность смены location.
Описание примитивов доступно в [Librados C API](https://docs.ceph.com/en/squid/rados/api/librados/).

### RGW adapter

SLOYRA обращается к объектному gateway через разрешённый S3-совместимый интерфейс.
Storage class сопоставляется data pool в конфигурации RGW.
Lifecycle transitions могут перемещать данные между storage classes.
Это механизм RGW, со своей очередью и семантикой.
Описание приведено в [RGW placement and storage classes](https://docs.ceph.com/en/squid/radosgw/placement/).

Popularity controller не правит внутренние RGW RADOS objects напрямую.
Он не копирует скрытые multipart fragments вручную.
Он использует проверенный adapter capability set.
Если требуемое действие не поддержано релизом RGW, controller остаётся в observe mode.

## Разделение режимов

| Режим | Единица управления | Владелец metadata |
| --- | --- | --- |
| RADOS managed | SLOYRA immutable object/chunk | SLOYRA catalog |
| RGW lifecycle | S3 object version/class | RGW |
| RGW managed copy | S3 object version с mapping | RGW плюс SLOYRA mapping |
| Observe only | Агрегат popularity | Внешний backend |

Режим выбирается для namespace.
Нельзя одновременно применять два независимых владельца переноса.
Observe mode публикует рекомендации и не перемещает данные.
Проектный managed copy включается после проверки conditional copy semantics.

## Архитектура controller

```text
Nginx plugin
  -> local event spool
  -> telemetry collector
  -> popularity aggregation
  -> placement planner
  -> migration queue
  -> adapter executor
  -> location catalog
```

```text
read request
  -> access policy
  -> local cache
  -> location lookup
  -> versioned origin read
```

Чтение не зависит от доступности planner.
Оно зависит от пригодного location snapshot.
Snapshot имеет ограниченный срок и generation.
Критический путь не выполняет фоновое копирование объекта.

## Hot и cold pools

Hot pool хранит выбранные данные на SSD.
Cold pool хранит остальные данные на HDD.
Оба pool должны удовлетворять утверждённой политике надёжности.
Снижение replica count не относится к экономии placement.
Изменение erasure coding рассматривается отдельным проектом.

В примерах оба pool имеют три реплики.
Failure domain в примерах — host.
Реальный выбор должен соответствовать физической топологии.
Если используются rack domains, их сохраняют для обоих классов.
Наличие трёх SSD в одном host не выполняет требование трёх host domains.

### Проверка исходной конфигурации

Ниже приведены read-only команды Ceph.
Имена pools заменяются именами существующего deployment.
Команды не создают pools и не меняют CRUSH map.

```bash
ceph health detail
ceph osd tree
ceph osd crush rule ls
ceph osd crush rule dump
ceph osd pool ls detail
ceph osd pool get sloyra-hot size
ceph osd pool get sloyra-cold size
ceph osd pool get sloyra-hot min_size
ceph osd pool get sloyra-cold min_size
ceph osd pool get sloyra-hot crush_rule
ceph osd pool get sloyra-cold crush_rule
```

Результаты сохраняются в deployment evidence.
Проверка не ограничивается совпадением имён правил.
Нужно проверить фактические device classes и failure domains.
Health warnings не игнорируются ради переноса.

## Location catalog

Каталог хранит активное местоположение immutable identity.
Ключ включает tenant, namespace, object ID и content version.
Для chunk mode добавляются layout version и byte range.
Catalog generation монотонно растёт при смене location.

```text
CatalogEntry:
  identity
  generation
  active_location
  content_length
  content_digest
  placement_class
  durability_policy_id
  policy_revision
  last_verified_at
  pending_migration
  retention_state
```

`active_location` содержит непрозрачный locator адаптера.
Клиент не конструирует имя pool из URL пользователя.
Полный locator не возвращается публичному клиенту.
Секретные credentials не хранятся в catalog entry.

### Атомарное переключение

Переключение выполняется compare-and-swap по generation.
Ожидаемая generation записывается в migration plan.
CAS изменяет только location той же immutable identity.
Новая версия объекта создаёт отдельную запись.

```text
CAS(
  key = identity,
  expected_generation = 42,
  expected_active = cold_location,
  next_generation = 43,
  next_active = hot_location
)
```

Успех CAS публикует новый location.
Неуспех CAS требует перечитать актуальную запись.
Автоматический blind overwrite после конфликта запрещён.
Контроллер может признать выполненный конкурентом перенос эквивалентным.
Для этого проверяются identity, digest и durability policy.

### Транзакционность

Catalog update должен быть атомарным и устойчивым к сбою.
Выбранный backend каталога обязан поддерживать CAS либо эквивалентную транзакцию.
Eventual consistency без дополнительных барьеров недостаточна для удаления источника.
Реплика каталога не становится writer без fencing.
Backup каталога проверяется отдельным restore drill.

## Имена и версии

Object name включает или однозначно отображает immutable version.
Переиспользование имени для других байтов запрещено в managed mode.
Chunk name включает chunk layout version.
Одинаковый byte offset при новой нарезке не означает тот же chunk.

Пользовательский путь может ссылаться на актуальную object version.
Этот alias обновляется отдельно от location.
Читатель фиксирует версию до получения первого байта.
Нельзя склеить начало v17 с концом v18.

## Протокол safe copy-verify-switch-delete

### Фаза 1. Планирование

Planner выбирает candidate по score и capacity budget.
Он проверяет свежесть telemetry snapshot.
Он проверяет pin, retention и текущую migration state.
Он резервирует capacity в целевом классе.
План содержит reason и ожидаемую пользу.

### Фаза 2. Lease

Executor получает lease на конкретную identity.
Lease содержит owner ID, срок и fencing token.
Токен монотонно увеличивается при смене владельца.
Все изменяющие состояние шаги проверяют token.
Потерявший lease исполнитель прекращает новые операции.

### Фаза 3. Снимок источника

Executor читает текущий catalog entry.
Он фиксирует generation, locator, length и digest.
Он подтверждает immutable version.
Если источник mutable, adapter обязан обеспечить versioned read.
Без этой возможности автоматический перенос запрещён.

### Фаза 4. Создание цели

Цель получает уникальный migration locator.
Она не становится active до завершения проверки.
Повтор команды создания идемпотентен по migration ID.
Наличие объекта с другим digest считается конфликтом.
Существующая совпадающая цель может быть повторно проверена.

### Фаза 5. Копирование

Копирование выполняется с ограничением bandwidth и concurrency.
Progress сохраняется в migration journal.
Restart не обязан начинать весь большой объект заново, если adapter безопасно поддерживает resume.
Resume подтверждает границы и checksum уже записанных блоков.
Неподтверждённый частичный объект никогда не публикуется.

### Фаза 6. Проверка

Проверяются длина, версия и digest содержимого.
Проверяется требуемая durability policy цели.
Подтверждается завершение записи по семантике adapter.
Необходимо различать успешный transport write и выполненную policy.
Health gate может задержать дальнейшее переключение.

### Фаза 7. Переключение

Catalog CAS меняет активное местоположение.
Операция указывает expected generation и fencing token.
После CAS новые readers получают новый locator.
Старые readers сохраняют доступ к прежнему locator.
Обе копии временно существуют одновременно.

### Фаза 8. Период удержания

Источник помечается retirement candidate.
Начинается минимальный delayed-delete interval.
Система ожидает завершения readers старой generation.
Она учитывает максимальный срок location snapshot.
Одного истечения таймера недостаточно при живых читателях.

### Фаза 9. Финальная проверка

Garbage collector заново читает catalog.
Он убеждается, что источник не является active location.
Он проверяет generation и migration ID.
Он проверяет отсутствие reader leases и действующих pins.
Он проверяет retention/legal hold contract, если он включён.

### Фаза 10. Удаление старой копии

Удаляется только конкретный old locator.
Удаление идемпотентно.
Ответ «уже отсутствует» завершает GC успешно.
Ошибка удаления оставляет debt и повтор с backoff.
Ошибка не откатывает автоматически успешный CAS.

### Фаза 11. Reconciliation

Journal сверяется с catalog и физическими objects.
Лишние временные копии классифицируются.
Неизвестные объекты не удаляются без доказательства принадлежности.
Все операции имеют audit trail.
Capacity reservation освобождается после подтверждённого завершения.

## Состояния migration

| Состояние | Active location | Разрешённое продолжение |
| --- | --- | --- |
| planned | Источник | Получить lease |
| leased | Источник | Снимок версии |
| copying | Источник | Завершить или отменить |
| verifying | Источник | Подтвердить цель |
| verified | Источник | Catalog CAS |
| switched | Цель | Drain readers |
| retiring | Цель | Ждать barriers |
| deleting | Цель | Удалить источник |
| completed | Цель | Освободить reservation |
| failed_pre_switch | Источник | Удалить временную цель |
| failed_post_switch | Цель | Восстановить GC |
| paused | По journal | Перепроверить lease и state |

Нельзя переходить `copying → deleting`.
Нельзя удалять источник при `verified`, но без CAS.
Нельзя по локальной памяти считать CAS успешным после timeout.
Необходимо перечитать catalog и определить outcome.

## Активные читатели

Reader фиксирует catalog generation.
Reader lease имеет ограниченный срок и продление.
Завершение чтения освобождает lease.
Потеря reader процесса освобождается через expiry.
GC учитывает uncertainty window после expiry.

Если adapter не поддерживает регистрацию каждого reader, нужен иной доказуемый барьер.
Например, bounded locator cache TTL плюс максимальная длительность чтения.
Такой режим требует запрета неограниченных прямых чтений старого locator.
Без верхней границы безопасное автоматическое удаление невозможно.
В этом случае old copy сохраняется до ручной проверки либо отдельного tracking решения.

### Медленный клиент

Клиент может читать дольше nominal transfer time.
Lease продлевается независимо от скорости выдачи.
Длительность ограничивается server policy.
Перед принудительным завершением фиксируется причина timeout.
GC не прерывает живого читателя незаметно.

### Повтор чтения после switch

Новый запрос получает актуальный location.
Продолжение старого запроса может использовать старый handle.
HTTP retry клиента создаёт новый logical request.
Если клиент указывает version, версия сохраняется.
Если client alias сменился, применяется version resolution policy.

## Pinning

Pin защищает выбранную identity либо placement class.
Он имеет owner, reason, scope и expiry.
Object pin не распространяется автоматически на все будущие версии.
Namespace pin применяется только при явной конфигурации.

| Тип pin | Эффект |
| --- | --- |
| stay_hot | Запрещает demotion из SSD |
| stay_cold | Запрещает автоматический promotion |
| no_migration | Запрещает смену location |
| retain_source | Запрещает GC старой копии |
| warm_until | Даёт временный admission priority |

Конфликт pin разрешается по документированному приоритету.
Retention запрет удаления сильнее желания освободить capacity.
Истечение pin делает объект кандидатом, а не немедленно удаляет его.
Сумма закреплённых bytes входит в capacity planning.

## Versioning и удаление пользовательского объекта

Удаление alias не всегда означает удаление всех версий.
Version retention policy принадлежит владельцу данных.
Placement controller не сокращает её ради экономии.
Tombstone распространяется в access plane до освобождения copies.

Старые версии могут оставаться в cold pool.
Они учитываются как retained bytes.
Экономический отчёт не исключает их без пояснения.
Delete marker и физическое освобождение bytes — разные события.

## RGW lifecycle

RGW lifecycle может выполнять transitions между storage classes.
Такая transition не является мгновенным RPC переноса.
Controller должен наблюдать фактическое завершение.
Обновление policy не доказывает завершение миграции.

Сама popularity model находится в SLOYRA.
Нельзя предполагать встроенное ранжирование RGW по SLOYRA events.
Adapter переводит решение в разрешённый механизм lifecycle либо managed copy.
Поддержка фильтров, tags и versioned transitions проверяется для конкретного релиза.

Для больших групп cold objects допустима политика возрастного перехода.
Популярные исключения должны быть выражены поддерживаемым механизмом.
Если нельзя безопасно исключить hot object, такой режим не включают автоматически.
Неподдержанная policy возвращает capability error.

### Managed copy поверх RGW

Это отдельный проектный режим.
Он должен сохранять metadata, content type и нужные атрибуты доступа.
Он должен корректно обрабатывать version IDs.
Он не должен подменять S3 ETag универсальной checksum.
Для multipart и шифрованных объектов это особенно важно.

Проверка использует доверенный end-to-end digest либо adapter validation contract.
Copy outcome проверяется до обновления mapping.
Удаление старого объекта подчиняется RGW versioning и retention.
Нельзя считать логический delete немедленным освобождением физической ёмкости.

## Проверка capabilities

```yaml
placement:
  adapter: rados_managed
  capabilities_required:
    immutable_version_read: true
    conditional_catalog_switch: true
    content_integrity_verification: true
    reader_retirement_barrier: true
    idempotent_target_creation: true
    idempotent_source_delete: true
    retention_check: true
```

Отсутствующая capability блокирует apply mode.
Observe mode может продолжать собирать статистику.
Capability report сохраняется при каждом обновлении adapter.
Версия библиотеки и версия Ceph входят в report.

## Телеметрия для backend

Backend получает period aggregates и quality markers.
Период по умолчанию в примерах равен одной минуте.
Агрегат включает tenant, identity и source scope.
Один object family не объединяет разные версии для удаления.

```json
{
  "schema_version": 1,
  "window_start": "2026-09-27T10:00:00Z",
  "window_seconds": 60,
  "tenant_id": "demo-media",
  "content_version": "v17",
  "range_start": 0,
  "range_length": 4194304,
  "logical_request_count": 120,
  "requested_bytes": 503316480,
  "served_bytes": 503316480,
  "cache_hit_bytes": 499122176,
  "fill_count": 1,
  "origin_attempt_count": 1,
  "origin_bytes": 4194304,
  "confidence": 1.0,
  "window_revision": 1
}
```

В примере 120 запросов по 4 MiB.
Из origin пришёл только один кусок 4 MiB.
119 последующих выдач обслужены локальным кешем.
Backend видит высокий спрос и низкий текущий physical pressure.

## Backpressure migration queue

Promotion и demotion имеют раздельные очереди.
Пользовательские чтения имеют больший приоритет.
Ceph recovery и backfill должны оставаться учтёнными в health gate.
Controller не пытается «исправить» перегруженный кластер массовыми переносами.

| Сигнал | Действие controller |
| --- | --- |
| Рост origin latency | Снизить migration bandwidth |
| Недостаток hot capacity | Остановить promotion reservation |
| Недостаток cold capacity | Остановить demotion |
| Health policy breach | Пауза новых migrations |
| Telemetry gap | Заморозить demotion по отсутствию спроса |
| Catalog unavailable | Запрет CAS и delete |
| Lease backend unavailable | Не брать новых owners |
| GC debt выше лимита | Снизить создание duplicate copies |

Пауза не делает частичную цель active.
Уже переключённые объекты остаются на цели.
GC может быть приостановлен отдельно от чтения.
Состояние паузы видимо в monitoring.

## Резерв свободного места

Перенос временно требует двух копий.
Reservation учитывает полную физическую стоимость цели.
Для replicated pool это логический размер, умноженный на replica count.
Накладные расходы metadata и allocator учитываются дополнительным запасом.

```text
migration_reserve_raw >=
  max_inflight_logical_bytes × target_replication_factor
  + metadata_margin
```

Переход между классами не уменьшает физические bytes до GC.
В период overlapping copies bytes увеличиваются.
Capacity report показывает этот рост отдельно.
GC backlog не прячется в «неизвестное использование».

## Проектная конфигурация

```yaml
placement:
  mode: observe
  adapter: rados_managed
  classes:
    hot:
      pool: sloyra-hot
      expected_device_class: ssd
      durability_policy: replicated-3-host
    cold:
      pool: sloyra-cold
      expected_device_class: hdd
      durability_policy: replicated-3-host
  migration:
    max_concurrent_objects: 4
    max_bytes_per_second: 67108864
    max_inflight_logical_bytes: 8GiB
    lease_duration: 60s
    lease_renew_interval: 15s
    require_fencing: true
    verify: full_digest
  retirement:
    minimum_delay: 24h
    require_no_active_readers: true
    require_catalog_recheck: true
    require_retention_check: true
  health:
    pause_on_catalog_error: true
    pause_on_durability_mismatch: true
    freeze_demotions_on_telemetry_gap: true
telemetry:
  placement_window: 1m
  max_snapshot_age: 5m
```

Это проектный `sloyra.yaml`.
Названия pools должны соответствовать подготовленному Ceph deployment.
Сам файл не создаёт физические устройства или CRUSH topology.

## Доступы

Data reader имеет право читать только нужные namespaces.
Migration executor читает source и пишет target.
GC role получает отдельное право удаления.
Catalog writer не использует административный ключ Ceph.
Все credentials передаются через secret provider.

Tenant isolation проверяется и в mapping, и в adapter credentials.
Общий worker не должен принимать произвольный pool из запроса.
Catalog admin endpoints не доступны через публичный content route.
Audit содержит actor и operation ID.

## Восстановление после сбоя

После рестарта executor читает journal.
Он проверяет lease и fencing token.
Он перечитывает catalog.
Затем определяет последнюю подтверждённую фазу.
Локальный кэш статуса не считается источником истины.

Если catalog указывает source, target не выдаётся автоматически.
Если catalog указывает target, source не считается active.
Если catalog generation изменился неожиданно, операция идёт на reconciliation.
Удаление без уверенного определения active location запрещено.

## Наблюдаемость

Каждый migration имеет trace ID.
Отдельно измеряются queue wait, copy, verify, switch и retire delay.
Показываются bytes в planned, copying, duplicate и GC debt состояниях.
Счётчик failed migrations разделяется по фазам.

```text
sloyra_placement_planned_bytes
sloyra_placement_copy_bytes_total
sloyra_placement_verify_failures_total
sloyra_placement_catalog_conflicts_total
sloyra_placement_retired_bytes
sloyra_placement_gc_debt_bytes
sloyra_placement_oldest_pending_seconds
```

Это проектные имена метрик.
Object ID не добавляется как label каждой time series.
Подробная идентичность доступна через audit search.

## Приёмочные сценарии отказов

Все сценарии требуют сохранения доступной подтверждённой версии.
Источник удаляется только после полного протокола retirement.
Поле `delete_source` обозначает допустимость немедленного удаления в данном состоянии.

### M01. Сбой до начала копирования

```yaml
case: before_copy
migration_state: leased
active_location: source
target_state: none
expected_action: resume_after_lease_check
delete_source: false
require_audit: true
```

Reservation освобождается либо подтверждается заново.

### M02. Обрыв посередине копирования

```yaml
case: partial_copy
migration_state: copying
active_location: source
target_state: partial
expected_action: resume_or_discard_target
delete_source: false
require_audit: true
```

Partial target остаётся вне каталога.

### M03. Заполнение целевого pool

```yaml
case: target_full
migration_state: copying
active_location: source
target_state: partial
expected_action: pause_and_release_unused_reserve
delete_source: false
require_audit: true
```

Исходная версия доступна клиентам.

### M04. Несовпадение digest

```yaml
case: digest_mismatch
migration_state: verifying
active_location: source
target_state: invalid
expected_action: quarantine_target
delete_source: false
require_audit: true
```

Переключение не выполняется.

### M05. Источник вернул иную версию

```yaml
case: wrong_version
migration_state: copying
active_location: source
target_state: invalid
expected_action: abort_identity_mismatch
delete_source: false
require_audit: true
```

Байты разных версий не склеиваются.

### M06. Timeout до подтверждения CAS

```yaml
case: catalog_timeout_before
migration_state: verified
active_location: unknown
target_state: verified
expected_action: read_catalog_outcome
delete_source: false
require_audit: true
```

Повторный blind write запрещён.

### M07. Конкурент обновил generation

```yaml
case: cas_conflict
migration_state: verified
active_location: current_catalog
target_state: verified
expected_action: reconcile_generation
delete_source: false
require_audit: true
```

Победитель определяется каталогом.

### M08. Потеря lease перед switch

```yaml
case: lease_lost
migration_state: verified
active_location: source
target_state: verified
expected_action: stop_fenced_writer
delete_source: false
require_audit: true
```

Старый token не меняет location.

### M09. Сбой сразу после успешного CAS

```yaml
case: post_switch_crash
migration_state: switched
active_location: target
target_state: verified
expected_action: resume_retirement
delete_source: false
require_audit: true
```

Чтение идёт из target после проверки catalog.

### M10. Старый reader ещё читает

```yaml
case: reader_active
migration_state: retiring
active_location: target
target_state: verified
expected_action: wait_reader_barrier
delete_source: false
require_audit: true
```

Delayed delete timer не заменяет reader drain.

### M11. Не истёк TTL старого locator

```yaml
case: locator_cache
migration_state: retiring
active_location: target
target_state: verified
expected_action: wait_locator_expiry
delete_source: false
require_audit: true
```

Новые на узле запросы могут знать прежний locator.

### M12. Pin удержания source добавлен после switch

```yaml
case: pin_added
migration_state: retiring
active_location: target
target_state: verified
expected_action: hold_gc
delete_source: false
require_audit: true
```

Pin проверяется повторно перед delete.

### M13. Retention запрещает физическое удаление

```yaml
case: retention_hold
migration_state: retiring
active_location: target
target_state: verified
expected_action: hold_until_retention_release
delete_source: false
require_audit: true
```

Экономия bytes пока не признана.

### M14. Удаление source вернуло transient error

```yaml
case: source_delete_error
migration_state: deleting
active_location: target
target_state: verified
expected_action: retry_gc_with_backoff
delete_source: false
require_audit: true
```

Active location не откатывается автоматически.

### M15. Source удалён предыдущей попыткой

```yaml
case: source_already_absent
migration_state: deleting
active_location: target
target_state: verified
expected_action: complete_idempotently
delete_source: true
require_audit: true
```

Already absent считается подтверждённым результатом GC.

### M16. Целевой класс нарушил health gate

```yaml
case: target_health_bad
migration_state: verified
active_location: source
target_state: verified
expected_action: pause_before_switch
delete_source: false
require_audit: true
```

Факт копирования ещё не разрешает публикацию.

### M17. Потеря popularity events

```yaml
case: telemetry_gap
migration_state: planned
active_location: source
target_state: none
expected_action: freeze_demotion
delete_source: false
require_audit: true
```

Отсутствие событий не считается охлаждением.

### M18. Найдена цель без завершённого journal

```yaml
case: orphan_target
migration_state: unknown
active_location: catalog_lookup
target_state: unknown
expected_action: verify_ownership_then_reconcile
delete_source: false
require_audit: true
```

Неизвестные objects не удаляются по шаблону имени.

### M19. Старый controller возобновился после failover

```yaml
case: old_controller
migration_state: verified
active_location: current_catalog
target_state: verified
expected_action: reject_stale_fencing_token
delete_source: false
require_audit: true
```

Новый owner сохраняет исключительность.

### M20. Все retirement barriers выполнены

```yaml
case: clean_retirement
migration_state: retiring
active_location: target
target_state: verified
expected_action: delete_exact_old_locator
delete_source: true
require_audit: true
```

Catalog, readers, pins и retention проверены заново.

## Пошаговый пример переноса

Исходный объект имеет identity `demo/video/v17/chunk-4`.
Размер составляет 4 MiB.
Catalog generation равна 42.
Active location находится в cold pool.
Target pool имеет ту же durability policy.

```yaml
plan:
  migration_id: mig-demo-0001
  expected_generation: 42
  source_class: cold
  target_class: hot
  object_length: 4194304
  policy_revision: p12
  reason: sustained_backend_pressure
lease:
  owner: placement-02
  fencing_token: 108
copy:
  source_bytes: 4194304
  target_bytes: 4194304
verify:
  length_match: true
  digest_match: true
  durability_policy_match: true
switch:
  expected_generation: 42
  new_generation: 43
  outcome: committed
retirement:
  old_readers: 0
  locator_grace_complete: true
  retention_allows_delete: true
  pin_allows_delete: true
  catalog_still_points_to_target: true
gc:
  source_deleted: true
  outcome: complete
```

При трёх репликах source занимал 12 MiB raw payload.
До удаления source одновременно существуют ещё 12 MiB target payload.
Пиковая сумма payload двух locations равна 24 MiB.
После GC остаётся 12 MiB, теперь в SSD классе.
Metadata и allocator overhead в этом примере не включены.
Перенос не уменьшил общий объём уникальных данных.

## Ограничения v0.1

- Нет автоматического изменения CRUSH topology.
- Нет автоматического снижения числа реплик.
- Нет прямого изменения внутренних объектов RGW.
- Нет удаления source только по факту успешной копии.
- Нет предположения о мгновенности lifecycle transitions.
- Нет универсального равенства ETag и digest содержимого.
- Нет применения устаревшего score без quality проверки.
- Нет обещания уменьшить total bytes одним переносом SSD → HDD.

## Источники

- [Ceph CRUSH maps, Squid](https://docs.ceph.com/en/squid/rados/operations/crush-map/) — CRUSH, device classes и правила pools.
- [Ceph pools](https://docs.ceph.com/en/latest/rados/operations/pools/) — связь pool policy и размещения.
- [RGW placement and storage classes, Squid](https://docs.ceph.com/en/squid/radosgw/placement/) — storage classes и lifecycle transitions.
- [Librados C API, Squid](https://docs.ceph.com/en/squid/rados/api/librados/) — границы объектных операций RADOS.

Первоисточники проверены 27 сентября 2026 года.
Ссылки Squid фиксируют документационную ветку, а не заявляют поддержку продукта.
Поддержка конкретного deployment определяется capability tests.
Catalog, controller, wire schemas и state machines являются проектом SLOYRA.

---

# Ёмкость, стоимость и проверка экономического эффекта

> Статус: проектная спецификация v0.1.

## Как читать заявление «до 70%»

В постановке задачи владелец продукта заявил экономию дискового пространства до 70%.
Это исходное продуктовое утверждение.
Оно не является результатом независимого измерения в этом документе.
Для публикации результата нужен воспроизводимый отчёт с исходной базой сравнения.

SLOYRA может уменьшать долю данных на дорогих SSD.
Он может уменьшать избыточные локальные копии кеша.
Он может снижать нагрузку на origin.
Эти эффекты измеряются отдельно.
Перенос байтов с SSD на HDD сам по себе не удаляет байты.
При неизменной replication policy общий origin payload остаётся тем же.

Фраза «70% экономии» должна сопровождаться названием показателя.
Допустимы «70% занятой SSD ёмкости» или «70% расходов по указанной модели».
«70% общего дискового пространства» требует отдельного доказательства сокращения копий.
Нельзя подменять один показатель другим в отчёте.

## Единицы

`1 KiB = 1024 bytes`.
`1 MiB = 1024 KiB`.
`1 GiB = 1024 MiB`.
`1 TiB = 1024 GiB`.
`1 TB = 10^12 bytes`.

Цена поставщика может использовать TB.
Внутренние метрики могут использовать bytes.
Отчёт обязан явно указать преобразование.
В таблицах этого документа используется TiB.
Условные цены не являются предложением поставщика.

## Четыре уровня ёмкости

### Уникальные логические данные

Это сумма размеров уникальных retained versions.
Две разные версии файла считаются разными данными.
Два tenant могут хранить одинаковые байты раздельно по политике.
Межтенантная дедупликация не предполагается.

### Origin raw payload

Это физический payload после replication либо erasure coding.
Для трёх реплик размер payload приблизительно равен `3 × logical bytes`.
Metadata, allocation rounding и служебные структуры считаются дополнительно.
Не следует выдавать payload estimate за точный `ceph df` результат.

### Локальные кеши

Это RAM и дисковые копии на edge узлах.
Они могут дублировать одни и те же origin bytes.
SSD/HDD cache bytes входят в общий дисковый footprint.
RAM bytes показываются отдельной строкой.
Копия на каждом edge не является репликой durability origin.

### Выделенная инфраструктура

Закупленная или арендованная ёмкость больше занятой.
Нужны запас свободного места, recovery reserve и рост.
Освобождение bytes не всегда сразу уменьшает счёт поставщика.
Экономия occupied capacity и cash cost различается.

## Обозначения

| Символ | Значение |
| --- | --- |
| U | Уникальный logical retained payload |
| r | Коэффициент replication |
| f | Доля U в hot SSD class |
| 1-f | Доля U в cold HDD class |
| C_s | Занятые SSD bytes origin |
| C_h | Занятые HDD bytes origin |
| E_s | Локальные SSD cache bytes |
| E_h | Локальные HDD cache bytes |
| M | Metadata и allocation overhead |
| D | Временные duplicate migration bytes |
| p_s | Условная цена SSD за TiB за период |
| p_h | Условная цена HDD за TiB за период |
| q | Отношение p_h / p_s |
| h | Требуемая доля свободной ёмкости |

Во всех сравнениях r и durability policy сохраняются.
Изменение failure domain также не считается бесплатной оптимизацией.
Для смешанных размеров объекты суммируются по bytes, а не по count.
Доля 30% файлов не обязательно означает 30% объёма.

## Базовые формулы

```text
C_s = U × r × f
C_h = U × r × (1 - f)
C_origin = C_s + C_h = U × r
```

```text
C_disk_total = C_s + C_h + E_s + E_h + M + D
```

```text
ssd_reduction = 1 - C_s_after / C_s_before
```

```text
total_reduction = 1 - C_disk_total_after / C_disk_total_before
```

При нулевой baseline величине reduction не определена.
Отчёт показывает `not_applicable`.
Отрицательная экономия означает увеличение расхода.
Она не округляется до нуля.

## Пример 1. На 70% меньше занятой SSD ёмкости

Исходные уникальные данные составляют 100 TiB.
Replication factor равен 3 до и после изменения.
Изначально весь origin находится на SSD.
Origin payload занимает 300 TiB SSD.

После классификации hot set составляет 30 TiB logical.
Cold set составляет 70 TiB logical.
Hot payload занимает 90 TiB SSD.
Cold payload занимает 210 TiB HDD.

```text
SSD reduction = (300 - 90) / 300
SSD reduction = 0.70 = 70%
```

```text
Total origin payload before = 300 TiB
Total origin payload after = 90 + 210 = 300 TiB
Total origin payload reduction = 0%
```

Это корректный пример 70% уменьшения занятой SSD ёмкости.
Он не подтверждает уменьшение общего объёма данных на 70%.
Временные migration copies могут повышать пиковый footprint.

## Пример 2. Стоимость при том же размещении

Пусть SSD стоит 100 условных единиц за TiB за период.
Пусть HDD стоит 20 условных единиц за TiB за период.
Это иллюстративные допущения, не рыночные цены.
Репликация и 100 TiB logical остаются прежними.

```text
Before = 300 × 100 = 30 000
After = 90 × 100 + 210 × 20
After = 9 000 + 4 200 = 13 200
Savings = 30 000 - 13 200 = 16 800
Savings rate = 16 800 / 30 000 = 56%
```

Сокращение SSD bytes на 70% здесь даёт 56% экономии capacity cost.
CPU, RAM, сеть и сопровождение ещё не учтены.
Тарифы и оплачиваемый резерв могут изменить итог.

## Пример 3. Когда модель стоимости даёт 70%

При `q = 0,2` для 70% capacity cost reduction нужен меньший hot set.
Выберем `f = 0,125`.
То есть 12,5% logical bytes остаются на SSD.
87,5% переходят на HDD.

```text
After / Before = f + q × (1 - f)
After / Before = 0.125 + 0.2 × 0.875
After / Before = 0.125 + 0.175 = 0.30
Cost reduction = 1 - 0.30 = 70%
```

Для U=100 TiB и r=3 получаем 37,5 TiB SSD и 262,5 TiB HDD.
При условных ценах 100 и 20 стоимость равна 9 000.
Baseline стоимость равна 30 000.
Общий origin payload по-прежнему равен 300 TiB.
Удовлетворение latency SLO при таком hot set нужно проверить измерением.

## Пример 4. Настоящее сокращение суммарных дисковых копий

Рассмотрим гипотетическую сеть из 24 edge узлов.
Origin содержит 100 TiB logical с тремя репликами.
Origin payload составляет 300 TiB.
До оптимизации каждый edge хранит 30 TiB локального дискового кеша.
Суммарный local cache payload равен `24 × 30 = 720 TiB`.

```text
Before = 300 + 720 = 1 020 TiB
```

Допустим, измеренный рабочий набор позволяет оставить 0,25 TiB на каждом edge.
Суммарный local cache payload становится `24 × 0,25 = 6 TiB`.
Origin остаётся 300 TiB с прежними тремя репликами.

```text
After = 300 + 6 = 306 TiB
Reduction = (1 020 - 306) / 1 020
Reduction = 714 / 1 020 = 70%
```

Здесь уменьшается именно общий дисковый payload.
Причина — удаление избыточных кешированных копий.
Реплики origin не сокращаются.
Пример не доказывает достижимость такого рабочего набора в реальной нагрузке.
Он показывает, какие исходные условия нужны для данного процента.
Сеть, latency и origin IOPS после сокращения кеша должны пройти испытание.

## Пример 5. Почему baseline важен

Пусть origin занимает 300 TiB.
Локальные кеши занимают 30 TiB.
Даже полное удаление кешей освобождает лишь 30 TiB.

```text
Maximum cache-only reduction = 30 / (300 + 30)
Maximum cache-only reduction ≈ 9.09%
```

Заявить 70% общего пространства на такой базе нельзя.
Перенос origin на HDD не меняет этот предел по total bytes.
Сжатие либо дедупликация были бы другими механизмами.
Они не включаются в базовую модель без отдельной реализации и замера.

## Стоимость классов

```text
CapacityCost = C_s × p_s + C_h × p_h
```

Если baseline целиком SSD:

```text
CostRatio = f + q × (1 - f)
CostSaving = (1 - f) × (1 - q)
```

Доля SSD bytes и отношение цен определяют модельный эффект.
При `q = 1` перенос не даёт capacity cost savings.
При `f = 1` все данные остаются на SSD.
При `f = 0` модельная экономия равна `1-q`.
Полностью холодный класс может не удовлетворять производительности.

## Полная стоимость владения

Capacity cost является одной частью TCO.
Нужно учитывать стоимость управляющих узлов.
Нужно учитывать RAM кеша.
Нужно учитывать дополнительную CPU работу проверки и политики.
Нужно учитывать migration network traffic.
Нужно учитывать мониторинг, сопровождение и время инженеров.

```text
TCO =
  storage_capacity
  + compute
  + memory
  + network
  + software
  + operations
  + migration_overhead
```

```text
NetSavings = TCO_before - TCO_after
NetSavingsRate = NetSavings / TCO_before
```

Если после размещения требуется больше HDD для IOPS, эта ёмкость входит в TCO.
Если освобождённый SSD остаётся оплаченным, immediate cash savings может быть нулевой.
При этом avoided future purchase может оставаться положительным.
Оба эффекта отражаются раздельно.

## Требуемая выделенная ёмкость

Занятая ёмкость не должна достигать всей установленной.
Пусть h — доля свободного резерва.
Тогда required provisioned capacity равна occupied / (1-h).

```text
Provisioned = Occupied / (1 - h)
```

При occupied=300 TiB и h=0,2 требуется 375 TiB.
При occupied=90 TiB и h=0,2 требуется 112,5 TiB SSD.
При occupied=210 TiB и h=0,2 требуется 262,5 TiB HDD.
Суммарный provisioned payload budget остаётся 375 TiB.
Разделение по устройствам меняет цену, а не сумму этой модели.

Округление до физических дисков выполняется после расчёта.
Резерв для отказа хоста может требовать большего запаса.
Самое заполненное OSD важнее одного среднего значения кластера.
Allocation skew следует измерять на deployment.

## Временные копии при миграции

Во время copy-before-delete одновременно существуют две locations.
Extra bytes равны размеру переноса с коэффициентом redundancy цели.
При delayed delete overlap может сохраняться сутками.
Это входит в capacity forecast.

```text
PeakOriginPayload = StableOriginPayload + DuplicateMigrationPayload
```

Если одновременно копируются 8 GiB logical при r=3:
`DuplicateMigrationPayload = 24 GiB`.
Если GC задержал 10 TiB logical:
`GC debt raw payload = 30 TiB`.
Второе значение может существенно влиять на резерв.

GC debt не признаётся уже полученной экономией.
Экономия bytes фиксируется после физического освобождения.
Catalog switch завершает routing change, а не capacity reclamation.

## Время переноса

Минимальное время по клиентскому каналу считается как bytes / bandwidth.
Реальная длительность дополнительно зависит от Ceph, проверки и concurrency.
Репликационный трафик внутри кластера не следует подменять клиентским bandwidth.

```text
T_min = LogicalBytes / EffectiveLogicalCopyRate
```

При 64 MiB/s перенос 1 TiB требует минимум 16 384 секунды.
Это 4 часа 33 минуты 4 секунды.
Перенос 10 TiB требует минимум 163 840 секунд.
Это 45 часов 30 минут 40 секунд.
Проверка полным перечитыванием может добавить значительный I/O.

Нельзя обещать мгновенную экономию большого cold set.
План внедрения показывает время достижения steady state.
Дни миграции включаются в отчёт отдельной фазой.

## Износ SSD

Частые promotions увеличивают writes.
Score должен учитывать частоту повторного перемещения.
Запись временной копии, retry и revalidation считаются отдельно.
Пользовательская экономия capacity не отменяет write amplification.

```text
PolicyWriteAmplification =
  physical_cache_write_bytes / admitted_logical_bytes
```

Это policy-level показатель.
Он не заменяет внутренний flash write amplification устройства.
Показания устройства могут использовать другую единицу и границы учёта.
Оба ряда сохраняются с ясным названием.

## Полезность admission

Выгодно кешировать байты, которые будут прочитаны повторно.
Однократный scan большого архива может занять tier без выгоды.
Для таких данных pass-through экономит локальные writes.
Цена решения — возможный второй miss при повторном запросе.

```text
ReuseRatio = ReusedAdmittedBytes / AdmittedBytes
```

Отношение может превышать 1 при многократной выдаче.
Нулевой denominator даёт null.
Период ожидания повторного использования должен быть указан.
Слишком короткое окно недооценивает длинный хвост спроса.

## Граница бюджета

Модель ищет минимум стоимости при заданных SLO.
Она не ищет минимальную ёмкость любой ценой.
Ограничения включают p95/p99 latency, error rate и origin load.
Durability policy остаётся неизменной.

```text
minimize TCO
subject to:
  p99_latency <= target
  error_rate <= target
  origin_iops <= budget
  origin_bandwidth <= budget
  durability_policy = approved_policy
  tenant_isolation = preserved
```

Результат с нарушением SLO не считается успешной экономией.
Он остаётся точкой эксперимента.
Для production выбирается подтверждённая допустимая точка.

## Методика сравнительного испытания

### Шаг 1. Зафиксировать corpus

Сохранить список объектов и версий.
Сохранить logical size каждого объекта.
Сохранить retention rules.
Сохранить replication и failure domains.
Зафиксировать действующие pins.

### Шаг 2. Зафиксировать workload

Сохранить распределение запросов по времени.
Сохранить размеры Range.
Сохранить tenant mix.
Сохранить долю отменённых запросов.
Сохранить долю новых версий.

### Шаг 3. Выбрать baseline

Baseline должен представлять реально используемую конфигурацию.
Нельзя искусственно переполнить кеш ради большего процента улучшения.
Нельзя сравнивать cold start одной системы с прогретой другой.
Нельзя уменьшать durability только в опытной группе.

### Шаг 4. Провести warm-up

Прогреть систему по одинаковому сценарию.
Отдельно измерить время и bytes прогрева.
Начало steady state определить заранее.
Не удалять неудобные участки результата после просмотра данных.

### Шаг 5. Измерить steady state

Собирать latency распределения.
Собирать request и byte hit ratio.
Собирать origin attempts и bytes.
Собирать occupied и provisioned capacity.
Собирать migration и GC debt.

### Шаг 6. Проверить аварийный режим

Отключить один кеширующий узел в тестовой среде.
Проверить origin spike и recovery time.
Проверить Ceph placement при холодном локальном кеше.
Зафиксировать стоимость и качество восстановления.
Экономия, работающая только до первого restart, недостаточна.

### Шаг 7. Рассчитать эффект

Сначала сравнить bytes.
Затем применить одинаковую тарифную модель.
Затем добавить compute, memory и operations.
Затем проверить SLO constraints.
Вывести интервалы неопределённости и ограничения эксперимента.

## Обязательные поля отчёта

```yaml
measurement:
  report_id: demo-capacity-001
  status: illustrative_only
  product_revision: unimplemented-spec-v0.1
  duration_hours: 24
  warmup_hours: 2
  workload_trace_id: demo-trace
  corpus_revision: demo-corpus-v1
  logical_retained_tib: 100
  origin_redundancy:
    before: replicated-3-host
    after: replicated-3-host
  baseline:
    origin_ssd_tib: 300
    origin_hdd_tib: 0
    edge_disk_tib: 0
  treatment:
    origin_ssd_tib: 90
    origin_hdd_tib: 210
    edge_disk_tib: 0
  result:
    ssd_payload_reduction_percent: 70
    total_origin_payload_reduction_percent: 0
    measured_performance: null
    production_validated: false
```

Это пример структуры будущего отчёта.
Он не свидетельствует о проведённом тесте.
Реальный отчёт добавляет наблюдаемые показатели и сырые данные.

## Частые ошибки интерпретации

| Ошибка | Правильное представление |
| --- | --- |
| 70% объектов ушло на HDD | Считать долю bytes |
| 70% SSD освобождено | Отдельно считать total disk |
| Цена HDD в пять раз ниже | Учесть provisioned и IOPS |
| Origin misses упали | Проверить logical demand и retries |
| Средняя latency хорошая | Проверить tail latency |
| Каталог переключён | Дождаться physical GC |
| Удалены старые версии | Проверить retention baseline |
| Снижена репликация | Не приписывать эффект placement |
| Кеш в RAM | Учесть RAM cost отдельно |
| Нет metadata в формуле | Пометить payload estimate |

## Анализ чувствительности стоимости

В следующей таблице baseline находится целиком на SSD.
Replication и logical payload одинаковы до и после.
Числа показывают только capacity cost reduction.
Они рассчитаны по `(1-f) × (1-q)`.
Производительность для каждой точки требует отдельной проверки.

| Hot bytes на SSD | q=0,10 | q=0,20 | q=0,30 | q=0,40 | q=0,50 |
| --- | --- | --- | --- | --- | --- |
| 0% | 90.0% | 80.0% | 70.0% | 60.0% | 50.0% |
| 5% | 85.5% | 76.0% | 66.5% | 57.0% | 47.5% |
| 10% | 81.0% | 72.0% | 63.0% | 54.0% | 45.0% |
| 15% | 76.5% | 68.0% | 59.5% | 51.0% | 42.5% |
| 20% | 72.0% | 64.0% | 56.0% | 48.0% | 40.0% |
| 25% | 67.5% | 60.0% | 52.5% | 45.0% | 37.5% |
| 30% | 63.0% | 56.0% | 49.0% | 42.0% | 35.0% |
| 35% | 58.5% | 52.0% | 45.5% | 39.0% | 32.5% |
| 40% | 54.0% | 48.0% | 42.0% | 36.0% | 30.0% |
| 45% | 49.5% | 44.0% | 38.5% | 33.0% | 27.5% |
| 50% | 45.0% | 40.0% | 35.0% | 30.0% | 25.0% |
| 55% | 40.5% | 36.0% | 31.5% | 27.0% | 22.5% |
| 60% | 36.0% | 32.0% | 28.0% | 24.0% | 20.0% |
| 65% | 31.5% | 28.0% | 24.5% | 21.0% | 17.5% |
| 70% | 27.0% | 24.0% | 21.0% | 18.0% | 15.0% |
| 75% | 22.5% | 20.0% | 17.5% | 15.0% | 12.5% |
| 80% | 18.0% | 16.0% | 14.0% | 12.0% | 10.0% |
| 85% | 13.5% | 12.0% | 10.5% | 9.0% | 7.5% |
| 90% | 9.0% | 8.0% | 7.0% | 6.0% | 5.0% |
| 95% | 4.5% | 4.0% | 3.5% | 3.0% | 2.5% |
| 100% | 0.0% | 0.0% | 0.0% | 0.0% | 0.0% |

## Анализ чувствительности hot capacity

Здесь U=100 TiB и r=3.
Metadata, local cache и migration overlap исключены из таблицы.
Суммарный origin payload каждой строки равен 300 TiB.

| Hot fraction | SSD TiB | HDD TiB | SSD reduction | Total reduction |
| --- | --- | --- | --- | --- |
| 0% | 0.0 | 300.0 | 100% | 0% |
| 5% | 15.0 | 285.0 | 95% | 0% |
| 10% | 30.0 | 270.0 | 90% | 0% |
| 15% | 45.0 | 255.0 | 85% | 0% |
| 20% | 60.0 | 240.0 | 80% | 0% |
| 25% | 75.0 | 225.0 | 75% | 0% |
| 30% | 90.0 | 210.0 | 70% | 0% |
| 35% | 105.0 | 195.0 | 65% | 0% |
| 40% | 120.0 | 180.0 | 60% | 0% |
| 45% | 135.0 | 165.0 | 55% | 0% |
| 50% | 150.0 | 150.0 | 50% | 0% |
| 55% | 165.0 | 135.0 | 45% | 0% |
| 60% | 180.0 | 120.0 | 40% | 0% |
| 65% | 195.0 | 105.0 | 35% | 0% |
| 70% | 210.0 | 90.0 | 30% | 0% |
| 75% | 225.0 | 75.0 | 25% | 0% |
| 80% | 240.0 | 60.0 | 20% | 0% |
| 85% | 255.0 | 45.0 | 15% | 0% |
| 90% | 270.0 | 30.0 | 10% | 0% |
| 95% | 285.0 | 15.0 | 5% | 0% |
| 100% | 300.0 | 0.0 | 0% | 0% |

## Расчётные профили размещения

Каждый профиль использует неизменную тройную репликацию.
Цены выражены в условных единицах за TiB за период.
Стоимость baseline предполагает весь origin на SSD.
Это арифметические fixtures для реализации калькулятора.

### E01. small_catalog

```yaml
logical_tib: 10
replication_before: 3
replication_after: 3
hot_fraction: 0.3
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 9.000
  hdd_payload_tib: 21.000
  total_payload_tib: 30.000
  baseline_cost: 3000.00
  treatment_cost: 1320.00
  capacity_cost_saving_percent: 56.000
  total_payload_saving_percent: 0
```

### E02. medium_catalog

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0.3
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 90.000
  hdd_payload_tib: 210.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 13200.00
  capacity_cost_saving_percent: 56.000
  total_payload_saving_percent: 0
```

### E03. large_catalog

```yaml
logical_tib: 1000
replication_before: 3
replication_after: 3
hot_fraction: 0.3
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 900.000
  hdd_payload_tib: 2100.000
  total_payload_tib: 3000.000
  baseline_cost: 300000.00
  treatment_cost: 132000.00
  capacity_cost_saving_percent: 56.000
  total_payload_saving_percent: 0
```

### E04. mostly_hot

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0.8
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 240.000
  hdd_payload_tib: 60.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 25200.00
  capacity_cost_saving_percent: 16.000
  total_payload_saving_percent: 0
```

### E05. balanced

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0.5
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 150.000
  hdd_payload_tib: 150.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 18000.00
  capacity_cost_saving_percent: 40.000
  total_payload_saving_percent: 0
```

### E06. small_hot_set

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0.125
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 37.500
  hdd_payload_tib: 262.500
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 9000.00
  capacity_cost_saving_percent: 70.000
  total_payload_saving_percent: 0
```

### E07. price_close

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0.3
ssd_price: 100
hdd_price: 80
expected:
  ssd_payload_tib: 90.000
  hdd_payload_tib: 210.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 25800.00
  capacity_cost_saving_percent: 14.000
  total_payload_saving_percent: 0
```

### E08. price_equal

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0.3
ssd_price: 100
hdd_price: 100
expected:
  ssd_payload_tib: 90.000
  hdd_payload_tib: 210.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 30000.00
  capacity_cost_saving_percent: 0.000
  total_payload_saving_percent: 0
```

### E09. cheap_cold

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0.3
ssd_price: 100
hdd_price: 10
expected:
  ssd_payload_tib: 90.000
  hdd_payload_tib: 210.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 11100.00
  capacity_cost_saving_percent: 63.000
  total_payload_saving_percent: 0
```

### E10. all_hot

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 1
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 300.000
  hdd_payload_tib: 0.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 30000.00
  capacity_cost_saving_percent: 0.000
  total_payload_saving_percent: 0
```

### E11. all_cold_model

```yaml
logical_tib: 100
replication_before: 3
replication_after: 3
hot_fraction: 0
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 0.000
  hdd_payload_tib: 300.000
  total_payload_tib: 300.000
  baseline_cost: 30000.00
  treatment_cost: 6000.00
  capacity_cost_saving_percent: 80.000
  total_payload_saving_percent: 0
```

### E12. fractional_corpus

```yaml
logical_tib: 2.5
replication_before: 3
replication_after: 3
hot_fraction: 0.2
ssd_price: 100
hdd_price: 20
expected:
  ssd_payload_tib: 1.500
  hdd_payload_tib: 6.000
  total_payload_tib: 7.500
  baseline_cost: 750.00
  treatment_cost: 270.00
  capacity_cost_saving_percent: 64.000
  total_payload_saving_percent: 0
```

## Проверки отчёта перед использованием

Каждая проверка относится к конкретному источнику искажения результата.
Они нужны для честной публикации экономического эффекта.

### Q01. Состав данных

```yaml
check: corpus_equal
required: true
result: pending_measurement
```

Действие: Сравнить manifest версий и их размеры.
Признак искажения: Из treatment исчезли редко читаемые файлы.
Коррекция: Пересчитать результат на одинаковом corpus.

### Q02. Надёжность

```yaml
check: durability_equal
required: true
result: pending_measurement
```

Действие: Сравнить replication и failure domains.
Признак искажения: После перехода осталось меньше реплик.
Коррекция: Выделить изменение durability в отдельный эксперимент.

### Q03. Сроки хранения

```yaml
check: retention_equal
required: true
result: pending_measurement
```

Действие: Сравнить retention и число исторических версий.
Признак искажения: Удаление старых версий сочли эффектом tiering.
Коррекция: Вернуть общий retention baseline.

### Q04. Границы кеша

```yaml
check: cache_scope
required: true
result: pending_measurement
```

Действие: Перечислить все edge узлы и tiers.
Признак искажения: Часть HDD кешей не включена в treatment.
Коррекция: Добавить все физические дисковые копии.

### Q05. Временные копии

```yaml
check: migration_overlap
required: true
result: pending_measurement
```

Действие: Измерить GC debt на момент отчёта.
Признак искажения: Source copies ещё существуют.
Коррекция: Отложить вывод о bytes до завершения GC.

### Q06. Цены

```yaml
check: price_basis
required: true
result: pending_measurement
```

Действие: Сохранить одинаковые единицы и период.
Признак искажения: TB и TiB смешаны в расчёте.
Коррекция: Преобразовать bytes перед применением тарифа.

### Q07. Качество выдачи

```yaml
check: latency_slo
required: true
result: pending_measurement
```

Действие: Сравнить p95 и p99 по tenant.
Признак искажения: Среднее улучшилось при плохом p99.
Коррекция: Признать точку не прошедшей SLO.

### Q08. Нагрузка источника

```yaml
check: origin_budget
required: true
result: pending_measurement
```

Действие: Сравнить origin IOPS и bytes.
Признак искажения: Уменьшение кеша перегрузило backend.
Коррекция: Увеличить working set budget и повторить.

### Q09. Восстановление

```yaml
check: cold_restart
required: true
result: pending_measurement
```

Действие: Провести cold-cache scenario.
Признак искажения: Steady state хороший, restart даёт ошибки.
Коррекция: Включить recovery reserve в профиль.

### Q10. Реальная оплата

```yaml
check: paid_capacity
required: true
result: pending_measurement
```

Действие: Сопоставить occupied и provisioned.
Признак искажения: SSD освободились, но оплачиваются прежние узлы.
Коррекция: Разделить cash savings и deferred purchase.

### Q11. Накладные расходы

```yaml
check: metadata_cost
required: true
result: pending_measurement
```

Действие: Сопоставить payload модель и фактический df.
Признак искажения: Мелкие chunks дали большой metadata overhead.
Коррекция: Добавить overhead и повторить capacity model.

### Q12. Полнота данных

```yaml
check: sample_quality
required: true
result: pending_measurement
```

Действие: Проверить gap markers и окно наблюдения.
Признак искажения: Ночная нагрузка потеряна телеметрией.
Коррекция: Повторить измерение с полным trace.

## Итоговые правила публикации

Показывать baseline и treatment рядом.
Показывать logical, raw payload и provisioned capacity отдельно.
Показывать долю SSD, total bytes и cost отдельными метриками.
Указывать, какие расходы исключены из модели.
Сохранять replication, retention и failure domains в сравнении.
Не объявлять проектный расчёт измеренным результатом.
Не объявлять верхний наблюдённый эффект гарантией для любой нагрузки.
Атрибутировать исходное «до 70%» владельцу продукта до независимой проверки.

## Источники и исходные допущения

- [Ceph pools](https://docs.ceph.com/en/latest/rados/operations/pools/) — параметры pools и размещение реплик.
- [Ceph CRUSH maps, Squid](https://docs.ceph.com/en/squid/rados/operations/crush-map/) — failure domains и device classes.
- [RGW placement and storage classes](https://docs.ceph.com/en/squid/radosgw/placement/) — связь класса хранения с data pool.

Внешние технические факты проверены 27 сентября 2026 года.
Числа стоимости являются условными параметрами модели.
Рыночные цены и готовые бенчмарки здесь не заявляются.
Все проценты в примерах получены из приведённых формул.
Эффект до 70% остаётся заявлением владельца до воспроизводимого измерения.

---

# Платформы, сборки и развертывание

> Статус: проектная спецификация v0.1; сборки и совместимость требуют приемки.

## Назначение

Этот раздел описывает целевую среду SLOYRA.
Он задает требования к будущим пакетам и процедуре внедрения.
Наличие записи в таблице не подтверждает существование готовой сборки.
Проверенная совместимость появляется после выпуска протокола приемки.

SLOYRA проектируется как отдельный компонент для Nginx.
Основной модуль обрабатывает запросы внутри worker-процессов.
Локальный агент управляет состоянием и фоновой работой.
Lua-политики расширяют правила допуска и маршрутизации.
Контроллер размещения использует агрегированные сведения о спросе.

В этом репозитории документация не заменяет поставку серверных бинарных файлов.
Команды будущего CLI отмечены словом «проектная».
Имена пакетов и репозитории поставщика до публикации не определены.
Устанавливать случайные одноименные пакеты из сторонних источников нельзя.

## Уровни статуса

| Статус | Значение | Разрешенный вывод |
| --- | --- | --- |
| Целевая платформа | Включена в план реализации | Для нее проектируют пакет |
| Квалификация | Нужны отдельные испытания | Совместимость не подтверждена |
| Проверено в лаборатории | Есть воспроизводимый протокол | Подходит для описанного стенда |
| Принято для эксплуатации | Выполнены критерии конкретного релиза | Можно применять в согласованном профиле |
| Вне базового профиля | Не входит в начальную матрицу | Нужна отдельная инженерная оценка |
| Снято с поддержки | Версия исключена из матрицы выпуска | Требуется план обновления |

Статус относится к точному сочетанию компонентов.
Проверка дистрибутива сама по себе недостаточна.
В запись входят версия Nginx и параметры его сборки.
Также входят архитектура CPU и версия системных библиотек.
Для дискового кеша указывают файловую систему и параметры монтирования.
Для Ceph указывают клиентскую библиотеку и версию кластера.

## Целевая матрица ОС

| ОС | Архитектура | Целевой профиль | Статус SLOYRA |
| --- | --- | --- | --- |
| Ubuntu Server 22.04 LTS | x86_64 | Модуль, агент, кеш | Целевой; приемка обязательна |
| Ubuntu Server 24.04 LTS | x86_64 | Модуль, агент, кеш | Целевой; приемка обязательна |
| Debian 12 | x86_64 | Модуль, агент, кеш | Целевой; учитывать цикл поддержки ОС |
| Debian 13 | x86_64 | Модуль, агент, кеш | Целевой; приемка обязательна |
| Rocky Linux 9 | x86_64 | Модуль, агент, кеш | Целевой; приемка обязательна |
| AlmaLinux 9 | x86_64 | Модуль, агент, кеш | Целевой; приемка обязательна |
| Перечисленные Linux | arm64 | Модуль и агент | Отдельная квалификация |
| Ubuntu более новой LTS | x86_64 | Возможное расширение | Отдельная квалификация |
| Другие Linux с glibc | x86_64 | Возможная адаптация | Вне базовой матрицы |
| Alpine Linux с musl | x86_64 | Возможная отдельная сборка | Вне базовой матрицы |
| FreeBSD | amd64 | Исследовательский перенос | Вне базовой матрицы |
| Windows Server | x86_64 | Не определен | Вне базовой матрицы |
| macOS | arm64, x86_64 | Разработка документации и инструментов | Не серверный профиль |

Официальный жизненный цикл ОС и поддержка SLOYRA независимы.
Обновление ОС не означает автоматическую совместимость модуля.
Устаревшая ОС может оставаться технически работоспособной.
Это не отменяет требований к исправлениям безопасности.

Ubuntu описывает LTS и сроки обслуживания в собственной документации.
Для проекта выбраны 22.04 и 24.04 как отдельные целевые профили.
[Официальные выпуски Ubuntu](https://ubuntu.com/project/docs/release-team/list-of-releases/).

Debian публикует состояние stable, oldstable и LTS отдельно.
При подготовке релиза SLOYRA команда повторно сверяет эти сведения.
[Официальные выпуски Debian](https://www.debian.org/releases/).

Rocky Linux 9 использует профиль x86-64-v2 для x86-платформы.
Оператор должен проверять требования ОС к процессору.
Проект SLOYRA не снижает минимальные требования дистрибутива.
[Официальные выпуски Rocky Linux](https://docs.rockylinux.org/releases/).

Для AlmaLinux минорный выпуск следует оценивать в контексте текущей ветки.
Сохранение старого minor без обновлений не является базовым профилем.
[Официальные примечания AlmaLinux](https://wiki.almalinux.org/release-notes/).

## Архитектуры процессора

Основная архитектура начального проекта — x86_64.
Сборка должна избегать неописанных инструкций конкретного процессора.
Использование AVX или других расширений требует runtime-проверки.
Базовый модуль не должен завершаться с illegal instruction на допустимом CPU.

arm64 рассматривается как отдельный профиль.
Нужны испытания атомарных операций и выравнивания структур.
Нужны испытания shared-memory взаимодействия worker-процессов.
Нужны испытания поведения LuaJIT на выбранной платформе.
Нужны измерения стоимости хеширования и проверки подписей.
Нужна отдельная проверка формата дискового индекса.

Переносимые данные не должны зависеть от адресов указателей.
Размеры сериализованных полей задаются явно.
Порядок байтов для форматов хранения фиксируется спецификацией.
Путь arm64 не должен читать x86-специфичный дамп памяти как индекс.

## Целевой стек реализации

| Компонент | Язык или среда | Ответственность |
| --- | --- | --- |
| HTTP-модуль | C17 | Фазы Nginx, допуск, чтение кеша |
| Политики | Lua 5.1 API, LuaJIT при квалификации | Условия доступа и адаптеры |
| Локальный агент | Rust | WAL, выгрузка событий, управление состоянием |
| Контроллер размещения | Rust | Агрегация спроса и задания миграции |
| Конфигурация | YAML | Декларативное описание профиля |
| API управления | JSON поверх защищенного HTTP | Планирование и диагностика |
| Схемы событий | Версионированный JSON или эквивалентный wire-format | Совместимый обмен |
| Метрики | Формат совместимый с Prometheus | Наблюдаемость |

Выбор языка не является заявлением о завершенной реализации.
Реестр сборки фиксирует конкретный компилятор.
Rust toolchain должен быть закреплен для каждого релиза.
LuaJIT должен иметь закрепленную ревизию или пакетную версию.
Локальные изменения библиотек перечисляют в ведомости компонентов.

## Профиль ядра Linux

Базовый проект ориентирован на штатные поддерживаемые ядра выбранных ОС.
Универсальный минимум по одному номеру ядра не задается.
Дистрибутивы переносят исправления без смены базового номера.
Функциональные пробы важнее поверхностного сравнения версий.

| Возможность | Использование | Требование |
| --- | --- | --- |
| epoll | Событийный цикл Nginx | Базовый путь |
| sendfile | Отдача локальных объектов | Пригодность проверяется измерением |
| mmap | Опциональное отображение индекса | Нужны лимиты и обработка ошибок |
| fdatasync | Фиксация журнала | Проверяется crash-тестами |
| rename | Публикация готового файла | В пределах одной файловой системы |
| cgroup v2 | Ограничения ресурсов | Предпочтительный профиль при наличии |
| namespaces | Контейнерная изоляция | Для контейнерного профиля |
| io_uring | Возможная оптимизация | Не требуется для базового запуска |
| PSI | Диагностика давления | Опциональная телеметрия |
| O_DIRECT | Исследовательская оптимизация | Не включается автоматически |

Отсутствие io_uring не должно отключать кеширование.
Отключение sendfile не должно менять целостность ответа.
Поддержка fsync на выбранном носителе проверяется отдельно.
Барьер записи в ОС не заменяет гарантий контроллера хранения.

## Память

RAM-кеш получает явный бюджет.
Бюджет кеша не равен общему лимиту RSS процесса.
В RSS входят буферы соединений и служебные структуры.
В системной памяти также находится page cache файловой системы.
Контейнер может учитывать page cache в своем memory cgroup.

План емкости должен учитывать:

- данные горячего кеша;
- индекс объектов;
- таблицы сегментов;
- счетчики популярности;
- очередь событий;
- буферы чтения origin;
- TLS-состояние соединений;
- Lua VM каждого worker;
- память локального агента;
- запас для graceful reload.

При reload старые и новые worker могут некоторое время сосуществовать.
Это временно повышает потребление памяти.
Нагрузочный тест обязан включать reload на рабочей нагрузке.
Оценка только по состоянию покоя непригодна для приемки.

## Накопители

NVMe обозначает интерфейс доступа к устройству.
SSD обозначает тип носителя.
HDD обозначает магнитный диск.
Политика SLOYRA использует измеренную стоимость доступа и емкость.
Название уровня является понятной оператору меткой.

| Уровень | Целевой тип данных | Основной ограничитель |
| --- | --- | --- |
| RAM | Наиболее частые небольшие объекты и сегменты | Объем памяти |
| NVMe SSD | Горячие наборы с низкой допустимой задержкой | Запись, очередь, износ |
| SSD | Часто читаемые объекты с умеренной задержкой | Емкость и endurance |
| HDD | Более холодные последовательные данные | IOPS и время позиционирования |

Наличие NVMe не гарантирует одинаковую производительность устройств.
Объем свободного места не описывает реальный запас IOPS.
Параллельный backfill может менять поведение диска.
Тесты должны включать одновременную выдачу и запись.

Не следует размещать единственную origin-копию внутри каталога кеша.
Кеш должен оставаться восстанавливаемым.
Каталог WAL требует отдельного расчета надежности.
Потеря кеша и потеря очереди телеметрии имеют разные последствия.

## Файловые системы

| Файловая система | Целевой статус | Предмет проверки |
| --- | --- | --- |
| ext4 | Базовый кандидат | rename, fsync, inode, восстановление |
| XFS | Базовый кандидат | аллокация, quota, восстановление |
| Btrfs | Отдельная квалификация | CoW, фрагментация, снимки |
| ZFS | Отдельная квалификация | ARC, записи, взаимодействие кешей |
| tmpfs | Только отдельные временные данные | учет RAM и перезапуск |
| NFS | Не базовый путь локального кеша | задержки и семантика блокировок |
| CephFS | Отдельный интеграционный профиль | семантика клиента и изоляция |

Для локального кеша предпочтителен выделенный mount.
Разделение упрощает диагностику заполнения.
Проверка места должна учитывать свободные inode.
Маленькие сегменты способны исчерпать inode раньше байтов.

Параметры монтирования фиксируют в паспорте узла.
Переход на другую файловую систему требует повторной приемки.
Документ не предписывает форматирование существующих дисков.
Разметку и перенос данных выполняет процедура хранения организации.

## Nginx и динамический модуль

Проектный способ поставки — динамический модуль.
Модуль должен собираться для совместимого Nginx.
Флаг `--with-compat` относится к совместимости динамических модулей.
Он не дает универсальную гарантию для произвольной пары бинарных файлов.
[Официальная сборка Nginx](https://nginx.org/en/docs/configure.html).

В протоколе сборки сохраняют:

- полную версию Nginx;
- источник пакета;
- параметры configure;
- компилятор и его версию;
- архитектуру;
- версии OpenSSL и PCRE;
- идентификатор сборки модуля;
- результат загрузки модуля;
- результат `nginx -t`;
- результат HTTP smoke-теста.

Нельзя считать два пакета одной версии побитово совместимыми.
Дистрибутив может добавлять патчи.
Внешний поставщик может менять набор опций.
Сборка Nginx из исходников — отдельный профиль.

Модуль не должен блокировать event loop дисковой миграцией.
Фоновая работа передается агенту или специально выделенным исполнителям.
Сбой агента не должен приводить к сегментации worker.
Совместимость локального протокола проверяется до активации поколения.

## Вариант на Lua

Lua-путь полезен для быстрой реализации политик.
Он требует выбранного и проверенного способа интеграции Lua с Nginx.
Документ не предполагает, что любой пакет Nginx уже содержит Lua.
Наличие LuaJIT в системе не добавляет HTTP-хуки автоматически.

Для Lua-профиля фиксируют:

- поставщика HTTP-интеграции;
- версию Lua runtime;
- доступные фазы исполнения;
- правила загрузки модулей;
- лимиты CPU;
- лимиты памяти;
- обработку исключений;
- способ тестирования политик;
- ограничения на сетевой ввод-вывод.

Политика допуска не выполняет неограниченные циклы.
Политика не загружает код из запроса.
Политика не пишет секреты в диагностический вывод.
Результат политики представляется ограниченным набором решений.
Ошибка политики имеет отдельный reason code.

Native и Lua-профили должны совпадать по внешнему контракту.
Различие производительности измеряется отдельно.
Результаты одного профиля не распространяются автоматически на другой.

## Связь с Ceph

Модуль на edge-узле не должен иметь административный ключ Ceph.
Он передает события локальному агенту.
Контроллер получает агрегированный спрос и вычисляет предложения.
Исполнитель размещения применяет согласованные задания.

Ceph поддерживает device classes и CRUSH rules для выбора групп OSD.
Это позволяет создавать разные правила для SSD и HDD.
Само наличие классов не переносит прикладной объект по популярности.
Логику выбора и безопасной миграции необходимо реализовать отдельно.
[Официальные CRUSH maps](https://docs.ceph.com/en/latest/rados/operations/crush-map/).

Версии Ceph квалифицируются как отдельная матрица.
Выбор ветки должен учитывать актуальный цикл ее обслуживания.
Текущий перечень upstream не равен перечню принятых интеграций SLOYRA.
[Официальные релизы Ceph](https://docs.ceph.com/en/latest/releases/).

| Граница | Протокол приемки |
| --- | --- |
| SLOYRA → контроллер | Схема событий, повторы, версии |
| Контроллер → Ceph | Доступные API, права, timeout |
| Hot pool → cold pool | Копирование, checksum, commit |
| Cold pool → hot pool | Доступность исходной версии |
| Клиент → объект | Согласованность locator и generation |
| Ceph degraded | Пауза миграций и продолжение чтений |

S3, RADOS и CephFS являются разными интеграционными путями.
Нельзя описывать перенос pool как одинаковую операцию для всех трех.
Адаптер обязан объявлять свои возможности.
Матрица допускается на уровне adapter version.

## Варианты размещения

### Один edge-узел

Подходит для лаборатории и первого профиля.
Модуль и агент работают на одном сервере.
Конфигурация приходит из локального файла.
Телеметрия может выгружаться во внешний контроллер.
Потеря узла означает потерю его временного кеша.

### Несколько edge-узлов

Каждый узел имеет независимый локальный кеш.
Популярность агрегируется с учетом идентификатора узла.
Повторная доставка событий не должна увеличивать спрос.
Балансировщик должен учитывать готовность каждого узла.
Поэтапное обновление выполняется группами.

### Контейнер

Контейнерный образ фиксируется по digest.
Каталог кеша монтируется отдельно от образа.
WAL находится на устойчивом volume.
Пользователь контейнера не должен быть root без необходимости.
Лимиты памяти и файловых дескрипторов задаются явно.
Режим host network требует отдельной оценки.

### Виртуальная машина

Дисковый профиль описывает виртуальный и физический уровни.
Общий datastore может конкурировать с другими VM.
Переподписка CPU учитывается при измерении хвостовой задержки.
Снимок VM не заменяет согласованный backup конфигурации.
Перенос VM требует проверки локального идентификатора узла.

### Bare metal

Паспорт узла содержит модель диска и firmware.
Дисковые очереди измеряются при реальной конкурентной нагрузке.
NUMA-топология фиксируется для крупных серверов.
Профиль IRQ и CPU affinity не меняют без измерений.
Замена накопителя оформляется как изменение платформы.

## Проектная структура каталогов

Пути ниже описывают целевую поставку.
Пакет должен публиковать фактические пути в манифесте.

```text
/etc/sloyra/
  sloyra.yaml
  policies/
  trust/
  secrets.d/
/var/lib/sloyra/
  state/
  wal/
  checkpoints/
  placement/
/var/cache/sloyra/
  nvme/
  ssd/
  hdd/
/run/sloyra/
  agent.sock
  agent.pid
/var/log/sloyra/
  audit.jsonl
  agent.jsonl
```

Конфигурация доступна для чтения обслуживающему пользователю.
Каталог secrets.d имеет более строгие права.
WAL доступен только агенту.
Каталог кеша доступен worker и процессу управления кешем.
Общие права на запись для всех пользователей не допускаются.

## Права компонентов

| Ресурс | Nginx worker | Агент | Контроллер |
| --- | --- | --- | --- |
| Данные кеша | Чтение; ограниченная публикация по реализации | Управление по политике | Нет прямого доступа |
| Секрет проверки подписи | Только нужный материал | Загрузка и ротация | По отдельному контракту |
| WAL | Передача через IPC | Чтение и запись | Нет локального доступа |
| HTTP metrics | Нет управления | Публикация | Сбор при необходимости |
| Ceph credential | Нет | Обычно нет | Ограниченные возможности адаптера |
| Конфигурация | Чтение активного поколения | Проверка и подготовка | Создание желаемого состояния |

Разделение пользователей не отменяет проверки IPC.
Unix socket проверяет права и идентичность peer.
Размер сообщения ограничивается до выделения памяти.
Неизвестная версия сообщения отклоняется.

## Сетевая связность

| Направление | Назначение | Базовое правило |
| --- | --- | --- |
| Клиент → Nginx | Выдача контента | Существующий HTTP/TLS профиль |
| Nginx → origin | Получение отсутствующих данных | Только разрешенные origin |
| Nginx → агент | Локальная передача событий | Unix socket или выделенный loopback |
| Агент → collector | Телеметрия спроса | Аутентифицированный канал |
| Оператор → metrics | Сбор метрик | Сеть управления |
| Контроллер → Ceph | Размещение | Отдельная учетная запись |
| Агент → licensing | Онлайн-разрешение лицензии | В поставке 2.1.74: HTTPS, heartbeat 60 секунд, lease не более 300 секунд; HTTP допустим только на loopback |

Адреса endpoints задают в конфигурации среды.
Публичный metrics endpoint по умолчанию не проектируется.
Недоступность collector не должна блокировать клиентский ответ.
Очередь при этом обязана иметь предел.

## Реальные команды диагностики Linux

Следующие команды относятся к существующим системным инструментам.
Их доступность зависит от установленного набора пакетов.
Они не устанавливают SLOYRA.

```bash
uname -a
uname -m
cat /etc/os-release
getconf LONG_BIT
getconf PAGESIZE
```

```bash
nginx -v
nginx -V
nginx -t
```

`nginx -V` выводит версию и параметры сборки.
`nginx -t` проверяет конфигурацию и доступность используемых файлов.
Результат проверки необходимо читать целиком.
Успешный синтаксический тест не является нагрузочным тестом.
[Официальные параметры Nginx](https://nginx.org/en/docs/switches.html).

```bash
findmnt -T /var/cache
stat -f /var/cache
df -h /var/cache
df -i /var/cache
```

```bash
free -h
cat /proc/meminfo
cat /proc/pressure/memory
cat /proc/pressure/io
```

```bash
systemctl status nginx --no-pager
systemctl show nginx -p User -p Group -p LimitNOFILE
journalctl -u nginx --since '15 minutes ago' --no-pager
```

Вывод `nginx -T` может содержать чувствительную конфигурацию.
Поэтому его не следует автоматически вкладывать в публичный отчет.
Отчет сначала проходит редактирование секретов.
Путь к ключу сам по себе также может раскрывать устройство среды.

## Проектная подготовка пакета

Будущий выпуск должен содержать подписанный манифест.
Манифест перечисляет бинарные файлы и контрольные суммы.
Отдельно задается совместимый профиль Nginx.
Оператор получает документированный способ проверки подписи.
Доверенный публичный ключ поступает независимым согласованным каналом.

Пример структуры проектного манифеста:

```yaml
release:
  product: sloyra
  version: 0.1.0-example
  channel: qualification
  manifest_schema: 1
platform:
  os_family: debian
  os_release: '13'
  architecture: x86_64
nginx:
  version: exact-version-required
  build_fingerprint: sha256:example
artifacts:
  - role: nginx_module
    path: ngx_http_sloyra_module.so
    sha256: replace-with-release-digest
  - role: local_agent
    path: sloyra-agent
    sha256: replace-with-release-digest
compatibility:
  configuration_schema: 1
  telemetry_schema: 1
  local_protocol: 1
```

Значения example не проходят production-проверку.
Манифест не должен разрешать произвольную подмену digest.
Пакет без соответствующей записи совместимости не активируют.

## Проектный порядок установки

1. Зафиксировать паспорт сервера.
2. Снять исходные показатели Nginx.
3. Проверить запас ресурсов.
4. Определить профиль origin и Ceph.
5. Получить конкретный релиз SLOYRA.
6. Проверить подпись и контрольные суммы.
7. Сопоставить профиль Nginx с манифестом.
8. Подготовить отдельные каталоги состояния.
9. Подготовить конфигурацию в режиме наблюдения.
10. Проверить конфигурацию проектным валидатором.
11. Проверить Nginx после подключения модуля.
12. Запустить лабораторный smoke-тест.
13. Включить один canary-узел.
14. Сравнить результаты с исходным профилем.
15. Оформить допуск к постепенному расширению.

Ни один шаг не предполагает форматирование дисков.
Ни один шаг не требует удаления origin-копии.
Подключение к production выполняют по принятому окну изменения.

## Проектные команды SLOYRA

Интерфейс ниже является контрактом будущей реализации.
Эти бинарные файлы не подтверждены как доступные в репозитории.

```text
sloyra-agent config validate --file /etc/sloyra/sloyra.yaml
sloyra-agent doctor --config /etc/sloyra/sloyra.yaml
sloyra-agent status --format json
sloyra-agent capabilities --format json
```

Валидатор должен завершаться без изменения активной конфигурации.
Doctor должен быть читающей диагностикой.
Сетевые проверки doctor указываются в отчете.
Команда status не должна скрывать частичную деградацию.
Capabilities возвращает только реально доступные функции.

Проектные коды завершения:

| Код | Значение |
| --- | --- |
| 0 | Проверка пройдена |
| 2 | Ошибка аргументов или формата |
| 3 | Несовместимая схема |
| 4 | Недоступная зависимость |
| 5 | Несоответствие платформы |
| 6 | Нарушение обязательного инварианта |
| 7 | Недостаточно прав на диагностику |

## Пример подключения модуля

Фрагмент использует реальные директивы Nginx.
Обработка переменной SLOYRA_CONFIG является проектным контрактом модуля.
Фактическое имя библиотеки фиксируется поставкой.

```nginx
load_module /usr/lib/nginx/modules/ngx_http_sloyra_module.so;
env SLOYRA_CONFIG=/etc/sloyra/sloyra.yaml;
```

Bootstrap будущего модуля читает путь из разрешенного окружения.
Привязка маршрутов поступает из проверенного compiled snapshot.
Простая загрузка библиотеки еще не включает защиту всех маршрутов.

Отсутствующий модуль приводит к отказу конфигурационного теста.
Активацию нельзя продолжать после такого результата.
Контрольный запрос должен подтвердить фактическую привязку access gate.

## SELinux и AppArmor

Политики безопасности остаются включенными в базовом проекте.
Отказ доступа сначала диагностируют по audit-событию.
Права не расширяют до общего разрешения всех операций.
Пакет должен поставлять минимально необходимое описание доступа.

Для SELinux квалифицируют:

- чтение конфигурации;
- доступ к сокету агента;
- чтение кеша;
- публикацию нового сегмента;
- доступ к журналу;
- исходящее соединение с origin.

Для AppArmor квалифицируют те же ресурсы.
Профиль должен использовать реальные пути поставки.
Изменение каталога кеша требует обновления соответствующего разрешения.
Ошибка политики ОС должна иметь отдельный диагностический след.

## Системный сервис агента

Будущий systemd unit должен задавать пользователя.
Он должен задавать ограничение открытых файлов.
Он должен ограничивать доступные каталоги записи.
Он должен поддерживать корректное завершение с фиксацией WAL.
Он не должен маскировать непрерывный цикл аварийных перезапусков.

Пример требований к unit:

```ini
[Unit]
Description=SLOYRA local agent - proposed unit
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=sloyra
Group=sloyra
ExecStart=/usr/libexec/sloyra-agent run --config /etc/sloyra/sloyra.yaml
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/sloyra /var/cache/sloyra /run/sloyra
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
```

Это проектный пример, требующий адаптации поставщиком пакета.
Значение LimitNOFILE является примером профиля.
Права группы Nginx на socket задаются отдельно.
Фактический набор hardening-параметров проверяют на выбранной ОС.

## Квалификация нового профиля

Для новой платформы создают отдельную карточку.
Карточка содержит владельца и набор артефактов.
Тесты выполняют на реальном кандидате сборки.
Сборка с иными флагами не наследует результат автоматически.

```yaml
qualification:
  id: platform-example-001
  status: planned
  owner: platform-team
  os:
    family: ubuntu
    release: '24.04'
    kernel: record-exact-value
  cpu:
    architecture: x86_64
    model: record-exact-value
  nginx:
    version: record-exact-value
    configure_fingerprint: record-digest
  storage:
    filesystem: xfs
    mount_options: record-exact-value
    device_model: record-exact-value
  evidence:
    build_log: pending
    functional_report: pending
    load_report: pending
    recovery_report: pending
```

Карточка без evidence остается в статусе planned.
Обновление kernel security patch оценивают по политике релиза.
Изменение major Nginx требует полной проверки ABI.
Изменение формата индекса требует проверки отката.

## Передача в эксплуатацию

Оператор получает конфигурацию с описанием параметров.
Он получает паспорт совместимости.
Он получает контрольные суммы поставки.
Он получает порядок отката.
Он получает адреса внутренних dashboards.
Он получает критерии остановки миграций.
Он получает сведения о владельцах компонентов.
Он получает перечень известных ограничений.

Подтверждение оплаты запускает срок лицензии по условиям продукта.
Подписанный пакет продления применяется командой `license-update --bundle`;
порядок приведён в `docs/LICENSING.md` внутри персонального ZIP поставки.
В выпуске 2.1.74 учитываются одновременно действующие разрешения серверов:
heartbeat выполняется раз в 60 секунд, срок lease не превышает 300 секунд.
После потери связи место освобождается по истечении последнего разрешения;
вернувшийся сервер снова должен получить свободное место.
Новые запросы после истечения lease получают 503; уже начатые ответы
не прерываются. Мгновенное определение обрыва связи не обещается.
Установка не должна придумывать дополнительные коммерческие условия.

## Результат установки

Установка считается технически завершенной после проверки выдачи.
Должен успешно обслуживаться легитимный контрольный запрос.
Должен отклоняться заведомо недействительный запрос по правилам допуска.
Должна наблюдаться работа хотя бы одного уровня кеша.
Должна подтверждаться доставка контрольного события телеметрии.
Должен работать безопасный возврат к предыдущему профилю.

Акт установки не заменяет нагрузочную приемку.
Нагрузочная приемка не заменяет проверку восстановления.
Обе процедуры входят в готовность production-профиля.

---

# Эксплуатация, изменение конфигурации и восстановление

> Статус: проектная спецификация v0.1; сборки и совместимость требуют приемки.

## Эксплуатационная модель

SLOYRA разделяет выдачу контента и управление состоянием.
Worker Nginx отвечает за клиентский путь.
Локальный агент обслуживает журнал и фоновые очереди.
Контроллер размещения принимает решения по агрегированному спросу.
Ceph хранит постоянные объекты в согласованных классах размещения.

Каждая часть имеет собственное состояние готовности.
Зеленый статус процесса не означает готовность всей системы.
Доступный origin не означает исправность локального кеша.
Доставленные события не означают успешную миграцию объекта.

## Ответственность команд

| Роль | Ответственность |
| --- | --- |
| Владелец сервиса | Требования к доступу и поведению клиента |
| Edge-оператор | Nginx, модуль, кеш, сетевой путь |
| Platform-оператор | ОС, ресурсы, mounts, обновления |
| Storage-оператор | Ceph, pools, емкость, recovery |
| Security-оператор | Ключи, политики допуска, аудит |
| Разработчик SLOYRA | Исправления компонентов и совместимость |
| Владелец лицензии | Оплата, продление и учет активаций |

Одна организация может совмещать роли.
Для каждого production-изменения все равно нужен ответственный.
Автоматическое действие контроллера связывается с владельцем политики.
Безымянная ручная миграция не является допустимым аудитом.

## Инварианты работы

- Запрос проходит допуск до выдачи защищенного содержимого.
- Попадание в кеш не отменяет проверку доступа.
- Недействительная подпись не становится cache miss.
- Решение об отказе не должно прогревать защищенный кеш.
- Готовый сегмент публикуется только после проверки целостности.
- Метаданные объекта содержат его поколение.
- Удаление единственной постоянной копии не является eviction.
- Ошибка телеметрии не блокирует event loop Nginx.
- Переполнение очереди имеет измеримый режим деградации.
- Повтор событий не должен удваивать спрос.
- Ceph degradation приостанавливает рискованные перемещения.
- Предыдущая конфигурация сохраняется до успешной приемки новой.

## Режимы внедрения

| Режим | Допуск | Кеш | Размещение |
| --- | --- | --- | --- |
| observe | Сбор решений без новой блокировки | По согласованному профилю | Только расчет |
| enforce | Применение проверенной политики | Включен | По отдельно принятой политике |
| cache_bypass | Политика допуска сохраняется | Обход кеша | Может быть приостановлено |
| placement_paused | Допуск работает | Кеш работает | Новые миграции не запускаются |
| drain | Новые запросы снимаются балансировщиком | Завершение текущих чтений | Фоновые задачи останавливаются |
| recovery | Ограниченный профиль | Проверенные данные | Изменения заморожены |

Названия режимов являются проектным контрактом.
Режим observe нельзя считать защитой от нелегитимного доступа.
Его применяют только там, где исходная защита остается достаточной.
Переход к enforce требует анализа ложных отказов.

## Готовность узла

Readiness должна отражать способность безопасно обслуживать запросы.
Liveness должна отражать наличие работающего процесса.
Разделение предотвращает бесконечный restart при недоступном origin.
Перезапуск не исправляет внешнюю сетевую аварию.

Проектные условия readiness:

- конфигурация загружена;
- схема конфигурации совместима;
- набор ключей доступен;
- локальный протокол агента совместим;
- назначенный режим допуска определен;
- индекс кеша читается либо включен согласованный обход;
- обязательные проверки платформы пройдены;
- время достаточно синхронизировано для подписи.

Недоступность collector может означать degraded readiness.
Точное влияние задается профилем среды.
Недоступность ключей проверки не должна скрываться за общим «healthy».

## Начальная линия измерений

До включения SLOYRA измеряют существующий путь выдачи.
Нужны реальные распределения размеров объектов.
Нужны доли полных и диапазонных запросов.
Нужны QPS и throughput по времени суток.
Нужны доли повторного доступа.
Нужны объемы чтения origin.
Нужны ресурсы Nginx и задержки дисков.

Сохраняют одинаковое окно для сравнения.
Окно должно включать пик нагрузки.
Измерения связывают с набором входных запросов.
Различие входной нагрузки не следует приписывать изменению кеша.

## Ежедневная проверка

Оператор проверяет общую выдачу.
Затем проверяет частоту отказов по причинам.
Затем проверяет кеш и origin.
Затем проверяет очередь телеметрии.
Затем проверяет очередь миграций.

Практический перечень:

- доля успешных легитимных запросов;
- p95 и p99 времени ответа;
- изменение числа 403;
- источник 403: SLOYRA или upstream;
- request hit ratio;
- byte hit ratio;
- объем физических origin fetch;
- свободные байты и inode;
- размер WAL;
- возраст самого старого недоставленного события;
- отставание агрегатора;
- незавершенные миграции;
- состояние Ceph;
- ошибки проверки checksum;
- изменения конфигурации за последние сутки.

Отдельно проверяют тренды.
Нормальное текущее значение может скрывать быстрое заполнение диска.
Уменьшение origin traffic может сопровождаться ростом ложных 403.
Поэтому экономический эффект всегда проверяют вместе с доступностью.

## Еженедельная проверка

Проверяют запас емкости для роста каталога.
Сравнивают фактическую популярность с прогнозом.
Проверяют долю объектов, многократно меняющих уровень.
Проверяют случаи принудительной остановки миграций.
Проверяют ошибки политики и необработанные reason codes.
Проверяют свежесть backup конфигурации.
Проверяют срок действия ключей и сертификатов.
Проверяют план ручного продления лицензии.

Не требуется вручную перемещать объекты с каждым отчетом.
Цель проверки — выявить систематическую ошибку политики.
Разовое отклонение в пределах принятого бюджета не требует настройки.

## Емкость локального кеша

Кеш имеет верхнюю квоту.
До физического заполнения диска должен оставаться резерв.
Резерв нужен журналам, временным файлам и публикации сегментов.
Порог зависит от размера объектов и конкурентной записи.

Пример расчетного профиля:

```yaml
cache_capacity_example:
  raw_volume_bytes: 2000000000000
  cache_budget_bytes: 1500000000000
  temporary_budget_bytes: 100000000000
  operating_reserve_bytes: 400000000000
  status: planning_example
```

Все числа в примере иллюстративны.
Емкость по маркировке диска отличается от доступной файловой емкости.
Размер replicated Ceph pool нельзя сравнивать напрямую с logical bytes.
Запас считается отдельно для каждого уровня.

## Контроль inode

Сегментный кеш увеличивает количество файлов.
Размер сегмента влияет на число inode.
Хранение каждого счетчика отдельным файлом не является базовым решением.
Индекс должен учитывать число объектов и сегментов.

Диагностика:

```bash
df -i /var/cache
stat -f /var/cache
```

Низкий остаток inode может возникнуть при большом остатке байтов.
Alert должен покрывать оба ресурса.
Очистка кеша выполняется через управляемое eviction.
Удалять произвольные файлы из активного индекса вручную нельзя.

## Резервирование RAM

Лимит RAM-кеша задают ниже доступной памяти процесса.
Резерв должен покрывать пиковую конкуренцию запросов.
Также нужен запас на обновление конфигурации.
На узлах с несколькими tenants задают отдельные доли.

Не следует лечить рост RSS только уменьшением cache budget.
Причиной могут быть очереди, TLS, Lua или утечка.
Сначала сопоставляют RSS с управляемыми категориями памяти.
Неучтенная разница становится отдельным предметом анализа.

Пример модели:

```text
memory_peak =
    ram_cache_budget
  + shared_index_budget
  + worker_private_peak
  + agent_peak
  + telemetry_buffer_peak
  + reload_overlap_reserve
  + operating_margin
```

Page cache учитывается в модели узла отдельно.
При cgroup-ограничениях его учет проверяют экспериментально.
Swap не используется как способ увеличить эффективный RAM-кеш.

## Ограничение origin

Origin получает только необходимые физические чтения.
Одновременные запросы к одному сегменту объединяются.
Для разных сегментов действуют отдельные пределы конкуренции.
Таймаут клиента не должен оставлять бесконечный fill.

Профиль должен задавать:

- максимальные параллельные fill на узел;
- максимальные fill на origin;
- предел ожидающих запросов на один ключ;
- предел размера временного объекта;
- таймаут соединения;
- таймаут первого байта;
- таймаут простоя чтения;
- общий deadline;
- retry budget;
- допустимость stale при конкретном типе ошибки.

Повтор после ошибки ограничен.
Синхронный повтор всеми worker создает вторичную перегрузку.
Jitter и backoff должны применяться к фоновым повторениям.
Авторизационные ошибки origin не маскируются бесконечным retry.

## Политика популярности

Популярность рассчитывается по легитимному спросу.
Входом служит событие logical_request.
Cache lookup описывает работу отдельных уровней.
Origin fetch описывает физическую загрузку.

Считать популярность только по origin fetch нельзя.
Горячий объект после прогрева перестанет создавать fetch.
Это ошибочно сделает его «холодным».
Контроллер должен видеть спрос, обслуженный кешем.

Счетчики нормализуют по интервалу наблюдения.
Интервалы с потерями помечают признаком качества.
Неполная телеметрия не должна провоцировать массовую демоцию.
Для принятия решения используется минимальная уверенность.

## Гистерезис

Порог повышения и порог понижения различаются.
Объект должен некоторое время сохранять новый режим спроса.
После перемещения действует cooldown.
Ограничение числа миграций снижает лишние записи.

Пример проектной логики:

```text
promote when:
  observed_demand >= promote_threshold
  confidence >= required_confidence
  hot_pool_free >= required_reserve
  no_active_migration

demote when:
  observed_demand <= demote_threshold
  cold_duration >= required_cold_duration
  source_generation_is_current
  no_active_migration
```

Конкретные пороги подбирают по workload.
Одинаковые пороги для мелких файлов и больших видео могут быть неэффективны.
Разделение классов должно оставаться понятным оператору.

## Ограничение миграций

Миграция конкурирует с клиентскими чтениями.
Бюджет задается в байтах и количестве заданий.
На HDD отдельно ограничивают конкурентные случайные операции.
На SSD учитывают объем записи и износ.

| Сигнал | Реакция контроллера |
| --- | --- |
| Рост клиентской p99 | Уменьшить фоновой бюджет |
| Ceph degraded | Приостановить новые миграции |
| Недостаточный запас hot pool | Остановить promotion |
| Недостаточный запас cold pool | Остановить demotion |
| Отставание telemetry | Снизить уверенность решений |
| Ошибки checksum | Изолировать объект и остановить его задания |
| Устаревший locator | Перечитать поколение перед commit |

Пауза должна быть видна в status и метриках.
Задание не считается failed только из-за административной паузы.
Пауза не должна отменять проверку целостности уже скопированных данных.

## Обновление конфигурации

Конфигурация получает идентификатор поколения.
Новое поколение сначала проходит синтаксическую проверку.
Затем проходят семантические проверки.
Затем готовятся зависимости и ключи.
После этого поколение активируется атомарно.

Старое поколение остается доступно для завершения запросов.
Активный запрос не должен получить половину старых и новых правил.
Отчет содержит desired generation и active generation.
Их расхождение дольше принятого окна вызывает alert.

Проектная последовательность:

```text
read candidate
parse schema
validate limits
resolve dependencies
prepare generation
run preflight
activate generation
observe canary
confirm or roll back
```

Секреты не должны попадать в diff открытого аудита.
В аудите достаточно идентификатора секрета и его версии.
Проверка существования секрета не должна печатать содержимое.

## Изменение правил допуска

Изменение правил доступа влияет на пользователей немедленно.
Перед включением сравнивают решения старой и новой политики.
Результат сравнения группируют по причинам.
Секреты, токены и полные URL не становятся labels.

Нужно проверить:

- валидную подпись;
- истекшую подпись;
- подпись из будущего;
- неизвестный key id;
- неправильный ресурс;
- изменение query-параметров;
- Range-запрос;
- повторное обращение;
- IPv4 и IPv6 при привязке;
- поведение доверенного reverse proxy.

При обнаружении ложных отказов откатывают конкретное поколение.
Отключать всю защиту для обхода одной ошибки не является стандартным шагом.
Временно исключенный маршрут получает владельца и срок пересмотра.

## Ротация ключей

Новый ключ проверки публикуют до начала его использования.
Период перекрытия покрывает срок жизни ранее выданных токенов.
После переключения наблюдают обращения со старым key id.
Удаление старого ключа выполняют по подтвержденному критерию.

Процедура не предполагает публикацию приватного ключа на edge.
Точный набор материалов зависит от алгоритма подписи.
Симметричные секреты требуют отдельной модели распространения.
Ключи окружений development и production не смешивают.

Ротация журналируется:

```json
{
  "event_type": "keyset_activated",
  "keyset_version": "keys-example-12",
  "previous_version": "keys-example-11",
  "config_generation": 42,
  "actor_type": "operator",
  "result": "success"
}
```

Сам ключ в событие не включается.
Ошибка распространения ключей не должна выдавать 200 для невалидной подписи.
Она должна давать диагностируемое состояние недоступной политики.

## Время

Проверка подписи зависит от времени.
Каждый узел должен использовать управляемую синхронизацию часов.
Числовая величина допуска skew задается политикой.
Не следует автоматически увеличивать допуск при любой ошибке времени.

События имеют wall-clock timestamp и локальный sequence.
Sequence позволяет обнаружить порядок внутри потока.
Wall-clock нужен для окон анализа.
Резкий скачок времени не должен нарушить дедупликацию.

Диагностика времени зависит от используемого сервиса синхронизации.
`timedatectl status` доступен на системах с соответствующим systemd.
Отсутствие команды не является само по себе доказательством несинхронности.

## Обновление бинарных компонентов

Перед обновлением фиксируют текущие версии.
Проверяют совместимость новой схемы состояния.
Проверяют допустимость возврата к предыдущему бинарному файлу.
Сохраняют старый пакет и манифест.

Обновление выполняют на canary-узле.
Canary получает репрезентативную часть запросов.
Сравнение должно учитывать разницу прогрева кеша.
Массовое расширение начинается после завершения окна наблюдения.

Критерии остановки расширения:

- рост ложных 403;
- нарушение целостности ответов;
- рост аварийных завершений worker;
- недопустимый рост p99;
- неснижающийся telemetry backlog;
- несовместимость local protocol;
- ошибка восстановления индекса;
- неожиданный рост origin traffic.

Числовые пределы задаются до изменения.
Их нельзя выбирать задним числом для оправдания результата.

## Состояние и backup

Кеш обычно можно восстановить из origin.
Конфигурацию и ключевые метаданные восстанавливать сложнее.
Поэтому backup различает классы данных.

| Данные | Назначение копии | Приоритет |
| --- | --- | --- |
| Конфигурация | Восстановить правила | Обязательный |
| Версии политик | Воспроизвести решение | Обязательный |
| Ссылки на секреты | Восстановить привязки | Обязательный |
| Секреты | Восстановить право проверки | По процедуре secret storage |
| Locator метаданные | Найти актуальную копию | Обязательный для соответствующего адаптера |
| Audit log | Разобрать изменение | Обязательный по политике хранения |
| WAL | Сохранить недоставленные события | По выбранной надежности |
| Локальные файлы кеша | Ускорить прогрев | Не обязательный backup origin |

Backup секретов имеет собственное шифрование и права.
Снимок каталога кеша без согласованного индекса может быть непригоден.
Процедура восстановления тестируется на отдельном стенде.
Наличие архивного файла не доказывает восстановимость.

## Журнал WAL

WAL хранит события до подтверждения устойчивого приема.
Подтверждение доставки привязано к диапазону sequence.
Удаление сегмента WAL возможно после продвижения checkpoint.
Частично записанный хвост обрабатывается согласно формату записи.

После аварии агент:

1. Находит последний подтвержденный checkpoint.
2. Проверяет заголовки сегментов.
3. Проверяет контрольные суммы записей.
4. Отделяет неполный хвост.
5. Восстанавливает очередность.
6. Повторяет неподтвержденную доставку.
7. Публикует отчет восстановления.

Повторная доставка должна быть безопасна для агрегатора.
Без дедупликации recovery превращает старые запросы в новый спрос.
Поврежденный WAL нельзя молча считать пустым.

## Ограничение телеметрии

Очередь имеет лимиты по байтам и времени.
Проект допускает агрегирование, если оно сохраняет достаточную семантику.
Переход к агрегированию отражается в качестве данных.
Потеря событий получает отдельный счетчик и интервал.

При нехватке места приоритеты должны быть явными.
Audit-события и события спроса имеют разное назначение.
Отбрасывание отладочного trace не равно потере logical_request.
Контроллер размещения получает сигнал о неполном наблюдении.

## Лицензионная эксплуатация

Начало срока связывается с подтвержденной оплатой.
Продление выполняется вручную.
Администратор фиксирует количество активаций.
Этот учет должен быть доступен для сверки.

До реализации необходимо определить offline-поведение.
Документация не назначает произвольный grace period.
Документация не назначает автоматическое списание.
Документация не добавляет новый SLA.

Система должна показывать состояние лицензии оператору.
Публичный клиентский ответ не раскрывает платежные данные.
Ошибка лицензии имеет отдельную диагностическую категорию.
Точное влияние на выдачу задается утвержденной политикой продукта.

## Отключение узла на обслуживание

Узел сначала снимают с приема новых запросов.
Оставшиеся запросы получают время на завершение.
Фоновые миграции останавливают на безопасной границе.
Агент фиксирует текущий WAL.
После этого выполняется остановка по процедуре среды.

Таймаут drain должен соответствовать максимальным допустимым запросам.
Длинная видеосессия не должна удерживать узел бесконечно.
Принудительно оборванные запросы считаются и анализируются отдельно.
Возврат узла начинается с preflight и readiness.

## Сводное диагностическое состояние

Пример внутренней сводки для оператора:
Это диагностическая проекция нескольких проверок.
Транспортные ответы health/readiness определены в [контрактах API](41-api-contracts.md).

```json
{
  "diagnostic_view_version": 1,
  "node_id": "edge-example-01",
  "mode": "enforce",
  "readiness": "degraded",
  "active_config_generation": 42,
  "desired_config_generation": 42,
  "components": {
    "access": "ready",
    "cache": "ready",
    "telemetry": "buffering",
    "placement": "paused"
  },
  "reasons": [
    "collector_unavailable",
    "placement_paused_by_policy"
  ]
}
```

Status должен быть небольшим и ограниченным по размеру.
Большой список объектов возвращается через отдельный paginated endpoint.
Операция чтения status не должна запускать дорогое сканирование кеша.

## Категории ошибок эксплуатации

| Код | Смысл | Первая проверка |
| --- | --- | --- |
| SLY-CFG-001 | Схема конфигурации неизвестна | Версия schema |
| SLY-CFG-002 | Значение нарушает ограничение | Путь параметра |
| SLY-CFG-003 | Зависимость конфигурации недоступна | Секрет или файл |
| SLY-ABI-001 | Модуль несовместим с Nginx | Манифест и nginx -V |
| SLY-IPC-001 | Агент недоступен | Socket и процесс |
| SLY-IPC-002 | Версия протокола отличается | Версии компонентов |
| SLY-ACC-001 | Подпись недействительна | Key id и канонизация |
| SLY-ACC-002 | Срок подписи истек | Время узла и срок токена |
| SLY-ACC-003 | Ключ проверки неизвестен | Версия keyset |
| SLY-ACC-004 | Политика не завершилась | Ошибка или бюджет Lua |
| SLY-CCH-001 | Уровень кеша заполнен | Байты и inode |
| SLY-CCH-002 | Контрольная сумма не совпала | Объект и generation |
| SLY-CCH-003 | Индекс содержит устаревшую ссылку | Наличие и поколение файла |
| SLY-CCH-004 | Временная запись не завершена | Origin и дисковые ошибки |
| SLY-TEL-001 | Collector недоступен | Сеть и сертификат |
| SLY-TEL-002 | Очередь достигла лимита | WAL и возраст события |
| SLY-TEL-003 | WAL поврежден | Сегмент и checkpoint |
| SLY-TEL-004 | Событие отклонено схемой | Версия producer и collector |
| SLY-PLC-001 | Миграция остановлена политикой | Причина pause |
| SLY-PLC-002 | Недостаточно емкости назначения | Pool reserve |
| SLY-PLC-003 | Поколение объекта изменилось | Locator и origin metadata |
| SLY-PLC-004 | Проверка копии не пройдена | Checksum и длина |
| SLY-PLC-005 | Ceph нездоров | Health detail |
| SLY-LIC-001 | Не определено состояние лицензии | Учетная запись и связь |

Это проектный реестр кодов.
Он должен версионироваться вместе с реализацией.
Новый код не может молча менять смысл старого.
Публичный reason code может быть менее подробным внутреннего.

## Runbook: заполнение локального диска

Сигнал: падает свободное место или растут ошибки записи.
Сначала определить конкретный mount.
Проверить байты, inode и объем временных файлов.
Сопоставить заполнение с WAL и кешем.

Безопасные действия:

1. Остановить прием новых объектов в проблемный уровень.
2. Сохранить чтение проверенных существующих объектов.
3. Ограничить фоновые promotion.
4. Проверить работу штатного eviction.
5. Подтвердить резерв на других уровнях.
6. Выполнить управляемое освобождение через индекс.
7. Наблюдать отсутствие повторного быстрого заполнения.

Не следует удалять mount или форматировать устройство.
Не следует вручную удалять активные WAL-сегменты.
Не следует считать перенос файлов командой shell согласованной миграцией.

Критерий завершения: восстановлен резерв и прекратились ошибки записи.
Доказательство: график места, событие eviction и контрольная выдача.

## Runbook: рост RSS

Сигнал: RSS приближается к лимиту или происходит OOM.
Определить процесс и поколение worker.
Проверить одновременное существование старых worker после reload.
Сравнить RSS с RAM-кешем, очередями и количеством соединений.

Безопасные действия:

1. Зафиксировать профиль нагрузки.
2. Проверить память агента отдельно от Nginx.
3. Ограничить новые фоновые задачи.
4. При подтвержденном давлении уменьшить admission в RAM.
5. Не увеличивать лимит без расчета памяти узла.
6. Собрать разрешенный диагностический профиль.
7. Проверить возврат к базовому потреблению после спада нагрузки.

Критерий завершения: устойчивый запас памяти при том же workload.
Доказательство: распределение памяти и отсутствие новых OOM.

## Runbook: cache stampede

Сигнал: множество origin fetch для одного ключа.
Проверить ключ кеша и generation.
Проверить single-flight lock и время ожидания.
Отделить разные Range-сегменты от одного сегмента.

Безопасные действия:

1. Ограничить fill concurrency на origin.
2. Проверить срок жизни блокировки.
3. Проверить ошибки владельца fill.
4. Убедиться, что ожидающие запросы не начинают fill одновременно.
5. Проверить допустимую stale-политику для маршрута.
6. Проверить jitter фонового обновления.
7. Сравнить logical_request и origin_fetch после исправления.

Критерий завершения: физические чтения соответствуют ожидаемым fill.
Доказательство: трасса одного ключа и график origin concurrency.

## Runbook: массовые 403 по сроку подписи

Сигнал: растет reason expired_signature.
Проверить время edge и сервиса выдачи токенов.
Проверить единицы timestamp: секунды или миллисекунды.
Проверить timezone только в отображении, не в epoch-сравнении.

Безопасные действия:

1. Выбрать обезличенные контрольные запросы.
2. Сравнить issued_at, expires_at и время проверки.
3. Проверить новую версию keyset и политики.
4. Исправить источник неверного времени.
5. При регрессии вернуть предыдущее поколение политики.
6. Повторить положительные и отрицательные тесты.
7. Проверить, что не расширен срок действия всех токенов без решения владельца.

Критерий завершения: валидные токены принимаются, истекшие отклоняются.
Доказательство: тесты границ времени и снижение ложных отказов.

## Runbook: растет telemetry backlog

Сигнал: увеличивается возраст самого старого события.
Проверить collector, сеть и сертификат.
Проверить скорость поступления и скорость подтверждения.
Проверить ошибки схемы отдельно от транспортных повторов.

Безопасные действия:

1. Сохранить WAL и checkpoint.
2. Ограничить второстепенные диагностические события.
3. Приостановить решения на неполных данных.
4. Восстановить канал доставки.
5. Ограничить скорость replay, чтобы не перегрузить collector.
6. Проверить дедупликацию после повтора.
7. Дождаться уменьшения возраста очереди.

Критерий завершения: очередь укладывается в принятый бюджет задержки.
Доказательство: подтвержденный sequence и метрика dropped events.

## Runbook: поврежденный сегмент кеша

Сигнал: checksum или длина не совпали.
Определить объект, сегмент и generation.
Запретить выдачу подозрительного сегмента.
Проверить, есть ли исправная origin-копия.

Безопасные действия:

1. Пометить сегмент карантином в индексе.
2. Прекратить его продвижение между уровнями.
3. Сохранить минимальные диагностические метаданные.
4. Загрузить новую копию с проверкой версии.
5. Проверить checksum и ожидаемую длину.
6. Атомарно опубликовать исправную копию.
7. Проверить соседние сегменты при признаках аппаратной ошибки.

Критерий завершения: чтение дает проверенные байты текущего поколения.
Доказательство: результат checksum и контрольный Range-запрос.

## Runbook: повреждение WAL

Сигнал: агент обнаружил неверный заголовок или checksum записи.
Определить, поврежден ли только незавершенный хвост.
Проверить последний устойчивый checkpoint.
Не изменять исходные сегменты до сохранения диагностической копии.

Безопасные действия:

1. Остановить автоматическое удаление проблемного сегмента.
2. Изолировать поврежденный диапазон.
3. Запустить проверенный recovery-путь.
4. Восстановить все целые подтверждаемые записи.
5. Зафиксировать невосстановимый диапазон как потерю данных наблюдения.
6. Снизить уверенность placement-решений соответствующего окна.
7. Проверить, что повторная доставка не удвоила счетчики.

Критерий завершения: очередь продолжает работу с явным отчетом потерь.
Доказательство: recovery report и непрерывность нового sequence.

## Runbook: Ceph degraded

Сигнал: health ухудшился или растет recovery/backfill.
Проверить состояние у storage-оператора.
Отличить проблему данных от предупреждения инфраструктуры.
Не изменять CRUSH map автоматически в ответ на любой alert.

Безопасные действия:

1. Приостановить новые миграции SLOYRA.
2. Сохранить доступный путь чтения.
3. Проверить незавершенные задания copy и commit.
4. Ограничить дополнительные фоновые чтения.
5. Дождаться согласованного разрешения storage-оператора.
6. Проверить актуальность locator перед возобновлением.
7. Возобновить ограниченную часть очереди.

Критерий завершения: cluster state допускает согласованный бюджет миграций.
Доказательство: health snapshot и успешная контрольная миграция.

## Runbook: откат релиза

Сигнал: кандидат нарушает согласованный критерий приемки.
Остановить расширение релиза.
Сохранить журналы и версию конфигурации.
Проверить совместимость старого бинарного файла с новым состоянием.

Безопасные действия:

1. Снять canary с новых запросов.
2. Дождаться drain в установленном окне.
3. Вернуть совместимую пару модуль–агент.
4. Вернуть предыдущее поколение конфигурации.
5. При несовместимом индексе использовать проверенный rebuild.
6. Проверить допуск, выдачу и телеметрию.
7. Вернуть узел в балансировку после readiness.

Критерий завершения: исходный профиль восстановлен без скрытой потери состояния.
Доказательство: версии компонентов и повторный smoke-тест.

## Отчет об инциденте

Отчет строится вокруг наблюдаемого воздействия.
Нужно указать затронутые запросы и временной интервал.
Нужно разделить предположение и подтвержденную причину.
Нужно показать действия и их результат.

```yaml
incident:
  id: incident-example
  started_at: record-utc-time
  detected_at: record-utc-time
  resolved_at: pending
  impact:
    routes: []
    nodes: []
    legitimate_requests_affected: unknown
  symptoms:
    - record-observed-signal
  confirmed_cause: pending
  mitigation: pending
  evidence:
    dashboard_snapshot: pending
    config_generation: pending
    component_versions: pending
  follow_up:
    owner: pending
    acceptance_test: pending
```

Необходимо сохранять только нужные данные запроса.
Authorization и подписанные URL редактируются.
Инцидент не закрывают одним исчезновением alert.
Нужен контроль отсутствия повторения на сопоставимой нагрузке.

---

# Метрики, события, аудит и эксплуатационные панели

> Статус: проектная спецификация v0.1; сборки и совместимость требуют приемки.

## Назначение наблюдаемости

Наблюдаемость должна отвечать на четыре вопроса.
Получил ли легитимный клиент правильный объект.
Почему запрос был принят или отклонен.
Где система фактически прочитала байты.
Как наблюденный спрос повлиял на размещение.

Отдельный график hit ratio не отвечает на все эти вопросы.
Высокий hit ratio может сочетаться с ложными отказами.
Низкий origin traffic может быть следствием падения входной нагрузки.
Экономию следует сопоставлять с качеством выдачи и составом нагрузки.

## Три потока событий

| Событие | Семантика | Основной потребитель |
| --- | --- | --- |
| logical_request | Один легитимный прикладной запрос | Оценка спроса |
| cache_lookup | Проверка одного уровня кеша | Диагностика кеша |
| origin_fetch | Физическое получение недостающих данных | Нагрузка на origin |

Один logical_request может породить несколько cache_lookup.
Несколько logical_request могут использовать один origin_fetch.
Один Range-запрос может затронуть несколько сегментов.
Один origin_fetch может завершиться после отключения части клиентов.

Счетчик всех обращений должен отдельно видеть запрещенные запросы.
Запрещенный запрос не включают в подтвержденный спрос на содержимое.
Иначе массовый парсинг может искусственно сделать объект горячим.

## Общий конверт события

Пример проектного события согласован с контрактом API.
Полный транспортный контракт находится в [справочнике API](41-api-contracts.md).

```json
{
  "schema_version": 1,
  "event_type": "logical_request",
  "event_id": {
    "producer_id": "edge-example-01",
    "producer_epoch": 31,
    "sequence": 1042
  },
  "observed_at": "2026-09-27T10:00:00Z",
  "tenant_id": "tenant-example",
  "object_id_hash": "hmac-sha256:h1:example-object-a",
  "identity_hash_version": "h1",
  "content_version": "v17",
  "representation_id": "source",
  "range": {
    "start": 0,
    "length": 1048576
  },
  "requested_bytes": 1048576,
  "requested_bytes_known": true,
  "sample_probability": 1.0,
  "policy_revision": "p12",
  "request_id": "request-example",
  "method": "GET",
  "served_bytes": 1048576,
  "status": 206,
  "cache_summary": "hit",
  "metadata_demand": false
}
```

Object id должен быть стабильным внутри согласованной модели каталога.
Он не обязан совпадать с URL.
Request id не должен содержать токен авторизации.
Поля example обозначают демонстрационные значения.

## Идентичность и дедупликация

Event id должен быть уникален между перезапусками узла.
Sequence монотонен внутри producer_epoch.
После restart устойчиво создается новый producer_epoch.
Collector подтверждает устойчиво сохраненный диапазон.

Дедупликация выполняется до обновления счетчиков популярности.
Признак duplicate не должен повышать demand.
Окно хранения дедупликации согласуют с максимальным replay.
Поздний replay за пределами окна требует отдельного решения.

Для агрегированных событий идентичность окна включает:

- producer;
- период наблюдения;
- объект и поколение;
- тип агрегации;
- версию схемы;
- revision агрегата.

Повтор одного агрегата не складывается с самим собой.
Исправленный агрегат явно замещает предыдущую revision.
Смешивание delta и absolute counters запрещено без признака типа.

## Событие cache_lookup

Ниже показана диагностическая проекция для журнала и панели.
Она не является полным transport envelope и не отправляется как batch entry.
Поля lookup добавляются к идентичности события по схеме контракта API.

```json
{
  "diagnostic_view_version": 1,
  "event_type": "cache_lookup",
  "request_id": "request-example",
  "object_id": "opaque-object-id",
  "content_version": "v17",
  "payload": {
    "tier": "nvme",
    "segment_id": "segment-example",
    "result": "hit",
    "lookup_seconds": 0.0004,
    "available_bytes": 1048576
  }
}
```

Result принимает ограниченный набор значений.
Базовые значения: hit, miss, stale, bypass, error.
Quarantined сегмент возвращает error или отдельную согласованную причину.
Нельзя одновременно считать один lookup hit и miss.

Lookup отражает попытку чтения конкретного уровня.
Сумма lookup по уровням может превышать число запросов.
Поэтому denominator для общего hit ratio выбирают отдельно.

## Событие origin_fetch

Ниже показана диагностическая проекция физической попытки.
В транспортном контракте общий fill_id связывает попытки,
а origin_attempt_id уникален для каждого retry.
Поле fetch_id этой проекции отображает origin_attempt_id.

```json
{
  "diagnostic_view_version": 1,
  "event_type": "origin_fetch",
  "fetch_id": "fetch-example",
  "object_id": "opaque-object-id",
  "content_version": "v17",
  "payload": {
    "origin_id": "origin-main",
    "segment_id": "segment-example",
    "result": "complete",
    "received_bytes": 1048576,
    "duration_seconds": 0.032,
    "waiter_count": 18,
    "retry_index": 0
  }
}
```

Waiter count описывает объединение запросов.
Он не является дополнительным количеством физических чтений.
Retry index позволяет отделить полезный fill от повторов.
Received bytes включает реально полученные байты выбранного fetch.

## События размещения

Placement event связывается с job id.
Job id не становится постоянной меткой метрики.
Полный путь хранится в журнале задания.
Сводные счетчики используют низкокардинальные labels.

Минимальные этапы:

```text
planned
admitted
copy_started
copy_complete
verify_started
verify_complete
locator_committed
source_retention_started
source_cleanup_complete
completed
```

Ошибки и паузы отражаются отдельным полем состояния.
После copy_complete объект еще не считается мигрированным.
Завершение определяется безопасным commit и принятой политикой очистки.

## Качество данных наблюдения

Контроллер должен видеть не только счетчики.
Ему нужен признак полноты наблюдения.
Потери событий не должны интерпретироваться как исчезновение спроса.
Пауза агента не должна превращать все объекты в холодные.

Пример описания окна:

```yaml
observation_window:
  starts_at: '2026-09-27T09:00:00Z'
  ends_at: '2026-09-27T10:00:00Z'
  expected_producers: 12
  reporting_producers: 11
  delivery_completeness: partial
  dropped_events: 1500
  late_events: 230
  placement_eligible: false
  reason: incomplete_producer_coverage
```

Процент полноты должен иметь определенный denominator.
Если denominator неизвестен, значение обозначают unknown.
Ноль событий и отсутствие данных — разные состояния.

## Метрики

Префикс всех собственных метрик — `sloyra_`.
Время измеряется в секундах.
Размер измеряется в байтах.
Счетчики накопления имеют суффикс `_total`.
Количество текущих элементов выражается gauge.
Распределения задержек выражаются histogram.

Имена и labels в этом разделе — проектный контракт.
До реализации exporter не существует автоматически.
Фактическая версия схемы должна публиковаться самим exporter.

## Ограничения labels

Разрешенные категории обычно включают node, component и tier.
Tenant допустим только при ограниченном согласованном числе tenants.
Route id должен быть нормализованным именем маршрута.
HTTP status допускается как ограниченный набор кодов.

Не следует использовать labels:

- полный URL;
- query string;
- IP клиента;
- request id;
- object id;
- segment id;
- user id;
- подписанный токен;
- строку ошибки произвольной длины;
- job id;
- путь к файлу;
- timestamp.

Детальные идентификаторы относятся к логам и трассам.
Число рядов оценивают до включения новой метки.
Неограниченный label способен перегрузить систему мониторинга.

## Модель метрик запроса

`requests_total` считает решения для входных запросов.
`cache_requests_total` считает итоговый результат обслуживания из кеша.
`cache_lookups_total` считает проверки отдельных уровней.
Это три разных счетчика.

Для многосегментного запроса итог может быть partial_hit.
Байтовые метрики отдельно показывают обслуженную долю.
Запрос с частичным попаданием не следует считать полным hit.
Доля полностью кешированных запросов не равна byte hit ratio.

## Реестр метрик

### `sloyra_requests_total`

Тип: counter.
Метки: `decision, route_id`.

Запросы, для которых получено решение допуска.
allow, deny, error

Интерпретация: 403 от origin не считается deny модуля.

### `sloyra_access_decisions_total`

Тип: counter.
Метки: `reason, policy_id`.

Решения политики с нормализованной причиной.
valid_signature, expired_signature, invalid_signature, policy_error

Интерпретация: Reason должен принадлежать закрытому реестру.

### `sloyra_access_duration_seconds`

Тип: histogram.
Метки: `policy_id`.

Время проверки доступа от входа до решения.
Бакеты выбирают после базового измерения.

Интерпретация: Время ожидания origin сюда не входит.

### `sloyra_request_duration_seconds`

Тип: histogram.
Метки: `route_id, result`.

Полное время клиентского запроса.
complete, aborted, failed

Интерпретация: Отдельно анализируют влияние длинной передачи больших файлов.

### `sloyra_response_bytes_total`

Тип: counter.
Метки: `route_id, source`.

Байты, фактически отданные клиенту.
ram, nvme, ssd, hdd, origin, mixed

Интерпретация: Прерванный ответ учитывает только выданные байты.

### `sloyra_inflight_requests`

Тип: gauge.
Метки: `route_id`.

Запросы, еще не завершившие обработку.
Неотрицательное целое.

Интерпретация: Рост без throughput может указывать на зависимость или очередь.

### `sloyra_cache_requests_total`

Тип: counter.
Метки: `tier, result`.

Итоговое обслуживание легитимного запроса кешем.
hit, partial_hit, miss, bypass, error; tier может быть mixed

Интерпретация: Один запрос учитывается один раз в итоговом результате.

### `sloyra_cache_lookups_total`

Тип: counter.
Метки: `tier, result`.

Проверки конкретного уровня кеша.
ram, nvme, ssd, hdd; hit, miss, stale, error

Интерпретация: Сумма по уровням не равна числу клиентских запросов.

### `sloyra_cache_bytes_total`

Тип: counter.
Метки: `tier, result`.

Байты полезного обслуживания из уровня кеша.
served, requested_missing

Интерпретация: Знаменатель byte hit ratio строится из сопоставимых счетчиков.

### `sloyra_cache_lookup_duration_seconds`

Тип: histogram.
Метки: `tier`.

Время поиска и проверки записи уровня.
Бакеты отдельно подбирают для RAM и диска.

Интерпретация: Не включает передачу всех байтов клиенту.

### `sloyra_cache_used_bytes`

Тип: gauge.
Метки: `tier`.

Байты опубликованных данных, учтенные индексом.
Неотрицательное число байтов.

Интерпретация: Сверяют с файловой системой при подозрении на orphan files.

### `sloyra_cache_capacity_bytes`

Тип: gauge.
Метки: `tier`.

Разрешенный бюджет уровня.
Положительное число для включенного уровня.

Интерпретация: Это квота SLOYRA, не физическая емкость диска.

### `sloyra_cache_objects`

Тип: gauge.
Метки: `tier`.

Количество объектов с опубликованными сегментами.
Один объект может присутствовать на нескольких уровнях.

Интерпретация: Сумма уровней не дает уникальный размер каталога.

### `sloyra_cache_segments`

Тип: gauge.
Метки: `tier, state`.

Количество записей сегментов в индексе.
ready, filling, quarantined, evicting

Интерпретация: Рост filling при нулевом throughput требует проверки fill.

### `sloyra_cache_evictions_total`

Тип: counter.
Метки: `tier, reason`.

Удаления восстанавливаемых копий из локального кеша.
capacity, ttl, invalidated, policy

Интерпретация: Не использовать для удаления постоянной origin-копии.

### `sloyra_cache_admission_rejections_total`

Тип: counter.
Метки: `tier, reason`.

Отказы принять новый объект на уровень.
low_value, no_capacity, too_large, invalid_metadata

Интерпретация: Рост может быть нормальной работой политики емкости.

### `sloyra_cache_integrity_failures_total`

Тип: counter.
Метки: `tier, kind`.

Несоответствие проверяемых байтов или метаданных.
checksum, length, generation

Интерпретация: Даже редкие события требуют установления причины.

### `sloyra_cache_orphan_bytes`

Тип: gauge.
Метки: `tier`.

Выявленные байты вне согласованного индекса.
Обновляется только после законченного reconcile.

Интерпретация: Ноль при незавершенном сканировании не означает отсутствие orphan.

### `sloyra_cache_reconcile_duration_seconds`

Тип: histogram.
Метки: `tier, result`.

Продолжительность сверки файлов и индекса.
complete, failed, interrupted

Интерпретация: Слишком частая сверка конкурирует с выдачей.

### `sloyra_origin_requests_total`

Тип: counter.
Метки: `origin_id, result`.

Физические попытки получения данных origin.
complete, timeout, rejected, failed, cancelled

Интерпретация: Каждый retry является отдельной физической попыткой.

### `sloyra_origin_bytes_total`

Тип: counter.
Метки: `origin_id, result`.

Байты, прочитанные у origin.
accepted, discarded

Интерпретация: Discarded показывает цену ошибок поколений и прерванных fill.

### `sloyra_origin_duration_seconds`

Тип: histogram.
Метки: `origin_id, result`.

Длительность физической попытки fetch.
Те же result, что у origin_requests_total.

Интерпретация: Не смешивать с ожиданием в локальной очереди.

### `sloyra_origin_inflight`

Тип: gauge.
Метки: `origin_id`.

Текущие выполняемые физические fetch.
Не превышает согласованный concurrency budget.

Интерпретация: При превышении бюджета проверяют область действия лимита.

### `sloyra_origin_queue_depth`

Тип: gauge.
Метки: `origin_id`.

Ожидающие разрешения на физический fetch.
Неотрицательное число задач.

Интерпретация: Не равно количеству клиентов, ожидающих один fill.

### `sloyra_fill_waiters`

Тип: gauge.
Метки: `tier`.

Клиентские запросы, ожидающие существующий fill.
Учитываются по фактическому состоянию ожидания.

Интерпретация: Рост полезен при stampede, но требует ограничения времени.

### `sloyra_fill_coalesced_total`

Тип: counter.
Метки: `tier`.

Запросы, присоединенные к уже запущенному fill.
Один waiter увеличивает счетчик один раз.

Интерпретация: Показывает экономию физических чтений при конкурентном спросе.

### `sloyra_fill_wait_duration_seconds`

Тип: histogram.
Метки: `tier, result`.

Время ожидания объединенного fill.
served, timeout, cancelled, failed

Интерпретация: Хвост задержки может расти даже при низком origin QPS.

### `sloyra_telemetry_events_total`

Тип: counter.
Метки: `event_type`.

Созданные события до передачи в очередь.
logical_request, cache_lookup, origin_fetch, placement

Интерпретация: Допуск к подсчету определяется семантикой конкретного события.

### `sloyra_telemetry_delivered_total`

Тип: counter.
Метки: `event_type`.

События, устойчиво подтвержденные collector.
Только после принятого acknowledgement.

Интерпретация: Повтор транспортного ответа не должен увеличивать счетчик дважды.

### `sloyra_telemetry_duplicates_total`

Тип: counter.
Метки: `event_type`.

Повторы, распознанные приемной стороной.
Имеют тот же event id.

Интерпретация: Не изменяют счетчики спроса после дедупликации.

### `sloyra_telemetry_dropped_total`

Тип: counter.
Метки: `event_type, reason`.

События, потерянные по известной причине.
queue_limit, invalid_schema, corrupt_record, retention_limit

Интерпретация: Потери привязывают к интервалу качества наблюдения.

### `sloyra_telemetry_queue_bytes`

Тип: gauge.
Метки: `queue`.

Размер ожидающих доставки данных.
wal, memory

Интерпретация: Сравнивают с лимитом и скоростью прироста.

### `sloyra_telemetry_queue_limit_bytes`

Тип: gauge.
Метки: `queue`.

Настроенный верхний предел очереди.
Положительное число байтов.

Интерпретация: Лимит не отменяет необходимый резерв файловой системы.

### `sloyra_telemetry_oldest_event_age_seconds`

Тип: gauge.
Метки: `queue`.

Возраст самого старого неподтвержденного события.
При пустой очереди равен нулю.

Интерпретация: Лучше отражает свежесть данных, чем один размер очереди.

### `sloyra_telemetry_export_duration_seconds`

Тип: histogram.
Метки: `collector_id, result`.

Продолжительность отправки batch до ответа.
accepted, rejected, timeout, failed

Интерпретация: Сетевое подтверждение должно иметь документированную долговечность.

### `sloyra_telemetry_retries_total`

Тип: counter.
Метки: `collector_id, reason`.

Повторные попытки отправки batch.
timeout, unavailable, transient_rejection

Интерпретация: Постоянная ошибка схемы не должна создавать бесконечный retry.

### `sloyra_wal_segments`

Тип: gauge.
Метки: `state`.

Число сегментов локального журнала.
active, pending, acknowledged, quarantined

Интерпретация: Acknowledged сегменты должны освобождаться по принятой retention.

### `sloyra_wal_recovery_total`

Тип: counter.
Метки: `result`.

Запуски восстановления журнала.
complete, partial, failed

Интерпретация: Partial требует отчета о диапазоне невосстановленных записей.

### `sloyra_wal_fsync_duration_seconds`

Тип: histogram.
Метки: `result`.

Время фиксации журнала на носитель.
success, error

Интерпретация: Рост влияет на задержку устойчивой постановки в очередь.

### `sloyra_placement_queue_depth`

Тип: gauge.
Метки: `action, state`.

Число заданий контроллера размещения.
promote, demote; queued, running, paused

Интерпретация: Не складывают completed с активной очередью.

### `sloyra_placement_jobs_total`

Тип: counter.
Метки: `action, result`.

Завершенные задания размещения.
complete, cancelled, failed, superseded

Интерпретация: Superseded означает, что решение заменено новым поколением.

### `sloyra_placement_bytes_total`

Тип: counter.
Метки: `source_tier, target_tier, result`.

Байты копирования между классами хранения.
copied, verified, discarded

Интерпретация: Copied и verified не складывают как полезный трафик.

### `sloyra_placement_duration_seconds`

Тип: histogram.
Метки: `action, result`.

Длительность задания от admission до финала.
complete, failed, cancelled

Интерпретация: Время нахождения в общей очереди измеряется отдельно.

### `sloyra_placement_oldest_job_age_seconds`

Тип: gauge.
Метки: `action`.

Возраст самого старого ожидающего задания.
Ноль при отсутствии очереди.

Интерпретация: Высокий возраст может быть следствием административной паузы.

### `sloyra_placement_paused`

Тип: gauge.
Метки: `reason`.

Активность причины остановки новых миграций.
0 или 1; причины из закрытого реестра.

Интерпретация: Одновременно могут действовать несколько причин.

### `sloyra_placement_confidence`

Тип: gauge.
Метки: `cohort`.

Оценка пригодности окна спроса для решений.
Диапазон от 0 до 1 с определенной методикой.

Интерпретация: Без методики нельзя показывать число как вероятность истины.

### `sloyra_placement_generation_conflicts_total`

Тип: counter.
Метки: `adapter`.

Отказы commit из-за изменения поколения объекта.
Конфликт optimistic concurrency.

Интерпретация: Повтор должен начинаться с перечитывания актуального locator.

### `sloyra_placement_integrity_failures_total`

Тип: counter.
Метки: `adapter, kind`.

Ошибки проверки целевой копии.
checksum, length, metadata

Интерпретация: Источник сохраняется до установления целостности назначения.

### `sloyra_config_reload_total`

Тип: counter.
Метки: `result`.

Попытки активировать новое поколение.
accepted, rejected, rolled_back

Интерпретация: Rejected не должен менять активное поколение.

### `sloyra_config_generation_info`

Тип: gauge.
Метки: `generation, schema_version`.

Идентификатор активной конфигурации со значением 1.
В каждый момент один активный набор на компонент.

Интерпретация: Исторические поколения не должны бесконечно сохраняться exporter.

### `sloyra_component_ready`

Тип: gauge.
Метки: `component`.

Готовность компонента к его функции.
0 или 1.

Интерпретация: Общий process up не заменяет эту метрику.

### `sloyra_component_restarts_total`

Тип: counter.
Метки: `component, reason`.

Перезапуски, известные supervisor или агенту.
crash, operator, upgrade, unknown

Интерпретация: Если причина неизвестна, не следует угадывать upgrade.

### `sloyra_policy_errors_total`

Тип: counter.
Метки: `policy_id, kind`.

Ошибки выполнения Lua или native policy.
exception, timeout, memory_limit, invalid_result

Интерпретация: Текст исключения находится в отредактированном логе.

### `sloyra_clock_offset_seconds`

Тип: gauge.
Метки: `source`.

Оценка отклонения часов по доступному источнику.
Знак и метод измерения документируются.

Интерпретация: При отсутствии данных используют отдельный unavailable status.

### `sloyra_build_info`

Тип: gauge.
Метки: `version, revision, target`.

Идентичность запущенного бинарного компонента.
Значение ряда равно 1.

Интерпретация: Позволяет сопоставить инцидент с точным релизом.

## Расчет hit ratio

Request hit ratio отвечает на вопрос о полностью кешированных запросах.
Пример ниже использует проектные метрики.
Нужен одинаковый scope по узлам и маршрутам.

```promql
sum(rate(sloyra_cache_requests_total{result="hit"}[5m]))
/
clamp_min(
  sum(rate(sloyra_cache_requests_total{result=~"hit|partial_hit|miss"}[5m])),
  0.000001
)
```

Исключение bypass должно быть явно указано на dashboard.
Иначе высокий показатель может скрывать большой обход кеша.
Для нулевой нагрузки панель должна показывать «нет запросов».
Технический clamp в запросе не делает нулевую нагрузку успешным результатом.

Byte hit ratio строят по полезным байтам.
Необходимо исключить повторные промежуточные чтения одного ответа.
Байты между уровнями кеша не являются клиентской экономией.

Пример логического расчета:

```text
byte_hit_ratio =
  client_bytes_served_from_cache
  /
  all_client_content_bytes
```

Метрика `sloyra_response_bytes_total` может дать этот denominator.
Source mixed требует дополнительного разложения по реально выданным байтам.
Реализация должна предоставить однозначную учетную модель mixed.
До этого нельзя строить финансовый отчет по приблизительному графику.

## Оценка latency

Пример p99 для проектной histogram:

```promql
histogram_quantile(
  0.99,
  sum by (le, route_id) (
    rate(sloyra_request_duration_seconds_bucket[5m])
  )
)
```

Histogram buckets выбирают под реальное распределение.
Слишком широкие бакеты ухудшают точность оценки.
Среднее нескольких p99 не является общим p99.
При недостаточном числе запросов панель показывает объем выборки.

Prometheus описывает расчет quantile по histogram и ограничения summary.
Реализация dashboard должна следовать семантике выбранного типа метрик.
[Официальная документация Prometheus](https://prometheus.io/docs/practices/histograms/).

## Сигналы alert

Пороги ниже должны задаваться профилем эксплуатации.
Документ не обещает универсальный коммерческий SLA.
Перед включением alert фиксируют владельца и способ проверки.

| Сигнал | Что сравнивать | Первая реакция |
| --- | --- | --- |
| Ложные 403 | Контрольные легитимные запросы и reason | Проверить время и поколение политики |
| Origin overload | Inflight, очередь и latency origin | Ограничить fill, проверить stampede |
| Disk reserve low | Свободные байты и скорость расхода | Приостановить admission уровня |
| Inode reserve low | Свободные inode и число сегментов | Проверить размер сегмента и eviction |
| Memory pressure | RSS, cgroup pressure, reload overlap | Разделить кеш и служебную память |
| Telemetry stale | Oldest event age и delivery rate | Проверить collector и WAL |
| Events lost | Приращение dropped_total | Пометить окно неполным |
| Corruption detected | Integrity failures | Карантин и повторная проверка |
| Migration stuck | Возраст задания и этап | Проверить Ceph и locator |
| Configuration diverged | Desired и active generations | Проверить ошибку активации |
| Policy failure | Ошибки Lua/native policy | Откатить проблемное поколение |
| Exporter missing | Scrape up и readiness | Проверить сеть управления |

Alert без runbook должен считаться незавершенным элементом внедрения.
Alert не должен повторяться каждую минуту без нового воздействия.
Группировка выполняется по сервису и подтвержденной причине.

## Окна наблюдения

Короткое окно показывает быстрый сбой.
Длинное окно помогает отличить всплеск от устойчивого изменения.
Для ночного низкого трафика нужны минимальные объемы выборки.
Для больших объектов важно учитывать длительные активные запросы.

Пример проектного профиля:

```yaml
observability_profile_example:
  fast_window: 5m
  trend_window: 1h
  comparison_window: 24h
  minimum_requests_for_ratio: 100
  minimum_samples_for_latency: 1000
  status: illustrative_only
```

Эти значения являются примером для настройки.
Они не заменяют измерение конкретного workload.
Инцидент с единичной порчей данных важен независимо от объема выборки.

## Структурированные журналы

Логи должны быть машиночитаемыми.
Каждая запись содержит уровень и event code.
Идентификаторы связывают запись с конфигурацией и запросом.
Сообщение для человека не используется как стабильный ключ агрегации.

```json
{
  "timestamp": "2026-09-27T10:00:00Z",
  "level": "warn",
  "component": "cache",
  "code": "SLY-CCH-002",
  "message": "Segment integrity check failed",
  "node_id": "edge-example-01",
  "request_id": "request-example",
  "object_id": "opaque-object-id",
  "segment_id": "segment-example",
  "content_version": "v17",
  "tier": "nvme",
  "action": "quarantine",
  "config_generation": 42
}
```

Полное содержимое файла в лог не попадает.
Подписанный query string в лог не попадает.
Authorization и Cookie не попадают в общий журнал.
Режим debug должен иметь срок и ограничение объема.

## Аудит управления

Audit отличается от отладочного лога.
Он фиксирует, кто изменил состояние и что было активировано.
В аудите сохраняют результат и идентификатор операции.
Отклоненное изменение также должно оставлять запись.

Обязательные типы audit:

- публикация конфигурации;
- активация конфигурации;
- откат конфигурации;
- смена набора ключей;
- изменение режима допуска;
- ручная пауза placement;
- ручное возобновление placement;
- изменение квоты кеша;
- изменение разрешенного origin;
- подтверждение исключения;
- учет активации лицензии;
- экспорт диагностического пакета.

Audit не должен хранить секретные значения параметров.
Для сравнения допускается digest нормализованной конфигурации.
Digest не заменяет защищенное хранение исходной версии.

## Трассировка запроса

Проектная трасса содержит основные этапы.
Высокочастотные внутренние операции могут агрегироваться.
Трассировка должна иметь ограниченный бюджет CPU и объема.

```text
http.request
  access.evaluate
  cache.resolve_generation
  cache.lookup.ram
  cache.lookup.nvme
  cache.wait_for_fill
  origin.fetch
  cache.publish_segment
  response.stream
  telemetry.enqueue
```

Origin fetch может принадлежать общей задаче нескольких запросов.
Связь тогда представляют ссылкой, а не ложным единственным родителем.
Клиентский trace id не принимают как доверенное доказательство личности.
Подробные traces выборочно сохраняют по согласованной политике.

## Панель качества выдачи

Первая строка панели содержит нагрузку и долю успеха.
Рядом отображаются 403 с разделением источника.
Далее показывают latency и выданные байты.
Оператор должен видеть масштаб воздействия до внутренних деталей.

Обязательные разрезы:

- маршрут;
- узел;
- версия релиза;
- поколение конфигурации;
- класс размера объекта;
- полный или Range-запрос;
- результат допуска.

Object id используют через drill-down в ограниченный поиск журналов.
Не следует добавлять миллионы объектов в постоянные Prometheus labels.

## Панель кеша

Показывают request hit ratio и byte hit ratio рядом.
Показывают долю bypass отдельно.
Показывают распределение обслуженных байтов по уровням.
Показывают capacity, used и reserve каждого уровня.
Показывают ошибки целостности.
Показывают admission rejection по причинам.
Показывают eviction и повторный скорый admission.

Высокий churn означает частую запись без устойчивой пользы.
Причина может быть в размере кеша или политике популярности.
Он не исправляется автоматически повышением TTL всех объектов.

## Панель телеметрии

Показывают скорость генерации и подтверждения событий.
Показывают oldest age рядом с queue bytes.
Показывают drops и duplicates.
Показывают состояние WAL recovery.
Показывают версии producer и collector.
Показывают полноту наблюдения по node cohort.

Отсутствующий узел должен быть видимым отсутствием.
Он не должен выглядеть как узел с нулевой популярностью объектов.

## Панель размещения

Показывают queued, running и paused jobs.
Показывают скорость копирования и проверенных байтов.
Показывают источник и назначение миграций.
Показывают текущие pause reasons.
Показывают конфликты поколений.
Показывают ошибки проверки целевой копии.
Показывают Ceph health из доверенного источника.

Миграции показывают вместе с клиентской latency.
Увеличение фоновой скорости не является успехом при ухудшении выдачи.

## Доказательство экономии

Заявление об экономии до 70% рассматривается как целевой результат.
Его нужно проверять на согласованном наборе данных.
Документирование процентов требует указать baseline.
Также нужно определить, что именно измеряется.

Возможные разные величины:

- логический объем уникального каталога;
- физические байты локальных кешей;
- физические байты SSD pool;
- физические байты HDD pool;
- байты с учетом репликации;
- байты с учетом erasure coding;
- стоимость занимаемой емкости;
- стоимость необходимого оборудования.

Перенос из SSD в HDD меняет размещение и стоимость.
Сам по себе такой перенос не уменьшает логический объем файлов.
Уменьшение дублирования кеша может уменьшить физический расход.
Эти эффекты нужно считать раздельно.

Формула для определенного показателя:

```text
saving_percent =
  100 * (baseline_value - measured_value) / baseline_value
```

Baseline должен быть больше нуля.
Обе величины имеют одинаковую единицу и область учета.
Сравнение использует одинаковые требования к надежности.
Снижение числа реплик не выдается за эффект популярности.

## Диагностический пакет

Проектный пакет содержит минимально достаточные сведения.
Сбор должен быть читающим и ограниченным по времени.
Перед экспортом выполняют редактирование секретов.

Состав:

- версии компонентов;
- digest конфигурации;
- список включенных возможностей;
- readiness по компонентам;
- краткие ресурсные показатели;
- ошибки за выбранный интервал;
- размер и возраст очередей;
- сведения о файловой системе;
- манифест платформы;
- результаты целостности;
- причины паузы placement.

В пакет не включают ключи подписи.
В пакет не включают содержимое пользовательских объектов.
В пакет не включают полные токены доступа.
Оператор видит итоговый список файлов до передачи.

## Приемка наблюдаемости

Каждая метрика должна иметь определенную семантику.
Для counter проверяют поведение при restart.
Для gauge проверяют отсутствие ложного нуля при unavailable.
Для histogram проверяют бакеты и единицы.
Для labels проверяют верхнюю границу cardinality.
Для logs проверяют редактирование секретов.
Для audit проверяют actor и результат операции.
Для event stream проверяют replay и дедупликацию.

Панель считается принятой после воспроизведения контрольного сбоя.
Оператор должен найти его причину по доступным данным.
Наличие красивого графика без диагностической ценности недостаточно.

---

# Приемочные испытания, нагрузочные профили и сценарии восстановления

> Статус: проектная спецификация v0.1; сборки и совместимость требуют приемки.

## Назначение

Этот раздел задает проверяемые критерии будущей реализации.
Он не является отчетом о выполненных тестах.
Статус каждого сценария до запуска — planned.
Passed присваивается только при наличии воспроизводимого evidence.

Приемка охватывает корректность ответа и восстановления.
Производительность проверяют при одинаковых входных условиях.
Экономию проверяют при одинаковых требованиях к надежности.
Наличие документации не заменяет выполнение испытаний.

## Уровни испытаний

| Уровень | Предмет | Типичный результат |
| --- | --- | --- |
| Unit | Чистая функция и пограничные значения | Детерминированный отчет |
| Component | Модуль, агент или адаптер отдельно | Контрактный тест |
| Integration | Совместная работа компонентов | Сценарий с реальными границами |
| System | Полный путь клиент–edge–origin | Контрольный ответ и события |
| Load | Заданный workload | Ресурсы и распределения задержки |
| Recovery | Перезапуск и отказ зависимостей | Восстановленное состояние |
| Qualification | Точная платформа и сборка | Паспорт совместимости |
| Acceptance | Профиль конкретного внедрения | Подписанный внутренний протокол |

Не каждый unit-тест нужен в эксплуатационном отчете.
Но каждое пользовательское обещание должно иметь системное доказательство.
Тесты на mock не доказывают реальную семантику диска.
Тесты без конкуренции не доказывают корректность shared state.

## Стенд

Стенд должен быть изолирован от production-данных.
В нем используют синтетические объекты и отдельные ключи.
Ceph-проверки используют выделенные тестовые pools.
Fault injection не применяется к production без отдельной процедуры.

Паспорт стенда содержит:

- модель CPU;
- число доступных vCPU;
- объем RAM;
- лимиты cgroup;
- версию ОС;
- версию ядра;
- файловую систему;
- параметры монтирования;
- модель накопителей;
- версию Nginx;
- configure fingerprint;
- версии SLOYRA;
- версии Lua runtime;
- версию Ceph client;
- версию Ceph cluster;
- topology и failure domains;
- bandwidth и latency между узлами.

## Контрольный каталог

Каталог должен содержать разные размеры объектов.
Содержимое генерируется воспроизводимо.
Для каждого объекта заранее известны длина и checksum.
Есть несколько поколений одного логического объекта.

Пример состава:

```yaml
test_catalog:
  seed: 42027
  cohorts:
    - name: tiny
      object_bytes: 4096
      objects: 10000
    - name: small
      object_bytes: 65536
      objects: 10000
    - name: medium
      object_bytes: 1048576
      objects: 1000
    - name: large
      object_bytes: 1073741824
      objects: 20
    - name: non_aligned
      object_bytes: 1048593
      objects: 100
  generations_per_selected_object: 3
  status: illustrative_profile
```

Числа являются примером стенда.
Реальный каталог приемки соответствует workload заказчика.
Для Range нужны объекты с известным содержимым каждого смещения.
Повторяющиеся нулевые байты недостаточны для проверки перестановки сегментов.

## Форма результата

```yaml
test_result:
  case_id: SLY-QA-001
  status: planned
  release: candidate-example
  platform_id: platform-example
  started_at: null
  completed_at: null
  input_digest: null
  expected: null
  observed: null
  evidence:
    response_digest: null
    metrics_snapshot: null
    log_excerpt: null
  reviewer: null
  issue_id: null
```

Статусы: planned, running, passed, failed, blocked, waived.
Waived требует основания и владельца исключения.
Blocked не считается passed.
Flaky-тест не скрывается повторным запуском до случайного успеха.

## Правила набора сценариев

Каждый сценарий ниже задает самостоятельный риск.
Параметры выбирают из паспорта стенда.
Контрольные суммы считают независимым инструментом.
Метрики сверяют с журналом генератора нагрузки.

Проверки разрушения состояния выполняют на копии тестовых данных.
План не содержит команд форматирования или удаления production-дисков.
Для аварии применяют управляемый fault injector стенда.
Отключение компонента должно иметь ограниченный blast radius.

## Критерии остановки испытания

- Повреждены данные за пределами тестового каталога.
- Нагрузка попала на неразрешенный origin.
- Секрет оказался в общем журнале.
- Контроллер выбрал production pool.
- Генератор нагрузки потерял контроль над скоростью.
- Фактический расход места вышел за бюджет стенда.
- Произошла недиагностируемая потеря устойчивого состояния.

При остановке сохраняют evidence.
Повтор разрешается после устранения причины выхода за границы.
Результат остановленного теста не считается успешным.

## Каталог приемочных сценариев

## Платформа и загрузка

### SLY-QA-001: Загрузка совместимого модуля

Предусловие: Установлена точная пара Nginx и модуля из манифеста.
Действие: Выполнить конфигурационный тест и контрольный HTTP-запрос.
Ожидаемый результат: Модуль загружен; ответ совпадает с эталоном.

Доказательство: Версии, configure fingerprint и digest ответа.
Инвариант: Совместимость относится к этой паре бинарных файлов.

### SLY-QA-002: Несовместимый ABI

Предусловие: Подготовлен отдельный кандидат с несовместимой сборкой Nginx.
Действие: Попытаться проверить конфигурацию на изолированном стенде.
Ожидаемый результат: Несовместимость обнаружена до приема клиентской нагрузки.

Доказательство: Код ошибки и отсутствие активированного поколения.
Инвариант: Нельзя продолжать запуск после неуспешного preflight.

### SLY-QA-003: Отсутствующая библиотека

Предусловие: В образе кандидата отсутствует обязательная runtime-зависимость.
Действие: Запустить preflight сборки.
Ожидаемый результат: Получено понятное сообщение с именем зависимости.

Доказательство: Отчет зависимостей без секретов.
Инвариант: Не допускается молчаливое отключение защиты.

### SLY-QA-004: Архитектура arm64

Предусловие: Собран отдельный arm64-кандидат и эталонный каталог.
Действие: Выполнить запросы, reload и восстановление индекса.
Ожидаемый результат: Байты и состояния совпадают с контрактом x86_64.

Доказательство: Паспорт arm64 и контрольные суммы.
Инвариант: Результат x86_64 не подменяет этот тест.

### SLY-QA-005: Ограничение файловых дескрипторов

Предусловие: Задан небольшой тестовый LimitNOFILE.
Действие: Повысить число соединений до границы ограничения.
Ожидаемый результат: Ошибка ресурса обрабатывается без повреждения worker state.

Доказательство: Число fd, ошибки и восстановление после спада.
Инвариант: Новые запросы не приводят к бесконечному CPU loop.

### SLY-QA-006: SELinux enforcement

Предусловие: Включена политика SELinux для тестового профиля.
Действие: Выполнить чтение кеша, IPC и запрос к origin.
Ожидаемый результат: Разрешенные пути работают; запрещенный путь фиксируется audit.

Доказательство: Policy version и обезличенные audit записи.
Инвариант: Проверка не требует отключения SELinux.

### SLY-QA-007: AppArmor enforcement

Предусловие: Загружен профиль AppArmor с путями поставки.
Действие: Выполнить штатные операции и запрещенное чтение вне профиля.
Ожидаемый результат: Штатные операции разрешены; выход за профиль отклонен.

Доказательство: Профиль и результаты обеих проб.
Инвариант: Один успех HTTP не доказывает корректность всех прав.

### SLY-QA-008: Отсутствующий mount кеша

Предусловие: Конфигурация указывает отдельный mount.
Действие: Запустить кандидат при недоступном mount на стенде.
Ожидаемый результат: Узел не записывает кеш в случайный корневой каталог.

Доказательство: Readiness, причина ошибки и фактический mount.
Инвариант: Проверяется идентичность файловой системы, а не только путь.

### SLY-QA-009: Смена страницы памяти

Предусловие: Профиль платформы отличается допустимым размером страницы.
Действие: Выполнить операции shared memory и mmap индекса.
Ожидаемый результат: Выравнивание не нарушает данные и границы буферов.

Доказательство: Параметры платформы и sanitizer либо эквивалентная проверка.
Инвариант: Формат диска не зависит от размера страницы памяти.

### SLY-QA-010: Контейнерный read-only root

Предусловие: Контейнер имеет read-only root и отдельные writable volumes.
Действие: Запустить модуль и агент с полным smoke-тестом.
Ожидаемый результат: Запись происходит только в разрешенные volumes.

Доказательство: Список mounts и события отказа ненужных записей.
Инвариант: Пакет не требует произвольной записи в образ.

## Конфигурация и политики

### SLY-QA-011: Синтаксическая ошибка YAML

Предусловие: Активна корректная конфигурация.
Действие: Подать кандидат с нарушенной структурой YAML.
Ожидаемый результат: Валидатор отклоняет кандидат; активное поколение сохранено.

Доказательство: Путь ошибки и active generation.
Инвариант: Ошибка кандидата не изменяет работающие правила.

### SLY-QA-012: Неизвестный ключ

Предусловие: Кандидат содержит опечатку в обязательном параметре.
Действие: Выполнить строгую проверку схемы.
Ожидаемый результат: Неизвестный ключ обнаружен с полным путем.

Доказательство: Диагностика поля без вывода секрета.
Инвариант: Опечатка не превращается в незаметный default.

### SLY-QA-013: Несовместимая schema version

Предусловие: Версия схемы выше поддерживаемой компонентом.
Действие: Попытаться подготовить поколение.
Ожидаемый результат: Подготовка отклоняется до изменения состояния.

Доказательство: Версии producer и consumer конфигурации.
Инвариант: Нельзя частично интерпретировать неизвестную схему.

### SLY-QA-014: Отрицательная квота

Предусловие: В тестовом кандидате задан отрицательный cache budget.
Действие: Выполнить семантическую проверку.
Ожидаемый результат: Параметр отклонен с объяснением единиц и диапазона.

Доказательство: Код SLY-CFG-002 и путь параметра.
Инвариант: Преобразование к unsigned не создает огромную квоту.

### SLY-QA-015: Переполнение числового поля

Предусловие: Кандидат содержит число больше диапазона uint64.
Действие: Прочитать и проверить конфигурацию.
Ожидаемый результат: Переполнение отклонено без wraparound.

Доказательство: Точное входное значение и сообщение валидатора.
Инвариант: Нельзя молча обрезать число.

### SLY-QA-016: Атомарное поколение

Предусловие: Идет длительный контрольный ответ по старой политике.
Действие: Активировать новое поколение с иными квотами.
Ожидаемый результат: Запрос использует согласованное поколение; новые запросы получают новое.

Доказательство: Request trace и поколения в audit.
Инвариант: Один запрос не смешивает несовместимые части конфигурации.

### SLY-QA-017: Ошибка подготовки секрета

Предусловие: Новый кандидат ссылается на отсутствующий secret id.
Действие: Выполнить preflight кандидата.
Ожидаемый результат: Активация отклонена; старый keyset доступен.

Доказательство: Код ошибки и active keyset version.
Инвариант: Значение отсутствующего секрета не угадывается.

### SLY-QA-018: Исключение Lua

Предусловие: Тестовая политика выбрасывает контролируемое исключение.
Действие: Отправить запрос в соответствующий маршрут.
Ожидаемый результат: Результат соответствует утвержденной политике ошибки; worker жив.

Доказательство: Policy error metric и безопасный reason code.
Инвариант: Исключение не позволяет выдать защищенные байты.

### SLY-QA-019: Лимит исполнения Lua

Предусловие: Политика выполняет ограниченно моделируемую долгую операцию.
Действие: Превысить установленный бюджет в тестовом runtime.
Ожидаемый результат: Выполнение прервано или изолировано согласованным механизмом.

Доказательство: Длительность и отсутствие блокировки остальных запросов.
Инвариант: Лимит должен быть реализуемым в выбранном runtime.

### SLY-QA-020: Откат конфигурации

Предусловие: Активирован кандидат с обнаруженной регрессией.
Действие: Вернуть предыдущее сохраненное поколение.
Ожидаемый результат: Восстановлены прежние решения и параметры.

Доказательство: Audit rollback и контрольный набор запросов.
Инвариант: Откат не должен зависеть от ручного пересоздания секрета.

## Допуск и подписи

### SLY-QA-021: Валидный токен

Предусловие: Известны ключ, ресурс и допустимый срок.
Действие: Отправить запрос с корректной подписью.
Ожидаемый результат: Защищенный объект выдан с правильными байтами.

Доказательство: Ответ, reason valid_signature и logical_request.
Инвариант: Допуск предшествует чтению защищенного содержимого.

### SLY-QA-022: Измененная подпись

Предусловие: У корректного токена изменен один байт подписи.
Действие: Отправить запрос к прогретому объекту.
Ожидаемый результат: Запрос получает 403 по политике; тело объекта не выдается.

Доказательство: Код ответа и отсутствие полезных байтов.
Инвариант: Hit в кеше не обходит проверку подписи.

### SLY-QA-023: Истекший токен

Предусловие: Срок действия закончился за пределами допустимого skew.
Действие: Выполнить запрос к холодному объекту.
Ожидаемый результат: Получен отказ с внутренней причиной expired_signature.

Доказательство: Время проверки и отсутствие origin fetch.
Инвариант: Запрещенный запрос не прогревает кеш.

### SLY-QA-024: Токен из будущего

Предусловие: Issued_at находится за разрешенной границей.
Действие: Проверить запрос при синхронизированных часах.
Ожидаемый результат: Токен отклонен согласно временной политике.

Доказательство: Обезличенные временные поля и reason.
Инвариант: Будущее время не увеличивает срок доступа неограниченно.

### SLY-QA-025: Неизвестный key id

Предусловие: Токен подписан ключом вне активного keyset.
Действие: Отправить запрос на canary.
Ожидаемый результат: Получен контролируемый отказ без fallback на случайный ключ.

Доказательство: Key id, keyset version и reason unknown_key.
Инвариант: Неизвестный ключ не считается сетевой ошибкой origin.

### SLY-QA-026: Подмена ресурса

Предусловие: Валидная подпись получена для одного object id.
Действие: Использовать ее для другого защищенного объекта.
Ожидаемый результат: Доступ ко второму объекту отклонен.

Доказательство: Оба нормализованных идентификатора и результат.
Инвариант: Scope подписи связывает токен с ресурсом.

### SLY-QA-027: Канонизация query

Предусловие: Есть согласованная схема порядка и encoding параметров.
Действие: Проверить эквивалентные и семантически измененные варианты.
Ожидаемый результат: Эквивалентные формы обрабатываются одинаково; изменение scope отклонено.

Доказательство: Таблица входов и решений.
Инвариант: Двойное декодирование не расширяет доступ.

### SLY-QA-028: Ротация keyset

Предусловие: Старый и новый ключи находятся в периоде перекрытия.
Действие: Выполнить запросы обоими ключами до и после завершения overlap.
Ожидаемый результат: Решения совпадают с утвержденным временем ротации.

Доказательство: Keyset audit и временные границы.
Инвариант: Старый ключ не удаляется раньше допустимых токенов.

### SLY-QA-029: Недоверенный forwarded header

Предусловие: Клиент может самостоятельно послать X-Forwarded-For.
Действие: Проверить запрос вне доверенного proxy и через доверенный proxy.
Ожидаемый результат: Идентичность клиента определяется только принятой trust chain.

Доказательство: Trace нормализованного источника без полного IP в метриках.
Инвариант: Заголовок не становится доказательством доверия.

### SLY-QA-030: Отказ политики на RAM hit

Предусловие: Объект уже находится в RAM-кеше.
Действие: Отправить запрос, запрещенный отдельным правилом tenant.
Ожидаемый результат: Возвращается 403; содержимое RAM не выдается.

Доказательство: Decision metric и отсутствие response content bytes.
Инвариант: Политика применяется независимо от уровня хранения.

## HTTP и Range

### SLY-QA-031: Полный GET

Предусловие: Объект известной длины отсутствует в кеше.
Действие: Прочитать объект целиком и повторить GET.
Ожидаемый результат: Оба ответа совпадают с эталоном.

Доказательство: Checksum обоих ответов и fetch count.
Инвариант: Первый fill не изменяет байтовый контракт.

### SLY-QA-032: HEAD

Предусловие: Есть объект с известными метаданными.
Действие: Сравнить HEAD с соответствующим GET.
Ожидаемый результат: Заголовки согласованы; HEAD не отдает тело.

Доказательство: Статус, Content-Length и число переданных байтов.
Инвариант: HEAD не должен учитываться как полный просмотр всех байтов.

### SLY-QA-033: Один диапазон

Предусловие: Известно содержимое объекта по смещениям.
Действие: Запросить диапазон внутри одного сегмента.
Ожидаемый результат: Отданы правильные смещения и Content-Range.

Доказательство: Checksum диапазона и заголовки.
Инвариант: Смещение не округляется наружу в клиентском ответе.

### SLY-QA-034: Диапазон на границе сегмента

Предусловие: Начало и конец пересекают границу сегмента.
Действие: Прочитать Range из двух сегментов.
Ожидаемый результат: Ответ соединяет правильные части без пропуска и повтора.

Доказательство: Побайтовое сравнение и segment traces.
Инвариант: Граница внутреннего кеша не видна как изменение данных.

### SLY-QA-035: Последний неполный сегмент

Предусловие: Размер объекта не кратен segment size.
Действие: Прочитать последние байты и весь последний сегмент.
Ожидаемый результат: Отдано ровно существующее число байтов.

Доказательство: Длина и checksum хвоста.
Инвариант: Неинициализированные байты не попадают в ответ.

### SLY-QA-036: Диапазон вне объекта

Предусловие: Запрошено начало за концом объекта.
Действие: Выполнить запрос к холодному и прогретому состоянию.
Ожидаемый результат: HTTP-поведение одинаково и соответствует выбранному контракту.

Доказательство: Статус и метаданные обоих ответов.
Инвариант: Некорректный Range не провоцирует огромную аллокацию.

### SLY-QA-037: Suffix Range

Предусловие: Объект имеет известный хвост.
Действие: Запросить последние N байтов.
Ожидаемый результат: Отдан правильный хвост после нормализации диапазона.

Доказательство: Ожидаемый offset и response digest.
Инвариант: Отрицательные значения не попадают в unsigned arithmetic.

### SLY-QA-038: Несколько диапазонов

Предусловие: В профиле явно определена поддержка multipart ranges.
Действие: Запросить несколько непересекающихся диапазонов.
Ожидаемый результат: Результат соответствует объявленной capability.

Доказательство: Multipart headers либо документированный отказ.
Инвариант: Неподдерживаемый режим не выдается за частичный успех.

### SLY-QA-039: Изменение поколения во время fill

Предусловие: Origin способен переключить объект на новую generation.
Действие: Сменить generation между чтением метаданных и завершением fill.
Ожидаемый результат: Смешанная версия не публикуется как готовая.

Доказательство: Две версии метаданных и факт отбрасывания копии.
Инвариант: Один объект не собирается из разных поколений.

### SLY-QA-040: Отмена клиентом

Предусловие: Клиент закрывает соединение в середине большого ответа.
Действие: Повторить отмену при единственном клиенте и при других waiters.
Ожидаемый результат: Ресурсы освобождены; общий fill сохраняется только по принятой политике.

Доказательство: Active requests, fill state и фактические байты.
Инвариант: Отмена одного waiter не ломает ответы остальных.

## Уровни кеша

### SLY-QA-041: Попадание RAM

Предусловие: Сегмент опубликован в RAM.
Действие: Повторить разрешенный запрос.
Ожидаемый результат: Origin не читается; байты совпадают с эталоном.

Доказательство: Tier ram и отсутствие нового origin fetch.
Инвариант: RAM hit проходит проверку generation.

### SLY-QA-042: Попадание NVMe

Предусловие: RAM отсутствует, сегмент опубликован на NVMe.
Действие: Прочитать объект при нормальном состоянии диска.
Ожидаемый результат: Данные обслужены выбранным уровнем.

Доказательство: Lookup nvme hit и response checksum.
Инвариант: Наличие записи индекса не заменяет чтение правильного файла.

### SLY-QA-043: Попадание SSD

Предусловие: Объект расположен в SSD-профиле отдельно от NVMe.
Действие: Запросить объект и проверить учет tier.
Ожидаемый результат: SSD учитывается своим уровнем без смешения классов.

Доказательство: Metric labels и путь монтирования.
Инвариант: Название tier соответствует реальному ресурсу профиля.

### SLY-QA-044: Попадание HDD

Предусловие: Холодный объект опубликован в HDD-кеше.
Действие: Прочитать последовательный Range под умеренной нагрузкой.
Ожидаемый результат: Ответ корректен; задержка отражает фактическое чтение.

Доказательство: HDD lookup и latency histogram.
Инвариант: Медленное чтение не подменяется ложным miss.

### SLY-QA-045: Частичное попадание

Предусловие: Половина нужных сегментов прогрета.
Действие: Прочитать диапазон через теплую и холодную части.
Ожидаемый результат: Прочитаны только недостающие сегменты; ответ полный.

Доказательство: Partial_hit, byte ratio и fetch map.
Инвариант: Полный запрос не учитывается одновременно как hit и miss.

### SLY-QA-046: Отказ admission крупного объекта

Предусловие: Объект превышает настроенный размер admission.
Действие: Прочитать его через допустимый origin путь.
Ожидаемый результат: Ответ возможен без неконтролируемого заполнения кеша.

Доказательство: Admission reason too_large и cache used bytes.
Инвариант: Лимит кеширования не должен искажать разрешенные байты.

### SLY-QA-047: TTL и generation

Предусловие: Есть сегмент с истекшим TTL и неизменным origin.
Действие: Выполнить запрос по выбранной политике проверки актуальности.
Ожидаемый результат: Поведение соответствует stale/revalidate контракту.

Доказательство: Метаданные freshness и количество origin обращений.
Инвариант: Просроченность и повреждение не смешиваются.

### SLY-QA-048: Eviction под чтением

Предусловие: Несколько клиентов читают один опубликованный сегмент.
Действие: Запустить eviction этого сегмента на тестовом уровне.
Ожидаемый результат: Текущие чтения завершаются корректно; новые используют доступный путь.

Доказательство: Refcount или эквивалентное доказательство и checksum.
Инвариант: Удаление индекса не закрывает чужой активный descriptor.

### SLY-QA-049: Quarantine поврежденных байтов

Предусловие: В копии тестового кеша изменен байт сегмента.
Действие: Выполнить чтение с включенной проверкой.
Ожидаемый результат: Сегмент изолирован и не выдан как правильный.

Доказательство: Integrity failure и последующий verified refill.
Инвариант: Поврежденные данные не продвигаются на другие уровни.

### SLY-QA-050: Orphan после аварии

Предусловие: Существует завершенный файл без committed index entry.
Действие: Выполнить reconcile после перезапуска.
Ожидаемый результат: Файл распознан как orphan и обработан принятой процедурой.

Доказательство: Reconcile report и orphan bytes.
Инвариант: Случайный файл не признается достоверным только по имени.

## Конкуренция и нагрузка

### SLY-QA-051: Одновременный холодный ключ

Предусловие: Кеш пуст для одного сегмента.
Действие: Одновременно отправить большую группу одинаковых запросов.
Ожидаемый результат: Физические fetch ограничены single-flight контрактом.

Доказательство: Logical count, waiter count и origin count.
Инвариант: Популярность учитывает всех легитимных клиентов.

### SLY-QA-052: Смерть владельца fill

Предусловие: Группа клиентов ожидает fill одного сегмента.
Действие: Аварийно завершить владельца fill на стенде.
Ожидаемый результат: Ожидающие освобождены или переданы новому владельцу.

Доказательство: Lock state и время восстановления.
Инвариант: Блокировка не остается вечной.

### SLY-QA-053: Разные горячие ключи

Предусловие: Каталог содержит несколько независимых горячих объектов.
Действие: Нагрузить их одновременно.
Ожидаемый результат: Нет глобальной блокировки, сериализующей все ключи.

Доказательство: Throughput и распределение ожидания по ключам.
Инвариант: Лимит origin сохраняется при независимых fill.

### SLY-QA-054: Медленный origin

Предусловие: Origin искусственно задерживает первый байт.
Действие: Создать нагрузку до установленного concurrency budget.
Ожидаемый результат: Очередь ограничена; таймауты классифицированы.

Доказательство: Inflight, queue depth и latency.
Инвариант: Worker не блокирует event loop синхронным ожиданием.

### SLY-QA-055: Медленный клиент

Предусловие: Клиент читает большой ответ с низкой скоростью.
Действие: Параллельно выполнять быстрые небольшие запросы.
Ожидаемый результат: Быстрые запросы остаются в принятом бюджете задержки.

Доказательство: Две latency выборки и память соединений.
Инвариант: Большой ответ не удерживает неограниченный RAM buffer.

### SLY-QA-056: Смена горячего набора

Предусловие: Нагрузка сначала сосредоточена на группе A.
Действие: Переключить спрос на непересекающуюся группу B.
Ожидаемый результат: Политика адаптируется с предусмотренным hysteresis.

Доказательство: Временной ряд admission и размещения.
Инвариант: Старые горячие объекты не занимают tier бесконечно.

### SLY-QA-057: Широкое последовательное сканирование

Предусловие: Есть рабочий hot set и большой одноразовый каталог.
Действие: Выполнить последовательное чтение одноразового каталога.
Ожидаемый результат: Admission ограничивает вытеснение полезного набора.

Доказательство: Hit ratio hot set до и после и eviction reasons.
Инвариант: Однократный объем не равен устойчивой популярности.

### SLY-QA-058: Reload на пике

Предусловие: Узел работает на согласованной пиковой нагрузке.
Действие: Активировать новое поколение без изменения поведения.
Ожидаемый результат: Нет потери корректности; память overlap укладывается в бюджет.

Доказательство: RSS, active workers и error rate.
Инвариант: Результат idle reload не подменяет этот сценарий.

### SLY-QA-059: Cgroup memory pressure

Предусловие: Кандидат ограничен тестовым memory cgroup.
Действие: Довести нагрузки до границы принятого memory budget.
Ожидаемый результат: Admission и очереди ограничиваются до неконтролируемого роста.

Доказательство: Memory events и деградационный статус.
Инвариант: Нельзя обещать защиту от OOM без фактического измерения.

### SLY-QA-060: Устойчивый длительный прогон

Предусловие: Workload включает повторные, cold и Range-запросы.
Действие: Выполнить согласованный многочасовой soak.
Ожидаемый результат: Нет монотонной утечки памяти, fd и состояния заданий.

Доказательство: Тренды ресурсов и счетчики ошибок.
Инвариант: Пиковый краткий benchmark не заменяет длительную устойчивость.

## Семантика телеметрии

### SLY-QA-061: Спрос при полном hit

Предусловие: Объект целиком обслуживается RAM-кешем.
Действие: Отправить известное число разрешенных запросов.
Ожидаемый результат: Logical_request отражает все запросы при отсутствии fetch.

Доказательство: Счетчик генератора и принятые event ids.
Инвариант: Горячий кеш не скрывает популярность от Ceph-контроллера.

### SLY-QA-062: Отказы исключены из спроса

Предусловие: Смешаны валидные и невалидные подписи.
Действие: Выполнить известную смесь запросов.
Ожидаемый результат: Спрос содержит только разрешенные прикладные обращения.

Доказательство: Decision totals и агрегат demand.
Инвариант: Парсинг не повышает популярность защищенного объекта.

### SLY-QA-063: Один fetch для многих запросов

Предусловие: Single-flight объединяет группу клиентов.
Действие: Сверить события после завершения fill.
Ожидаемый результат: Много logical_request связано с одной физической загрузкой.

Доказательство: Fetch id, request ids и число waiters.
Инвариант: Физическая нагрузка и прикладной спрос не смешиваются.

### SLY-QA-064: Повтор одного события

Предусловие: Collector уже принял событие с event id.
Действие: Повторно передать ту же запись.
Ожидаемый результат: Запись дедуплицирована; спрос не увеличен.

Доказательство: Duplicates counter и неизменный агрегат.
Инвариант: Transport retry не создает новый просмотр.

### SLY-QA-065: Повтор batch после timeout

Предусловие: Collector сохранил batch, но ответ потерян.
Действие: Повторить batch агентом.
Ожидаемый результат: Все записи учтены ровно один раз на уровне агрегации.

Доказательство: Checkpoint и dedup evidence.
Инвариант: Timeout отправителя не доказывает отсутствие приема.

### SLY-QA-066: Новый producer epoch

Предусловие: Есть неподтвержденный WAL предыдущего запуска.
Действие: Перезапустить агент и создать новые события.
Ожидаемый результат: Старый replay и новый sequence не конфликтуют.

Доказательство: Тройки producer_id, producer_epoch и sequence.
Инвариант: Сброс локального счетчика не создает одинаковые event ids.

### SLY-QA-067: Позднее событие

Предусловие: Окно агрегации закрыто по обычному расписанию.
Действие: Доставить допустимо позднее событие.
Ожидаемый результат: Применяется документированная correction или late policy.

Доказательство: Revision агрегата и late counter.
Инвариант: Поздний спрос не добавляется в случайное текущее окно.

### SLY-QA-068: Частичный producer coverage

Предусловие: Один из ожидаемых узлов не доставляет данные.
Действие: Сформировать окно размещения.
Ожидаемый результат: Окно помечено неполным; рискованные demotion ограничены.

Доказательство: Expected/reporting producers и confidence.
Инвариант: Отсутствие узла не интерпретируется как нулевой спрос.

### SLY-QA-069: Неизвестная версия события

Предусловие: Producer отправляет неподдерживаемую schema version.
Действие: Принять batch на collector.
Ожидаемый результат: Получен явный отказ либо согласованный quarantine.

Доказательство: Schema error без изменения demand.
Инвариант: Неизвестные поля не должны менять обязательную семантику.

### SLY-QA-070: Агрегат заменяющей revision

Предусловие: Для одного окна уже принят absolute aggregate.
Действие: Передать исправленную revision с большим номером.
Ожидаемый результат: Новое значение заменяет старое по контракту.

Доказательство: Итог окна и audit revision.
Инвариант: Absolute значения не суммируются как delta.

## Очереди и WAL

### SLY-QA-071: Collector недоступен

Предусловие: Агент имеет доступный WAL и согласованный запас.
Действие: Отключить тестовый collector на ограниченный интервал.
Ожидаемый результат: Клиентская выдача продолжается; очередь растет измеримо.

Доказательство: Queue bytes, oldest age и latency клиента.
Инвариант: Недоступная аналитика не блокирует event loop.

### SLY-QA-072: Возврат collector

Предусловие: В WAL накоплен известный набор событий.
Действие: Восстановить collector и разрешить ограниченный replay.
Ожидаемый результат: Очередь уменьшается без перегрузки приемника.

Доказательство: Export rate, acknowledgements и итог demand.
Инвариант: Replay учитывает текущий входящий поток.

### SLY-QA-073: Лимит memory queue

Предусловие: Установлен небольшой тестовый лимит памяти очереди.
Действие: Подать события быстрее устойчивой записи.
Ожидаемый результат: Применяется объявленный backpressure или downgrade.

Доказательство: Queue limit и явная причина деградации.
Инвариант: Память не растет без границы.

### SLY-QA-074: Лимит WAL bytes

Предусловие: WAL достиг тестового верхнего предела.
Действие: Продолжить ограниченный входящий поток.
Ожидаемый результат: Потери или агрегация отражены явно по принятой политике.

Доказательство: Dropped counters и quality interval.
Инвариант: Потерю наблюдения нельзя скрыть успешным status.

### SLY-QA-075: Неполный хвост записи

Предусловие: На тестовой копии WAL оборвана последняя запись.
Действие: Запустить recovery.
Ожидаемый результат: Восстановлены целые записи; хвост обработан отдельно.

Доказательство: Recovery report и перечень sequence.
Инвариант: Неполный хвост не делает весь журнал пустым.

### SLY-QA-076: Повреждение середины WAL

Предусловие: В тестовом сегменте изменены байты одной записи.
Действие: Проверить и восстановить журнал.
Ожидаемый результат: Диапазон повреждения обнаружен и изолирован.

Доказательство: Checksum failure и известный диапазон потерь.
Инвариант: Поврежденные записи не отправляются как достоверные.

### SLY-QA-077: Ошибка fsync

Предусловие: Fault injector возвращает ошибку устойчивой записи.
Действие: Попытаться зафиксировать batch событий.
Ожидаемый результат: Устойчивость не подтверждается ложным acknowledgement.

Доказательство: Fsync error и состояние checkpoint.
Инвариант: Успешный write не подменяет гарантию выбранного WAL режима.

### SLY-QA-078: Завершение с очередью

Предусловие: Есть неподтвержденные события и активный exporter.
Действие: Выполнить штатный shutdown в тестовом окне.
Ожидаемый результат: Состояние закреплено; restart продолжает доставку.

Доказательство: До/после checkpoint и event set.
Инвариант: Shutdown не теряет очередь молча.

### SLY-QA-079: Подтверждение вне порядка

Предусловие: Collector подтверждает несколько batch с перестановкой.
Действие: Проверить продвижение checkpoint.
Ожидаемый результат: Удаляются только диапазоны, безопасные по контракту.

Доказательство: Acknowledgement timeline и оставшиеся сегменты.
Инвариант: Более новый ack не означает подтверждение пропуска.

### SLY-QA-080: Длительный backlog и retention

Предусловие: Возраст очереди достигает принятого тестового предела.
Действие: Выполнить retention policy на изолированных данных.
Ожидаемый результат: Удаленные интервалы отражены как потеря наблюдения.

Доказательство: Retention audit и quality markers.
Инвариант: Контроллер не принимает отсутствие старых событий за холодность.

## Ceph и размещение

### SLY-QA-081: Расчет promotion

Предусловие: Объект имеет устойчивый спрос выше порога.
Действие: Сформировать полное окно и запустить planner.
Ожидаемый результат: Создано одно обоснованное задание promotion.

Доказательство: Demand inputs, policy version и job record.
Инвариант: Planner учитывает спрос, обслуженный локальным кешем.

### SLY-QA-082: Расчет demotion

Предусловие: Объект холоден дольше принятого интервала.
Действие: Выполнить planner при полном наблюдении.
Ожидаемый результат: Создано допустимое задание в cold pool.

Доказательство: Cold duration и target pool.
Инвариант: Одного короткого спада недостаточно при включенном hysteresis.

### SLY-QA-083: Cooldown после миграции

Предусловие: Объект только что сменил класс.
Действие: Подать пограничные колебания спроса.
Ожидаемый результат: Контроллер не перемещает его обратно до нужного условия.

Доказательство: Timeline решений и cooldown state.
Инвариант: Частый churn должен быть ограничен политикой.

### SLY-QA-084: Недостаточная емкость назначения

Предусловие: Target pool не имеет требуемого резерва.
Действие: Попытаться допустить новое задание.
Ожидаемый результат: Задание приостановлено с понятной причиной.

Доказательство: Capacity snapshot и pause reason.
Инвариант: Копирование не начинается по устаревшей оценке места.

### SLY-QA-085: Ceph degraded перед стартом

Предусловие: Кластер теста находится в согласованном degraded состоянии.
Действие: Запустить планируемую очередь миграций.
Ожидаемый результат: Новые рискованные задания не стартуют.

Доказательство: Health snapshot и placement_paused.
Инвариант: Пауза размещения не подменяет доступность клиентских чтений.

### SLY-QA-086: Ошибка копирования

Предусловие: Fault injector обрывает копию в target pool.
Действие: Выполнить миграцию одного тестового объекта.
Ожидаемый результат: Locator не переключен; источник доступен.

Доказательство: Job stage, source digest и target state.
Инвариант: Неполная копия не считается завершенной миграцией.

### SLY-QA-087: Ошибка checksum назначения

Предусловие: После копирования тестовая цель повреждена.
Действие: Выполнить этап verify.
Ожидаемый результат: Commit отклонен; исходная копия сохранена.

Доказательство: Integrity failure и неизменный locator.
Инвариант: Copy complete не равен verified complete.

### SLY-QA-088: Конфликт generation при commit

Предусловие: Origin изменил объект после начала миграции.
Действие: Попытаться переключить locator старого задания.
Ожидаемый результат: Optimistic commit отклонен как устаревший.

Доказательство: Expected/actual generation и conflict counter.
Инвариант: Старое задание не заменяет более новый объект.

### SLY-QA-089: Повтор команды миграции

Предусловие: Одно job id уже находится в running или completed.
Действие: Повторно доставить команду исполнителю.
Ожидаемый результат: Исполнение идемпотентно относительно состояния задания.

Доказательство: Количество фактических копий и job audit.
Инвариант: Повтор команды не создает независимую вторую миграцию.

### SLY-QA-090: Сбой после locator commit

Предусловие: Locator переключен, но очистка источника еще не завершена.
Действие: Перезапустить executor на стенде.
Ожидаемый результат: Состояние восстановлено; чтение идет по корректному locator.

Доказательство: Committed generation и cleanup resume.
Инвариант: Recovery не откатывает указатель на неподходящую версию.

## Диагностика и безопасность журналов

### SLY-QA-091: Разделение 403

Предусловие: Модуль и origin способны вернуть 403.
Действие: Вызвать обе причины на тестовых маршрутах.
Ожидаемый результат: Источники различимы в метриках и логах.

Доказательство: Две записи reason/source.
Инвариант: Origin authorization failure не объявляется блокировкой парсинга.

### SLY-QA-092: Редактирование токена

Предусловие: Запрос содержит контрольную строку в секретном поле.
Действие: Собрать access, error, audit и debug logs.
Ожидаемый результат: Контрольная секретная строка отсутствует.

Доказательство: Результат поиска по диагностическому пакету.
Инвариант: Debug не отменяет редактирование секретов.

### SLY-QA-093: Ограничение cardinality

Предусловие: Генератор использует множество object ids и URL.
Действие: Снять число рядов exporter после нагрузки.
Ожидаемый результат: Количество рядов остается в принятом лимите.

Доказательство: Series count и список labels.
Инвариант: Object id не появляется как постоянный metric label.

### SLY-QA-094: Counter reset

Предусловие: Есть накопленные метрики и известный входящий поток.
Действие: Перезапустить компонент и построить rate.
Ожидаемый результат: Dashboard корректно обрабатывает reset без отрицательной нагрузки.

Доказательство: Raw samples и итоговая панель.
Инвариант: Counter не обязан сохраняться между процессами.

### SLY-QA-095: Histogram единицы

Предусловие: Контрольные операции имеют известные задержки.
Действие: Сравнить значения histogram с измерением генератора.
Ожидаемый результат: Используются секунды; порядок величин совпадает.

Доказательство: Buckets, sum, count и независимый timer.
Инвариант: Миллисекунды не записываются в поле seconds.

### SLY-QA-096: Отсутствие scrape

Предусловие: Exporter недоступен при работающей выдаче.
Действие: Временно изолировать сеть мониторинга.
Ожидаемый результат: Панель показывает отсутствие данных, а не нулевую нагрузку.

Доказательство: Up metric и alert timeline.
Инвариант: Missing series не превращается в доказательство здоровья.

### SLY-QA-097: Audit неуспешной активации

Предусловие: Кандидат конфигурации отклоняется валидатором.
Действие: Выполнить попытку активации с известным actor.
Ожидаемый результат: Audit содержит actor, candidate и rejected result.

Доказательство: Запись audit и прежняя active generation.
Инвариант: Отказ изменения также подлежит прослеживаемости.

### SLY-QA-098: Trace общего fill

Предусловие: Несколько клиентов разделяют физический fetch.
Действие: Собрать выборочные traces для этой группы.
Ожидаемый результат: Общий fetch связан корректно без ложного единственного владельца.

Доказательство: Span links или эквивалентные связи.
Инвариант: Trace не приписывает один fetch всем запросам как отдельный.

### SLY-QA-099: Размер диагностического пакета

Предусловие: Логи содержат большой тестовый объем.
Действие: Собрать пакет с заданным лимитом.
Ожидаемый результат: Пакет ограничен; исключенные данные перечислены.

Доказательство: Manifest пакета и фактический размер.
Инвариант: Сбор диагностики не заполняет рабочий диск.

### SLY-QA-100: Качество окна с drops

Предусловие: Часть logical_request потеряна управляемым способом.
Действие: Сформировать агрегат популярности.
Ожидаемый результат: Окно помечено неполным и не вызывает массовую демоцию.

Доказательство: Dropped metric, quality field и решения planner.
Инвариант: График спроса должен показывать ограничение данных.

## Восстановление и обслуживание

### SLY-QA-101: Полный restart узла

Предусловие: Есть прогретый кеш и неподтвержденная телеметрия.
Действие: Перезапустить тестовый узел обычным способом.
Ожидаемый результат: Индекс и очередь восстановлены по контракту.

Доказательство: Ответы, checkpoint и active generation.
Инвариант: Теплый restart не теряет правила доступа.

### SLY-QA-102: Аварийный restart Nginx

Предусловие: Агент работает, worker выполняют запросы.
Действие: Аварийно завершить тестовый worker.
Ожидаемый результат: Остальные процессы сохраняют работоспособность; новый worker готов.

Доказательство: Supervisor log и клиентская выборка.
Инвариант: Shared memory не содержит вечных lock владельца.

### SLY-QA-103: Restart агента

Предусловие: Nginx обслуживает нагрузку с локальным IPC.
Действие: Перезапустить только агент.
Ожидаемый результат: Поведение выдачи соответствует принятому degraded profile.

Доказательство: IPC errors, latency и восстановленная очередь.
Инвариант: Worker не падает вместе с агентом.

### SLY-QA-104: Read-only filesystem

Предусловие: Тестовый mount переводится в управляемый read-only режим стенда.
Действие: Попытаться заполнить новый сегмент.
Ожидаемый результат: Ошибка записи диагностируется; существующие корректные чтения возможны.

Доказательство: Filesystem state и cache error code.
Инвариант: Нет бесконечного повторения записи в event loop.

### SLY-QA-105: Отсутствие inode

Предусловие: Тестовый filesystem достигает своего inode budget.
Действие: Выполнить admission нового сегмента.
Ожидаемый результат: Ошибка inode отличается от недостающих байтов.

Доказательство: Df inode evidence и admission result.
Инвариант: Большой free bytes не маскирует невозможность записи.

### SLY-QA-106: Drain длительного ответа

Предусловие: Есть длинная передача и короткие новые запросы.
Действие: Снять узел с балансировки и начать drain.
Ожидаемый результат: Новые запросы не поступают; активные завершаются в политике окна.

Доказательство: Balancer state и active request timeline.
Инвариант: Drain имеет измеримый конец.

### SLY-QA-107: Откат бинарного кандидата

Предусловие: Новый кандидат не менял несовместимо persistent schema.
Действие: Выполнить штатный rollback пары модуль–агент.
Ожидаемый результат: Возвращен прежний контракт допуска, кеша и событий.

Доказательство: Build info и повторный smoke.
Инвариант: Откат проверяет совместимость пары компонентов.

### SLY-QA-108: Несовместимый формат индекса

Предусловие: Кандидат изменил формат локального индекса.
Действие: Попытаться запустить старый бинарный файл с этим индексом.
Ожидаемый результат: Формат отклонен либо применяется заранее проверенный rebuild.

Доказательство: Version check и recovery report.
Инвариант: Старый код не интерпретирует новые байты наугад.

### SLY-QA-109: Восстановление backup конфигурации

Предусловие: Есть защищенная копия конфигурации и ссылки на секреты.
Действие: Развернуть новый тестовый узел из backup.
Ожидаемый результат: Восстановлены принятые правила и active generation.

Доказательство: Config digest, key references и контрольные запросы.
Инвариант: Наличие backup доказывается восстановлением, а не списком файлов.

### SLY-QA-110: Потеря локального кеша

Предусловие: Origin исправен; локальный тестовый кеш недоступен.
Действие: Ввести согласованный профиль обхода и выполнить запросы.
Ожидаемый результат: Данные восстановимы с контролируемой нагрузкой origin.

Доказательство: Fetch budget и checksum ответов.
Инвариант: Кеш не содержит единственную постоянную копию.

## Экономика, лицензия и выпуск

### SLY-QA-111: Измерение физических байтов

Предусловие: Есть baseline с определенными replication и EC параметрами.
Действие: Сравнить профиль SLOYRA на том же каталоге и требованиях надежности.
Ожидаемый результат: Отчет разделяет logical bytes, physical bytes и классы хранения.

Доказательство: Методика, исходные числа и формула.
Инвариант: Нельзя называть перенос SSD→HDD сокращением логического каталога.

### SLY-QA-112: Проверка цели до 70 процентов

Предусловие: Определены конкретный показатель и baseline.
Действие: Измерить экономию без изменения требований к доступности.
Ожидаемый результат: Публикуется фактический процент и условия, даже если цель не достигнута.

Доказательство: Сырые данные и воспроизводимый расчет.
Инвариант: Верхняя цель не превращается в гарантированный результат.

### SLY-QA-113: Цена прогрева

Предусловие: Есть холодный и устойчиво прогретый этапы workload.
Действие: Измерить оба этапа одинаковым генератором.
Ожидаемый результат: Отчет отдельно показывает startup overhead и steady state.

Доказательство: Origin bytes, latency и длительность этапов.
Инвариант: Холодный baseline не сравнивается с теплым кандидатом без оговорки.

### SLY-QA-114: Ограничение износа SSD

Предусловие: Известен объем фоновых записей и клиентской выдачи.
Действие: Выполнить workload со сменой горячего набора.
Ожидаемый результат: Отчет показывает migration writes и churn относительно полезных чтений.

Доказательство: Placement bytes и admission/eviction timeline.
Инвариант: Уменьшение latency не скрывает неограниченный объем записи.

### SLY-QA-115: Начало оплаченного срока

Предусловие: В тестовой учетной системе есть созданный, но неоплаченный заказ.
Действие: Затем подтвердить оплату контролируемым событием.
Ожидаемый результат: Начало срока связано с подтверждением по утвержденной модели.

Доказательство: Audit события и даты активации.
Инвариант: Создание чернового заказа само по себе не запускает срок.

### SLY-QA-116: Ручное продление

Предусловие: Лицензия приближается к окончанию тестового срока.
Действие: Выполнить утвержденную ручную процедуру продления.
Ожидаемый результат: Новый срок отражен после подтвержденного действия.

Доказательство: Аудит администратора и состояние лицензии.
Инвариант: Автоматическое списание не предполагается.

### SLY-QA-117: Учет активаций

Предусловие: В поставке 2.1.74 одна тестовая лицензия имеет лимит в одно активное место.
Действие: Активировать сервер, прекратить heartbeat, после expiry занять место
другим сервером и вернуть первый сервер.
Ожидаемый результат: Пока место занято, вернувшийся сервер получает отказ по лимиту;
история UUID не мешает занять освободившееся место.

Доказательство: `active_instances`, сроки `lease_expires_at` и ответы API.
Инвариант: Число одновременно действующих разрешений не превышает лимит.

### SLY-QA-118: Ограниченное разрешение при потере связи

Предусловие: Поставка 2.1.74 активирована, получена lease и прогрет кеш объекта.
Действие: Прервать связь с licensing и после срока текущей lease повторить
подписанный запрос к прогретому объекту.
Ожидаемый результат: Новый запрос получает 503. Heartbeat имеет интервал
60 секунд, разрешение действует не более 300 секунд с момента выдачи.

Доказательство: Срок из квитанции и HTTP-результат; фактический контракт
описан в `docs/LICENSING.md` внутри персонального ZIP выпуска.
Инвариант: Потеря связи не продлевает разрешение; начатые ответы не прерываются.

### SLY-QA-119: Проверка полноты release evidence

Предусловие: Кандидат прошел обязательные функциональные и recovery tests.
Действие: Сверить манифест поставки с отчетами.
Ожидаемый результат: Каждый бинарный digest связан с результатами своей платформы.

Доказательство: Release evidence index.
Инвариант: Отчет другой сборки не подтверждает текущий пакет.

### SLY-QA-120: Финальная canary приемка

Предусловие: Есть базовая линия и заранее согласованные пределы.
Действие: Выполнить ограниченное внедрение с окном наблюдения.
Ожидаемый результат: Решение о расширении или rollback принято по заданным критериям.

Доказательство: Canary report, метрики и audit решения.
Инвариант: Пороги не изменяются задним числом ради успешного статуса.

## Нагрузочные профили

Числовые профили ниже предназначены для проектирования стенда.
Они не являются гарантией пропускной способности продукта.
Перед запуском их приводят к реальному оборудованию и workload.

### Профиль преимущественно горячего набора

Большинство запросов приходится на небольшой набор объектов.
Hot set должен помещаться в выбранный быстрый уровень.
Небольшая доля обращений идет к случайным холодным объектам.
Профиль проверяет пользу RAM и отсутствие лишних origin fetch.

```yaml
load_profile_example:
  name: hot_set
  request_distribution:
    hot: 0.90
    warm: 0.08
    cold: 0.02
  read_mix:
    full_get: 0.30
    range_get: 0.65
    head: 0.05
  phases:
    warmup: 15m
    steady: 60m
    cooldown: 10m
  acceptance_limits: defined_per_platform
```

Показатели warmup не исключают из отчета.
Их показывают отдельно от steady state.
При повторе теста фиксируют начальное состояние кеша.

### Профиль смены популярности

В середине теста горячая группа объектов меняется.
Новая группа имеет сопоставимые размеры файлов.
Контроллер должен обнаружить изменение спроса.
Нужно измерить стоимость адаптации и число лишних миграций.

```yaml
load_profile_example:
  name: popularity_shift
  groups:
    - id: group_a
      active_window: first_half
    - id: group_b
      active_window: second_half
  overlap_ratio: 0.10
  minimum_observation_windows: 6
  collect:
    - client_latency
    - origin_bytes
    - migration_bytes
    - cache_churn
    - placement_confidence
```

Нельзя оценить скорость адаптации по одному итоговому hit ratio.
Нужен временной ряд перехода между группами.
Следует сравнить hysteresis и отсутствие hysteresis на стенде.
Изменение параметров должно иметь измеримый эффект.

### Профиль длинного хвоста

Каждый объект получает мало запросов.
Совокупный каталог значительно больше кеша.
Профиль проверяет admission и защиту от бессмысленной записи.
Система не должна пытаться сохранять все прочитанные данные.

Измеряют:

- долю отказов admission;
- стоимость однократной записи;
- вытеснение полезных объектов;
- число повторных обращений после eviction;
- нагрузку HDD;
- объем origin traffic;
- расход inode.

### Профиль конкуренции миграций и выдачи

Клиентская нагрузка остается постоянной.
Фоновый бюджет миграций повышают ступенями.
На каждой ступени выдерживают устойчивое окно.
Предел выбирают по качеству клиентской выдачи.

Измеряют одновременно:

- p95 и p99 ответа;
- origin latency;
- Ceph latency;
- скорость verified copy;
- очередь дисков;
- свободную емкость;
- pauses контроллера;
- ошибки checksum.

Максимальная скорость копирования не является единственной целью.
Приоритеты должны соответствовать принятому сервисному профилю.

## Проверка runbooks

Runbook принимается после практического прохождения.
Оператор, не писавший реализацию, должен суметь выполнить диагностику.
Ему доступны только предусмотренные команды, панели и журналы.
Нераскрытая внутренняя информация разработчика не считается частью процедуры.

| Runbook | Контрольный сценарий | Что должен установить оператор |
| --- | --- | --- |
| Заполнение диска | SLY-QA-041–050 и SLY-QA-105 | Байты или inode, cache или WAL |
| Рост RSS | SLY-QA-058–060 | Кеш, очередь, соединения или overlap |
| Stampede | SLY-QA-051–054 | Один ключ или общий перегруз origin |
| Истекшие подписи | SLY-QA-023–028 | Время, keyset или политика |
| Backlog | SLY-QA-071–080 | Приемник, транспорт, schema или емкость |
| Порча сегмента | SLY-QA-049 | Локальная копия и проверенный origin |
| WAL recovery | SLY-QA-075–079 | Хвост, середина и checkpoint |
| Ceph degraded | SLY-QA-085 | Причина pause и допустимость чтений |
| Откат | SLY-QA-107–109 | Совместимость бинарных файлов и состояния |

Диапазоны в таблице служат навигацией по каталогу.
Для реального прогона выбирают конкретные применимые case ids.
Успех одного кейса не закрывает все сценарии runbook.

## Матрица приоритетов дефектов

| Категория | Примеры | Влияние на выпуск |
| --- | --- | --- |
| Нарушение конфиденциальности | Выдача объекта без допуска | Блокирует выпуск |
| Порча данных | Смешанные поколения, неправильные Range байты | Блокирует выпуск |
| Потеря устойчивого состояния | Ложный WAL ack, удаление единственной копии | Блокирует выпуск |
| Неконтролируемый ресурс | Бесконечная очередь, deadlock event loop | Блокирует принятый профиль |
| Неверный учет спроса | Дубли увеличивают популярность | Блокирует автоматический placement |
| Недостоверная диагностика | Потеря данных отображается как healthy | Блокирует соответствующую функцию |
| Ограничение платформы | Одна ОС не проходит квалификацию | Исключает конкретный профиль |
| Косметический дефект | Ошибка отображения без потери смысла | Решение владельца релиза |

Дефект не снижают в приоритете только из-за редкости.
Важны возможные последствия и воспроизводимость.
Исключение должно иметь границы и срок пересмотра.

## Release checklist

### Код и компоненты

- Зафиксированы версии модуля и агента.
- Зафиксирована версия Lua-политик.
- Зафиксирована версия placement controller.
- Зафиксированы зависимости и их лицензии.
- Подготовлена ведомость компонентов.
- Проверены compiler warnings принятого профиля.
- Выполнены применимые sanitizers на тестовых сборках.
- Выполнены проверки границ parser и сериализации.
- Нет известных блокирующих дефектов.

### Совместимость

- Список ОС соответствует выполненной квалификации.
- У каждой архитектуры есть собственное evidence.
- Версия Nginx соответствует манифесту.
- Параметры сборки Nginx сохранены.
- Версии local protocol согласованы.
- Схема конфигурации закреплена.
- Схема событий закреплена.
- Формат WAL имеет правила восстановления.
- Формат индекса имеет правила отката.
- Ceph adapter имеет отдельный compatibility record.

### Эксплуатация

- Опубликован порядок установки фактического пакета.
- Проверены подпись и digest поставки.
- Опубликованы реальные имена команд.
- Проектные команды заменены подтвержденным интерфейсом.
- Проверены права service user.
- Проверены SELinux или AppArmor для выбранной ОС.
- Проверены log rotation и retention.
- Проверен backup конфигурации.
- Проверено восстановление backup.
- Проверен drain узла.
- Проверен rollback.

### Наблюдаемость

- Каждый alert имеет владельца.
- Каждый критический alert имеет runbook.
- Метрики не содержат секретных labels.
- Логи редактируют токены.
- Dashboard различает missing и zero.
- Hit ratio имеет определенный denominator.
- Byte ratio не складывает промежуточные копирования.
- Telemetry quality учитывается planner.
- Audit содержит actor и результат.
- Диагностический пакет имеет лимит размера.

### Продуктовые границы

- Указано, какие функции входят в этот релиз.
- Не заявлены непроверенные платформы.
- Не заявлены неподтвержденные сертификаты.
- Не заявлен универсальный процент экономии.
- Указаны условия выполненного измерения экономии.
- Начало лицензии согласовано с подтверждением оплаты.
- Ручное продление отражено в интерфейсе.
- Определен способ учета активаций администратором.
- Offline-политика утверждена либо явно исключена из готовности.
- Не добавлены неутвержденные цены или SLA.

## Протокол готовности

```yaml
release_acceptance:
  release_id: candidate-example
  decision: pending
  accepted_platforms: []
  excluded_platforms: []
  required_cases:
    total: 120
    passed: 0
    failed: 0
    blocked: 0
    waived: 0
  workload_profile: pending
  evidence_index: pending
  known_limitations: []
  rollback_verified: false
  operations_owner: pending
  engineering_owner: pending
  approval_record: pending
```

Число 120 относится к базовому каталогу этого документа.
Конкретный релиз может добавлять обязательные кейсы.
Неприменимость кейса оформляется обоснованием.
Нельзя уменьшать denominator, просто удаляя неуспешные тесты из отчета.

## Завершение приемки

Результат приемки связывается с точным digest поставки.
После пересборки значимой части результат пересматривается.
Критические исправления требуют повторения затронутых сценариев.
Повтор всех проверок выбирают по характеру изменения.

Выпуск готов к расширению после подтверждения поведения canary.
Оператор имеет все необходимые инструкции и наблюдаемость.
Откат остается доступен на протяжении согласованного окна.
Фактические ограничения публикуются вместе с версией.

---

# Справочник конфигурации

> Статус: проектная спецификация v0.1. Все параметры СлойРА в этой главе задают предлагаемый контракт реализации.

## Назначение и область действия

Файл `sloyra.yaml` объединяет настройки плагина и фоновых компонентов.
Он не является конфигурацией стандартного Nginx или Ceph.
Реальные директивы Nginx загружают модуль и подключают проверенный профиль.
СлойРА должна прочитать YAML, проверить типы и подготовить immutable snapshot.
До реализации валидатора примеры нельзя считать готовым производственным комплектом.

Каждая карточка ниже описывает один leaf-параметр.
Путь с точками обозначает вложенные ключи YAML.
Пример в карточке является отдельным фрагментом файла.
Он не содержит всех зависимостей и не предназначен для самостоятельного запуска.
Повторяющиеся верхние ключи из разных карточек необходимо объединять структурно.
Дублирование одного YAML-ключа в готовом файле является ошибкой.

Числа ниже — профиль лабораторного стенда, а не зашитые defaults выпуска.
При отсутствии поля применяется значение из manifest конкретного релиза.
Если manifest не определяет default для обязательного поля, конфигурация отклоняется.
Нельзя автоматически делать защищённый маршрут публичным из-за пропущенной настройки.

## Типы и единицы

| Тип | Правило |
| --- | --- |
| boolean | Только YAML true или false; строки on/off и yes/no запрещены |
| integer | Целое число с проверкой диапазона до преобразования к машинному типу |
| ratio | Конечное десятичное число; NaN и infinity запрещены |
| bytes | Положительное значение B, KiB, MiB, GiB или TiB |
| duration | Явная единица ms, s, m, h или d; отрицательное время запрещено |
| enum | Точное значение из схемы данной версии |
| path | Абсолютный локальный путь с отдельной проверкой прав и mount |
| string | UTF-8 строка в пределах лимитов схемы |
| nullable | null или явно описанная альтернативная форма |

KiB равен 1024 байтам.
MiB равен 1024 KiB.
GiB равен 1024 MiB.
TiB равен 1024 GiB.
В примерах пропуск единицы считается ошибкой.
Размер в байтах не смешивается с размером в битах.
Скорость с окончанием `per_second` интерпретирует размер за одну секунду.
В нормализованном JSON-представлении конфигурации длительности переводятся в целые миллисекунды,
а объём — в целое количество байтов с указанием версии схемы.
Поля событий и метрик сохраняют единицы своих именованных контрактов,
например duration_seconds и lookup_seconds измеряются в секундах.

## Применение поколения

1. Прочитать файл с ограничением размера и глубины.
2. Отклонить duplicate keys, неизвестные поля и неподдерживаемые теги YAML.
3. Проверить типы, диапазоны и межполевые ограничения.
4. Разрешить ссылки на секреты без помещения секретов в диагностический вывод.
5. Проверить доступность локальных путей и владельцев файлов.
6. Сформировать digest эффективной конфигурации после нормализации.
7. Подготовить новое поколение policy, маршрутов и ресурсов.
8. Выполнить локальную проверку готовности поколения.
9. Атомарно переключить новые запросы на новое поколение.
10. Сохранить старое поколение до завершения активных запросов.

Сбой любого подготовительного шага оставляет активным предыдущее поколение.
Новый файл на диске не равен успешно применённой конфигурации.
Ответ validate не равен ответу apply.
Применение должно возвращать идентификатор поколения и итог проверки.
При частичном rollout на несколько узлов готовность оценивается по каждому узлу.

## Приоритет настроек

Параметры имеют явное значение из файла или default manifest.
Переменная окружения указывает местоположение файла.
Она не даёт клиенту возможность переопределять поля через HTTP.
Заголовки запроса не меняют endpoint Ceph, путь кеша или keyring.
Автоматическая подстановка произвольных переменных окружения в YAML запрещена.
Ссылки на секреты разрешает отдельный adapter с ограниченным namespace.

Общие свойства наследуются только внутри заранее описанного профиля.
Значение tenant-specific quota не может превышать физический предел узла.
Разрешения tenant-specific policy могут быть уже общесистемных.
Они не расширяют полномочия поверх обязательных ограничений deployment.

## Совместимость с примерами других глав

Тематические главы содержат сокращённые профили и поясняющий псевдокод.
Эта глава фиксирует предлагаемые полные пути параметров для реализации схемы.
Расширение схемы требует описать поле и обновить примеры вместе с валидатором.
Наличие поля в примере не подтверждает его поддержку опубликованной сборкой.
До выпуска проводится проверка всех примеров против единой JSON Schema.

## Сервер и интеграция

### `server.schema_version`

Версия схемы всего файла конфигурации.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `1` |
| Область | `server` |

**Ограничение:** Неподдерживаемая основная версия отклоняется до запуска listener.

Фрагмент проектной конфигурации:

```yaml
server:
  schema_version: 1
```

**Проверка результата:** Проверить номер в effective configuration и manifest сборки.

Контрольный отрицательный пример: `999`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.node_id`

Постоянное имя узла в телеметрии и диагностике.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `edge-lab-01` |
| Область | `server` |

**Ограничение:** Уникально внутри deployment; не использовать динамический IP в качестве идентичности.

Фрагмент проектной конфигурации:

```yaml
server:
  node_id: edge-lab-01
```

**Проверка результата:** После рестарта имя сохраняется, boot_id меняется.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.deployment_id`

Граница независимого развёртывания и пространства идентификаторов.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `lab-storage` |
| Область | `server` |

**Ограничение:** Два независимых кластера не должны случайно объединять статистику.

Фрагмент проектной конфигурации:

```yaml
server:
  deployment_id: lab-storage
```

**Проверка результата:** Сопоставить deployment в агенте и получателе телеметрии.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.integration.profile`

Выбор способа включения логики в путь Nginx.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `native` |
| Область | `server` |

**Ограничение:** Допустимы native и lua; профиль квалифицируется отдельной сборкой.

Фрагмент проектной конфигурации:

```yaml
server:
  integration:
    profile: native
```

**Проверка результата:** Проверить, что одновременно не включены два обработчика одного маршрута.

Контрольный отрицательный пример: `hybrid-auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.integration.config_source`

Источник ссылки на конфигурационный snapshot.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `environment` |
| Область | `server` |

**Ограничение:** В этой редакции допустим environment; содержимое запроса не является источником.

Фрагмент проектной конфигурации:

```yaml
server:
  integration:
    config_source: environment
```

**Проверка результата:** Сверить путь у master и нового поколения workers.

Контрольный отрицательный пример: `http_header`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.integration.config_environment_name`

Имя переменной с путём к файлу настроек.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `SLOYRA_CONFIG` |
| Область | `server` |

**Ограничение:** Значение переменной передаётся через разрешённый механизм Nginx env.

Фрагмент проектной конфигурации:

```yaml
server:
  integration:
    config_environment_name: SLOYRA_CONFIG
```

**Проверка результата:** При отсутствии переменной новый worker не принимает защищённые маршруты.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.integration.route_binding`

Привязка HTTP-маршрутов к проверенному набору правил.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `compiled_snapshot` |
| Область | `server` |

**Ограничение:** Выпуск поколения требует согласованного snapshot маршрутов и политик.

Фрагмент проектной конфигурации:

```yaml
server:
  integration:
    route_binding: compiled_snapshot
```

**Проверка результата:** Сверить generation на всех workers узла.

Контрольный отрицательный пример: `per_request_yaml`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.agent_socket`

Локальная IPC-точка между модулем и агентом.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/run/sloyra/agent.sock` |
| Область | `server` |

**Ограничение:** Абсолютный путь вне каталогов, доступных на запись непривилегированным клиентам.

Фрагмент проектной конфигурации:

```yaml
server:
  agent_socket: /run/sloyra/agent.sock
```

**Проверка результата:** Проверить владельца, группу и отказ соединения постороннему пользователю.

Контрольный отрицательный пример: `../../tmp/agent.sock`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.state_dir`

Каталог durable состояния агента и checkpoint.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/var/lib/sloyra` |
| Область | `server` |

**Ограничение:** Не совпадает с удаляемым каталогом кеша; отдельная квота.

Фрагмент проектной конфигурации:

```yaml
server:
  state_dir: /var/lib/sloyra
```

**Проверка результата:** Рестарт не теряет подтверждённый watermark очереди.

Контрольный отрицательный пример: `relative/state`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.shutdown_grace`

Время завершения фоновых операций при остановке.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `30s` |
| Область | `server` |

**Ограничение:** Не отменяет запрет удаления исходной копии при незавершённой миграции.

Фрагмент проектной конфигурации:

```yaml
server:
  shutdown_grace: 30s
```

**Проверка результата:** Проверить остаток очереди и восстановление после принудительной остановки.

Контрольный отрицательный пример: `-1s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.config_max_bytes`

Ограничение размера входного файла конфигурации.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `2MiB` |
| Область | `server` |

**Ограничение:** Проверяется до разбора YAML и создания вложенных объектов.

Фрагмент проектной конфигурации:

```yaml
server:
  config_max_bytes: 2MiB
```

**Проверка результата:** Большой файл отклоняется с диагностикой размера.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.config_max_depth`

Лимит вложенности структуры при разборе.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `16` |
| Область | `server` |

**Ограничение:** Рекурсивные alias и циклические графы запрещены независимо от лимита.

Фрагмент проектной конфигурации:

```yaml
server:
  config_max_depth: 16
```

**Проверка результата:** Проверить контролируемый отказ на глубоко вложенном документе.

Контрольный отрицательный пример: `-1`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.config_reload_debounce`

Окно объединения повторных сигналов изменения файла.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `500ms` |
| Область | `server` |

**Ограничение:** Публикуется только полностью записанный проверенный snapshot.

Фрагмент проектной конфигурации:

```yaml
server:
  config_reload_debounce: 500ms
```

**Проверка результата:** Частые записи не порождают бесконечный reload и churn workers.

Контрольный отрицательный пример: `-500ms`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.metrics.listen`

Адрес локальной диагностической точки.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `127.0.0.1:9464` |
| Область | `server` |

**Ограничение:** Публичный bind требует отдельного сетевого и аутентификационного профиля.

Фрагмент проектной конфигурации:

```yaml
server:
  metrics:
    listen: 127.0.0.1:9464
```

**Проверка результата:** Убедиться, что endpoint недоступен из клиентской сети.

Контрольный отрицательный пример: `:not-a-port`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `server.metrics.path`

Путь выдачи агрегированных счётчиков.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `/metrics` |
| Область | `server` |

**Ограничение:** Не размещать под тем же публичным location, что защищённые файлы.

Фрагмент проектной конфигурации:

```yaml
server:
  metrics:
    path: /metrics
```

**Проверка результата:** Scrape получает только агрегаты без object_id и подписей ссылок.

Контрольный отрицательный пример: `metrics`.
Валидатор должен отклонить его для данного поля без применения части настроек.

## Доступ и политика Lua

### `access.enabled`

Включение политики допуска для маршрутов.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** false допустимо лишь для явно публичного маршрута, защищённые маршруты должны быть отвергнуты валидатором.

Фрагмент проектной конфигурации:

```yaml
access:
  enabled: true
```

**Проверка результата:** При выключении на защищённом маршруте конфигурация не применяется.

Контрольный отрицательный пример: `maybe`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.policy_bundle`

Путь к проверенному пакету политик Lua.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/etc/sloyra/policy.bundle` |
| Область | `access` |

**Ограничение:** Владелец и digest пакета входят в манифест конфигурации.

Фрагмент проектной конфигурации:

```yaml
access:
  policy_bundle: /etc/sloyra/policy.bundle
```

**Проверка результата:** Повреждённый пакет не заменяет последнюю рабочую политику.

Контрольный отрицательный пример: `../policy.lua`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.policy_revision`

Версия правил авторизации, присутствующая в аудите.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `access-v1` |
| Область | `access` |

**Ограничение:** Меняется при изменении смысла правил, даже без изменения Lua ABI.

Фрагмент проектной конфигурации:

```yaml
access:
  policy_revision: access-v1
```

**Проверка результата:** По отказу можно восстановить активную редакцию политики.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.algorithm`

Алгоритм проверки подписи URL в целевом профиле.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `hmac_sha256` |
| Область | `access` |

**Ограничение:** Не принимать алгоритм из недоверенного поля токена.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    algorithm: hmac_sha256
```

**Проверка результата:** Токен с изменённым алгоритмом отвергается до выдачи тела.

Контрольный отрицательный пример: `none`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.keyring`

Реестр ключей проверки с идентификаторами kid.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/etc/sloyra/keys.json` |
| Область | `access` |

**Ограничение:** Секретный материал не копируется в effective configuration и журналы.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    keyring: /etc/sloyra/keys.json
```

**Проверка результата:** Ротация нового и старого kid проверена на отдельном наборе ссылок.

Контрольный отрицательный пример: `http://keys.example`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.max_ttl`

Максимальная разрешённая жизнь выдаваемой ссылки.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `15m` |
| Область | `access` |

**Ограничение:** Слишком большой exp отклоняется даже при корректной подписи.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    max_ttl: 15m
```

**Проверка результата:** Проверить ссылку с действительной подписью и чрезмерным сроком.

Контрольный отрицательный пример: `-15m`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.clock_skew`

Допуск рассогласования часов при проверке срока.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `30s` |
| Область | `access` |

**Ограничение:** Это ограниченный допуск, а не бессрочное продление при неисправных часах.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    clock_skew: 30s
```

**Проверка результата:** Сравнить отказ на границах exp плюс/минус допуск.

Контрольный отрицательный пример: `-30s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.require_exp`

Обязательное ограничение срока действия подписи.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** В защищённом профиле false не допускается.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    require_exp: true
```

**Проверка результата:** Ссылка без exp возвращает 403 и внутренний reason code.

Контрольный отрицательный пример: `yesplease`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.require_kid`

Обязательный выбор ключа из локального keyring.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** Неизвестный kid не вызывает перебор всех ключей или удалённый lookup в worker.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    require_kid: true
```

**Проверка результата:** Проверить время отказа на случайных kid.

Контрольный отрицательный пример: `sometimes`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.max_token_bytes`

Ограничение размера представления токена до декодирования.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `4KiB` |
| Область | `access` |

**Ограничение:** Лимит относится к wire bytes, не к количеству Unicode-символов.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    max_token_bytes: 4KiB
```

**Проверка результата:** Отклонить чрезмерный токен без большого выделения памяти.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.bind_method`

Включение разрешённого метода в подписанную область.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** Правило совместимости GET и HEAD задаётся явно в профиле подписи.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    bind_method: true
```

**Проверка результата:** Токен на GET не даёт права произвольной записи.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.bind_tenant`

Включение пространства арендатора в проверяемое сообщение.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** Обязательный инвариант многопользовательского режима.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    bind_tenant: true
```

**Проверка результата:** Токен tenant-a не читает одноимённый объект tenant-b.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.signature.bind_version`

Привязка разрешения к неизменяемой версии объекта.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** При false отдельно требуется модель прав на latest и повторная проверка версии.

Фрагмент проектной конфигурации:

```yaml
access:
  signature:
    bind_version: true
```

**Проверка результата:** Новая версия файла не наследует старую ссылку автоматически.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.canonicalization.profile`

Правила канонизации пути и query перед подписью.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `strict_v1` |
| Область | `access` |

**Ограничение:** Один профиль используется выдающим приложением и сервером доставки.

Фрагмент проектной конфигурации:

```yaml
access:
  canonicalization:
    profile: strict_v1
```

**Проверка результата:** Сверить тест-векторы для percent encoding, Unicode и повторных ключей.

Контрольный отрицательный пример: `double_decode`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.canonicalization.duplicate_query`

Обработка повторных значимых параметров.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `reject` |
| Область | `access` |

**Ограничение:** Повтор sig, exp, kid или tenant отклоняется, порядок пар не скрывает конфликт.

Фрагмент проектной конфигурации:

```yaml
access:
  canonicalization:
    duplicate_query: reject
```

**Проверка результата:** Проверить запрос с двумя exp и противоположным порядком.

Контрольный отрицательный пример: `last_wins`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.identity.trusted_proxy_hops`

Число доверенных hops для извлечения адреса клиента.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `0` |
| Область | `access` |

**Ограничение:** Положительное значение требует списка доверенных сетей в проверенном access.policy_bundle;
список входит в digest политики и не поступает из HTTP-заголовков.

Фрагмент проектной конфигурации:

```yaml
access:
  identity:
    trusted_proxy_hops: 0
```

**Проверка результата:** Поддельный X-Forwarded-For от прямого клиента не влияет на идентичность.

Контрольный отрицательный пример: `-1`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.rate_limit.enabled`

Включение бюджета легитимного клиента.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** Превышение бюджета отличается от недействительной подписи и возвращает 429.

Фрагмент проектной конфигурации:

```yaml
access:
  rate_limit:
    enabled: true
```

**Проверка результата:** Проверить счётчики allow, deny и throttle отдельно.

Контрольный отрицательный пример: `always`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.rate_limit.requests_per_second`

Скорость пополнения token bucket в демонстрационном профиле.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `20` |
| Область | `access` |

**Ограничение:** Положительное число; масштаб лимита фиксируется: узел или кластер.

Фрагмент проектной конфигурации:

```yaml
access:
  rate_limit:
    requests_per_second: 20
```

**Проверка результата:** Два узла не выдаются за общий лимит без распределённого координатора.

Контрольный отрицательный пример: `-20`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.rate_limit.burst`

Допустимый краткий всплеск запросов клиента.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `60` |
| Область | `access` |

**Ограничение:** Не меньше одного запроса; запас не равен числу одновременных соединений.

Фрагмент проектной конфигурации:

```yaml
access:
  rate_limit:
    burst: 60
```

**Проверка результата:** Проверить плавное восстановление после burst и Retry-After.

Контрольный отрицательный пример: `-60`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.lua.instruction_budget`

Ограничение объёма исполнения политики на запрос.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `100000` |
| Область | `access` |

**Ограничение:** Не заменяет лимит времени для FFI или внешнего вызова.

Фрагмент проектной конфигурации:

```yaml
access:
  lua:
    instruction_budget: 100000
```

**Проверка результата:** Зацикленная тестовая политика завершает запрос контролируемо.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.lua.memory_budget`

Предел дополнительной памяти policy invocation.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `8MiB` |
| Область | `access` |

**Ограничение:** Нативные выделения должны учитываться отдельно и не обходить квоту.

Фрагмент проектной конфигурации:

```yaml
access:
  lua:
    memory_budget: 8MiB
```

**Проверка результата:** Большая таблица входных атрибутов не вызывает OOM всего worker.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.lua.deadline`

Проектный бюджет выполнения локального access decision.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `5ms` |
| Область | `access` |

**Ограничение:** Это deadline политики, а не всей передачи большого объекта.

Фрагмент проектной конфигурации:

```yaml
access:
  lua:
    deadline: 5ms
```

**Проверка результата:** Задержка политики видна отдельно от origin latency.

Контрольный отрицательный пример: `-5ms`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.denial.cache_control`

Запрет сохранения персонального отказа в общем кеше.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `no_store` |
| Область | `access` |

**Ограничение:** 403 не должен становиться shared ответом для другого клиента.

Фрагмент проектной конфигурации:

```yaml
access:
  denial:
    cache_control: no_store
```

**Проверка результата:** После отказа разрешённый клиент получает собственное решение.

Контрольный отрицательный пример: `public`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `access.audit.redact_query`

Исключение секретных параметров URL из журналов.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `access` |

**Ограничение:** Обязательная редакция sig, token и ключевых атрибутов сессионного доступа.

Фрагмент проектной конфигурации:

```yaml
access:
  audit:
    redact_query: true
```

**Проверка результата:** Поиск тестовой подписи в audit export не находит совпадений.

Контрольный отрицательный пример: `sometimes`.
Валидатор должен отклонить его для данного поля без применения части настроек.

## Локальные уровни кеша

### `cache.identity_schema`

Версия формата идентичности кеша.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `1` |
| Область | `cache` |

**Ограничение:** Смена требует отдельного namespace и плана очистки старого поколения.

Фрагмент проектной конфигурации:

```yaml
cache:
  identity_schema: 1
```

**Проверка результата:** Старый reader не интерпретирует ключи новой версии.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.namespace`

Дополнительное разделение данных по политике маршрута.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `tenant-a/media` |
| Область | `cache` |

**Ограничение:** Не заменяет обязательный tenant_id в ключе.

Фрагмент проектной конфигурации:

```yaml
cache:
  namespace: tenant-a/media
```

**Проверка результата:** Совпадающие URL из разных namespace не используют чужой entry.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.chunking.enabled`

Разделение больших объектов на независимые кешируемые куски.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `cache` |

**Ограничение:** Chunk принадлежит конкретной версии целого объекта.

Фрагмент проектной конфигурации:

```yaml
cache:
  chunking:
    enabled: true
```

**Проверка результата:** Смешение разных версий в одном ответе запрещено.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.chunking.size`

Размер базового куска в демонстрационном профиле.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `4MiB` |
| Область | `cache` |

**Ограничение:** Положителен и согласован с layout_version; последний кусок может быть короче.

Фрагмент проектной конфигурации:

```yaml
cache:
  chunking:
    size: 4MiB
```

**Проверка результата:** Измерить amplification для случайных Range разных размеров.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.chunking.layout_version`

Версия схемы нарезки объекта.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `1` |
| Область | `cache` |

**Ограничение:** Изменение размера куска требует новой версии либо однозначного включения размера в ключ.

Фрагмент проектной конфигурации:

```yaml
cache:
  chunking:
    layout_version: 1
```

**Проверка результата:** Существующие чанки не переинтерпретируются после reload.

Контрольный отрицательный пример: `-1`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.chunking.max_ranges`

Предел поддерживаемых диапазонов одного запроса.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `8` |
| Область | `cache` |

**Ограничение:** Обработка избытка диапазонов задаётся HTTP-профилем, а не выдачей неверных байтов.

Фрагмент проектной конфигурации:

```yaml
cache:
  chunking:
    max_ranges: 8
```

**Проверка результата:** Проверить multipart и путь контролируемого fallback.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.ram.enabled`

Допуск размещения тел объектов в памяти.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `cache` |

**Ограничение:** Отключение не отменяет память для служебных структур и текущих readers.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    ram:
      enabled: true
```

**Проверка результата:** Метрика payload уменьшается после освобождения активных ссылок.

Контрольный отрицательный пример: `enabled`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.ram.budget`

Максимальный учитываемый объём RAM tier.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `8GiB` |
| Область | `cache` |

**Ограничение:** Сумма payload, metadata и резерва fits в лимит процесса и cgroup.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    ram:
      budget: 8GiB
```

**Проверка результата:** Проверить устойчивый RSS после заполнения и вытеснения.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.ram.max_entry`

Предел одного размещаемого entry в RAM.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `16MiB` |
| Область | `cache` |

**Ограничение:** Не больше общего бюджета RAM; chunk считается entry при сегментном режиме.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    ram:
      max_entry: 16MiB
```

**Проверка результата:** Большой объект не вытесняет весь рабочий набор одним admission.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.nvme.enabled`

Подключение быстрого дискового уровня.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `cache` |

**Ограничение:** Путь обязан находиться на ожидаемом mount, а не на случайном root filesystem.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    nvme:
      enabled: true
```

**Проверка результата:** После потери mount tier исключается из записи.

Контрольный отрицательный пример: `maybe`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.nvme.path`

Каталог данных и временных файлов NVMe кеша.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/var/cache/sloyra/nvme` |
| Область | `cache` |

**Ограничение:** Запрещены пересечения с origin, WAL и другим tier.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    nvme:
      path: /var/cache/sloyra/nvme
```

**Проверка результата:** Проверить mount identity и права before admission.

Контрольный отрицательный пример: `../../origin`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.nvme.budget`

Логический предел локального NVMe кеша.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `800GiB` |
| Область | `cache` |

**Ограничение:** Должен оставлять место для временных файлов, filesystem overhead и резервов.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    nvme:
      budget: 800GiB
```

**Проверка результата:** Сравнить бюджет, реальный free space и квоту filesystem.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.nvme.low_watermark`

Цель фонового освобождения после достижения верхнего порога.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.75` |
| Область | `cache` |

**Ограничение:** 0 <= low < high < emergency < 1.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    nvme:
      low_watermark: 0.75
```

**Проверка результата:** Вытеснение завершается у low, а не колеблется у high.

Контрольный отрицательный пример: `1.2`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.nvme.high_watermark`

Порог запуска активного вытеснения.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.85` |
| Область | `cache` |

**Ограничение:** Порог применяется к определённому знаменателю бюджета, не ко всему диску произвольно.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    nvme:
      high_watermark: 0.85
```

**Проверка результата:** Зафиксировать начало eviction и темп incoming fills.

Контрольный отрицательный пример: `-0.1`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.nvme.emergency_watermark`

Порог остановки новых необязательных записей.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.93` |
| Область | `cache` |

**Ограничение:** Оставшийся резерв предназначен для завершения уже начатых безопасных операций.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    nvme:
      emergency_watermark: 0.93
```

**Проверка результата:** Новые fills не съедают последние свободные блоки.

Контрольный отрицательный пример: `1.0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.nvme.max_write_bytes_per_second`

Бюджет фоновой записи на быстрый диск за секунду.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `256MiB` |
| Область | `cache` |

**Ограничение:** Контентная выдача имеет отдельный приоритет над перемещением.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    nvme:
      max_write_bytes_per_second: 256MiB
```

**Проверка результата:** Измерить задержку чтения при непрерывном заполнении.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.ssd.enabled`

Опциональный SATA/SAS SSD tier между NVMe и HDD.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `false` |
| Область | `cache` |

**Ограничение:** Включать только при наличии отдельного устройства и измеренной пользы.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    ssd:
      enabled: false
```

**Проверка результата:** Сравнить latency и стоимость записи с вариантом без этого уровня.

Контрольный отрицательный пример: `sometimes`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.ssd.path`

Каталог необязательного SSD tier.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/var/cache/sloyra/ssd` |
| Область | `cache` |

**Ограничение:** Правила mount identity и непересечения совпадают с другими дисковыми уровнями.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    ssd:
      path: /var/cache/sloyra/ssd
```

**Проверка результата:** Ошибочная привязка к тому же диску обнаруживается в doctor.

Контрольный отрицательный пример: `relative/cache`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.ssd.budget`

Бюджет промежуточного SSD кеша.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `1600GiB` |
| Область | `cache` |

**Ограничение:** Не суммировать как независимую физическую ёмкость, если tiers используют один volume.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    ssd:
      budget: 1600GiB
```

**Проверка результата:** Отчёт содержит device identity и effective quota.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.hdd.enabled`

Допуск кеширования тёплой выборки на HDD.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `cache` |

**Ограничение:** HDD кеш на edge отделён от HDD origin в Ceph.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    hdd:
      enabled: true
```

**Проверка результата:** Потеря HDD кеша восстанавливается из origin.

Контрольный отрицательный пример: `maybe`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.hdd.path`

Каталог локального HDD кеша.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/var/cache/sloyra/hdd` |
| Область | `cache` |

**Ограничение:** Не допускается запись в директорию неподключённого mount.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    hdd:
      path: /var/cache/sloyra/hdd
```

**Проверка результата:** Агент проверяет ожидаемое устройство перед каждым переводом в healthy.

Контрольный отрицательный пример: `../hdd`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.hdd.budget`

Бюджет локальной тёплой выборки.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `8TiB` |
| Область | `cache` |

**Ограничение:** Учесть inodes для множества мелких chunks и резерв временных файлов.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    hdd:
      budget: 8TiB
```

**Проверка результата:** Проверить inode exhaustion отдельно от byte exhaustion.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.tiers.hdd.background_concurrency`

Число параллельных фоновых операций HDD tier.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `2` |
| Область | `cache` |

**Ограничение:** Подбирается по seek latency и влиянию на активных читателей.

Фрагмент проектной конфигурации:

```yaml
cache:
  tiers:
    hdd:
      background_concurrency: 2
```

**Проверка результата:** Сравнить p99 чтения при одной и нескольких операциях.

Контрольный отрицательный пример: `-2`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.lease`

Время владения конкретным заполнением.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `30s` |
| Область | `cache` |

**Ограничение:** Lease продлевается активным владельцем; takeover использует fencing.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    lease: 30s
```

**Проверка результата:** После зависания owner появляется один новый автор публикации.

Контрольный отрицательный пример: `0s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.waiter_deadline`

Максимальное ожидание клиента за другим fill.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `2s` |
| Область | `cache` |

**Ограничение:** Истечение deadline не разрешает неограниченный herd к origin.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    waiter_deadline: 2s
```

**Проверка результата:** Проверить отдельную стратегию отказа или лимитированного bypass.

Контрольный отрицательный пример: `-2s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.max_waiters_per_key`

Ограничение числа ожидающих один объект клиентов.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `256` |
| Область | `cache` |

**Ограничение:** Лимит памяти включает структуры ожидания и отмены клиентов.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    max_waiters_per_key: 256
```

**Проверка результата:** Популярная премьера не создаёт неограниченную очередь в worker.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.max_attempts`

Общий предел физических попыток одного fill.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `2` |
| Область | `cache` |

**Ограничение:** Включает первую попытку; retry только для безопасного чтения той же версии.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    max_attempts: 2
```

**Проверка результата:** origin_attempt_id уникален для каждой попытки.

Контрольный отрицательный пример: `-1`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.publication`

Способ публикации завершённого кеш-entry.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `verified_atomic` |
| Область | `cache` |

**Ограничение:** Сначала проверяются длина и checksum, затем становится видимым имя entry.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    publication: verified_atomic
```

**Проверка результата:** Reader никогда не видит временный неполный файл.

Контрольный отрицательный пример: `partial_visible`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.max_inflight_bytes`

Общий резерв незавершённых заполнений узла.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `512MiB` |
| Область | `cache` |

**Ограничение:** Резерв выделяется до начала загрузки; unknown length требует bounded streaming.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    max_inflight_bytes: 512MiB
```

**Проверка результата:** Счётчик возвращается к нулю после отмены всех запросов.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.max_inflight_objects`

Предел одновременно заполняемых идентичностей.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `128` |
| Область | `cache` |

**Ограничение:** Отдельный лимит от соединений и от количества ожидающих клиентов.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    max_inflight_objects: 128
```

**Проверка результата:** Много мелких уникальных запросов не обходят byte budget.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.origin_connect_timeout`

Время установления соединения с origin endpoint.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `500ms` |
| Область | `cache` |

**Ограничение:** Измеряется отдельно от чтения тела и бюджета retry.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    origin_connect_timeout: 500ms
```

**Проверка результата:** Недоступный endpoint не удерживает все fill slots.

Контрольный отрицательный пример: `-1ms`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.fill.origin_read_timeout`

Предельный интервал отсутствия прогресса чтения тела.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `5s` |
| Область | `cache` |

**Ограничение:** Не является максимальным временем скачивания большого объекта.

Фрагмент проектной конфигурации:

```yaml
cache:
  fill:
    origin_read_timeout: 5s
```

**Проверка результата:** Медленно поступающие полезные байты учитываются по установленной политике.

Контрольный отрицательный пример: `-5s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.freshness.immutable_versions`

Режим неизменяемой версии источника.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `cache` |

**Ограничение:** Если версия не гарантирована, требуется revalidation и запрет склейки неизвестных chunks.

Фрагмент проектной конфигурации:

```yaml
cache:
  freshness:
    immutable_versions: true
```

**Проверка результата:** Повторный fetch одной версии имеет ту же длину и checksum.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.freshness.stale_on_revocation`

Разрешение устаревшей выдачи после отзыва доступа.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `false` |
| Область | `cache` |

**Ограничение:** В защищённом профиле единственное допустимое значение false.

Фрагмент проектной конфигурации:

```yaml
cache:
  freshness:
    stale_on_revocation: false
```

**Проверка результата:** Отозванный клиент не получает stale entry даже при origin outage.

Контрольный отрицательный пример: `true`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.freshness.metadata_ttl`

Срок локального кеша метаданных и каталога.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `60s` |
| Область | `cache` |

**Ограничение:** TTL согласован с revocation и lease; freshness тела не равна freshness прав.

Фрагмент проектной конфигурации:

```yaml
cache:
  freshness:
    metadata_ttl: 60s
```

**Проверка результата:** Проверить смену расположения без потери неизменяемой идентичности.

Контрольный отрицательный пример: `-60s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.eviction.policy`

Функция выбора жертвы вытеснения.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `benefit_per_byte` |
| Область | `cache` |

**Ограничение:** Формула и её версия включаются в snapshot политики.

Фрагмент проектной конфигурации:

```yaml
cache:
  eviction:
    policy: benefit_per_byte
```

**Проверка результата:** Решение воспроизводится по записанным features без случайной смены версии.

Контрольный отрицательный пример: `random_forever`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.eviction.min_residency`

Минимальная выдержка после размещения.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `5m` |
| Область | `cache` |

**Ограничение:** Аварийный недостаток места может переопределить обычный cooldown.

Фрагмент проектной конфигурации:

```yaml
cache:
  eviction:
    min_residency: 5m
```

**Проверка результата:** Проверить уменьшение churn без накопления заблокированных entries.

Контрольный отрицательный пример: `-5m`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.eviction.active_reader_grace`

Срок ожидания штатного завершения readers перед уборкой.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `60s` |
| Область | `cache` |

**Ограничение:** По истечении нельзя удалять единственную origin-копию; локальные readers сохраняют безопасную ссылку.

Фрагмент проектной конфигурации:

```yaml
cache:
  eviction:
    active_reader_grace: 60s
```

**Проверка результата:** Длинный reader остаётся байтово корректным при eviction.

Контрольный отрицательный пример: `-60s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.admission.min_observations`

Минимальное число наблюдений для обычного допуска объекта.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `2` |
| Область | `cache` |

**Ограничение:** Отдельная политика допускает pin и заранее известный горячий выпуск.

Фрагмент проектной конфигурации:

```yaml
cache:
  admission:
    min_observations: 2
```

**Проверка результата:** Однократный scan не вытесняет устойчивый hot set.

Контрольный отрицательный пример: `-2`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.admission.max_object_fraction`

Максимальная доля бюджета уровня для одного объекта.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.1` |
| Область | `cache` |

**Ограничение:** При chunking лимит целого объекта и лимит entry различаются.

Фрагмент проектной конфигурации:

```yaml
cache:
  admission:
    max_object_fraction: 0.1
```

**Проверка результата:** Один большой архив не захватывает быстрый уровень целиком.

Контрольный отрицательный пример: `1.5`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.integrity.algorithm`

Контроль целостности опубликованного тела.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `sha256` |
| Область | `cache` |

**Ограничение:** Checksum хранится отдельно от HTTP ETag, который может иметь иной смысл.

Фрагмент проектной конфигурации:

```yaml
cache:
  integrity:
    algorithm: sha256
```

**Проверка результата:** Corrupt entry исключается из индекса до следующей выдачи.

Контрольный отрицательный пример: `etag_only`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.integrity.verify_on_read`

Полная проверка тела при каждом чтении кеша.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `false` |
| Область | `cache` |

**Ограничение:** false не отменяет проверку при fill и фоновый scrub.

Фрагмент проектной конфигурации:

```yaml
cache:
  integrity:
    verify_on_read: false
```

**Проверка результата:** Оценить CPU overhead и окно обнаружения silent corruption.

Контрольный отрицательный пример: `maybe`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `cache.integrity.scrub_bytes_per_second`

Бюджет фоновой проверки локальных entries за секунду.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `32MiB` |
| Область | `cache` |

**Ограничение:** Scrub уступает активной выдаче и миграции при нехватке IO.

Фрагмент проектной конфигурации:

```yaml
cache:
  integrity:
    scrub_bytes_per_second: 32MiB
```

**Проверка результата:** Сравнить backlog проверки с размером кеша.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

## События и статистика спроса

### `telemetry.enabled`

Включение контура сбора статистики.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `telemetry` |

**Ограничение:** Отключение помечает спрос неизвестным и замораживает автоматические понижения класса.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  enabled: true
```

**Проверка результата:** Placement видит gap вместо искусственного нулевого спроса.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.logical_request`

Учёт логических допущенных обращений независимо от hit.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `telemetry` |

**Ограничение:** HEAD и безтельные проверки имеют отдельный тип полезной нагрузки.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  logical_request: true
```

**Проверка результата:** Сумма спроса не исчезает при росте hit ratio.

Контрольный отрицательный пример: `sometimes`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.cache_lookup`

Учёт проверенных локальных уровней.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `telemetry` |

**Ограничение:** Один запрос может иметь несколько lookup и только один итог доставки.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  cache_lookup: true
```

**Проверка результата:** Не суммировать tier lookups как число клиентов.

Контрольный отрицательный пример: `sometimes`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.origin_fetch`

Учёт физических обращений и попыток к исходному хранилищу.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `telemetry` |

**Ограничение:** Collapse followers не создают дополнительные фиктивные fetch events.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  origin_fetch: true
```

**Проверка результата:** Число origin попыток соответствует фактическим запросам адаптера.

Контрольный отрицательный пример: `sometimes`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.event_schema`

Основная версия envelope события.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `1` |
| Область | `telemetry` |

**Ограничение:** Изменение единиц и обязательных полей требует новой версии.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  event_schema: 1
```

**Проверка результата:** Старый collector отклоняет неподдерживаемую версию без ложного ACK.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.delivery`

Семантика повтора пакетов до подтверждения.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `at_least_once` |
| Область | `telemetry` |

**Ограничение:** Дедупликация обязательна; exactly once без протокола не заявляется.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  delivery: at_least_once
```

**Проверка результата:** Повтор принятого batch не меняет агрегат спроса.

Контрольный отрицательный пример: `best_effort_exactly_once`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.endpoint`

Адрес проектного приёмника статистики.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `https://collector.example.invalid/v1/events` |
| Область | `telemetry` |

**Ограничение:** Только HTTPS с проверкой сертификата; домен примера не является сервисом.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  endpoint: https://collector.example.invalid/v1/events
```

**Проверка результата:** При ошибке TLS пакет остаётся в локальной очереди.

Контрольный отрицательный пример: `http://collector.example.invalid`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.spool_path`

Durable очередь неподтверждённых событий.

| Свойство | Контракт |
| --- | --- |
| Тип | `path` |
| Значение примера | `/var/lib/sloyra/spool` |
| Область | `telemetry` |

**Ограничение:** Не должна находиться в директории очищаемого payload кеша.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  spool_path: /var/lib/sloyra/spool
```

**Проверка результата:** Restart восстанавливает последовательность и не меняет event_id.

Контрольный отрицательный пример: `../cache`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.spool_limit`

Предельный объём очереди телеметрии.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `2GiB` |
| Область | `telemetry` |

**Ограничение:** Лимит включает записи, индексы и незавершённые сегменты WAL.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  spool_limit: 2GiB
```

**Проверка результата:** При заполнении создаётся сигнал gap и считается объём потерь.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.dedup_horizon`

Срок хранения принятых идентичностей на collector.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `72h` |
| Область | `telemetry` |

**Ограничение:** Должен покрывать maximum replay age; старые replay не принимаются как новые.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  dedup_horizon: 72h
```

**Проверка результата:** Повтор у границы окна проверяется отдельным тестом.

Контрольный отрицательный пример: `-72h`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.max_replay_age`

Предел возраста автоматически повторяемого события.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `48h` |
| Область | `telemetry` |

**Ограничение:** Не больше dedup_horizon с учётом clock skew и сети.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  max_replay_age: 48h
```

**Проверка результата:** Слишком старый spool проходит явную процедуру восстановления.

Контрольный отрицательный пример: `-48h`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.batch.max_events`

Максимум событий одного пакета.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `4096` |
| Область | `telemetry` |

**Ограничение:** Одновременно действует ограничение max_bytes.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  batch:
    max_events: 4096
```

**Проверка результата:** Пакет из мелких событий не переполняет приёмник по количеству.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.batch.max_bytes`

Размер сериализованного пакета перед передачей.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `4MiB` |
| Область | `telemetry` |

**Ограничение:** Collector ограничивает также распакованный размер для compressed batches.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  batch:
    max_bytes: 4MiB
```

**Проверка результата:** Одно чрезмерное событие отправляется в quarantine с причиной.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.batch.flush_interval`

Максимальная задержка отправки неполного пакета.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `1s` |
| Область | `telemetry` |

**Ограничение:** Таймер не создаёт пустые пакеты при отсутствии событий.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  batch:
    flush_interval: 1s
```

**Проверка результата:** Измерить задержку от observation до ACK на низкой нагрузке.

Контрольный отрицательный пример: `-1s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.time.max_future_skew`

Допуск события из будущего относительно collector.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `30s` |
| Область | `telemetry` |

**Ограничение:** Слишком будущие timestamps не двигают watermark окна.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  time:
    max_future_skew: 30s
```

**Проверка результата:** Узел с испорченными часами виден в отдельной диагностике.

Контрольный отрицательный пример: `-30s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.time.allowed_lateness`

Окно исправления агрегата поздними событиями.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `10m` |
| Область | `telemetry` |

**Ограничение:** Поздние данные после закрытия учитываются отдельным механизмом correction.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  time:
    allowed_lateness: 10m
```

**Проверка результата:** Повтор correction не удваивает вклад события.

Контрольный отрицательный пример: `-10m`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.popularity.short_half_life`

Период полураспада оперативного спроса.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `15m` |
| Область | `telemetry` |

**Ограничение:** Меньше long_half_life; монотонное время используется для интервала.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  popularity:
    short_half_life: 15m
```

**Проверка результата:** Краткая премьера отражается без мгновенного забывания стабильного набора.

Контрольный отрицательный пример: `0s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.popularity.long_half_life`

Период полураспада устойчивого спроса.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `24h` |
| Область | `telemetry` |

**Ограничение:** Не меньше короткого окна; восстановление из checkpoint сохраняет возраст.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  popularity:
    long_half_life: 24h
```

**Проверка результата:** Периодический дневной контент не мигрирует после каждого спада.

Контрольный отрицательный пример: `0s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.popularity.short_weight`

Вес короткой компоненты итоговой оценки.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.7` |
| Область | `telemetry` |

**Ограничение:** В диапазоне от 0 до 1, второй вес определяется формулой профиля.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  popularity:
    short_weight: 0.7
```

**Проверка результата:** Сверить ручной расчёт score на синтетической последовательности.

Контрольный отрицательный пример: `1.7`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.popularity.body_sampling_probability`

Вероятность выборки телесных logical_request для аналитики.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `1.0` |
| Область | `telemetry` |

**Ограничение:** При sampling 0 < p <= 1; estimator и error bounds сохраняются в агрегате.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  popularity:
    body_sampling_probability: 1.0
```

**Проверка результата:** Снижение p не выглядит как падение реального спроса.

Контрольный отрицательный пример: `0.0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.popularity.max_active_identities`

Ограничение числа отслеживаемых объектов в оперативном агрегаторе.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `1000000` |
| Область | `telemetry` |

**Ограничение:** Вытеснение идентичности отмечает неопределённость вместо точного нуля.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  popularity:
    max_active_identities: 1000000
```

**Проверка результата:** Scan уникальных ключей не вызывает бесконтрольный рост памяти.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.retry.min_backoff`

Начальный интервал повтора недоставленного batch.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `250ms` |
| Область | `telemetry` |

**Ограничение:** Повтор включает jitter и не выполняется внутри Nginx worker.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  retry:
    min_backoff: 250ms
```

**Проверка результата:** При отказе collector число соединений остаётся ограниченным.

Контрольный отрицательный пример: `-250ms`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.retry.max_backoff`

Верхняя граница интервала повторной доставки.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `30s` |
| Область | `telemetry` |

**Ограничение:** Не меньше min_backoff; восстановление не отправляет весь backlog одновременно.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  retry:
    max_backoff: 30s
```

**Проверка результата:** Проверить плавное восстановление collector после длительного outage.

Контрольный отрицательный пример: `-30s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.retry.jitter`

Доля случайного разброса интервала отправки.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.2` |
| Область | `telemetry` |

**Ограничение:** От 0 до 1; RNG не используется как криптографический источник ключей.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  retry:
    jitter: 0.2
```

**Проверка результата:** Несколько узлов не синхронизируются в один retry spike.

Контрольный отрицательный пример: `1.5`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `telemetry.privacy.hash_object_keys`

Использование стабильного scoped идентификатора вместо пути клиента.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `telemetry` |

**Ограничение:** Salt относится к deployment и имеет описанную процедуру смены.

Фрагмент проектной конфигурации:

```yaml
telemetry:
  privacy:
    hash_object_keys: true
```

**Проверка результата:** Агрегатор объединяет один объект, журнал не раскрывает публичный путь.

Контрольный отрицательный пример: `maybe`.
Валидатор должен отклонить его для данного поля без применения части настроек.

## Размещение в Ceph

### `placement.local_enabled`

Фоновое перемещение локальных копий между cache tiers.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `placement` |

**Ограничение:** Не даёт агенту права удаления объектов из origin.

Фрагмент проектной конфигурации:

```yaml
placement:
  local_enabled: true
```

**Проверка результата:** Локальная эвакуация tier не создаёт delete в Ceph.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.enabled`

Включение операций размещения исходных объектов Ceph.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `false` |
| Область | `placement` |

**Ограничение:** Перед true необходимы адаптер, каталог, health gates и приёмка dry-run.

Фрагмент проектной конфигурации:

```yaml
placement:
  enabled: false
```

**Проверка результата:** Без включения разрешён только расчёт кандидатных действий.

Контрольный отрицательный пример: `maybe`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.dry_run`

Расчёт плана без выполнения перемещения origin.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `placement` |

**Ограничение:** Проектный безопасный начальный профиль; результат содержит would_move.

Фрагмент проектной конфигурации:

```yaml
placement:
  dry_run: true
```

**Проверка результата:** Отсутствуют изменения pool и операции delete при dry-run.

Контрольный отрицательный пример: `sometimes`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.adapter`

Способ обращения к исходному хранилищу.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `rados_catalog_v1` |
| Область | `placement` |

**Ограничение:** rados_catalog_v1 и rgw_storage_class_v1 являются разными проектными адаптерами.

Фрагмент проектной конфигурации:

```yaml
placement:
  adapter: rados_catalog_v1
```

**Проверка результата:** Приёмка фиксирует точную семантику версии и удаления выбранного адаптера.

Контрольный отрицательный пример: `ceph_magic`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.catalog.endpoint`

Адрес сервисного каталога размещения.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `https://catalog.example.invalid/v1` |
| Область | `placement` |

**Ограничение:** Каталог проверяет tenant scope, generation и fencing token.

Фрагмент проектной конфигурации:

```yaml
placement:
  catalog:
    endpoint: https://catalog.example.invalid/v1
```

**Проверка результата:** CAS conflict приводит к перечитыванию записи, а не слепому retry.

Контрольный отрицательный пример: `http://catalog.example.invalid`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.pools.hot`

Подготовленный пул горячего origin.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `content_hot` |
| Область | `placement` |

**Ограничение:** Пул и CRUSH rule созданы и проверены администратором Ceph заранее.

Фрагмент проектной конфигурации:

```yaml
placement:
  pools:
    hot: content_hot
```

**Проверка результата:** Фактические OSD класса соответствуют ожидаемому SSD профилю.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.pools.cold`

Подготовленный пул холодного origin.

| Свойство | Контракт |
| --- | --- |
| Тип | `string` |
| Значение примера | `content_cold` |
| Область | `placement` |

**Ограничение:** Не совпадает с hot в двухуровневом профиле; durability согласована отдельно.

Фрагмент проектной конфигурации:

```yaml
placement:
  pools:
    cold: content_cold
```

**Проверка результата:** Возврат объекта из cold сохраняет версию и checksum.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.scoring.formula_revision`

Версия функции полезности размещения.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `latency_per_byte_v1` |
| Область | `placement` |

**Ограничение:** Изменение формулы сначала проходит shadow comparison.

Фрагмент проектной конфигурации:

```yaml
placement:
  scoring:
    formula_revision: latency_per_byte_v1
```

**Проверка результата:** Журнал решения содержит входы, формулу и выбранный target.

Контрольный отрицательный пример: `latest`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.scoring.confidence_min`

Минимальная уверенность оценки для автоматического переноса.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.9` |
| Область | `placement` |

**Ограничение:** Не компенсирует отсутствие данных: gap всегда имеет отдельный veto.

Фрагмент проектной конфигурации:

```yaml
placement:
  scoring:
    confidence_min: 0.9
```

**Проверка результата:** При низкой уверенности объект остаётся в текущем классе.

Контрольный отрицательный пример: `1.9`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.scoring.freeze_demotions_on_gap`

Запрет понижения класса при неполной телеметрии.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `placement` |

**Ограничение:** Обязателен для профиля, где неизвестность нельзя трактовать как холодность.

Фрагмент проектной конфигурации:

```yaml
placement:
  scoring:
    freeze_demotions_on_gap: true
```

**Проверка результата:** Отказ очереди не вызывает массовый вывод горячих объектов на HDD.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.scoring.shadow_revision`

Параллельная формула без влияния на фактическое размещение.

| Свойство | Контракт |
| --- | --- |
| Тип | `nullable` |
| Значение примера | `null` |
| Область | `placement` |

**Ограничение:** null выключает shadow; другая редакция пишет только сравнительные решения.

Фрагмент проектной конфигурации:

```yaml
placement:
  scoring:
    shadow_revision: null
```

**Проверка результата:** Два расчёта не создают два исполняемых задания одному объекту.

Контрольный отрицательный пример: `""`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.migration.concurrency`

Число одновременных переносов origin.

| Свойство | Контракт |
| --- | --- |
| Тип | `integer` |
| Значение примера | `2` |
| Область | `placement` |

**Ограничение:** Ограничивается кластером, pool и tenant, а не только одним агентом.

Фрагмент проектной конфигурации:

```yaml
placement:
  migration:
    concurrency: 2
```

**Проверка результата:** При двух контроллерах общий лимит не удваивается.

Контрольный отрицательный пример: `0`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.migration.max_bytes_per_second`

Бюджет миграционного трафика за секунду.

| Свойство | Контракт |
| --- | --- |
| Тип | `bytes` |
| Значение примера | `128MiB` |
| Область | `placement` |

**Ограничение:** Отдельный от клиентского чтения и recovery Ceph.

Фрагмент проектной конфигурации:

```yaml
placement:
  migration:
    max_bytes_per_second: 128MiB
```

**Проверка результата:** При повышении client p99 бюджет миграции уменьшается или работа приостанавливается.

Контрольный отрицательный пример: `0B`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.migration.reserve_fraction`

Резерв целевого пула до начала копии.

| Свойство | Контракт |
| --- | --- |
| Тип | `ratio` |
| Значение примера | `0.15` |
| Область | `placement` |

**Ограничение:** Не отменяет реальные nearfull/full проверки и квоты Ceph.

Фрагмент проектной конфигурации:

```yaml
placement:
  migration:
    reserve_fraction: 0.15
```

**Проверка результата:** Большой объект не стартует, если резерв не покрывает полную физическую копию.

Контрольный отрицательный пример: `1.2`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.migration.copy_timeout`

Максимальная жизнь попытки копирования.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `30m` |
| Область | `placement` |

**Ограничение:** После истечения выполняется reconciliation состояния, исходник не удаляется.

Фрагмент проектной конфигурации:

```yaml
placement:
  migration:
    copy_timeout: 30m
```

**Проверка результата:** Сбой ответа после успешной копии не порождает лишнюю публикацию.

Контрольный отрицательный пример: `0s`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.migration.verify`

Критерий проверки новой копии до switch.

| Свойство | Контракт |
| --- | --- |
| Тип | `enum` |
| Значение примера | `length_and_checksum` |
| Область | `placement` |

**Ограничение:** HTTP ETag и размер по отдельности не доказывают checksum содержимого.

Фрагмент проектной конфигурации:

```yaml
placement:
  migration:
    verify: length_and_checksum
```

**Проверка результата:** Повреждённая копия остаётся неопубликованной.

Контрольный отрицательный пример: `length_only`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.migration.delete_grace`

Минимальная задержка уборки старой копии после switch.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `24h` |
| Область | `placement` |

**Ограничение:** Дополнительно обязательны reader leases, generation check и отсутствие pin/retention запрета.

Фрагмент проектной конфигурации:

```yaml
placement:
  migration:
    delete_grace: 24h
```

**Проверка результата:** Долгий активный reader переживает переключение каталога.

Контрольный отрицательный пример: `-24h`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.migration.min_residency`

Минимальное время между обычными переводами origin.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `6h` |
| Область | `placement` |

**Ограничение:** Аварийная эвакуация имеет отдельный режим и audit reason.

Фрагмент проектной конфигурации:

```yaml
placement:
  migration:
    min_residency: 6h
```

**Проверка результата:** Объект около порога не совершает десятки переносов в час.

Контрольный отрицательный пример: `-6h`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.health.pause_on_degraded`

Приостановка фоновых переносов при деградации Ceph.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `placement` |

**Ограничение:** Не останавливает допустимое чтение уже доступных объектов.

Фрагмент проектной конфигурации:

```yaml
placement:
  health:
    pause_on_degraded: true
```

**Проверка результата:** После unhealthy новые задания остаются planned, исходники сохраняются.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.health.pause_on_recovery`

Уступка ресурсов восстановлению Ceph.

| Свойство | Контракт |
| --- | --- |
| Тип | `boolean` |
| Значение примера | `true` |
| Область | `placement` |

**Ограничение:** Возобновление требует окна стабильности, а не одного успешного health poll.

Фрагмент проектной конфигурации:

```yaml
placement:
  health:
    pause_on_recovery: true
```

**Проверка результата:** Измерить recovery time и влияние migration traffic.

Контрольный отрицательный пример: `auto`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.health.resume_stable_for`

Период устойчивого здоровья перед возобновлением.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `10m` |
| Область | `placement` |

**Ограничение:** Сбрасывается при повторном деградированном событии.

Фрагмент проектной конфигурации:

```yaml
placement:
  health:
    resume_stable_for: 10m
```

**Проверка результата:** Flapping кластера не вызывает непрерывные start/stop копирования.

Контрольный отрицательный пример: `-10m`.
Валидатор должен отклонить его для данного поля без применения части настроек.

### `placement.pin.max_ttl`

Максимальная жизнь обычного административного pin.

| Свойство | Контракт |
| --- | --- |
| Тип | `duration` |
| Значение примера | `7d` |
| Область | `placement` |

**Ограничение:** Юридическое удержание и backup retention имеют отдельные обязательные механизмы.

Фрагмент проектной конфигурации:

```yaml
placement:
  pin:
    max_ttl: 7d
```

**Проверка результата:** Истёкший pin снова допускает оценку, но не принудительное удаление.

Контрольный отрицательный пример: `-7d`.
Валидатор должен отклонить его для данного поля без применения части настроек.

## Межполевые проверки

| Правило | Ошибка при нарушении |
| --- | --- |
| low < high < emergency < 1 | CFG_WATERMARK_ORDER |
| RAM max_entry <= RAM budget | CFG_ENTRY_EXCEEDS_TIER |
| Короткое окно < длинного окна | CFG_DECAY_WINDOW_ORDER |
| replay age + skew <= dedup horizon | CFG_REPLAY_OUTSIDE_DEDUP |
| min_backoff <= max_backoff | CFG_RETRY_INTERVAL_ORDER |
| Все пути tier различаются | CFG_OVERLAPPING_TIER_PATHS |
| State directory вне payload cache | CFG_STATE_IN_EVICTABLE_PATH |
| Активный placement имеет каталог | CFG_CATALOG_REQUIRED |
| Hot и cold имеют разную идентичность | CFG_POOL_ALIAS |
| Защищённый маршрут имеет access gate | CFG_ACCESS_REQUIRED |
| Публичный metrics listener имеет защиту | CFG_DIAGNOSTICS_EXPOSED |
| Выключенная telemetry замораживает demotion | CFG_UNKNOWN_DEMAND |

Названия диагностических кодов также являются проектным контрактом.
Код ошибки стабилен между совместимыми выпусками.
Человекочитаемое сообщение может расширяться без изменения кода.
Секретное значение никогда не входит в текст ошибки.
Путь поля возвращается без содержимого keyring.

## Формат результата проверки

```json
{
  "schema_version": 1,
  "valid": false,
  "generation_applied": false,
  "errors": [
    {
      "code": "CFG_WATERMARK_ORDER",
      "path": "cache.tiers.nvme.high_watermark",
      "message": "Expected low < high < emergency < 1"
    }
  ],
  "warnings": []
}
```

Этот объект иллюстрирует будущий ответ валидатора.
Он не является результатом выполнения команды в текущем проекте.
При valid=true возвращаются digest и список обязательных проверок окружения.
Некоторые проверки требуют доступа к локальным дискам или Ceph.
Поэтому синтаксическая проверка отдельно указывает environment_checks_pending.

## Секреты и вывод effective configuration

Effective configuration содержит итоговые значения после нормализации.
Секреты заменяются ссылками или маркером redacted.
Файл не должен содержать URL с паролями и query-подписями.
Диагностический digest секретного файла тоже может раскрывать совпадение ключей;
он доступен только административному контуру с ограниченными правами.

Права чтения обычной конфигурации не равны правам чтения ключей.
Worker получает минимальный материал, необходимый для проверки.
Placement credential не передаётся Lua policy.
Credential удаления origin не передаётся клиентскому обработчику.

## Изменения без потери состояния

Уменьшение budget прекращает новые admission и начинает контролируемое eviction.
Оно не выполняет мгновенное удаление открытых файлов.
Уменьшение concurrency не отменяет уже защищённые фазой verify операции.
Новый threshold влияет на новые решения, а не переписывает историю прошлых.
Смена keyring допускает перекрытие verify-ключей на ограниченный срок.
Отзыв ключа должен действовать и на содержимое в RAM.

Изменение identity_schema создаёт новую область ключей.
Изменение chunk layout не склеивает старые и новые chunks автоматически.
Изменение Ceph pools не перемещает содержимое одной правкой YAML.
Для него выполняется явная процедура переноса с каталогом и аудитом.

## Минимальная приёмка валидатора

- Проверить каждый корректный sample из этой главы.
- Проверить каждый отрицательный sample из этой главы.
- Проверить duplicate key на разных уровнях вложенности.
- Проверить unknown key с почти правильным написанием.
- Проверить переполнение integer до приведения к uint64.
- Проверить пустой файл и файл только с комментариями.
- Проверить циклический YAML alias и чрезмерное число anchors.
- Проверить отсутствие необязательного поля и обязательного поля отдельно.
- Проверить права чтения keyring от имени worker.
- Проверить отказ подготовки нового поколения без замены старого.
- Проверить очистку частично подготовленных ресурсов.
- Проверить audit entry для успешного и отклонённого apply.

## Связанные главы

[Архитектура](10-architecture.md) определяет владельцев состояния.
[Интеграция Nginx и Lua](11-nginx-lua.md) описывает подключение модуля.
[Популярность](21-popularity.md) задаёт смысл статистических параметров.
[Интеграция с Ceph](22-ceph-integration.md) описывает безопасный перенос.
[Контракты API](41-api-contracts.md) описывают обмен сообщениями.

---

# Контракты API, телеметрии и управления размещением

> Статус: проектная спецификация v0.1. Имена API и параметры в примерах задают целевой контракт; перечисленные endpoint не подтверждают существование готового сервера или SDK.

## Назначение интерфейсов

API связывает worker, локальный агент, агрегатор и адаптер размещения.
Клиентская выдача файлов не использует административные endpoint.
Публичный HTTP-путь к контенту остаётся отдельной поверхностью.
Операции API ограничиваются tenant scope и ролью вызывающей стороны.
Телеметрия сообщает наблюдённый спрос.
Каталог сообщает активное размещение конкретной неизменяемой версии.
Планировщик предлагает изменение размещения.
Исполнитель выполняет проверяемый план с журналом состояния.

Версия пути `/v1` означает версию API.
`schema_version` в событии означает версию телеметрии.
`server.schema_version` означает версию YAML-конфигурации.
Эти версии не заменяют друг друга.
Совместимость объявляется через capabilities.
Неизвестная major version отклоняется явно.

## Транспорт

Локальный control API передаётся через Unix domain socket.
Пример пути сокета: `/run/sloyra/control.sock`.
Публичный TCP-порт по умолчанию не создаётся.
Удалённая интеграция использует TLS с аутентификацией обеих сторон по выбранному профилю.
Доверенные сертификаты и tenant scope задаются конфигурацией.
HTTP-заголовок клиента не может расширить роль, выданную транспортной аутентификацией.

Запросы и ответы имеют Content-Type `application/json`.
JSON использует UTF-8.
Повторяющиеся ключи объекта отклоняются парсером SLOYRA.
Неизвестные обязательные enum отклоняются.
Неизвестные поля запроса отклоняются в строгом профиле.
Служебные расширения допускаются только внутри объявленного поля `extensions`.
Основа синтаксиса JSON описана в [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259.html).

## Единицы и численные типы

Размеры передаются в байтах.
Временные интервалы имеют суффиксы `_ms`, `_s` или `_ns`.
Абсолютное время передаётся строкой UTC с суффиксом Z.
Примеры времени следуют профилю [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339.html).
Проект v1 принимает целые числа JSON только в безопасном диапазоне 0..9007199254740991.
Дробные значения не принимаются вместо счётчиков и generation.
Отрицательные байты и sequence отклоняются.
Floating-point используется только для явно объявленных вероятностей и score.
NaN и Infinity не допускаются.

Producer sequence начинается с 1.
Producer epoch устойчиво изменяется при новом запуске или сбросе последовательности.
Одна тройка producer_id, producer_epoch, sequence никогда не переиспользуется для другого события.
До исчерпания безопасного диапазона producer завершает epoch и создаёт новый.
Переход epoch сохраняется до отправки событий нового запуска.
Генерация каталога также ограничена безопасным диапазоном wire v1.
Достижение предела требует нового ключа версии схемы, а не перехода через ноль.

## Разделение полномочий

| Роль | Разрешённые действия |
| --- | --- |
| `observer` | Health, capabilities и очищенная диагностика |
| `telemetry_producer` | Отправка событий своего producer scope |
| `telemetry_reader` | Чтение качества и receipts разрешённой области |
| `config_operator` | Валидация и активация снимков |
| `placement_planner` | Создание предложений плана |
| `placement_executor` | Выполнение разрешённого плана |
| `catalog_writer` | CAS после проверки lease и доказательств |
| `cache_operator` | Purge и управление локальным admission |
| `node_operator` | Drain и техническое обслуживание узла |

Идентификатор роли не передаётся как доверенный JSON-параметр.
Операция проверяет роль на сервере.
Права на telemetry не дают права менять каталог.
Права на план не дают права читать секрет Ceph.
Read-only диагностика не раскрывает подписи URL и locator credentials.

## Общая форма ошибки

Код ошибки стабилен в рамках API v1.
Человекочитаемый текст может уточняться без изменения протокола.
Клиент не разбирает смысл по строке message.
`retryable` означает допустимость повтора после устранения временного условия.
Он не означает, что повтор обязательно завершится успехом.

```json
{
  "api_version": 1,
  "request_id": "ctl-req-001",
  "error": {
    "code": "generation_conflict",
    "message": "Expected catalog generation does not match",
    "retryable": false,
    "details": {
      "expected_generation": 42,
      "actual_generation": 43,
      "resource": "catalog-object-a"
    }
  }
}
```

| HTTP status | Категория SLOYRA API |
| --- | --- |
| 400 | Неправильная структура запроса |
| 401 | Не пройдена аутентификация control API |
| 403 | Недостаточна административная роль или tenant scope |
| 404 | Ресурс отсутствует в доступной области |
| 409 | Конфликт generation, lease или idempotency |
| 413 | Превышен объявленный размер тела |
| 415 | Неподдерживаемый Content-Type |
| 422 | Синтаксически корректный, но недопустимый контракт |
| 429 | Ограничен административный budget |
| 503 | Компонент не готов принимать операцию |

Статусы административного API не меняют клиентские статусы контентного маршрута.
Например, 401 control API не означает новый режим пользовательской signed URL авторизации.
Необратимая ошибка не повторяется бесконечно.
Неисправное событие переносится в ограниченный dead-letter storage с очищенным описанием.

## Идемпотентность изменений

Каждая изменяющая операция содержит `operation_id`.
Ключ идемпотентности включает principal scope, endpoint family и operation_id.
Одинаковый ключ с одинаковым содержимым возвращает прежний receipt.
Одинаковый ключ с другим содержимым возвращает 409.
Сравнение содержимого выполняется по нормализованной структуре JSON.
Порядок ключей JSON не делает операцию другой.
Имена и типы значимых полей входят в сравнение.

Повтор after timeout сначала выясняет состояние прежней операции.
Если receipt утрачен клиентом, нельзя сразу выдавать новый operation_id для той же миграции.
Срок хранения receipts объявляется capabilities.
Клиент не повторяет неизвестно завершившуюся операцию после этого срока без reconciliation.
Операция cancel тоже идемпотентна.
Cancel подтверждает запрос остановки, а не мгновенное исчезновение работающего I/O.

## Согласованность телеметрии

Доставка событий имеет семантику at-least-once.
Сетевой повтор возможен и считается нормальной ситуацией.
Получатель дедуплицирует по event_id до применения события к агрегату.
Batch id удобен для транспорта, но не заменяет event id.
Один event может повторно появиться в другом batch.
Итоговый popularity не должен увеличиваться от такого повтора.

Receipt-пример с `contiguous_through: 180420` предполагает, что диапазон 1..180419 этого epoch уже устойчиво принят.
Один batch с sequence 180420 не позволяет самостоятельно подтвердить весь предшествующий диапазон.
Запись dedup и изменение агрегата выполняются атомарно либо через воспроизводимый журнал.
ACK выдаётся после устойчивой фиксации принятого события.
Сбой между dedup и aggregate не может потерять событие навсегда.
Сбой между aggregate и dedup не может удвоить вклад после повтора.
Получатель сохраняет достаточно информации для восстановления обеих частей.
Это проектное требование к обработке, а не обещание транспорта exactly-once.

## Семантика событий

| Событие | Когда создаётся | Вклад |
| --- | --- | --- |
| `logical_request` | На каждый допущенный запрос | Пользовательский спрос |
| `cache_lookup` | На каждую попытку tier/chunk lookup | Эффективность кеша |
| `origin_fetch` | На каждую физическую попытку origin | Нагрузка хранилища |
| `security_decision` | На отказ или throttle по политике | Безопасность, без popularity |
| `quality_gap` | При подтверждённой потере событий | Качество измерения |
| `placement_result` | При переходе job или завершении | Операционная история |

Cache hit обязательно создаёт logical_request.
HEAD создаёт logical_request, но requested_bytes и served_bytes тела равны нулю.
Метаданные HEAD измеряются отдельным metadata_demand.
Denied-запрос не создаёт admitted logical_request.
Один request может выполнить много cache_lookup.
Ни один cache_lookup не добавляет дополнительный голос пользовательской популярности.
Retry origin создаёт новый origin_attempt_id.
Общий fill_id связывает попытки и followers.
Частично прочитанные origin bytes учитываются даже при ошибке попытки.

## Базовый event envelope

```json
{
  "schema_version": 1,
  "event_type": "logical_request",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180420
  },
  "observed_at": "2026-09-27T10:00:00Z",
  "completed_at": "2026-09-27T10:00:00.120Z",
  "tenant_id": "demo-media",
  "object_id_hash": "hmac-sha256:h1:example-object-a",
  "identity_hash_version": "h1",
  "content_version": "v17",
  "representation_id": "source",
  "range": {
    "start": 0,
    "length": 4194304
  },
  "requested_bytes": 4194304,
  "requested_bytes_known": true,
  "sample_probability": 1.0,
  "policy_revision": "p12"
}
```

Значения hash с префиксом example являются условными идентификаторами, а не результатом криптографического расчёта.
`observed_at` обозначает время наблюдения producer.
`completed_at` обозначает завершение операции, если оно известно.
`ingested_at` добавляет получатель и не доверяет его значению от producer.
`range` может быть null только там, где тип события допускает отсутствие байтовой области.
При неизвестной длине `requested_bytes_known` равно false, а `requested_bytes` равно 0 как технический placeholder.
Потребитель не принимает этот placeholder за подтверждённый нулевой спрос.

## ACK и повтор

Receipt перечисляет принятые, уже известные и отклонённые event id.
Отправитель удаляет событие из spool только после durable accepted или durable duplicate.
Permanently rejected событие переносится в dead-letter и учитывается как потеря качества.
Временный отказ оставляет событие в spool для повтора.
Неоднозначный timeout не даёт права удалить batch.
При потере ответа повторяется исходный набор event id.

При out-of-order доставке хранится множество принятых последовательностей или эквивалентная интервальная структура.
Watermark `contiguous_through` не перескакивает через неизвестный gap.
Большой maximum_seen не означает, что все меньшие sequence приняты.
Закрытый epoch с неизвестными gap снижает качество соответствующего окна.
Потери не интерпретируются как падение спроса до нуля.
При недостаточном качестве планировщик замораживает опасные demotion.

## Каталог и immutable identity

Identity содержит tenant, object hash, content version и representation.
Chunk identity дополнительно содержит range и layout version.
CAS может переключить location только внутри одной identity.
Новая версия содержимого создаёт другую identity.
Source и destination сравниваются по ожидаемому digest и durability policy.
Сервер CAS проверяет verify receipt по собственному устойчивому журналу или доверенному подтверждению verifier.
Переданный клиентом флаг verified или строка digest сами по себе не доказывают целостность destination.
Receipt связывает точную identity, destination locator, digest, длину, durability policy и move id.
Перед cleanup повторно проверяется, что удаляемая копия не является текущей active и не удерживается reader lease.
Locator является непрозрачным идентификатором адаптера.
URL клиента не становится locator автоматически.

Переключение каталога не удаляет старую копию немедленно.
Сначала закрывается возможность новых reader на прежний active location.
Затем учитываются существующие lease и grace.
Удаление выполняется отдельной стадией job после проверки безопасности.
CAS conflict требует перечитать запись и перепланировать.
Blind overwrite поверх конкурентного изменения запрещён.

## Endpoint и примеры

Все ответы ниже являются образцами проектного контракта.
Идентификаторы можно подставлять в связанные примеры только в одном тестовом окружении.
GET endpoint не принимает JSON-тело; для него показан только ответ.

### API-01. Возможности сервиса

`GET /v1/capabilities`

Роль: `observer`.
Узнать версии схем и доступные ограничения до отправки изменяющей операции.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "service": "sloyra-agent",
  "build": "0.1.0-design",
  "profiles": [
    "native",
    "lua"
  ],
  "schemas": {
    "configuration": [
      1
    ],
    "telemetry": [
      1
    ],
    "catalog": [
      1
    ],
    "placement_plan": [
      1
    ]
  },
  "limits": {
    "request_body_bytes": 1048576,
    "batch_events": 4096,
    "batch_bytes": 1048576,
    "page_size": 500,
    "json_depth": 32
  },
  "retention": {
    "operation_receipt_s": 259200,
    "telemetry_dedup_s": 259200
  },
  "features": {
    "dry_run": true,
    "catalog_cas": true,
    "multipart_ranges": false,
    "placement_apply": false
  }
}
```

Клиент проверяет наличие нужной функции до использования endpoint.
Численные limits являются примером ответа конкретного профиля.
Capabilities не раскрывает secret refs и внутренние Ceph addresses.

Ошибки контракта:

- 503 capabilities_not_ready: bootstrap ещё не сформировал безопасный ответ.

### API-02. Liveness

`GET /v1/health/live`

Роль: `observer`.
Проверить работу цикла локального процесса.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "node_id": "edge-07",
  "boot_id": "boot-example-31",
  "live": true,
  "checked_at": "2026-09-27T10:00:00Z",
  "process": {
    "role": "agent",
    "uptime_s": 3600,
    "event_loop_responsive": true
  },
  "checks": [
    {
      "name": "main_loop",
      "state": "pass"
    },
    {
      "name": "watchdog",
      "state": "pass"
    }
  ]
}
```

Liveness не выполняет синхронный RPC к Ceph.
Недоступность collector не делает живой процесс dead.
HTTP 200 этого endpoint не разрешает подавать защищённый трафик.

Ошибки контракта:

- 503 process_unhealthy: основной цикл не выполняет объявленный контракт.

### API-03. Readiness

`GET /v1/health/ready`

Роль: `observer`.
Проверить готовность узла принимать новый трафик.

Проектный ответ: HTTP 503.

```json
{
  "api_version": 1,
  "node_id": "edge-07",
  "ready": false,
  "state": "degraded",
  "config_generation": 42,
  "checked_at": "2026-09-27T10:00:00Z",
  "routes": [
    {
      "route_id": "media",
      "ready": false,
      "reason": "required_keyring_unavailable"
    }
  ],
  "components": {
    "access": "unavailable",
    "cache": "ready",
    "telemetry": "ready",
    "placement": "paused"
  },
  "retry_after_s": 1
}
```

Готовность защищённого маршрута требует доступной обязательной политики.
Ошибка вспомогательной телеметрии может дать degraded без снятия readiness.
Проверка использует bounded-age результаты фоновых checks.

Ошибки контракта:

- 503 route_not_ready: хотя бы один обязательный маршрут не готов.

### API-04. Состояние узла

`GET /v1/nodes/edge-07`

Роль: `observer`.
Получить очищенный обзор поколений и занятых бюджетов.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "node_id": "edge-07",
  "boot_id": "boot-example-31",
  "state": "serving",
  "config_generation": 42,
  "workers": {
    "ready": 8,
    "draining": 0,
    "failed": 0
  },
  "resources": {
    "open_files": 1840,
    "open_file_budget": 65536,
    "memory_bytes": 8589934592
  },
  "tiers": [
    {
      "id": "ram",
      "state": "ready",
      "admission": "enabled"
    },
    {
      "id": "nvme",
      "state": "ready",
      "admission": "enabled"
    },
    {
      "id": "hdd",
      "state": "ready",
      "admission": "enabled"
    }
  ],
  "telemetry": {
    "pending_events": 124,
    "oldest_event_age_s": 2,
    "quality": "complete"
  }
}
```

Open files включает только наблюдаемый областью API budget.
Проценты вычисляет клиент из значений и документированного знаменателя.
Состояние не содержит абсолютные секретные пути и credentials.

Ошибки контракта:

- 404 node_unknown: узел отсутствует в доступной области.

### API-05. Валидация снимка

`POST /v1/config/validations`

Роль: `config_operator`.
Проверить пакет конфигурации без его активации.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "validate-43",
  "expected_active_generation": 42,
  "candidate": {
    "schema_version": 1,
    "artifact_ref": "artifact:config-43",
    "sha256": "example-config-digest",
    "policy_bundle_ref": "artifact:policy-p13"
  },
  "checks": {
    "schema": true,
    "routes": true,
    "secrets_resolvable": true,
    "module_compatibility": true
  }
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "validation_id": "validation-43",
  "state": "valid",
  "active_generation_unchanged": 42,
  "candidate_digest": "example-config-digest",
  "checks": [
    {
      "name": "schema",
      "state": "pass"
    },
    {
      "name": "routes",
      "state": "pass"
    },
    {
      "name": "secret_refs",
      "state": "pass"
    },
    {
      "name": "module_compatibility",
      "state": "pass"
    }
  ],
  "warnings": [],
  "expires_at": "2026-09-27T10:05:00Z"
}
```

Artifact ref указывает на предварительно загруженный контролируемый пакет.
API не читает произвольный путь файловой системы из запроса.
Валидация secret ref проверяет доступность, но не возвращает материал секрета.

Ошибки контракта:

- 409 active_generation_changed: требуется повторная валидация.
- 422 validation_failed: ответ содержит field paths и коды.

### API-06. Активация снимка

`POST /v1/config/activations`

Роль: `config_operator`.
Опубликовать проверенный снимок как новое поколение.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "activate-43",
  "validation_id": "validation-43",
  "candidate_digest": "example-config-digest",
  "expected_active_generation": 42,
  "next_generation": 43,
  "activation": {
    "mode": "atomic",
    "wait_for_workers": true,
    "timeout_ms": 30000
  }
}
```

Проектный ответ: HTTP 202.

```json
{
  "api_version": 1,
  "operation_id": "activate-43",
  "job_id": "config-job-43",
  "state": "accepted",
  "receipt_durable": true,
  "previous_generation": 42,
  "target_generation": 43,
  "links": {
    "job": "/v1/jobs/config-job-43"
  },
  "accepted_at": "2026-09-27T10:00:00Z"
}
```

202 означает принятую устойчивую задачу, а не готовность всех worker.
При ошибке нового поколения прежнее остаётся активным.
Истёкший validation receipt требует новой проверки candidate.

Ошибки контракта:

- 409 generation_conflict: active generation изменилась.
- 422 candidate_changed: digest не совпадает с validation receipt.

### API-07. История поколений

`GET /v1/config/generations?limit=2`

Роль: `observer`.
Проверить активное и предыдущее поколения.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "items": [
    {
      "generation": 43,
      "state": "active",
      "policy_revision": "p13",
      "activated_at": "2026-09-27T10:00:00Z",
      "worker_references": 8
    },
    {
      "generation": 42,
      "state": "draining",
      "policy_revision": "p12",
      "activated_at": "2026-09-27T09:00:00Z",
      "worker_references": 2
    }
  ],
  "page": {
    "limit": 2,
    "next_cursor": null,
    "snapshot_id": "config-list-43"
  }
}
```

Старое поколение может сохраняться, пока запросы удерживают ссылки.
Откат публикуется новым поколением с прежним содержимым.
Pagination cursor непрозрачен и не является offset таблицы.

Ошибки контракта:

- 400 invalid_cursor: cursor не относится к этой коллекции.

### API-08. Приём batch телеметрии

`POST /v1/telemetry/batches`

Роль: `telemetry_producer`.
Устойчиво принять события с дедупликацией по event id.

Проектный запрос:

```json
{
  "api_version": 1,
  "batch_id": "edge-07-31-batch-100",
  "producer_id": "edge-07",
  "producer_epoch": 31,
  "sent_at": "2026-09-27T10:00:01Z",
  "event_schema": 1,
  "events": [
    {
      "schema_version": 1,
      "event_type": "logical_request",
      "event_id": {
        "producer_id": "edge-07",
        "producer_epoch": 31,
        "sequence": 180420
      },
      "observed_at": "2026-09-27T10:00:00Z",
      "tenant_id": "demo-media",
      "object_id_hash": "hmac-sha256:h1:example-object-a",
      "identity_hash_version": "h1",
      "content_version": "v17",
      "representation_id": "source",
      "range": {
        "start": 0,
        "length": 4194304
      },
      "requested_bytes": 4194304,
      "requested_bytes_known": true,
      "sample_probability": 1.0,
      "policy_revision": "p12",
      "request_id": "req-420",
      "method": "GET",
      "served_bytes": 4194304,
      "status": 206,
      "cache_summary": "hit",
      "metadata_demand": false
    }
  ]
}
```

Проектный ответ: HTTP 202.

```json
{
  "api_version": 1,
  "receipt_id": "receipt-batch-100",
  "batch_id": "edge-07-31-batch-100",
  "durable": true,
  "accepted_event_ids": [
    {
      "producer_id": "edge-07",
      "producer_epoch": 31,
      "sequence": 180420
    }
  ],
  "duplicate_event_ids": [],
  "rejected": [],
  "quality": {
    "known_gaps": 0,
    "contiguous_through": 180420
  },
  "received_at": "2026-09-27T10:00:01Z"
}
```

Batch ограничен числом событий и размером после декомпрессии.
ACK не отправляется до устойчивой фиксации event ids.
Payload с уже известным event id и другими значимыми полями отклоняется как конфликт.

Ошибки контракта:

- 400 invalid_batch: оболочка не позволяет безопасно разобрать события.
- 409 event_identity_conflict: event id переиспользован.
- 429 ingest_overloaded: повтор по server delay.

### API-09. Получение receipt

`GET /v1/telemetry/receipts/receipt-batch-100`

Роль: `telemetry_reader`.
Восстановить результат после потери ответа batch-запроса.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "receipt_id": "receipt-batch-100",
  "batch_id": "edge-07-31-batch-100",
  "durable": true,
  "summary": {
    "accepted": 1,
    "duplicate": 0,
    "permanently_rejected": 0,
    "temporarily_rejected": 0
  },
  "accepted_event_ids": [
    {
      "producer_id": "edge-07",
      "producer_epoch": 31,
      "sequence": 180420
    }
  ],
  "duplicate_event_ids": [],
  "rejected": [],
  "expires_at": "2026-09-30T10:00:01Z"
}
```

Receipt доступен только producer или роли с соответствующим tenant scope.
Истечение receipt не означает, что события не были приняты.
При неизвестном исходе и утрате receipt клиент использует dedup-протокол, а не новые event ids.

Ошибки контракта:

- 404 receipt_unknown: идентификатор не найден в доступном retention.

### API-10. Watermark producer

`GET /v1/telemetry/producers/edge-07/epochs/31`

Роль: `telemetry_reader`.
Определить непрерывную принятую последовательность и реальные пропуски.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "producer_id": "edge-07",
  "producer_epoch": 31,
  "state": "open",
  "maximum_seen": 180430,
  "contiguous_through": 180420,
  "missing_intervals": [
    {
      "start": 180421,
      "end_inclusive": 180424
    }
  ],
  "accepted_intervals": [
    {
      "start": 180425,
      "end_inclusive": 180430
    }
  ],
  "dedup_lower_bound": 100000,
  "last_received_at": "2026-09-27T10:00:03Z",
  "quality": "gapped"
}
```

Maximum seen не является подтверждением всех предшествующих событий.
Missing intervals могут сократиться после запоздалой доставки.
Dedup lower bound не разрешает повторно считать более старые event ids как новые.

Ошибки контракта:

- 404 producer_epoch_unknown: epoch не зарегистрирован.

### API-11. Закрытие epoch

`POST /v1/telemetry/producers/edge-07/epochs/31/close`

Роль: `telemetry_producer`.
Завершить последовательность после drain producer.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "close-edge-07-31",
  "final_sequence": 180430,
  "declared_losses": [
    {
      "start": 180421,
      "end_inclusive": 180424,
      "reason": "spool_corruption"
    }
  ],
  "next_epoch": 32
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "producer_id": "edge-07",
  "producer_epoch": 31,
  "state": "closed_with_gaps",
  "final_sequence": 180430,
  "quality": {
    "complete": false,
    "lost_events": 4,
    "freeze_demotions": true
  },
  "receipt_durable": true
}
```

Epoch закрывается только после попытки выгрузить оставшийся spool.
Loss declaration не подменяет события нулевым спросом.
Поздние события закрытого epoch принимаются только по объявленной reconciliation policy.

Ошибки контракта:

- 409 epoch_already_closed_differently: другой final_sequence.
- 422 gap_declaration_inconsistent: интервалы не соответствуют известным данным.

### API-12. Качество временного окна

`GET /v1/telemetry/quality?tenant_id=demo-media&window_id=w100`

Роль: `telemetry_reader`.
Проверить пригодность измерений для решений о demotion.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "tenant_id": "demo-media",
  "window_id": "w100",
  "start": "2026-09-27T09:55:00Z",
  "end": "2026-09-27T10:00:00Z",
  "quality": {
    "state": "incomplete",
    "estimated_coverage": 0.97,
    "late_events": 12,
    "lost_events": 4,
    "clock_skewed_producers": 1
  },
  "sampling": {
    "probability": 1.0,
    "revision": "sample-p1"
  },
  "placement_use": {
    "promotions_allowed": true,
    "demotions_allowed": false,
    "reason": "incomplete_demand_window"
  }
}
```

Estimated coverage является оценкой и не превращается в гарантированную долю всех клиентов.
Неравномерная потеря одного tenant не размазывается по остальным.
Поздние события изменяют window revision и отражаются в следующем плане.

Ошибки контракта:

- 404 window_unknown: окно ещё не создано или истекло.

### API-13. Создание плана размещения

`POST /v1/placement/plans`

Роль: `placement_planner`.
Создать проверяемый план из конкретной версии агрегатов и каталога.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "plan-501",
  "tenant_id": "demo-media",
  "mode": "dry_run",
  "inputs": {
    "window_id": "w101",
    "window_revision": 3,
    "catalog_snapshot": "catalog-snapshot-88",
    "policy_revision": "placement-p9"
  },
  "budget": {
    "max_moves": 10,
    "max_bytes": 10737418240,
    "max_parallel_moves": 2
  },
  "constraints": {
    "freeze_demotions_on_gaps": true,
    "preserve_durability": true
  }
}
```

Проектный ответ: HTTP 201.

```json
{
  "api_version": 1,
  "plan_id": "plan-501",
  "state": "proposed",
  "mode": "dry_run",
  "revision": 1,
  "summary": {
    "promotions": 2,
    "demotions": 0,
    "bytes_to_copy": 8388608,
    "estimated_temporary_bytes": 8388608
  },
  "quality_gate": {
    "passed": true,
    "window_revision": 3
  },
  "expires_at": "2026-09-27T10:10:00Z"
}
```

Dry run не меняет каталог и не копирует данные.
План содержит immutable identities и ожидаемые generations.
Неполное окно не инициирует demotion только из-за отсутствующих обращений.

Ошибки контракта:

- 409 stale_window_revision: требуется новый расчёт.
- 422 placement_budget_invalid: бюджет не может обеспечить safe copy.

### API-14. Просмотр плана

`GET /v1/placement/plans/plan-501`

Роль: `observer`.
Получить решения и причины для ревью перед исполнением.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "plan_id": "plan-501",
  "revision": 1,
  "state": "proposed",
  "mode": "dry_run",
  "tenant_id": "demo-media",
  "operations": [
    {
      "move_id": "move-001",
      "identity_ref": "catalog-object-a",
      "expected_generation": 42,
      "source_class": "hdd",
      "target_class": "ssd",
      "bytes": 4194304,
      "reason": "sustained_backend_demand",
      "evidence": {
        "window_id": "w101",
        "backend_miss_bytes": 4194304,
        "logical_requests": 90
      },
      "durability_policy_id": "durability-r3"
    }
  ],
  "skipped": [
    {
      "identity_ref": "catalog-object-b",
      "reason": "active_reader_lease"
    }
  ],
  "apply_eligible": false
}
```

Reason отражает решение политики, а evidence — измерение с единицами.
Dry-run plan не становится исполняемым от изменения клиентского поля mode.
Для apply нужен отдельный проверенный план или явный процесс promotion плана.

Ошибки контракта:

- 404 plan_unknown: план отсутствует или недоступен principal.

### API-15. Запуск placement job

`POST /v1/placement/jobs`

Роль: `placement_executor`.
Начать исполнение предварительно одобренного apply-плана.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "execute-plan-502",
  "plan_id": "plan-502",
  "expected_plan_revision": 2,
  "expected_mode": "apply",
  "execution": {
    "max_parallel_moves": 2,
    "max_copy_bytes_per_s": 104857600,
    "cancel_on_quality_degradation": true
  }
}
```

Проектный ответ: HTTP 202.

```json
{
  "api_version": 1,
  "job_id": "placement-job-502",
  "operation_id": "execute-plan-502",
  "state": "accepted",
  "receipt_durable": true,
  "plan_id": "plan-502",
  "plan_revision": 2,
  "stages": [
    "prepare",
    "copy",
    "verify",
    "switch",
    "drain",
    "cleanup"
  ],
  "links": {
    "status": "/v1/jobs/placement-job-502",
    "cancel": "/v1/jobs/placement-job-502/cancel"
  }
}
```

202 не означает, что объект уже перемещён.
Исполнитель повторно проверяет generation перед каждой опасной стадией.
План с истёкшим сроком или изменённым качеством требует revalidation.

Ошибки контракта:

- 409 plan_revision_conflict: план изменился.
- 422 dry_run_plan: этот план не разрешён к исполнению.

### API-16. Состояние job

`GET /v1/jobs/placement-job-502`

Роль: `observer`.
Наблюдать стадию, результат и возможность отмены.

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "job_id": "placement-job-502",
  "kind": "placement",
  "state": "running",
  "revision": 8,
  "stage": "verify",
  "progress": {
    "moves_total": 2,
    "moves_completed": 0,
    "bytes_planned": 8388608,
    "bytes_copied": 4194304,
    "bytes_verified": 2097152
  },
  "current_move": {
    "move_id": "move-001",
    "identity_ref": "catalog-object-a",
    "source_preserved": true,
    "catalog_switched": false
  },
  "cancel": {
    "allowed": true,
    "effect": "retain_source_remove_unpublished_destination"
  },
  "updated_at": "2026-09-27T10:00:20Z",
  "terminal_result": null
}
```

Progress отражает конкретные единицы и не обещает точный ETA.
Revision растёт при значимом изменении состояния.
Terminal result заполняется только в конечном состоянии.

Ошибки контракта:

- 404 job_unknown: job не существует в доступном retention.

### API-17. Отмена job

`POST /v1/jobs/placement-job-502/cancel`

Роль: `placement_executor`.
Остановить дальнейшее исполнение с безопасной обработкой текущей стадии.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "cancel-job-502",
  "expected_job_revision": 8,
  "reason": "operator_requested",
  "cleanup_policy": "preserve_last_verified_copy"
}
```

Проектный ответ: HTTP 202.

```json
{
  "api_version": 1,
  "job_id": "placement-job-502",
  "state": "cancelling",
  "revision": 9,
  "cancel_requested": true,
  "current_stage": "verify",
  "planned_result": {
    "source_retained": true,
    "unpublished_destination_removed": true,
    "catalog_rollback_required": false
  },
  "receipt_durable": true
}
```

Отмена до switch сохраняет source как active.
Отмена после switch не делает слепой rollback generation.
Cleanup удаляет только копии, которые безопасно удалить по текущему каталогу.

Ошибки контракта:

- 409 job_revision_conflict: перечитать состояние перед отменой.
- 409 job_terminal: вернуть текущий конечный receipt без новой работы.

### API-18. Приостановка планировщика

`POST /v1/placement/control/pause`

Роль: `placement_executor`.
Прекратить запуск новых moves без повреждения выполняющихся операций.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "pause-placement-01",
  "scope": {
    "tenant_id": "demo-media"
  },
  "expected_control_revision": 12,
  "mode": "stop_new_moves",
  "reason": "maintenance_window"
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "control_revision": 13,
  "state": "paused",
  "scope": {
    "tenant_id": "demo-media"
  },
  "running_moves": 1,
  "new_moves_allowed": false,
  "running_move_policy": "finish_safe_boundary",
  "receipt_durable": true
}
```

Pause не означает немедленную остановку уже выполняемого copy.
Исполнитель достигает безопасной границы и журналирует её.
Чтение контента продолжается из текущего active location.

Ошибки контракта:

- 409 control_revision_conflict: параллельное изменение control state.

### API-19. Возобновление планировщика

`POST /v1/placement/control/resume`

Роль: `placement_executor`.
Разрешить новые moves после повторной проверки условий.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "resume-placement-01",
  "scope": {
    "tenant_id": "demo-media"
  },
  "expected_control_revision": 13,
  "revalidate_pending_plans": true
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "control_revision": 14,
  "state": "running",
  "revalidation": {
    "plans_ready": 1,
    "plans_expired": 2,
    "plans_conflicted": 0
  },
  "new_moves_allowed": true,
  "receipt_durable": true
}
```

Устаревшие планы не запускаются автоматически без revalidation.
Новый свободный budget проверяется после maintenance.
Resume не отменяет freeze_demotions, вызванный плохим качеством данных.

Ошибки контракта:

- 409 control_revision_conflict: состояние изменилось.
- 503 placement_dependencies_unready: необходимые компоненты ещё не готовы.

### API-20. Разрешение каталога

`POST /v1/catalog/resolve`

Роль: `observer`.
Найти active location конкретной immutable identity.

Проектный запрос:

```json
{
  "api_version": 1,
  "identity": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "identity_hash_version": "h1",
    "content_version": "v17",
    "representation_id": "source",
    "layout_version": 1,
    "range": {
      "start": 0,
      "length": 4194304
    }
  },
  "consistency": "authoritative"
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "identity": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "identity_hash_version": "h1",
    "content_version": "v17",
    "representation_id": "source",
    "layout_version": 1,
    "range": {
      "start": 0,
      "length": 4194304
    }
  },
  "generation": 42,
  "active_location": "locator:hdd:object-a-v17",
  "placement_class": "hdd",
  "content_length": 4194304,
  "content_digest": "sha256:example-content-digest",
  "durability_policy_id": "durability-r3",
  "last_verified_at": "2026-09-27T09:50:00Z",
  "pending_migration": null
}
```

Authoritative resolve нужен перед CAS, когда stale cache недопустим.
Публичный observer профиль может получить только placement class без locator.
Смена content_version требует другого resolve identity.

Ошибки контракта:

- 404 catalog_identity_unknown: точная identity отсутствует.
- 503 catalog_unavailable: неизвестный результат не заменяется догадкой.

### API-21. Выдача lease исполнителю

`POST /v1/catalog/leases`

Роль: `placement_executor`.
Получить ограниченное право выполнить переход identity.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "lease-move-001",
  "identity": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "identity_hash_version": "h1",
    "content_version": "v17",
    "representation_id": "source",
    "layout_version": 1,
    "range": {
      "start": 0,
      "length": 4194304
    }
  },
  "expected_generation": 42,
  "holder_id": "executor-a",
  "move_id": "move-001",
  "requested_ttl_s": 30
}
```

Проектный ответ: HTTP 201.

```json
{
  "api_version": 1,
  "lease_id": "lease-901",
  "identity": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "identity_hash_version": "h1",
    "content_version": "v17",
    "representation_id": "source",
    "layout_version": 1,
    "range": {
      "start": 0,
      "length": 4194304
    }
  },
  "holder_id": "executor-a",
  "fencing_token": 901,
  "expires_at": "2026-09-27T10:00:30Z",
  "renew_after_s": 10,
  "generation_at_issue": 42,
  "receipt_durable": true
}
```

Fencing token монотонен в области identity.
Истечение lease запрещает новым операциям holder менять каталог.
Сам факт наличия старого lease id не обходит более новый fencing token.

Ошибки контракта:

- 409 lease_held: действует другой исполнитель.
- 409 generation_conflict: identity уже переключена.

### API-22. Продление lease

`POST /v1/catalog/leases/lease-901/renew`

Роль: `placement_executor`.
Продлить текущий lease без смены владельца.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "renew-lease-901-1",
  "holder_id": "executor-a",
  "fencing_token": 901,
  "expected_generation": 42,
  "requested_ttl_s": 30
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "lease_id": "lease-901",
  "fencing_token": 901,
  "expires_at": "2026-09-27T10:00:50Z",
  "renew_after_s": 10,
  "state": "active",
  "receipt_durable": true
}
```

Продление ограничено серверным максимальным TTL.
Операция после утраты fencing возвращает конфликт, а не воскресший lease.
При неизвестном результате исполнитель прекращает опасные стадии до reconciliation.

Ошибки контракта:

- 409 lease_lost: другой владелец получил новый token.
- 409 lease_expired: требуется новая выдача lease и перепроверка каталога.

### API-23. CAS переключения каталога

`POST /v1/catalog/transitions`

Роль: `catalog_writer`.
Атомарно опубликовать проверенный destination.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "switch-move-001",
  "identity": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "identity_hash_version": "h1",
    "content_version": "v17",
    "representation_id": "source",
    "layout_version": 1,
    "range": {
      "start": 0,
      "length": 4194304
    }
  },
  "expected_generation": 42,
  "expected_active": "locator:hdd:object-a-v17",
  "next_generation": 43,
  "next_active": "locator:ssd:object-a-v17",
  "lease_id": "lease-901",
  "fencing_token": 901,
  "move_id": "move-001",
  "verification": {
    "content_digest": "sha256:example-content-digest",
    "bytes_verified": 4194304,
    "durability_policy_id": "durability-r3",
    "receipt_ref": "verify-receipt-001"
  }
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "transition_id": "transition-001",
  "applied": true,
  "generation": 43,
  "active_location": "locator:ssd:object-a-v17",
  "previous_location": "locator:hdd:object-a-v17",
  "source_delete_allowed": false,
  "cleanup_after": "reader-drain-and-grace",
  "receipt_durable": true
}
```

Сравниваются и generation, и expected_active, и immutable identity.
Next generation должен быть ровно expected generation плюс один.
Проверенный digest и durability сохраняются при смене класса.

Ошибки контракта:

- 409 generation_conflict: запись уже изменена.
- 409 fencing_rejected: token устарел.
- 422 verification_incomplete: destination нельзя публиковать.

### API-24. Освобождение lease

`POST /v1/catalog/leases/lease-901/release`

Роль: `placement_executor`.
Освободить право исполнителя после безопасного завершения.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "release-lease-901",
  "holder_id": "executor-a",
  "fencing_token": 901,
  "terminal_move_state": "completed"
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "lease_id": "lease-901",
  "state": "released",
  "fencing_token": 901,
  "release_recorded": true,
  "remaining_reader_leases": 0,
  "receipt_durable": true
}
```

Release исполнителя не уничтожает lease читателей.
Повтор release возвращает тот же terminal receipt.
Старый release не снимает новый lease другого holder.

Ошибки контракта:

- 409 fencing_rejected: lease уже принадлежит новому исполнителю.

### API-25. Purge локального кеша

`POST /v1/cache/purges`

Роль: `cache_operator`.
Инвалидировать точную область и оградить её от запоздалого fill.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "purge-object-a",
  "scope": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "content_version": "v17"
  },
  "mode": "invalidate_and_drain",
  "include_chunks": true,
  "include_metadata": true,
  "wait_for_readers": false
}
```

Проектный ответ: HTTP 202.

```json
{
  "api_version": 1,
  "purge_id": "purge-701",
  "state": "accepted",
  "invalidation_generation": 44,
  "scope_summary": {
    "whole_objects": 1,
    "chunk_entries": 4,
    "metadata_entries": 1
  },
  "active_readers": 2,
  "future_fills_fenced": true,
  "receipt_durable": true
}
```

Purge не удаляет origin object и не является операцией Ceph delete.
Запоздалый writer с прежним fence не публикует запись заново.
Читатели обрабатываются по выбранному drain policy.

Ошибки контракта:

- 403 purge_scope_forbidden: попытка выйти за tenant scope.
- 422 ambiguous_purge_scope: wildcard требует отдельного разрешённого режима.

### API-26. Инспекция записи кеша

`POST /v1/cache/inspect`

Роль: `observer`.
Получить очищенную диагностику конкретной записи.

Проектный запрос:

```json
{
  "api_version": 1,
  "identity": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "identity_hash_version": "h1",
    "content_version": "v17",
    "representation_id": "source",
    "layout_version": 1,
    "range": {
      "start": 0,
      "length": 4194304
    }
  },
  "include_readers": true,
  "include_fill": true
}
```

Проектный ответ: HTTP 200.

```json
{
  "api_version": 1,
  "identity": {
    "tenant_id": "demo-media",
    "object_id_hash": "hmac-sha256:h1:example-object-a",
    "identity_hash_version": "h1",
    "content_version": "v17",
    "representation_id": "source",
    "layout_version": 1,
    "range": {
      "start": 0,
      "length": 4194304
    }
  },
  "state": "published",
  "generation": 43,
  "tiers": [
    {
      "tier": "ram",
      "present": false
    },
    {
      "tier": "nvme",
      "present": true,
      "bytes": 4194304
    },
    {
      "tier": "hdd",
      "present": false
    }
  ],
  "readers": {
    "active": 2,
    "oldest_age_ms": 120
  },
  "fill": null,
  "integrity": {
    "verified": true,
    "digest_algorithm": "sha256"
  },
  "placement_hint": {
    "score_revision": "score-p9",
    "state": "hot"
  }
}
```

Inspect не читает всё тело ради диагностики.
Абсолютный cache path доступен только отдельному privileged профилю.
Точная identity отличает похожие URL с разными representation.

Ошибки контракта:

- 404 cache_entry_unknown: опубликованной записи нет.

### API-27. Drain узла

`POST /v1/nodes/edge-07/drain`

Роль: `node_operator`.
Прекратить принятие новой работы перед обслуживанием узла.

Проектный запрос:

```json
{
  "api_version": 1,
  "operation_id": "drain-edge-07",
  "expected_boot_id": "boot-example-31",
  "grace_ms": 30000,
  "components": {
    "http_admission": true,
    "new_fills": true,
    "new_moves": true
  },
  "on_deadline": "terminate_remaining_requests"
}
```

Проектный ответ: HTTP 202.

```json
{
  "api_version": 1,
  "job_id": "drain-job-07",
  "node_id": "edge-07",
  "state": "draining",
  "ready": false,
  "active": {
    "requests": 120,
    "fills": 3,
    "moves": 1
  },
  "deadline": "2026-09-27T10:00:30Z",
  "receipt_durable": true
}
```

Внешний балансировщик должен учитывать readiness или явный drain integration.
Boot id не позволяет старой команде случайно примениться к новому запуску.
Планы placement достигают безопасной стадии до завершения процесса.

Ошибки контракта:

- 409 boot_id_conflict: процесс уже перезапущен.

## Детализация событий и причинных связей

Событие публикует законченное наблюдение своего типа.
Начало и завершение одной origin-попытки не создают два одинаковых origin_fetch без phase.
Базовый профиль отправляет terminal origin_fetch после завершения или обрыва попытки.
Observed time сохраняет момент начала, completed time — момент завершения.
При падении producer незавершённые наблюдения отражаются в качестве данных.
Логический запрос связывается с fill_id, если ожидал shared fill.
Несколько logical_request вправе ссылаться на один fill_id.
Один fill_id вправе иметь несколько origin_attempt_id из-за retry.

### Пример: HEAD по известным метаданным

```json
{
  "schema_version": 1,
  "event_type": "logical_request",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180431
  },
  "observed_at": "2026-09-27T10:01:00Z",
  "completed_at": "2026-09-27T10:01:00.002Z",
  "tenant_id": "demo-media",
  "object_id_hash": "hmac-sha256:h1:example-object-a",
  "identity_hash_version": "h1",
  "content_version": "v17",
  "representation_id": "source",
  "request_id": "req-head-01",
  "method": "HEAD",
  "range": null,
  "requested_bytes": 0,
  "requested_bytes_known": true,
  "served_bytes": 0,
  "origin_bytes": 0,
  "metadata_demand": true,
  "cache_summary": "metadata_hit",
  "status": 200,
  "sample_probability": 1.0,
  "policy_revision": "p12"
}
```

HEAD увеличивает число допущенных обращений, но не выдаёт полный Content-Length за delivered bytes.
Metadata demand хранится отдельно от спроса на тело.

### Промах по одному RAM-чанку

```json
{
  "schema_version": 1,
  "event_type": "cache_lookup",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180432
  },
  "observed_at": "2026-09-27T10:01:01Z",
  "tenant_id": "demo-media",
  "object_id_hash": "hmac-sha256:h1:example-object-a",
  "identity_hash_version": "h1",
  "content_version": "v17",
  "representation_id": "source",
  "request_id": "req-range-01",
  "lookup_id": "lookup-1001",
  "tier": "ram",
  "unit": "chunk",
  "layout_version": 1,
  "range": {
    "start": 0,
    "length": 4194304
  },
  "outcome": "miss",
  "lookup_duration_us": 25,
  "requested_bytes": 1048576,
  "requested_bytes_known": true,
  "sample_probability": 1.0,
  "policy_revision": "p12"
}
```

Этот miss не увеличивает число logical_request.
Следующий lookup того же запроса на NVMe получит новый lookup_id и собственный outcome.

### Частично завершённая origin-попытка

```json
{
  "schema_version": 1,
  "event_type": "origin_fetch",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180433
  },
  "observed_at": "2026-09-27T10:01:02Z",
  "completed_at": "2026-09-27T10:01:02.500Z",
  "tenant_id": "demo-media",
  "object_id_hash": "hmac-sha256:h1:example-object-a",
  "identity_hash_version": "h1",
  "content_version": "v17",
  "representation_id": "source",
  "fill_id": "fill-2001",
  "origin_attempt_id": "attempt-2001-a",
  "attempt_number": 1,
  "origin_id": "origin-pool-a",
  "range": {
    "start": 0,
    "length": 4194304
  },
  "requested_bytes": 4194304,
  "requested_bytes_known": true,
  "received_bytes": 2097152,
  "outcome": "body_timeout",
  "http_status": 206,
  "cache_publish_result": "discarded_incomplete",
  "sample_probability": 1.0,
  "policy_revision": "p12"
}
```

Физически прочитанные 2097152 байта остаются нагрузкой origin даже при неудаче.
Незавершённый cache object не публикуется как полный.

### Повтор origin в рамках того же fill

```json
{
  "schema_version": 1,
  "event_type": "origin_fetch",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180434
  },
  "observed_at": "2026-09-27T10:01:02.600Z",
  "completed_at": "2026-09-27T10:01:02.800Z",
  "tenant_id": "demo-media",
  "object_id_hash": "hmac-sha256:h1:example-object-a",
  "identity_hash_version": "h1",
  "content_version": "v17",
  "representation_id": "source",
  "fill_id": "fill-2001",
  "origin_attempt_id": "attempt-2001-b",
  "attempt_number": 2,
  "origin_id": "origin-pool-b",
  "range": {
    "start": 0,
    "length": 4194304
  },
  "requested_bytes": 4194304,
  "requested_bytes_known": true,
  "received_bytes": 4194304,
  "outcome": "complete",
  "http_status": 206,
  "cache_publish_result": "published",
  "sample_probability": 1.0,
  "policy_revision": "p12"
}
```

Fill id сохранён, но attempt id новый.
Суммарная физическая нагрузка двух попыток равна 6291456 байтам, а не только размеру успешной попытки.

### Отказ доступа без пользовательского спроса

```json
{
  "schema_version": 1,
  "event_type": "security_decision",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180435
  },
  "observed_at": "2026-09-27T10:01:03Z",
  "tenant_id": "demo-media",
  "route_id": "media",
  "request_id": "req-denied-01",
  "action": "deny",
  "reason": "signature_mismatch",
  "http_status": 403,
  "policy_revision": "p12",
  "credential": {
    "scheme": "signed_url_v1",
    "kid": "key-public-id-7",
    "value": "REDACTED"
  },
  "cache_lookup_performed": false,
  "origin_fetch_performed": false,
  "popularity_eligible": false,
  "sample_probability": 1.0
}
```

Security event имеет сокращённую схему без обязательной identity объекта.
Маркер REDACTED иллюстрирует отсутствие credential; реальный event вообще не обязан содержать поле value.

### Подтверждённая потеря событий

```json
{
  "schema_version": 1,
  "event_type": "quality_gap",
  "event_id": {
    "producer_id": "edge-07",
    "producer_epoch": 31,
    "sequence": 180436
  },
  "observed_at": "2026-09-27T10:01:04Z",
  "tenant_id": "demo-media",
  "affected_producer_id": "edge-07",
  "affected_epoch": 31,
  "lost_intervals": [
    {
      "start": 180421,
      "end_inclusive": 180424
    }
  ],
  "reason": "spool_corruption",
  "affected_window_start": "2026-09-27T10:00:00Z",
  "affected_window_end": "2026-09-27T10:01:00Z",
  "loss_count_known": true,
  "lost_events": 4,
  "recommended_gate": "freeze_demotions",
  "sample_probability": 1.0
}
```

Quality gap не создаёт отрицательный demand.
Он меняет пригодность временного окна для решений о размещении.

## Арифметика collapse без двойного учёта

Рассмотрим три разрешённых GET одного чанка размером 4 MiB.
Каждый запрос делает RAM miss и NVMe miss.
Первый становится fill leader.
Два других становятся followers.
Origin успешно передаёт один чанк без retry.
Каждый клиент получает 4 MiB.

| Измерение | Значение | Основание |
| --- | --- | --- |
| logical_request | 3 | Три допущенных клиента |
| cache_lookup | 6 | Два lookup на каждого |
| origin_fetch | 1 | Одна физическая попытка |
| requested bytes | 12 MiB | Сумма трёх запросов |
| served bytes | 12 MiB | Три полные выдачи |
| origin received bytes | 4 MiB | Один fill |
| completed fills | 1 | Одна публикация |

Если первая origin-попытка прочитала 2 MiB и оборвалась, появляется второй origin_fetch.
При успешном полном retry origin received bytes становятся 6 MiB.
Logical requests остаются равными трём.
Если один follower отключился после 1 MiB, served bytes становятся 9 MiB.
Requested bytes остаются 12 MiB, поскольку отражают заявленный спрос.
Выбор popularity по requested или served bytes фиксируется policy_revision.

## Частично принятый batch

Отправитель обязан обработать результат каждого event id.
Нельзя удалить весь batch только потому, что HTTP status равен 202.
Нельзя повторять accepted events с новыми event ids.
Пример демонстрирует один принятый, один повторный и один навсегда отклонённый event.

```json
{
  "api_version": 1,
  "receipt_id": "receipt-partial-01",
  "batch_id": "batch-partial-01",
  "durable": true,
  "accepted_event_ids": [
    {
      "producer_id": "edge-07",
      "producer_epoch": 31,
      "sequence": 180440
    }
  ],
  "duplicate_event_ids": [
    {
      "producer_id": "edge-07",
      "producer_epoch": 31,
      "sequence": 180439
    }
  ],
  "rejected": [
    {
      "event_id": {
        "producer_id": "edge-07",
        "producer_epoch": 31,
        "sequence": 180441
      },
      "code": "negative_received_bytes",
      "permanent": true,
      "field": "received_bytes",
      "popularity_applied": false
    }
  ]
}
```

## Изменение schema и rolling update

Producer объявляет event_schema в batch.
Каждый event также содержит schema_version для самостоятельной проверки.
Расхождение batch schema и event schema отклоняет соответствующее событие.
Получатель заранее объявляет поддерживаемые версии через capabilities.
Новая необязательная метрика не должна менять смысл прежнего поля.
Изменение единицы измерения требует нового имени либо major schema.
Изменение смысла logical_request требует новой версии и миграции агрегатов.
Нельзя складывать несовместимые поля двух схем только из-за одинакового имени.

Переход начинается с поддержки чтения новой схемы на стороне получателя.
Затем обновляется producer на ограниченном наборе узлов.
Проверяется качество, объём и совпадение агрегатов на контрольной нагрузке.
После этого расширяется поставка producer.
Старая схема удаляется после окончания retention и spool replay.
Период двойного приёма не означает двойное создание одного события.

## Pagination и согласованное чтение

Коллекции используют `limit`, `cursor` и `snapshot_id`.
Cursor непрозрачен для клиента.
Он связывает порядок, фильтры, principal scope и снимок коллекции.
Изменение фильтра требует нового обхода.
Срок действия cursor ограничен.
Истёкший cursor возвращает явную ошибку и не начинает список с середины по догадке.

Snapshot pagination предотвращает пропуск записей из-за параллельного добавления.
Она не обещает глобально согласованный снимок всех разных сервисов.
План фиксирует конкретные версии каталога и demand window отдельно.
При изменении одного источника перед apply требуется revalidation.
Обычный GET job допускает новое состояние между двумя чтениями.
Expected revision защищает последующую мутацию от использования старого наблюдения.

## Edge cases восстановления

| Ситуация | Требуемое поведение |
| --- | --- |
| ACK потерян после commit | Повтор даёт duplicate без нового demand |
| Producer повторил event id с другим payload | Конфликт и карантин |
| Событие старше dedup horizon | Явный stale-event отказ или reconciliation, без слепого подсчёта |
| Sequence пришли не по порядку | Сохранять интервалы, не перескакивать watermark |
| Новый boot повторил epoch | Отклонить конфликт регистрации producer |
| Batch содержит неизвестный event type | Отклонить этот event с version error |
| Tenant не совпал с credential producer | 403, не частичный приём чужой области |
| Время producer далеко в будущем | Карантин либо окно низкого качества |
| План просрочен | Не применять до revalidation |
| Source изменился после prepare | CAS conflict, перечитать каталог |
| Destination скопирован, verify не завершён | Active location остаётся source |
| Lease истёк перед switch | Fencing запрещает переключение |
| Процесс упал после switch | Восстановить job из transition receipt |
| Cancel пришёл после switch | Достичь безопасного terminal state без blind rollback |
| Старый holder просит release | Не снимать новый lease |
| Purge пересёкся с cache fill | Старый fill не проходит generation fence |
| Узел перезапущен во время drain | Expected boot id защищает новый процесс |
| Receipt истёк до reconciliation | Требовать чтение authoritative state |

## Аудит управляющих операций

Audit event содержит principal id, роль, scope и operation_id.
Содержимое секретов не включается ни в request snapshot, ни в diff.
Для изменения конфигурации сохраняются digest старого и нового пакета.
Для CAS сохраняются expected generation и фактический результат сравнения.
Для placement сохраняются move_id, plan revision и verify receipt reference.
Для purge сохраняется точная область инвалидирования.
Для drain сохраняются boot id и установленный deadline.
Журнал аудита отделён от низкоприоритетной popularity telemetry.
Его устойчивость и поведение при переполнении задаются отдельным operational profile.

## Критерии готовности API v1

Клиент проходит сценарий потерянного ACK без двойного popularity.
CAS выдерживает двух конкурентных исполнителей одной identity.
Старый fencing token не выполняет ни switch, ни cleanup active copy.
Purge не удаляет origin и не допускает повторную публикацию старого fill.
Неправильный tenant scope отклоняется до чтения чужих данных.
Частичный batch сохраняет индивидуальную судьбу каждого события.
Health не блокируется на недоступном удалённом collector.
Drain отличает старый boot от нового.
Unknown fields, duplicate JSON keys и численное переполнение имеют явные ошибки.
Документация endpoints сверяется с машинной схемой поставки после реализации.

## Первичные источники

Протоколы SLOYRA, пути endpoint и схемы объектов в этой главе являются проектными.
Ссылки подтверждают используемые стандартные форматы и HTTP-основу.

- [RFC 8259: JSON](https://www.rfc-editor.org/rfc/rfc8259.html).
- [RFC 3339: даты и время](https://www.rfc-editor.org/rfc/rfc3339.html).
- [RFC 9110: HTTP semantics](https://www.rfc-editor.org/rfc/rfc9110.html).

---

# Сквозные профили нагрузки и размещения

> Статус: проектная спецификация v0.1.

## Назначение профилей

Профили связывают access policy, кеш и backend placement.
Они описывают конкретные типы нагрузки и контрольные вычисления.
Все значения являются проектными стартовыми параметрами.
Они не являются измеренной пропускной способностью готового продукта.
Перед production каждый профиль проверяется на целевом оборудовании.

Целевая архитектура использует плагин Nginx на C17.
LuaJIT 5.1 задаёт ограниченные политики.
Rust agent обрабатывает фоновые решения и переносы.
Чтение больших объектов не должно блокировать Nginx worker.

## Общие правила сравнения

Для каждого профиля фиксируются corpus и версии.
В workload указывается число допущенных запросов.
Нелегитимные запросы учитываются отдельно и получают 403 по access policy.
Они не повышают popularity содержимого.
Cache hit не освобождает запрос от проверки доступа.

Размеры в MiB и GiB используют основание 1024.
Скорость кодека в Mbit/s использует основание 1000, если явно указано.
Origin estimate ниже относится к body bytes.
Metadata и protocol overhead измеряются отдельно.
Процент cache hit в расчётах является допущением, подлежащим проверке.

## Три режима испытаний

| Режим | Что проверяется |
| --- | --- |
| Cold | Поведение без локальных готовых копий |
| Warm | Стабильное обслуживание заданного working set |
| Recovery | Возврат к SLO после потери части кеша |

Cold test ограничивает origin concurrency.
Warm test не включает warm-up в steady-state latency без пометки.
Recovery test сохраняет прежнюю durability origin.
Нельзя «улучшать» recovery удалением части corpus.

## Источники телеметрии

`logical_request` учитывает каждый допущенный запрос.
`cache_lookup` показывает hit/miss/stale/error по tier и chunk.
`origin_fetch` показывает каждую физическую попытку чтения.
`fill_id` объединяет retries одного заполнения.
`origin_attempt_id` различает физические попытки.

`requested_bytes` показывает нормализованный спрос.
`served_bytes` показывает фактически переданное тело.
`origin_bytes` показывает фактически загруженное тело origin.
Все профили сохраняют эти три величины раздельно.

## Как применять карточку

1. Сопоставить namespace и tenant scope.
2. Подтвердить модель immutable version.
3. Проверить права доступа и отзыв объекта.
4. Рассчитать working set по trace.
5. Выделить RAM, NVMe и HDD budgets.
6. Запустить observe mode.
7. Сравнить предложенный placement с фактическим спросом.
8. Провести cold, warm и recovery испытания.
9. Включить local apply на ограниченной группе.
10. Разрешить Ceph migrations после проверки barriers.

YAML в карточках является фрагментом проектного `sloyra.yaml`.
Он показывает решение по workload, а не полный каталог параметров.
Секреты и адреса реального deployment в примеры не входят.

## P01. Видео по запросу с перемоткой

### Модель содержимого

Профиль предназначен для неизменяемых видеофайлов и byte-range выдачи.
Клиенты читают последовательно, но могут перематывать в произвольное место.
Начальные участки имеют иной спрос, чем конец длинного файла.
Популярность рассчитывается на уровне кусков и объекта.
Продолжение сессии не закрепляет весь фильм в RAM.
Новая редакция видео получает новую content version.
Manifest и media payload учитываются раздельно.
Горячий первый кусок не означает горячий полный объект.

### Расчётная нагрузка

Расчётный поток содержит 1500 одновременных зрителей.
Средний bitrate принят равным 6 Mbit/s.
Это 750 000 bytes/s на зрителя.
Суммарное тело составляет 1 125 000 000 bytes/s.
В трёх одинаково нагруженных edge узлах получается 375 000 000 bytes/s на узел.
При byte hit ratio 92% origin body rate равен 90 000 000 bytes/s.
Это примерно 85,83 MiB/s.
Расчёт не включает seek amplification и retries.
Кусок 2 MiB покрывает примерно 2,80 секунды такого потока.
Один объект фильма может занимать несколько GiB.

### Фрагмент проектной конфигурации

```yaml
cache:
  profile: vod
  chunking:
    enabled: true
    size: 2MiB
    layout_version: 1
  tiers:
    ram:
      budget: 8GiB
    nvme:
      budget: 1TiB
    hdd:
      budget: 8TiB
  admission:
    min_request_credits: 2
    prefetch_max_chunks: 1
  freshness:
    immutable_versions: true
telemetry:
  popularity:
    short_half_life: 15m
    long_half_life: 24h
placement:
  mode: observe
  scoring:
    recovery_reserve_fraction: 0.10
access:
  policy: signed_session
```

### Планирование бюджета

Бюджеты указаны на один edge узел.
8 GiB RAM вмещают максимум 4096 payload chunks по 2 MiB.
Metadata, активные buffers и allocator overhead уменьшают это число.
1 TiB NVMe вмещает максимум 524 288 payload chunks.
Admission резервирует место до чтения origin.
Read-ahead ограничен одним следующим куском на активное направление.
Перемотка отменяет ненужный prefetch, если он ещё не начал полезную работу.
Ceph hot set выбирается по байтам и recovery cost.

### Пограничные случаи

Повтор одного сегмента после сетевого обрыва создаёт новый HTTP request.
Он может попасть в тот же уже готовый chunk.
Range на границе кусков создаёт два физических источника ответа.
При смене версии нельзя склеивать старый и новый chunks.
Невалидная подпись приводит к 403 до tier lookup.
Окончание токена в середине запроса обрабатывается зафиксированной access policy.
Начатая выдача не продлевает право доступа к следующему запросу.
Большая популярная премьера может получить временный pin с лимитом bytes.

### Наблюдаемость

Сравнивать throughput по узлам и tenant.
Измерять p99 времени до первого полезного media byte.
Отдельно считать seek miss и sequential miss.
Измерять wasted prefetch bytes после перемоток.
Проверять рост fill waiters во время премьеры.
Контролировать origin bytes после рестарта одного edge.
Сравнивать first-chunk hit и whole-object hit.
Следить за скоростью migration во время горячего релиза.

### Приёмочные проверки

| Сценарий | Ожидаемое поведение |
| --- | --- |
| 100 зрителей начинают один фильм | 100 logical requests, один fill первого общего chunk |
| Перемотка в ещё не читавшуюся область | Новый chunk demand и ограниченный fill |
| Клиент ушёл после первого куска | Prefetch не становится клиентским спросом |
| RAM полностью потерян | NVMe/HDD сохраняют доступные локальные copies |
| Смена версии видео | Новая identity не читает старые chunks |
| Ceph controller остановлен | Выдача использует последнюю пригодную location generation |

### Контрольная трасса

```yaml
case: vod_shared_start
logical_requests: 100
nominal_request_mib: 2
requested_mib: 200
served_mib: 200
origin_mib: 2
fill_count: 1
origin_attempt_count: 1
assumptions:
  all_clients_authorized: true
  client_cancellations: 0
  retries: 0
  body_counters_only: true
```

### Порядок внедрения

Сначала включить один namespace с устойчивыми версиями.
Собрать не менее полного суточного цикла просмотра для модели.
Отдельно воспроизвести премьерный всплеск.
Проверить cold start с ограниченным origin bandwidth.
После этого разрешить RAM/NVMe promotions.
Ceph demotions включить только при достаточном recovery reserve.

## P02. Каталог изображений и готовых вариантов

### Модель содержимого

Профиль предназначен для множества небольших изображений.
Один исходник может иметь размеры, форматы и варианты качества.
Каждый результат преобразования имеет representation ID.
Параметры width, height и format не удаляются из ключа без mapping.
Подписанный token можно исключить из identity только после access validation.
Профиль не предполагает генерацию вариантов самим плагином.
Генератор изображений рассматривается как отдельный origin.
Все готовые варианты желательно публиковать immutable.

### Расчётная нагрузка

Расчётный поток составляет 20 000 запросов в секунду.
Средний размер тела принят равным 80 KiB.
Суммарная выдача составляет 1 638 400 000 bytes/s.
Это 1562,5 MiB/s.
При byte hit ratio 95% origin получает 78,125 MiB/s body demand.
Четыре равномерных edge узла обслуживают по 5000 requests/s.
Corpus из миллиона вариантов занимает около 76,29 GiB payload.
Число исходников меньше числа готовых вариантов.
Разные форматы одного изображения могут иметь очень разные размеры.
Поэтому request hit и byte hit оцениваются отдельно.

### Фрагмент проектной конфигурации

```yaml
cache:
  profile: images
  chunking:
    enabled: false
  tiers:
    ram:
      budget: 4GiB
      max_entry: 1MiB
    nvme:
      budget: 200GiB
    hdd:
      enabled: false
  admission:
    min_request_credits: 2
    max_object_size: 16MiB
  freshness:
    immutable_versions: true
telemetry:
  popularity:
    short_half_life: 10m
    long_half_life: 12h
placement:
  mode: observe
  local_enabled: true
access:
  policy: signed_variant
```

### Планирование бюджета

RAM budget включает metadata.
При 80 KiB тела и 1 KiB metadata entry занимает минимум 81 KiB.
4 GiB вмещают не больше 51 781 такого entry без прочих накладных расходов.
Число 52 428, полученное только по payload, было бы завышенным.
NVMe может вместить corpus примера, но политика не обязана хранить его целиком.
HDD локального уровня выключен из-за характера коротких случайных чтений.
Ceph HDD остаётся возможным origin для длинного хвоста.
Admission учитывает пользу на байт и частоту повторного использования.

### Пограничные случаи

Запрос WebP не получает закешированный JPEG под тем же ключом.
Accept negotiation отражается в representation ID либо Vary contract.
Неподдержанный размер не создаёт бесконечное число вариантов.
Transform parameters проходят allowlist и upper bounds.
Один crawler с разрешёнными запросами не должен вытеснять весь RAM tenant.
Ошибки генератора не кешируются как успешное изображение.
Изменённый исходник создаёт новую версию производных вариантов.
Purge семейства вариантов должен иметь ограниченный scope и audit.

### Наблюдаемость

Измерять число активных representations на исходник.
Следить за cardinality keys и metadata RAM.
Отдельно считать origin render latency и network latency.
Сравнивать byte hit для тяжёлых форматов.
Измерять долю entries с единственным чтением.
Проверять evictions на один admitted MiB.
Контролировать queue генератора при массовом miss.
Следить за ошибками несовпадения content type и representation.

### Приёмочные проверки

| Сценарий | Ожидаемое поведение |
| --- | --- |
| Один путь с JPEG и WebP | Две identity и независимые fills |
| Разные подписи одного разрешённого варианта | Одна content identity после validation |
| Недопустимый width | Отказ до вызова генератора |
| Горячая миниатюра и холодный большой оригинал | Разные scores и placement |
| Обновление исходного изображения | Новые версии производных объектов |
| Миллион одноразовых вариантов | Admission защищает существующий hot set |

### Контрольная трасса

```yaml
case: image_same_variant
logical_requests: 50
nominal_request_mib: 0.078125
requested_mib: 3.90625
served_mib: 3.90625
origin_mib: 0.078125
fill_count: 1
origin_attempt_count: 1
assumptions:
  all_clients_authorized: true
  client_cancellations: 0
  retries: 0
  body_counters_only: true
```

### Порядок внедрения

Начать с заранее сформированных variants.
Зафиксировать canonical parameter order.
Проверить все поддерживаемые media types.
Включить RAM tier после измерения metadata overhead.
Origin render concurrency ограничить независимо от HTTP concurrency.
Перед расширением оценить число новых identity в секунду.

## P03. Приватные документы нескольких организаций

### Модель содержимого

Профиль предназначен для авторизованного доступа к документам.
Одинаковый путь в двух организациях не означает один объект.
Tenant ID является обязательной частью всех ключей.
Отзыв доступа должен действовать и на уже прогретый кеш.
Верификация access policy выполняется перед каждым запросом.
Персональные или секретные поля не попадают в metric labels.
Версии документов могут сохраняться по retention policy.
Смена текущего alias не удаляет исторические версии автоматически.

### Расчётная нагрузка

Расчётный поток составляет 800 HTTP requests/s.
Из них 700 requests/s относятся к metadata и HEAD.
100 requests/s передают body со средним размером 2 MiB.
Суммарная body выдача равна 200 MiB/s.
При byte hit ratio 80% origin body rate равен 40 MiB/s.
Metadata traffic измеряется отдельным рядом.
Logical request count равен 800 в секунду.
Body request count равен 100 в секунду.
Нельзя умножать все 800 запросов на средний размер body.
Отзыв объекта может происходить в любой момент рабочего дня.

### Фрагмент проектной конфигурации

```yaml
cache:
  profile: private_documents
  chunking:
    enabled: false
  tiers:
    ram:
      budget: 2GiB
      max_entry: 4MiB
    nvme:
      budget: 400GiB
    hdd:
      enabled: false
  admission:
    max_object_size: 32MiB
  freshness:
    immutable_versions: true
    stale_on_revocation: false
telemetry:
  metadata_demand_separate: true
  redact_object_names: true
placement:
  mode: observe
  tenant_scoped: true
access:
  policy: tenant_authorization
  require_current_revision: true
```

### Планирование бюджета

2 GiB RAM дают верхнюю payload границу 1024 документов по 2 MiB.
Метаданные и активные readers уменьшают доступное число entries.
400 GiB NVMe делятся на tenant quotas.
Минимальная доля tenant защищает небольшие организации от noisy neighbour.
Hard quota проверяется до reservation.
Ceph placement сохраняет retention и исторические версии.
Неизменная replication policy применяется к hot и cold pools.
Отдельное шифрование либо ключ tenant не теряются при переносе.

### Пограничные случаи

Доступ отозван после появления hot copy в RAM.
Следующий запрос получает отказ согласно новой access revision.
Если актуальность policy нельзя подтвердить, применяется установленный fail mode.
Горячесть объекта не даёт приоритета над отзывом.
HEAD не загружает всё тело ради прогрева.
Новая версия документа может иметь тот же пользовательский filename.
Filename не используется как единственный content version.
Download audit хранится отдельно от агрегированного popularity.

### Наблюдаемость

Проверять задержку распространения access revision.
Измерять tenant quota denials отдельно от disk full.
Контролировать количество stale authorization decisions.
Отдельно учитывать metadata requests и body transfers.
Проверять отсутствие raw document names в общих метриках.
Следить за pinned retained versions.
Измерять catalog latency для version resolution.
Проверять revoked object hits как недопустимое событие.

### Приёмочные проверки

| Сценарий | Ожидаемое поведение |
| --- | --- |
| Два tenant используют имя contract.pdf | Кеши и collapse разделены по tenant |
| 700 HEAD и 100 GET | 800 logical requests; bytes только у GET |
| Отзыв после warm-up | Hot entry не обходит access denial |
| Изменение файла с прежним именем | Новая content version |
| Retention удерживает старую версию | GC не сокращает срок хранения |
| Медленное скачивание во время migration | Reader lease удерживает старый source |

### Контрольная трасса

```yaml
case: document_head_and_get
logical_requests: 800
metadata_requests: 700
body_requests: 100
nominal_body_request_mib: 2
requested_mib: 200
served_mib: 200
origin_mib: 40
fill_count: 20
origin_attempt_count: 20
assumptions:
  all_clients_authorized: true
  client_cancellations: 0
  retries: 0
  body_counters_only: true
```

### Порядок внедрения

Начать с тестового tenant и синтетических документов.
Проверить отзыв при hits во всех tiers.
Проверить перезапуск policy agent.
Согласовать metadata redaction и audit retention.
После проверки isolation подключить несколько организаций с квотами.
Ceph migrations включить после проверки сохранения metadata и доступа.

## P04. Архивы и большие редко читаемые объекты

### Модель содержимого

Профиль предназначен для больших неизменяемых архивов.
Основной corpus читается редко.
Отдельные пользователи могут последовательно просканировать много данных.
Простое кеширование каждого прочитанного байта здесь невыгодно.
Admission требует признака повторного использования.
Локальный HDD применяется выборочно для устойчивого тёплого набора.
Большие объекты доступны частями через Range.
Целостность origin остаётся обязательной независимо от частоты чтения.

### Расчётная нагрузка

Расчётный поток содержит 20 body requests/s.
Каждый запрос читает нормализованный диапазон 8 MiB.
Суммарная выдача равна 160 MiB/s.
При byte hit ratio 15% origin body rate равен 136 MiB/s.
Corpus может составлять 200 TiB logical.
Большая часть corpus никогда не входит в локальный working set.
При chunk size 8 MiB выровненный запрос требует один кусок.
Невыровненный запрос того же размера может потребовать два куска.
При полном miss это даёт до 16 MiB физического чтения на 8 MiB спроса.
Range alignment distribution необходимо включить в trace.

### Фрагмент проектной конфигурации

```yaml
cache:
  profile: cold_archives
  chunking:
    enabled: true
    size: 8MiB
    layout_version: 1
  tiers:
    ram:
      budget: 1GiB
      max_entry: 8MiB
    nvme:
      budget: 200GiB
    hdd:
      budget: 4TiB
  admission:
    min_request_credits: 3
    sequential_scan_bypass: true
  freshness:
    immutable_versions: true
telemetry:
  popularity:
    short_half_life: 1h
    long_half_life: 7d
placement:
  mode: observe
  default_class: cold
access:
  policy: signed_download
```

### Планирование бюджета

1 GiB RAM вмещает максимум 128 payload chunks по 8 MiB.
Это небольшой буфер рабочего набора, а не хранилище всех архивов.
4 TiB HDD вмещают максимум 524 288 payload chunks.
Полный corpus в 200 TiB в этот кеш не помещается.
Scan bypass снижает writes, но оставляет нагрузку origin.
Origin bandwidth budget должен покрывать принятый scan traffic.
Ceph SSD выделяется только проверенным горячим фрагментам либо recovery reserve.
Большой архив не переносится целиком из-за одного популярного куска без отдельной policy.

### Пограничные случаи

Запрос последнего суффикса сначала требует достоверной длины.
Распаковщик может читать центральный каталог архива из конца файла.
Этот маленький горячий хвост полезно отличать от полного body.
Range flood ограничивается числом диапазонов на запрос.
Многодиапазонный запрос не создаёт неограниченное число одновременных fills.
Однократное законное скачивание не признаётся атакой только из-за размера.
Access и resource budgets решают разные задачи.
Снижение кеша не должно обрушить backend при массовом восстановлении архивов.

### Наблюдаемость

Измерять scan bytes и повторно использованные bytes.
Следить за HDD queue latency.
Проверять seek amplification на невыравненных Range.
Сравнивать whole-object и chunk popularity.
Контролировать долю cached bytes, прочитанных только один раз.
Измерять origin throughput на часовых окнах.
Следить за GC debt после крупных demotions.
Проверять популярность хвостов файлов отдельно от начала.

### Приёмочные проверки

| Сценарий | Ожидаемое поведение |
| --- | --- |
| Один scan всего corpus | Admission не вытесняет полезный рабочий набор |
| Повторяется только хвост архива | Повышается соответствующий chunk |
| Range 8 MiB пересекает границу | До двух fills при одном logical request |
| Суффикс при неизвестной длине | Сначала metadata resolution |
| Origin bandwidth исчерпан | Ограниченное ожидание и контролируемый отказ |
| Задача массового восстановления | Выделенный traffic budget и наблюдаемый приоритет |

### Контрольная трасса

```yaml
case: archive_cross_boundary
logical_requests: 1
nominal_request_mib: 8
requested_mib: 8
served_mib: 8
origin_mib: 16
fill_count: 2
origin_attempt_count: 2
assumptions:
  all_clients_authorized: true
  client_cancellations: 0
  retries: 0
  body_counters_only: true
```

### Порядок внедрения

Сначала собрать Range trace с распределением offsets.
Проверить разные chunk sizes на том же trace.
Сравнить cache writes с реально повторно использованными bytes.
Провести отдельный bulk restore test.
После этого выбрать admission threshold.
Ceph demotions запускать с запасом под временные copies.

## P05. Дистрибутивы, обновления и пакетные репозитории

### Модель содержимого

Профиль предназначен для массовых скачиваний одной версии ПО.
После релиза возникает резкий всплеск demand.
Позже большинство пакетов переходит в длинный хвост.
Пакеты публикуются под immutable version или digest.
Mutable repository metadata отделяется от payload.
Подпись пакета остаётся проверяемой конечным клиентом.
SLOYRA не заменяет доверие к подписи репозитория.
Collapse особенно полезен в первые минуты релиза.

### Расчётная нагрузка

Рассмотрим 200 клиентов, одновременно скачивающих пакет 256 MiB.
Суммарный логический спрос равен 51 200 MiB, то есть 50 GiB.
При идеальном local sharing достаточно загрузить payload пакета один раз на edge.
Для одного edge origin bytes равны 256 MiB без retries.
При восьми независимых cold edge эта нижняя граница равна 2 GiB.
Глобальный collapse между edge не предполагается автоматически.
Chunk size принят равным 4 MiB.
Пакет состоит из 64 полных chunks.
Один origin retry отдельного chunk добавляет только его фактические bytes.
Metadata requests не смешиваются с body popularity пакета.

### Фрагмент проектной конфигурации

```yaml
cache:
  profile: software_distribution
  chunking:
    enabled: true
    size: 4MiB
    layout_version: 1
  tiers:
    ram:
      budget: 4GiB
    nvme:
      budget: 2TiB
    hdd:
      budget: 8TiB
  fill:
    max_waiters_per_key: 512
    max_attempts: 2
  admission:
    min_request_credits: 1
  freshness:
    immutable_versions: true
telemetry:
  popularity:
    short_half_life: 30m
    long_half_life: 3d
placement:
  mode: observe
  release_pin_max_duration: 48h
access:
  policy: repository_access
```

### Планирование бюджета

4 GiB RAM вмещают максимум 1024 payload chunks по 4 MiB.
Это эквивалентно 16 пакетам по 256 MiB без metadata overhead.
2 TiB NVMe вмещают максимум 8192 пакета такого размера.
Release pin учитывает сумму bytes всех архитектур и платформ.
Большой pin не должен занимать всю квоту tenant.
После окончания окна релиза пакет охлаждается постепенно.
Cold pool хранит старые версии согласно retention.
Удаление устаревших версий является отдельной release policy, а не placement эффектом.

### Пограничные случаи

Repository index может меняться при неизменных пакетах.
Его freshness policy должна быть короче payload policy.
Range resume использует ту же content version.
Файл с тем же именем и иным digest обязан получить новую identity.
Client retry не умножает fill при работающем collapse.
Истечение waiter deadline не должно одновременно отправить всех клиентов на origin.
Prefetch релиза требует ограниченного явного списка пакетов.
Нельзя доверять произвольному URL для server-side prefetch.

### Наблюдаемость

Измерять release spike и время прогрева каждого edge.
Сравнивать fill count с количеством уникальных chunks.
Контролировать collapse waiters и timeouts.
Измерять origin attempts на успешный fill.
Проверять сохранение digest и длины пакетов.
Следить за pinned bytes по release ID.
Отдельно измерять metadata freshness failures.
Проверять recovery после перезапуска во время релиза.

### Приёмочные проверки

| Сценарий | Ожидаемое поведение |
| --- | --- |
| 200 клиентов запрашивают один chunk | 200 logical requests и один успешный fill |
| Одновременный релиз восьми edge | До восьми отдельных cold fills на chunk |
| Повтор после частичного origin error | Новый attempt при прежнем fill ID |
| Index обновился, пакет прежний | Меняется metadata, payload identity сохраняется |
| Pin релиза истёк | Объект переходит к обычному score без немедленного удаления |
| Несовпадение digest пакета | Quarantine и отсутствие публикации corrupt entry |

### Контрольная трасса

```yaml
case: software_chunk_fanout
logical_requests: 200
nominal_request_mib: 4
requested_mib: 800
served_mib: 800
origin_mib: 4
fill_count: 1
origin_attempt_count: 1
assumptions:
  all_clients_authorized: true
  client_cancellations: 0
  retries: 0
  body_counters_only: true
```

### Порядок внедрения

Начать с внутреннего тестового репозитория.
Воспроизвести release fan-out с ограниченным origin.
Проверить resume и validators у используемых клиентов.
Задать отдельный budget для metadata и payload.
После этого включить кратковременный release pin.
Ceph promotion делать по forecast только в пределах разрешённого списка версий.

## P06. Короткие live-фрагменты и быстро меняющиеся manifests

### Модель содержимого

Профиль предназначен для последовательности immutable live chunks.
Каждый chunk имеет собственный номер, версию и время публикации.
Manifest меняется чаще, чем payload готового chunk.
Политики кеширования manifest и media разделены.
Завершённый fragment публикуется атомарно.
Клиент не должен получить незавершённую запись как полный fragment.
Старые fragments имеют ограниченное окно востребованности.
Retention DVR определяется отдельно от локального cache TTL.

### Расчётная нагрузка

Рассмотрим 5000 зрителей одного канала.
Fragment появляется каждые 2 секунды.
Размер каждого fragment принят равным 1 MiB.
Средний media body rate одного зрителя равен 0,5 MiB/s.
Суммарный body rate равен 2500 MiB/s.
На одном edge ideal collapse требует 0,5 MiB/s origin для одной rendition.
Четыре независимых edge требуют минимум 2 MiB/s на эту rendition.
Несколько качеств умножают набор объектов, а не один payload.
Manifest polling добавляет отдельный поток запросов.
Пример исключает retries, missed fragments и DVR rewinds.

### Фрагмент проектной конфигурации

```yaml
cache:
  profile: live_fragments
  chunking:
    enabled: false
  tiers:
    ram:
      budget: 8GiB
      max_entry: 8MiB
    nvme:
      budget: 200GiB
    hdd:
      enabled: false
  admission:
    min_request_credits: 1
  fill:
    max_waiters_per_key: 2048
    waiter_deadline: 1s
  freshness:
    immutable_versions: true
telemetry:
  popularity:
    short_half_life: 2m
    long_half_life: 1h
placement:
  mode: observe
  minimum_migration_benefit_window: 10m
access:
  policy: signed_live_session
```

### Планирование бюджета

8 GiB RAM вмещают максимум 8192 fragments по 1 MiB.
Для одного качества при одном fragment каждые 2 секунды это 16 384 секунды payload.
Это около 4,55 часа, если весь RAM занят только этим каналом.
Реальный budget делится между каналами, качествами и active buffers.
NVMe хранит более длинное DVR окно по отдельной квоте.
HDD local tier выключен для near-live выдачи в этом профиле.
Ceph migration может быть бессмысленна для fragment с очень коротким сроком жизни.
Минимальное ожидаемое время выгоды должно превышать стоимость переноса.

### Пограничные случаи

Будущий fragment может ещё не существовать.
Краткий 404 от origin не должен отравлять кеш на длительный срок.
Negative caching использует отдельный малый TTL и reason.
Manifest не выдаётся по immutable media policy.
Clock skew влияет на допустимое окно fragment IDs.
Старый клиент DVR не должен получать новый fragment под прежним номером.
Перезапуск producer увеличивает stream generation.
Тот же sequence в новой generation является другой identity.

### Наблюдаемость

Измерять publish-to-first-byte delay.
Отдельно считать origin-not-yet-ready и permanent missing.
Следить за manifest age и clock skew.
Проверять collapse на каждой границе появления fragment.
Измерять delivery errors по rendition.
Сравнивать live и DVR demand.
Контролировать bytes fragments, истёкших без повторного использования.
Не запускать массовые Ceph migrations ради двухсекундного всплеска.

### Приёмочные проверки

| Сценарий | Ожидаемое поведение |
| --- | --- |
| Тысяча клиентов ждут новый fragment | Ограниченный общий fill после публикации |
| Origin ещё не опубликовал fragment | Короткий negative state без длительного poisoning |
| Manifest обновился | Применяется metadata freshness policy |
| Stream producer перезапущен | Новая generation разделяет content identity |
| Зритель отмотал DVR на час назад | Спрос старого fragment учитывается отдельно |
| Fragment истечёт раньше завершения migration | Planner отклоняет невыгодный перенос |

### Контрольная трасса

```yaml
case: live_fragment_fanout
logical_requests: 1000
nominal_request_mib: 1
requested_mib: 1000
served_mib: 1000
origin_mib: 1
fill_count: 1
origin_attempt_count: 1
assumptions:
  all_clients_authorized: true
  client_cancellations: 0
  retries: 0
  body_counters_only: true
```

### Порядок внедрения

Начать с одного канала и одной rendition.
Проверить границу публикации с синхронными клиентами.
Проверить future fragment requests и clock skew.
Добавить несколько качеств с отдельными budgets.
После этого включить DVR read path.
Ceph auto placement включать только для retained fragments с подтверждённой выгодой.

## Сравнение профилей

| Профиль | Единица кеша | Главный риск | Приоритет измерения |
| --- | --- | --- | --- |
| VOD | Chunk | Seek amplification | Byte hit и recovery |
| Images | Representation object | Cardinality вариантов | Metadata RAM |
| Documents | Versioned object | Отзыв доступа | Access freshness |
| Archives | Chunk | Cache pollution от scan | Reuse per admitted byte |
| Software | Chunk | Cold release stampede | Collapse и origin attempts |
| Live | Immutable fragment | Неправильная freshness | Publish-to-first-byte |

Один deployment может иметь несколько профилей.
Namespace выбирает профиль до admission.
Общий hardware budget распределяется между ними явно.
Нельзя суммировать независимые «максимальные» RAM budgets без проверки узла.

## Смешанная нагрузка

Пусть узел обслуживает VOD и приватные документы.
VOD создаёт высокий body throughput.
Документы создают требования к access freshness и latency metadata.
Общая очередь без приоритетов может ухудшить интерактивные операции.
Поэтому классы имеют отдельные concurrency budgets.

```yaml
server:
  traffic_classes:
    metadata:
      reserved_concurrency: 64
    interactive_body:
      reserved_concurrency: 128
    streaming_body:
      max_concurrency: 2048
    background:
      max_concurrency: 8
placement:
  migration_traffic_class: background
```

Значения являются примером планирования.
Reserved concurrency не резервирует физический bandwidth автоматически.
Scheduler обязан учитывать и bytes, и число операций.
Large transfers не должны удерживать все slots metadata.

## Пересмотр профиля

Профиль пересматривают при изменении corpus.
Профиль пересматривают при изменении распределения Range.
Профиль пересматривают при росте числа tenant.
Профиль пересматривают при смене retention policy.
Профиль пересматривают при переходе на другое оборудование.
Профиль пересматривают после крупных инцидентов cold start.

Изменение chunk size создаёт новую layout version.
Старые и новые entries могут временно сосуществовать.
Это учитывается в capacity reserve.
Смена formula revision проходит shadow scoring.
Изменение access policy не ждёт завершения popularity retraining.

## Универсальные отрицательные проверки

Нельзя выдать тело до успешной access проверки.
Нельзя смешать bytes разных content versions.
Нельзя публиковать partial fill.
Нельзя считать retry новым logical demand.
Нельзя считать prefetch клиентским запросом.
Нельзя забывать cache hits при передаче спроса backend.
Нельзя удалять Ceph source до catalog switch и reader barriers.
Нельзя уменьшать replication ради выполнения capacity target.
Нельзя называть проектную цифру измеренной производительностью.
Нельзя сравнивать профили с разными workload traces без пояснения.

## Источники и граница достоверности

- [Nginx proxy module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) — существующие cache и proxy механизмы.
- [Nginx slice module](https://nginx.org/en/docs/http/ngx_http_slice_module.html) — механизм диапазонного кеширования.
- [Ceph CRUSH maps](https://docs.ceph.com/en/squid/rados/operations/crush-map/) — device classes и pools.
- [RGW storage classes](https://docs.ceph.com/en/squid/radosgw/placement/) — границы backend размещения.

Первоисточники проверены 27 сентября 2026 года.
Все шесть профилей, budgets, latency goals и traffic assumptions являются проектными.
Расчёты показывают арифметику модели, а не производительность релиза SLOYRA.
