## ADDED Requirements ### Requirement: Ответ приёма отражает сохранность, а не разбор Приём SHALL отвечать `200` после того, как тело записано в сырой архив и доставка учтена строкой `delivery`, и MUST NOT ждать свёртки. Время ответа зависеть от ширины доставки MUST NOT. Порядок обязателен именно такой: тело на диск, затем строка учёта. Обратный дал бы учтённую доставку без данных. Отказ на любом из двух шагов — отказ приёма, и о нём отправителю говорится ошибкой: `400` для неразбираемой верхнеуровневой формы, `413` для тела сверх предела, `500` для отказа записи. Учёт доставки SHALL вестись на контексте, не отменяемом обрывом соединения: тело к этому моменту уже на диске, и отказ вставки из-за ушедшего клиента оставил бы тело без записи в журнале. Проверка формы тела при этом остаётся отменяемой — там отмена уместна. Причина разнесения измерена: свёртка 16 тысяч точек занимает 11 секунд, а `WriteTimeout` в Go ставится до вызова обработчика и потому является общим бюджетом на чтение тела, запись архива, учёт и свёртку. Исчерпав его, сервер считает, что отдал `200`, клиент получает обрыв, а запись `accessLog` называет статус `200` — то есть единственный канал наблюдаемости врёт. #### Scenario: Ответ отдан до свёртки - **WHEN** тело принято, записано в архив и учтено - **THEN** ответ `200` отдан - **AND** доставка в этот момент числится неразобранной #### Scenario: Ширина доставки не удлиняет ответ - **WHEN** приезжает доставка, свёртка которой занимает секунды - **THEN** время ответа не включает время свёртки #### Scenario: Обрыв соединения не оставляет тело без учёта - **WHEN** соединение обрывается после того, как тело записано в архив - **THEN** строка учёта доставки всё равно записывается ### Requirement: Несвёрнутая доставка числится неразобранной Учтённая, но ещё не свёрнутая доставка SHALL числиться в статусе `pending`, и этот статус SHALL быть единственным признаком того, что свёртка ещё должна произойти. Отдельного, живущего только в памяти представления той же очереди система иметь MUST NOT. Отсюда следуют три свойства, и они и есть смысл требования: - переполнять нечего — доставка ждёт свёртки в базе, а не в буфере, и «очередь переполнена» невыразимо; - падение процесса очереди не теряет — несвёрнутое остаётся `pending`; - подбор `pending` не является отдельной операцией — он совпадает с обычной работой свёртки. #### Scenario: Принятая доставка ждёт свёртки в базе - **WHEN** доставка учтена, но ещё не свёрнута - **THEN** её `parse_status` равен `pending` #### Scenario: Оборванный процесс не теряет несвёрнутое - **WHEN** процесс прекращается до того, как свёртка доставки завершилась - **THEN** доставка остаётся `pending` - **AND** следующий старт сворачивает её ### Requirement: Исход свёртки отражает доставку, а не обстоятельства Свёртка SHALL оставлять доставку в очереди — то есть **не менять** её статус, — когда работа не сделана по причине, к самой доставке не относящейся: отмена контекста и занятость базы после исчерпания повторов транзакции. Все прочие отказы разбора и записи точек (непонятое содержимое, невыводимый слой, нечитаемое или слишком большое тело, исчерпанный дедлайн свёртки, паника самой свёртки) SHALL давать `failed`: это свойства доставки, и повторять их бесполезно. Отказы, случившиеся **до** чтения тела, и отказ самой записи исхода статуса не меняют по другой причине — записать его нечем. Доставка остаётся `pending`, и это честно: этим разбором её не досмотрели. Паника свёртки SHALL перехватываться на той же границе, что пишет исход разбора, и превращаться в `failed`. Иначе она валит процесс целиком — фоновая горутина ничем не обёрнута, — а перезапуск берёт ту же доставку первой, то есть дефект одной доставки становится циклом перезапуска, при котором приём не работает вовсе. До разнесения ответа и свёртки ту же панику ловил транспорт, и стоила она одного ответа. Доставка в статусе `failed` в очередь свёртки возвращаться MUST NOT — её подбирает только пересборка журнала. Это названная граница: обратное означало бы бесконечный повтор заведомо безнадёжного. Без такого различения занятость базы — а свёртка теперь конкурирует с приёмом за неё штатно — стирала бы доставку с полки молча, и вернуть её могла бы только ручная операция с остановкой сервиса. #### Scenario: Занятая база не выводит доставку из очереди - **WHEN** свёртка не прошла из-за занятости базы - **THEN** доставка остаётся `pending` - **AND** следующий проход пробует её снова #### Scenario: Непонятое содержимое выводит доставку из очереди - **WHEN** свёртка не прошла из-за содержимого тела - **THEN** доставка получает статус `failed` - **AND** следующий проход её не выбирает ### Requirement: Свёртку ведёт один фоновый воркер в порядке журнала Свёртку принятых доставок SHALL вести одна горутина, обрабатывающая доставки в порядке журнала — `(received_at, id)`, как он определён capability пересборки. Распараллеливать свёртку MUST NOT. Достижимая гарантия называется точно: в порядке `(received_at, id)` сворачиваются все доставки, **видимые воркеру** на момент выборки. Доставка, ставшая видимой позже курсора прохода, подбирается следующим проходом; абсолютного порядка при конкурентных приёмах система не обещает. Последствие этого предела называется вслух: доставка без плотных метрик, свёрнутая раньше своей предшественницы, слоя не выведет и получит `failed` — то есть её точки в витрину не попадут до пересборки. Живое состояние в этом случае расходится с тем, что даёт `healthlog reindex`. Окно узкое (обе доставки должны приниматься одновременно, и только у автоматизации без плотных метрик), и изменение его сужает, а не открывает: прежде свёртка шла в порядке завершения обработчиков. Устранение предела — отдельный вопрос, оно требует удерживать порядок на самом приёме. Воркер SHALL продвигаться по неразобранным доставкам строго возрастающим курсором в пределах одного прохода. Курсор обязателен для завершимости: доставка, у которой не удалось записать даже исход разбора, остаётся `pending`, и проход без курсора выбирал бы её бесконечно. Приём SHALL будить воркер после того, как доставка учтена. Потеря сигнала отказом быть MUST NOT: доставка от этого не перестаёт числиться `pending`. Помимо сигнала воркер SHALL просыпаться периодически — иначе доставка, оставшаяся `pending` по причине выше, ждала бы следующей доставки, а ночью телефон молчит часами. Отказ отдельного прохода воркер SHALL переживать: отказ выборки пишется `ERROR` и прекращает проход, но не цикл. Отмена работы снаружи отказом при этом считаться MUST NOT — штатная остановка не должна писать `ERROR`. Воркер, умерший от временного отказа базы, остановил бы свёртку до конца жизни процесса, пока приём продолжал бы отвечать `200`. #### Scenario: Видимые доставки сворачиваются в порядке журнала - **GIVEN** несколько доставок числятся `pending` до начала прохода - **WHEN** воркер делает проход - **THEN** он сворачивает их в порядке `(received_at, id)` - **AND** доставка без плотных метрик наследует слой предшествующей ей по этому порядку доставки той же автоматизации, а не соседа по времени вставки #### Scenario: Доставка, не записавшая исход, не зацикливает проход - **WHEN** свёртка доставки не смогла записать исход разбора и оставила её `pending` - **THEN** проход воркера завершается, а не выбирает её повторно #### Scenario: Доставка без входящего потока всё равно подбирается - **GIVEN** доставка осталась `pending`, и новых доставок не приезжает - **WHEN** наступает очередное периодическое пробуждение - **THEN** воркер пробует свернуть её снова ### Requirement: Подбор неразобранного при старте — та же операция При старте система SHALL сворачивать доставки, числящиеся неразобранными, тем же путём, каким сворачивает вновь принятые: отдельного кода подбора существовать MUST NOT. Порядок журнала при подборе SHALL соблюдаться так же, как при обычной работе — подбор это тот же проход воркера, а не особый режим. Классификация исхода свёртки (свёрнуто; слой не выведен; содержимое не разобрано; прочий отказ; частичный разбор; несравнимые наборы полей) SHALL быть общей с пересборкой журнала: второй классификатор разошёлся бы с первым молча. Классифицироваться SHALL только ошибка свёртки; решение «работу прекратили снаружи» MUST NOT приниматься классификатором — у пересборки и у воркера контекст свёртки означает разное, и общая ветка отмены дала бы одному из них противоположный смысл. Счётчики частичного разбора и несравнимых наборов SHALL читаться только у успешной свёртки — у отказавшей они заполнены частично, и `partial` занижался бы, а по нему принимается решение о судьбе тела. #### Scenario: Доставки, оставшиеся неразобранными, подбираются при старте - **GIVEN** в учёте есть доставки со статусом `pending` - **WHEN** сервис стартует - **THEN** они сворачиваются в порядке `(received_at, id)` #### Scenario: Размер задолженности назван при старте - **WHEN** сервис стартует и неразобранные доставки есть - **THEN** их число попадает в лог одной записью уровня `INFO` ### Requirement: Остановка не оставляет доставку в неопределённом состоянии Остановка сервиса SHALL сперва прекращать приём, затем останавливать воркер. Обратный порядок оставил бы доставки, принятые после остановки воркера, никого не разбудившими. Инвариант остановки: после неё не существует доставки, которая числится разобранной, а записана частично; всё несвёрнутое остаётся `pending`. Обещать, что текущая доставка непременно досворачивается, система MUST NOT — бюджет остановки меньше бюджета свёртки, и такое обещание исполнялось бы не всегда. Свёртка SHALL идти на контексте, не отменяемом остановкой, а отмена SHALL проверяться **между** доставками. Причина названа: свёртка помечает доставку `failed` на ошибке, а `failed` воркер не подбирает — то есть отмена снаружи превратилась бы в свойство доставки. Исчерпание бюджета остановки отказом сервиса считаться MUST NOT: и штатное завершение воркера, и его прерывание оставляют состояние определённым. Факт SHALL называться предупреждением, а не ошибкой старта. #### Scenario: Остановка не оставляет половины - **WHEN** сервис останавливается во время свёртки доставки - **THEN** доставка либо свёрнута целиком, либо числится `pending` - **AND** частично записанных объектов от неё не остаётся #### Scenario: Приём прекращается раньше воркера - **WHEN** сервис останавливается - **THEN** приём перестаёт принимать раньше, чем останавливается воркер #### Scenario: Не уложились в бюджет остановки - **WHEN** воркер не успевает выйти в отведённый бюджет - **THEN** факт попадает в лог предупреждением - **AND** команда не сообщает об ошибке ### Requirement: Отставание воркера видно в логе Система SHALL писать `WARN`, когда доставка ждала свёртки дольше периода быстрого прохода синхронизации (пять минут): дольше этого срока очередь растёт, а не рассасывается. Ожидание считается от `received_at` до начала свёртки. Записей SHALL быть **одна на проход**, а не одна на доставку: задолженность в сотню тел давала бы сотню одинаковых предупреждений каждую минуту, и уровень, по которому вмешиваются, перестал бы что-либо значить. Строка называет число задержанных и худшее ожидание с идентификатором доставки. Метка SHALL вычисляться на выборке прохода, а не только по факту успешной свёртки: состояние «работа есть, прогресса нет» обязано быть отличимо от здорового пустого потока, иначе наблюдаемость молчит ровно там, где нужна. Задолженность, накопленную **до** старта, метка задержки помечать MUST NOT: она названа отдельной записью `INFO` о размере задолженности, а сотня одинаковых `WARN` при первом же старте обесценила бы уровень. Метка включается после того, как первый проход воркера завершился. Записи воркера значений точек и имён устройств содержать MUST NOT — как и любые записи свёртки. #### Scenario: Отставший воркер называет задержку - **GIVEN** первый проход воркера завершён - **WHEN** доставки дожидаются свёртки дольше пяти минут - **THEN** в лог идёт одна запись `WARN` на проход с числом задержанных и худшим ожиданием #### Scenario: Задолженность при старте не даёт шквала предупреждений - **GIVEN** неразобранными числятся доставки, накопленные до старта - **WHEN** воркер сворачивает их первым проходом - **THEN** записей `WARN` о задержке по ним нет ### Requirement: Длинный бюджет ответа принадлежит маршруту приёма Обработчик приёма SHALL выставлять собственный дедлайн записи ответа перед чтением тела, и этот дедлайн SHALL покрывать чтение тела вместе с отправкой ответа. Полагаться на общий `write_timeout` сервера система MUST NOT: он ставится до вызова обработчика и потому обрывает загрузку, идущую дольше него, — делая `read_timeout` обещанием, которого сервер не исполняет. Общий `write_timeout` сервера при этом расширяться MUST NOT: длинный бюджет нужен одному маршруту, а остальные теряли бы защиту от застрявшей записи ответа. Транспорт, не поддерживающий установки дедлайна, отказом приёма считаться MUST NOT: факт уходит в `DEBUG`, приём продолжается. #### Scenario: Медленная загрузка тела не обрывается - **WHEN** тело приезжает дольше, чем общий `write_timeout` сервера, но укладывается в `read_timeout` - **THEN** ответ доходит до отправителя #### Scenario: Прочие маршруты бюджета не наследуют - **WHEN** запрос идёт не на приём - **THEN** его бюджет записи ответа остаётся общим `write_timeout`