Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`, канал несёт только бит «есть работа». Переполнять нечего, падение процесса очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а не отдельный код. Классификация исхода общая с пересборкой журнала. - Исход разбора начал отражать доставку, а не обстоятельства: отмена и занятость базы статус не меняют (иначе конкуренция за базу выводила бы доставку из очереди навсегда), паника свёртки больше не валит процесс, а учёт доставки идёт через транзакцию с повторами. - Длинный бюджет ответа выдан маршруту приёма, а не всему серверу: `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с остальных маршрутов.
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
## 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`
|
||||
@@ -0,0 +1,17 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Разбор не влияет на код ответа приёма
|
||||
|
||||
Система MUST сохранять правило «сохранили — значит приняли»: исход разбора не
|
||||
меняет код ответа на доставку.
|
||||
|
||||
Разбор идёт **после** ответа, поэтому исход виден не в ответе и не в момент
|
||||
ответа, а асинхронно — в `delivery.parse_status` и в записи лога. Когда именно
|
||||
отдаётся ответ и кто сворачивает принятое, определяет capability `ingest`;
|
||||
здесь нормируется только то, что от разбора код ответа не зависит.
|
||||
|
||||
#### Scenario: Содержимое не разобралось
|
||||
|
||||
- **WHEN** тело сохранено в архив, но разбор его содержимого не удался
|
||||
- **THEN** ответ на приём остаётся `200`
|
||||
- **AND** исход виден в `delivery.parse_status` и в записи лога
|
||||
@@ -0,0 +1,115 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Отчёт, оракул и исход команды
|
||||
|
||||
Система SHALL завершать пересборку отчётом, который несёт счётчики
|
||||
(проиграно, свёрнуто, отказов по классам, тел без учётной записи, строк без
|
||||
тела, пропущенных файлов, повторов, объектов **до и после**) и **два
|
||||
отпечатка** — рабочей витрины и пересобранной, — с прямым ответом, совпали они
|
||||
или нет.
|
||||
|
||||
Отказы SHALL считаться **по классам**: слой не выводится, содержимое не
|
||||
разбирается, работа отложена по обстоятельствам, всё прочее. Невыведенный слой
|
||||
есть в каждом журнале и штатен; общий счётчик отправлял бы человека искать
|
||||
дефект там, где его нет. Отдельно называть человеку следует только нештатные
|
||||
отказы.
|
||||
|
||||
Отложенная доставка (занятость базы, отмена работы снаружи) SHALL считаться
|
||||
нештатной **для пересборки**, хотя для фоновой свёртки она штатна: пересборка
|
||||
идёт в свежий файл при единственном писателе, и такая доставка в собранной
|
||||
витрине просто отсутствует — вместе с теми, кто наследовал от неё слой. Классы
|
||||
при этом общие с фоновой свёрткой: второй классификатор разошёлся бы с первым
|
||||
молча.
|
||||
|
||||
Число объектов «было и стало» SHALL печататься рядом с отпечатками: отпечатки
|
||||
отвечают «да/нет», а решение о подмене необратимо, и по «да/нет» нельзя
|
||||
судить о **направлении** расхождения. Именно пара чисел — 1737 против 1742 —
|
||||
поймала прошлый дефект наследования слоя.
|
||||
|
||||
Отпечаток здесь оракул, а не украшение: число объектов к правилу разрешения
|
||||
столкновений нечувствительно — на координате всегда ровно одна точка, и правило
|
||||
выбирает, какая, а не сколько. «Объектов столько же» совпало бы и при заведомо
|
||||
сломанном правиле.
|
||||
|
||||
Отпечаток рабочей витрины SHALL сниматься **до** начала проигрывания, а число
|
||||
доставок в рабочей базе — до и после. Ненулевая разница SHALL называться в
|
||||
отчёте, и при ней процедура подмены печататься MUST NOT: доставки, приехавшие за
|
||||
время прогона, есть в рабочей базе и в архиве, но не в собранном файле, и
|
||||
подмена стёрла бы их учёт вместе с заголовками, которых в архиве нет.
|
||||
|
||||
Величины, которые не снимались, отчёт печатать MUST NOT. При отмене отпечаток
|
||||
пересобранной витрины и число доставок после прогона не измеряются вовсе —
|
||||
печатать их сравнение значило бы выдать неизмеренное за измеренное, причём в
|
||||
единственном оракуле задачи. Ожидаемые классы расхождения (новые доставки за время прогона,
|
||||
непереносимый признак запечатанного часа, исправленный разбор) SHALL называться
|
||||
отдельно от самого факта расхождения.
|
||||
|
||||
**Исход команды.** Расхождение отпечатков отказом быть MUST NOT: после
|
||||
исправления разбора оно ожидаемо и есть сам смысл пересборки. Отказ отдельной
|
||||
доставки отказом команды тоже MUST NOT быть: доставка, слой которой не
|
||||
выводится, — штатный исход.
|
||||
|
||||
Отказом команды SHALL быть: пустой журнал, отсутствие хотя бы одной свёрнутой
|
||||
доставки, отмена и любая ошибка окружения. Пустая витрина совпадает по
|
||||
отпечатку с пустой витриной, поэтому прогон по пустому журналу выглядит
|
||||
идеальной сходимостью — а все умолчания подыгрывают такому запуску: конфига
|
||||
может не быть вовсе, и тогда пути указывают в рабочий каталог процесса. Человек,
|
||||
выполнивший напечатанную процедуру, заменил бы витрину пустой.
|
||||
|
||||
Отчёт значений точек, имён метрик, имён устройств и содержимого тел содержать
|
||||
MUST NOT: отпечаток берёт содержимое хешем. Ограничение относится к отчёту в
|
||||
стандартном выводе; лог свёртки живёт по правилам спеки хранения, где координаты
|
||||
столкновения (метрика, слой, час) разрешены явно.
|
||||
|
||||
Отчёт идёт в стандартный вывод человеческим текстом. Прогресс длинного прогона
|
||||
SHALL идти в поток ошибок, а не смешиваться с отчётом: прогон на полном архиве
|
||||
молчит минутами, и зависший неотличим от идущего.
|
||||
|
||||
#### Scenario: Отчёт сравнивает отпечатки
|
||||
|
||||
- **WHEN** пересборка завершилась
|
||||
- **THEN** отчёт содержит отпечаток рабочей витрины и отпечаток пересобранной
|
||||
- **AND** прямо называет, совпали они или нет
|
||||
- **AND** называет, изменилось ли число доставок в рабочей базе за время прогона
|
||||
|
||||
#### Scenario: Расхождение отпечатков не является отказом
|
||||
|
||||
- **WHEN** отпечаток пересобранной витрины отличается от рабочей, и при этом
|
||||
хотя бы одна доставка свёрнута
|
||||
- **THEN** команда завершается успешно, а расхождение названо в отчёте
|
||||
|
||||
#### Scenario: Пустой журнал — отказ, а не идеальная сходимость
|
||||
|
||||
- **WHEN** в архиве не нашлось ни одного тела
|
||||
- **THEN** команда завершается ненулевым кодом
|
||||
- **AND** процедуры подмены не печатает
|
||||
|
||||
#### Scenario: Ни одна доставка не свернулась
|
||||
|
||||
- **WHEN** журнал непуст, но свернуть не удалось ни одной доставки
|
||||
- **THEN** команда завершается ненулевым кодом
|
||||
- **AND** процедуры подмены не печатает
|
||||
|
||||
#### Scenario: Приезд доставок за время прогона отменяет подмену
|
||||
|
||||
- **WHEN** число доставок в рабочей базе за время прогона изменилось
|
||||
- **THEN** отчёт называет разницу
|
||||
- **AND** процедуры подмены не печатает
|
||||
|
||||
#### Scenario: Отчёт после отмены не сравнивает неизмеренного
|
||||
|
||||
- **WHEN** прогон отменён
|
||||
- **THEN** отчёт не содержит ни ответа о совпадении отпечатков, ни разницы
|
||||
числа доставок
|
||||
|
||||
#### Scenario: Рабочей базы нет вовсе
|
||||
|
||||
- **WHEN** файла рабочей базы не существует
|
||||
- **THEN** пересборка идёт по одним подобранным телам
|
||||
- **AND** отчёт называет, что сверять не с чем и что заголовки доставок не
|
||||
восстанавливаются
|
||||
|
||||
#### Scenario: Отчёт не раскрывает данных о здоровье
|
||||
|
||||
- **WHEN** отчёт напечатан
|
||||
- **THEN** он не содержит ни значений точек, ни имён метрик, ни имён устройств
|
||||
@@ -0,0 +1,94 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Учёт частично разобранной доставки
|
||||
|
||||
Система SHALL отличать доставку, разобранную целиком, от доставки, в теле
|
||||
которой остались непокрытые разбором секции. Доставка с непустым списком
|
||||
непокрытых ключей MUST получать статус `partial`, а не `parsed`.
|
||||
|
||||
Статусы разбора:
|
||||
|
||||
```
|
||||
pending этим разбором ещё не смотрели — или смотрели, но работа не сделана
|
||||
по обстоятельствам (см. ниже)
|
||||
parsed разобрано всё, что в теле было
|
||||
partial разобрано покрытое; в теле остались непокрытые секции
|
||||
failed разобрать не удалось, точек нет
|
||||
```
|
||||
|
||||
Источник истины — список непокрытых ключей; статус производен от него и от
|
||||
факта отказа, в порядке `failed` → `partial` → `parsed`. Приоритет назван явно,
|
||||
чтобы читатели (ретеншен, статистика) спрашивали статус, а не сравнивали список
|
||||
со строкой.
|
||||
|
||||
**Отказ обстоятельств статуса не меняет вовсе.** Отмена работы снаружи и
|
||||
занятость базы дольше повторов транзакции означают «не сделано», а не «не
|
||||
выходит»: доставка остаётся `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** сохранённый список непокрытых ключей пуст
|
||||
Reference in New Issue
Block a user