Files
healthlog/openspec/changes/archive/2026-08-02-otvet-i-svyortka/specs/storage/spec.md
T
av 63bffe2865 Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`,
  канал несёт только бит «есть работа». Переполнять нечего, падение процесса
  очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а
  не отдельный код. Классификация исхода общая с пересборкой журнала.
- Исход разбора начал отражать доставку, а не обстоятельства: отмена и
  занятость базы статус не меняют (иначе конкуренция за базу выводила бы
  доставку из очереди навсегда), паника свёртки больше не валит процесс, а
  учёт доставки идёт через транзакцию с повторами.
- Длинный бюджет ответа выдан маршруту приёма, а не всему серверу:
  `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с
  остальных маршрутов.
2026-08-02 11:01:42 +03:00

7.0 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Учёт частично разобранной доставки

Система SHALL отличать доставку, разобранную целиком, от доставки, в теле которой остались непокрытые разбором секции. Доставка с непустым списком непокрытых ключей MUST получать статус partial, а не parsed.

Статусы разбора:

pending  этим разбором ещё не смотрели — или смотрели, но работа не сделана
         по обстоятельствам (см. ниже)
parsed   разобрано всё, что в теле было
partial  разобрано покрытое; в теле остались непокрытые секции
failed   разобрать не удалось, точек нет

Источник истины — список непокрытых ключей; статус производен от него и от факта отказа, в порядке failedpartialparsed. Приоритет назван явно, чтобы читатели (ретеншен, статистика) спрашивали статус, а не сравнивали список со строкой.

Отказ обстоятельств статуса не меняет вовсе. Отмена работы снаружи и занятость базы дольше повторов транзакции означают «не сделано», а не «не выходит»: доставка остаётся pending и будет свёрнута снова. Правило появилось не из аккуратности — фоновая свёртка failed не подбирает никогда, и без этого различения занятость базы (а с фоновой свёрткой конкуренция за неё штатная) выводила бы доставку из очереди навсегда. Различение живёт в одном месте: тот, кто пишет исход, и тот, кто классифицирует его в счётчики, спрашивают один предикат.

Дедлайн самой свёртки к обстоятельствам MUST NOT относиться: доставка, не уложившаяся в бюджет, не уложится в него и в следующий раз, а бесконечный повтор заведомо безнадёжного — это очередь, которая не движется.

Отказы, случившиеся до чтения тела (учётной записи нет, соседний запрос не прошёл), и отказ самой записи исхода статуса не меняют по другой причине — записать его нечем. Доставка остаётся pending, что честно: этим разбором её не досмотрели.

Список непокрытых ключей SHALL сохраняться рядом с доставкой — именами ключей, без содержимого секций. Он же ответ на вопрос «что останется потерянным, если тело удалить»: для stateOfMind доставки HAE единственный источник, в экспорте Apple его нет (находка 46). Поэтому список MUST сохраняться и при отказе разбора, если разбор успел его собрать: failed с непустым списком — законное состояние.

Запись списка MUST замещать прежнее значение целиком, включая замещение пустым: иначе доставка, все секции которой стали покрытыми, осталась бы partial навсегда.

Список — снимок покрытия на момент свёртки. Задача, которая начинает разбирать секцию, тем же изменением SHALL переводить partial-строки с этим ключом в pending; ретеншену позволено смотреть на partial только при соблюдении этого правила.

Статусы, поставленные разбором, который частичного исхода не различал, доверия не заслуживают: под parsed у них лежат и полностью разобранные доставки, и доставки без метрик вовсе. Такие строки MUST переводиться в pending — «этим разбором ещё не смотрели». Число точек у них до пересвёртки остаётся прежним: оно производно от объектов витрины, которые никуда не делись.

Scenario: Доставка с непокрытой секцией отмечается частичной

  • WHEN разбор доставки вернул непустой список непокрытых ключей
  • THEN parse_status доставки равен partial
  • AND список непокрытых ключей сохранён вместе с доставкой
  • AND точки покрытой секции сохранены как обычно

Scenario: Доставка без непокрытых секций остаётся parsed

  • WHEN разбор доставки не дал непокрытых ключей
  • THEN parse_status равен parsed
  • AND сохранённый список непокрытых ключей пуст

Scenario: Отказ разбора сильнее частичности

  • WHEN разбор доставки завершился ошибкой в самом разборе или в записи точек
  • THEN parse_status равен failed
  • AND список непокрытых ключей сохранён, если разбор успел его собрать

Scenario: Занятая база доставку из очереди не выводит

  • WHEN разбор не состоялся из-за занятости базы или отмены работы снаружи
  • THEN parse_status остаётся pending

Scenario: Пересвёртка после того, как секция стала покрытой

  • WHEN доставка со статусом partial сворачивается повторно разбором, который эту секцию покрывает
  • THEN parse_status становится parsed
  • AND сохранённый список непокрытых ключей пуст